Adjust tab indicator visual (#9219)

Now behaves like the non-compose indicator by showing the swipe progress too
This commit is contained in:
Ivan Iskandar 2023-03-17 09:20:25 +07:00 committed by GitHub
parent 4d3e13b0d1
commit 18e55aa25f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 10 deletions

View File

@ -56,7 +56,7 @@ fun TabbedDialog(
TabRow( TabRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
indicator = { TabIndicator(it[pagerState.currentPage]) }, indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
divider = {}, divider = {},
) { ) {
tabTitles.fastForEachIndexed { i, tab -> tabTitles.fastForEachIndexed { i, tab ->

View File

@ -69,7 +69,7 @@ fun TabbedScreen(
) { ) {
TabRow( TabRow(
selectedTabIndex = state.currentPage, selectedTabIndex = state.currentPage,
indicator = { TabIndicator(it[state.currentPage]) }, indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
) { ) {
tabs.forEachIndexed { index, tab -> tabs.forEachIndexed { index, tab ->
Tab( Tab(

View File

@ -65,7 +65,7 @@ fun LibraryContent(
} }
LibraryTabs( LibraryTabs(
categories = categories, categories = categories,
currentPageIndex = pagerState.currentPage, pagerState = pagerState,
getNumberOfMangaForCategory = getNumberOfMangaForCategory, getNumberOfMangaForCategory = getNumberOfMangaForCategory,
) { scope.launch { pagerState.animateScrollToPage(it) } } ) { scope.launch { pagerState.animateScrollToPage(it) } }
} }

View File

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import eu.kanade.presentation.category.visualName import eu.kanade.presentation.category.visualName
import tachiyomi.domain.category.model.Category import tachiyomi.domain.category.model.Category
import tachiyomi.presentation.core.components.PagerState
import tachiyomi.presentation.core.components.material.Divider import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.TabIndicator import tachiyomi.presentation.core.components.material.TabIndicator
import tachiyomi.presentation.core.components.material.TabText import tachiyomi.presentation.core.components.material.TabText
@ -15,22 +16,22 @@ import tachiyomi.presentation.core.components.material.TabText
@Composable @Composable
internal fun LibraryTabs( internal fun LibraryTabs(
categories: List<Category>, categories: List<Category>,
currentPageIndex: Int, pagerState: PagerState,
getNumberOfMangaForCategory: (Category) -> Int?, getNumberOfMangaForCategory: (Category) -> Int?,
onTabItemClick: (Int) -> Unit, onTabItemClick: (Int) -> Unit,
) { ) {
Column { Column {
ScrollableTabRow( ScrollableTabRow(
selectedTabIndex = currentPageIndex, selectedTabIndex = pagerState.currentPage,
edgePadding = 0.dp, edgePadding = 0.dp,
indicator = { TabIndicator(it[currentPageIndex]) }, indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
// TODO: use default when width is fixed upstream // TODO: use default when width is fixed upstream
// https://issuetracker.google.com/issues/242879624 // https://issuetracker.google.com/issues/242879624
divider = {}, divider = {},
) { ) {
categories.forEachIndexed { index, category -> categories.forEachIndexed { index, category ->
Tab( Tab(
selected = currentPageIndex == index, selected = pagerState.currentPage == index,
onClick = { onTabItemClick(index) }, onClick = { onTabItemClick(index) },
text = { text = {
TabText( TabText(

View File

@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -31,6 +32,7 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastMaxBy import androidx.compose.ui.util.fastMaxBy
import androidx.compose.ui.util.fastSumBy import androidx.compose.ui.util.fastSumBy
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlin.math.abs
@Composable @Composable
fun HorizontalPager( fun HorizontalPager(
@ -143,8 +145,17 @@ class PagerState(
val lazyListState = LazyListState(firstVisibleItemIndex = currentPage) val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
private val pageSize: Int
get() = visiblePages.firstOrNull()?.size ?: 0
private var _currentPage by mutableStateOf(currentPage) private var _currentPage by mutableStateOf(currentPage)
private val layoutInfo: LazyListLayoutInfo
get() = lazyListState.layoutInfo
private val visiblePages: List<LazyListItemInfo>
get() = layoutInfo.visibleItemsInfo
var currentPage: Int var currentPage: Int
get() = _currentPage get() = _currentPage
set(value) { set(value) {
@ -166,6 +177,31 @@ class PagerState(
} }
} }
private val closestPageToSnappedPosition: LazyListItemInfo?
get() = visiblePages.fastMaxBy {
-abs(
calculateDistanceToDesiredSnapPosition(
layoutInfo,
it,
SnapAlignmentStartToStart,
),
)
}
val currentPageOffsetFraction: Float by derivedStateOf {
val currentPagePositionOffset = closestPageToSnappedPosition?.offset ?: 0
val pageUsedSpace = pageSize.toFloat()
if (pageUsedSpace == 0f) {
// Default to 0 when there's no info about the page size yet.
0f
} else {
((-currentPagePositionOffset) / (pageUsedSpace)).coerceIn(
MinPageOffset,
MaxPageOffset,
)
}
}
fun updateCurrentPageBasedOnLazyListState() { fun updateCurrentPageBasedOnLazyListState() {
mostVisiblePageLayoutInfo?.let { mostVisiblePageLayoutInfo?.let {
currentPage = it.index currentPage = it.index
@ -189,6 +225,11 @@ class PagerState(
} }
} }
private const val MinPageOffset = -0.5f
private const val MaxPageOffset = 0.5f
internal val SnapAlignmentStartToStart: (layoutSize: Float, itemSize: Float) -> Float =
{ _, _ -> 0f }
// https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt // https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
private fun lazyListSnapLayoutInfoProvider( private fun lazyListSnapLayoutInfoProvider(
lazyListState: LazyListState, lazyListState: LazyListState,

View File

@ -1,27 +1,57 @@
package tachiyomi.presentation.core.components.material package tachiyomi.presentation.core.components.material
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TabPosition import androidx.compose.material3.TabPosition
import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import tachiyomi.presentation.core.components.Pill import tachiyomi.presentation.core.components.Pill
private fun Modifier.tabIndicatorOffset(
currentTabPosition: TabPosition,
currentPageOffsetFraction: Float,
) = composed {
val currentTabWidth by animateDpAsState(
targetValue = currentTabPosition.width,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
)
val offset by animateDpAsState(
targetValue = currentTabPosition.left + (currentTabWidth * currentPageOffsetFraction),
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
)
fillMaxWidth()
.wrapContentSize(Alignment.BottomStart)
.offset { IntOffset(x = offset.roundToPx(), y = 0) }
.width(currentTabWidth)
}
@Composable @Composable
fun TabIndicator(currentTabPosition: TabPosition) { fun TabIndicator(
currentTabPosition: TabPosition,
currentPageOffsetFraction: Float,
) {
TabRowDefaults.Indicator( TabRowDefaults.Indicator(
Modifier Modifier
.tabIndicatorOffset(currentTabPosition) .tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
) )