Adjust tab indicator visual (#9219)
Now behaves like the non-compose indicator by showing the swipe progress too
This commit is contained in:
parent
4d3e13b0d1
commit
18e55aa25f
6 changed files with 82 additions and 10 deletions
|
@ -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 ->
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) } }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
|
|
Reference in a new issue