diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt index 15af35449..31f306b73 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedDialog.kt @@ -56,7 +56,7 @@ fun TabbedDialog( TabRow( modifier = Modifier.weight(1f), selectedTabIndex = pagerState.currentPage, - indicator = { TabIndicator(it[pagerState.currentPage]) }, + indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) }, divider = {}, ) { tabTitles.fastForEachIndexed { i, tab -> diff --git a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt index d60a2dde2..65714a879 100644 --- a/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/components/TabbedScreen.kt @@ -69,7 +69,7 @@ fun TabbedScreen( ) { TabRow( selectedTabIndex = state.currentPage, - indicator = { TabIndicator(it[state.currentPage]) }, + indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) }, ) { tabs.forEachIndexed { index, tab -> Tab( diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt index 706deeb06..e8717d177 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryContent.kt @@ -65,7 +65,7 @@ fun LibraryContent( } LibraryTabs( categories = categories, - currentPageIndex = pagerState.currentPage, + pagerState = pagerState, getNumberOfMangaForCategory = getNumberOfMangaForCategory, ) { scope.launch { pagerState.animateScrollToPage(it) } } } diff --git a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt index 8901ce7cc..243ed0764 100644 --- a/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt +++ b/app/src/main/java/eu/kanade/presentation/library/components/LibraryTabs.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.unit.dp import eu.kanade.presentation.category.visualName 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.TabIndicator import tachiyomi.presentation.core.components.material.TabText @@ -15,22 +16,22 @@ import tachiyomi.presentation.core.components.material.TabText @Composable internal fun LibraryTabs( categories: List, - currentPageIndex: Int, + pagerState: PagerState, getNumberOfMangaForCategory: (Category) -> Int?, onTabItemClick: (Int) -> Unit, ) { Column { ScrollableTabRow( - selectedTabIndex = currentPageIndex, + selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp, - indicator = { TabIndicator(it[currentPageIndex]) }, + indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) }, // TODO: use default when width is fixed upstream // https://issuetracker.google.com/issues/242879624 divider = {}, ) { categories.forEachIndexed { index, category -> Tab( - selected = currentPageIndex == index, + selected = pagerState.currentPage == index, onClick = { onTabItemClick(index) }, text = { TabText( diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt index 601974db4..942e074db 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/Pager.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.fastSumBy import kotlinx.coroutines.flow.distinctUntilChanged +import kotlin.math.abs @Composable fun HorizontalPager( @@ -143,8 +145,17 @@ class PagerState( val lazyListState = LazyListState(firstVisibleItemIndex = currentPage) + private val pageSize: Int + get() = visiblePages.firstOrNull()?.size ?: 0 + private var _currentPage by mutableStateOf(currentPage) + private val layoutInfo: LazyListLayoutInfo + get() = lazyListState.layoutInfo + + private val visiblePages: List + get() = layoutInfo.visibleItemsInfo + var currentPage: Int get() = _currentPage 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() { mostVisiblePageLayoutInfo?.let { 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 private fun lazyListSnapLayoutInfoProvider( lazyListState: LazyListState, diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt index fcdebe75b..33c819a8f 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/material/Tabs.kt @@ -1,27 +1,57 @@ 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.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.width +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TabPosition import androidx.compose.material3.TabRowDefaults -import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 -fun TabIndicator(currentTabPosition: TabPosition) { +fun TabIndicator( + currentTabPosition: TabPosition, + currentPageOffsetFraction: Float, +) { TabRowDefaults.Indicator( Modifier - .tabIndicatorOffset(currentTabPosition) + .tabIndicatorOffset(currentTabPosition, currentPageOffsetFraction) .padding(horizontal = 8.dp) .clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)), )