Migrate to AndroidX HorizontalPager
Bug: 260660819 Test: Manually with Gallery Test: Manually with Settings Change-Id: I6b35c1cd4a233b4547291c99f5e55133e6a6df61
This commit is contained in:
@@ -108,7 +108,6 @@ task coverageReport(type: JacocoReport, dependsOn: "connectedDebugAndroidTest")
|
||||
|
||||
// Excludes files forked from Accompanist.
|
||||
"com/android/settingslib/spa/framework/compose/DrawablePainter*",
|
||||
"com/android/settingslib/spa/framework/compose/Pager*",
|
||||
|
||||
// Excludes inline functions, which is not covered in Jacoco reports.
|
||||
"com/android/settingslib/spa/framework/util/Collections*",
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spa.framework.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
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.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
/**
|
||||
* *************************************************************************************************
|
||||
* This file was forked from
|
||||
* https://github.com/google/accompanist/blob/main/pager/src/main/java/com/google/accompanist/pager/Pager.kt
|
||||
* and will be removed once it lands in AndroidX.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A horizontally scrolling layout that allows users to flip between items to the left and right.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerSample
|
||||
*
|
||||
* @param count the number of pages.
|
||||
* @param modifier the modifier to apply to this layout.
|
||||
* @param state the state object to be used to control or observe the pager's state.
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
||||
* composed from the end to the start and [PagerState.currentPage] == 0 will mean
|
||||
* the first item is located at the end.
|
||||
* @param itemSpacing horizontal spacing to add between items.
|
||||
* @param key the scroll position will be maintained based on the key, which means if you
|
||||
* add/remove items before the current visible item the item with the given key will be kept as the
|
||||
* first visible one.
|
||||
* @param content a block which describes the content. Inside this block you can reference
|
||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalPager(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState = rememberPagerState(),
|
||||
reverseLayout: Boolean = false,
|
||||
itemSpacing: Dp = 0.dp,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
key: ((page: Int) -> Any)? = null,
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
Pager(
|
||||
count = count,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
isVertical = false,
|
||||
reverseLayout = reverseLayout,
|
||||
itemSpacing = itemSpacing,
|
||||
verticalAlignment = verticalAlignment,
|
||||
key = key,
|
||||
contentPadding = contentPadding,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A vertically scrolling layout that allows users to flip between items to the top and bottom.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.VerticalPagerSample
|
||||
*
|
||||
* @param count the number of pages.
|
||||
* @param modifier the modifier to apply to this layout.
|
||||
* @param state the state object to be used to control or observe the pager's state.
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
||||
* composed from the bottom to the top and [PagerState.currentPage] == 0 will mean
|
||||
* the first item is located at the bottom.
|
||||
* @param itemSpacing vertical spacing to add between items.
|
||||
* @param key the scroll position will be maintained based on the key, which means if you
|
||||
* add/remove items before the current visible item the item with the given key will be kept as the
|
||||
* first visible one.
|
||||
* @param content a block which describes the content. Inside this block you can reference
|
||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
||||
*/
|
||||
@Composable
|
||||
fun VerticalPager(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState = rememberPagerState(),
|
||||
reverseLayout: Boolean = false,
|
||||
itemSpacing: Dp = 0.dp,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
key: ((page: Int) -> Any)? = null,
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
Pager(
|
||||
count = count,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
isVertical = true,
|
||||
reverseLayout = reverseLayout,
|
||||
itemSpacing = itemSpacing,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
key = key,
|
||||
contentPadding = contentPadding,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Pager(
|
||||
count: Int,
|
||||
modifier: Modifier,
|
||||
state: PagerState,
|
||||
reverseLayout: Boolean,
|
||||
itemSpacing: Dp,
|
||||
isVertical: Boolean,
|
||||
key: ((page: Int) -> Any)?,
|
||||
contentPadding: PaddingValues,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
require(count >= 0) { "pageCount must be >= 0" }
|
||||
|
||||
LaunchedEffect(count) {
|
||||
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
// Once a fling (scroll) has finished, notify the state
|
||||
LaunchedEffect(state) {
|
||||
// When a 'scroll' has finished, notify the state
|
||||
snapshotFlow { state.isScrollInProgress }
|
||||
.filter { !it }
|
||||
// initially isScrollInProgress is false as well and we want to start receiving
|
||||
// the events only after the real scroll happens.
|
||||
.drop(1)
|
||||
.collect { state.onScrollFinished() }
|
||||
}
|
||||
LaunchedEffect(state) {
|
||||
snapshotFlow { state.mostVisiblePageLayoutInfo?.index }
|
||||
.distinctUntilChanged()
|
||||
.collect { state.updateCurrentPageBasedOnLazyListState() }
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
LaunchedEffect(density, state, itemSpacing) {
|
||||
with(density) { state.itemSpacing = itemSpacing.roundToPx() }
|
||||
}
|
||||
|
||||
val pagerScope = remember(state) { PagerScopeImpl(state) }
|
||||
|
||||
// We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
|
||||
// as normal
|
||||
val consumeFlingNestedScrollConnection = remember(isVertical) {
|
||||
ConsumeFlingNestedScrollConnection(
|
||||
consumeHorizontal = !isVertical,
|
||||
consumeVertical = isVertical,
|
||||
pagerState = state,
|
||||
)
|
||||
}
|
||||
|
||||
if (isVertical) {
|
||||
LazyColumn(
|
||||
state = state.lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
reverseLayout = reverseLayout,
|
||||
contentPadding = contentPadding,
|
||||
userScrollEnabled = false,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
// We don't any nested flings to continue in the pager, so we add a
|
||||
// connection which consumes them.
|
||||
// See: https://github.com/google/accompanist/issues/347
|
||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
||||
// Constraint the content height to be <= than the height of the pager.
|
||||
.fillParentMaxHeight()
|
||||
.wrapContentSize()
|
||||
) {
|
||||
pagerScope.content(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
state = state.lazyListState,
|
||||
verticalAlignment = verticalAlignment,
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
|
||||
reverseLayout = reverseLayout,
|
||||
contentPadding = contentPadding,
|
||||
userScrollEnabled = false,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
// We don't any nested flings to continue in the pager, so we add a
|
||||
// connection which consumes them.
|
||||
// See: https://github.com/google/accompanist/issues/347
|
||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
||||
// Constraint the content width to be <= than the width of the pager.
|
||||
.fillParentMaxWidth()
|
||||
.wrapContentSize()
|
||||
) {
|
||||
pagerScope.content(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConsumeFlingNestedScrollConnection(
|
||||
private val consumeHorizontal: Boolean,
|
||||
private val consumeVertical: Boolean,
|
||||
private val pagerState: PagerState,
|
||||
) : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset = when (source) {
|
||||
// We can consume all resting fling scrolls so that they don't propagate up to the
|
||||
// Pager
|
||||
NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
return if (pagerState.currentPageOffset != 0f) {
|
||||
// The Pager is already scrolling. This means that a nested scroll child was
|
||||
// scrolled to end, and the Pager can use this fling
|
||||
Velocity.Zero
|
||||
} else {
|
||||
// A nested scroll child is still scrolling. We can consume all post fling
|
||||
// velocity on the main-axis so that it doesn't propagate up to the Pager
|
||||
available.consume(consumeHorizontal, consumeVertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.consume(
|
||||
consumeHorizontal: Boolean,
|
||||
consumeVertical: Boolean,
|
||||
): Offset = Offset(
|
||||
x = if (consumeHorizontal) this.x else 0f,
|
||||
y = if (consumeVertical) this.y else 0f,
|
||||
)
|
||||
|
||||
private fun Velocity.consume(
|
||||
consumeHorizontal: Boolean,
|
||||
consumeVertical: Boolean,
|
||||
): Velocity = Velocity(
|
||||
x = if (consumeHorizontal) this.x else 0f,
|
||||
y = if (consumeVertical) this.y else 0f,
|
||||
)
|
||||
|
||||
/**
|
||||
* Scope for [HorizontalPager] content.
|
||||
*/
|
||||
@Stable
|
||||
interface PagerScope {
|
||||
/**
|
||||
* Returns the current selected page
|
||||
*/
|
||||
val currentPage: Int
|
||||
|
||||
/**
|
||||
* The current offset from the start of [currentPage], as a ratio of the page width.
|
||||
*/
|
||||
val currentPageOffset: Float
|
||||
}
|
||||
|
||||
private class PagerScopeImpl(
|
||||
private val state: PagerState,
|
||||
) : PagerScope {
|
||||
override val currentPage: Int get() = state.currentPage
|
||||
override val currentPageOffset: Float get() = state.currentPageOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset for the given [page] from the current scroll position. This is useful
|
||||
* when using the scroll position to apply effects or animations to items.
|
||||
*
|
||||
* The returned offset can positive or negative, depending on whether which direction the [page] is
|
||||
* compared to the current scroll position.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
|
||||
*/
|
||||
fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
|
||||
return (currentPage - page) + currentPageOffset
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settingslib.spa.framework.compose
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.gestures.ScrollableState
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* *************************************************************************************************
|
||||
* This file was forked from
|
||||
* https://github.com/google/accompanist/blob/main/pager/src/main/java/com/google/accompanist/pager/PagerState.kt
|
||||
* and will be removed once it lands in AndroidX.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a [PagerState] that is remembered across compositions.
|
||||
*
|
||||
* Changes to the provided values for [initialPage] will **not** result in the state being
|
||||
* recreated or changed in any way if it has already
|
||||
* been created.
|
||||
*
|
||||
* @param initialPage the initial value for [PagerState.currentPage]
|
||||
*/
|
||||
@Composable
|
||||
fun rememberPagerState(
|
||||
@IntRange(from = 0) initialPage: Int = 0,
|
||||
): PagerState = rememberSaveable(saver = PagerState.Saver) {
|
||||
PagerState(
|
||||
currentPage = initialPage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
|
||||
*
|
||||
* In most cases, this will be created via [rememberPagerState].
|
||||
*
|
||||
* @param currentPage the initial value for [PagerState.currentPage]
|
||||
*/
|
||||
@Stable
|
||||
class PagerState(
|
||||
@IntRange(from = 0) currentPage: Int = 0,
|
||||
) : ScrollableState {
|
||||
// Should this be public?
|
||||
internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
|
||||
|
||||
private var _currentPage by mutableStateOf(currentPage)
|
||||
|
||||
// finds the page which has larger visible area within the viewport not including paddings
|
||||
internal val mostVisiblePageLayoutInfo: LazyListItemInfo?
|
||||
get() {
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
return layoutInfo.visibleItemsInfo.maxByOrNull {
|
||||
val start = maxOf(it.offset, 0)
|
||||
val end = minOf(
|
||||
it.offset + it.size,
|
||||
layoutInfo.viewportEndOffset - layoutInfo.afterContentPadding
|
||||
)
|
||||
end - start
|
||||
}
|
||||
}
|
||||
|
||||
internal var itemSpacing by mutableStateOf(0)
|
||||
|
||||
private val currentPageLayoutInfo: LazyListItemInfo?
|
||||
get() = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull {
|
||||
it.index == currentPage
|
||||
}
|
||||
|
||||
/**
|
||||
* [InteractionSource] that will be used to dispatch drag events when this
|
||||
* list is being dragged. If you want to know whether the fling (or animated scroll) is in
|
||||
* progress, use [isScrollInProgress].
|
||||
*/
|
||||
val interactionSource: InteractionSource
|
||||
get() = lazyListState.interactionSource
|
||||
|
||||
/**
|
||||
* The number of pages to display.
|
||||
*/
|
||||
@get:IntRange(from = 0)
|
||||
val pageCount: Int by derivedStateOf {
|
||||
lazyListState.layoutInfo.totalItemsCount
|
||||
}
|
||||
|
||||
/**
|
||||
* The index of the currently selected page. This may not be the page which is
|
||||
* currently displayed on screen.
|
||||
*
|
||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
||||
*/
|
||||
@get:IntRange(from = 0)
|
||||
var currentPage: Int
|
||||
get() = _currentPage
|
||||
internal set(value) {
|
||||
if (value != _currentPage) {
|
||||
_currentPage = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current offset from the start of [currentPage], as a ratio of the page width.
|
||||
*
|
||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
||||
*/
|
||||
val currentPageOffset: Float by derivedStateOf {
|
||||
currentPageLayoutInfo?.let {
|
||||
(-it.offset / (it.size + itemSpacing).toFloat()).coerceIn(-0.5f, 0.5f)
|
||||
} ?: 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* The target page for any on-going animations.
|
||||
*/
|
||||
private var animationTargetPage: Int? by mutableStateOf(null)
|
||||
|
||||
/**
|
||||
* Animate (smooth scroll) to the given page to the middle of the viewport.
|
||||
*
|
||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
||||
* complete.
|
||||
*
|
||||
* @param page the page to animate to. Must be >= 0.
|
||||
* @param pageOffset the percentage of the page size to offset, from the start of [page].
|
||||
* Must be in the range -1f..1f.
|
||||
*/
|
||||
suspend fun animateScrollToPage(
|
||||
@IntRange(from = 0) page: Int,
|
||||
@FloatRange(from = -1.0, to = 1.0) pageOffset: Float = 0f,
|
||||
) {
|
||||
requireCurrentPage(page, "page")
|
||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
||||
try {
|
||||
animationTargetPage = page
|
||||
|
||||
// pre-jump to nearby item for long jumps as an optimization
|
||||
// the same trick is done in ViewPager2
|
||||
val oldPage = lazyListState.firstVisibleItemIndex
|
||||
if (abs(page - oldPage) > 3) {
|
||||
lazyListState.scrollToItem(if (page > oldPage) page - 3 else page + 3)
|
||||
}
|
||||
|
||||
if (pageOffset.absoluteValue <= 0.005f) {
|
||||
// If the offset is (close to) zero, just call animateScrollToItem and we're done
|
||||
lazyListState.animateScrollToItem(index = page)
|
||||
} else {
|
||||
// Else we need to figure out what the offset is in pixels...
|
||||
lazyListState.scroll { } // this will await for the first layout.
|
||||
val layoutInfo = lazyListState.layoutInfo
|
||||
var target = layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.index == page }
|
||||
|
||||
if (target != null) {
|
||||
// If we have access to the target page layout, we can calculate the pixel
|
||||
// offset from the size
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = ((target.size + itemSpacing) * pageOffset).roundToInt()
|
||||
)
|
||||
} else if (layoutInfo.visibleItemsInfo.isNotEmpty()) {
|
||||
// If we don't, we use the current page size as a guide
|
||||
val currentSize = layoutInfo.visibleItemsInfo.first().size + itemSpacing
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = (currentSize * pageOffset).roundToInt()
|
||||
)
|
||||
|
||||
// The target should be visible now
|
||||
target = layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
|
||||
|
||||
if (target != null && target.size + itemSpacing != currentSize) {
|
||||
// If the size we used for calculating the offset differs from the actual
|
||||
// target page size, we need to scroll again. This doesn't look great,
|
||||
// but there's not much else we can do.
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = ((target.size + itemSpacing) * pageOffset).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// We need to manually call this, as the `animateScrollToItem` call above will happen
|
||||
// in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
|
||||
// the change. This is especially true when running unit tests.
|
||||
onScrollFinished()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly brings the item at [page] to the middle of the viewport.
|
||||
*
|
||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
||||
* complete.
|
||||
*
|
||||
* @param page the page to snap to. Must be >= 0.
|
||||
* @param pageOffset the percentage of the page size to offset, from the start of [page].
|
||||
* Must be in the range -1f..1f.
|
||||
*/
|
||||
suspend fun scrollToPage(
|
||||
@IntRange(from = 0) page: Int,
|
||||
@FloatRange(from = -1.0, to = 1.0) pageOffset: Float = 0f,
|
||||
) {
|
||||
requireCurrentPage(page, "page")
|
||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
||||
try {
|
||||
animationTargetPage = page
|
||||
|
||||
// First scroll to the given page. It will now be laid out at offset 0
|
||||
lazyListState.scrollToItem(index = page)
|
||||
updateCurrentPageBasedOnLazyListState()
|
||||
|
||||
// If we have a start spacing, we need to offset (scroll) by that too
|
||||
if (pageOffset.absoluteValue > 0.0001f) {
|
||||
currentPageLayoutInfo?.let {
|
||||
scroll {
|
||||
scrollBy((it.size + itemSpacing) * pageOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// We need to manually call this, as the `scroll` call above will happen in 1 frame,
|
||||
// which is usually too fast for the LaunchedEffect in Pager to detect the change.
|
||||
// This is especially true when running unit tests.
|
||||
onScrollFinished()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateCurrentPageBasedOnLazyListState() {
|
||||
// Then update the current page to our layout page
|
||||
mostVisiblePageLayoutInfo?.let {
|
||||
currentPage = it.index
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onScrollFinished() {
|
||||
// Clear the animation target page
|
||||
animationTargetPage = null
|
||||
}
|
||||
|
||||
override suspend fun scroll(
|
||||
scrollPriority: MutatePriority,
|
||||
block: suspend ScrollScope.() -> Unit
|
||||
) = lazyListState.scroll(scrollPriority, block)
|
||||
|
||||
override fun dispatchRawDelta(delta: Float): Float {
|
||||
return lazyListState.dispatchRawDelta(delta)
|
||||
}
|
||||
|
||||
override val isScrollInProgress: Boolean
|
||||
get() = lazyListState.isScrollInProgress
|
||||
|
||||
override fun toString(): String = "PagerState(" +
|
||||
"pageCount=$pageCount, " +
|
||||
"currentPage=$currentPage, " +
|
||||
"currentPageOffset=$currentPageOffset" +
|
||||
")"
|
||||
|
||||
private fun requireCurrentPage(value: Int, name: String) {
|
||||
require(value >= 0) { "$name[$value] must be >= 0" }
|
||||
}
|
||||
|
||||
private fun requireCurrentPageOffset(value: Float, name: String) {
|
||||
require(value in -1f..1f) { "$name must be >= -1 and <= 1" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default [Saver] implementation for [PagerState].
|
||||
*/
|
||||
val Saver: Saver<PagerState, *> = listSaver(
|
||||
save = {
|
||||
listOf<Any>(
|
||||
it.currentPage,
|
||||
)
|
||||
},
|
||||
restore = {
|
||||
PagerState(
|
||||
currentPage = it[0] as Int,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,21 @@
|
||||
|
||||
package com.android.settingslib.spa.widget.scaffold
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.android.settingslib.spa.framework.compose.HorizontalPager
|
||||
import com.android.settingslib.spa.framework.compose.rememberPagerState
|
||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit) {
|
||||
check(titles.isNotEmpty())
|
||||
@@ -52,7 +54,7 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit
|
||||
SettingsTab(
|
||||
title = title,
|
||||
selected = pagerState.currentPage == page,
|
||||
currentPageOffset = pagerState.currentPageOffset.absoluteValue,
|
||||
currentPageOffset = pagerState.currentPageOffsetFraction.absoluteValue,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(page)
|
||||
@@ -62,7 +64,7 @@ fun SettingsPager(titles: List<String>, content: @Composable (page: Int) -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalPager(count = titles.size, state = pagerState) { page ->
|
||||
HorizontalPager(pageCount = titles.size, state = pagerState) { page ->
|
||||
content(page)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user