From 282181adec0c4d613ae30bf660b2b0c133bab1e5 Mon Sep 17 00:00:00 2001 From: Chris Banes Date: Thu, 11 Mar 2021 19:13:43 +0000 Subject: [PATCH] Try out using Modifier.scrollable --- pager/api/pager.api | 5 +- .../com/google/accompanist/pager/Pager.kt | 35 ++------ .../google/accompanist/pager/PagerState.kt | 79 +++++++++++++------ 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/pager/api/pager.api b/pager/api/pager.api index eab331935..56c09ec93 100644 --- a/pager/api/pager.api +++ b/pager/api/pager.api @@ -15,15 +15,18 @@ public final class com/google/accompanist/pager/PagerScope$DefaultImpls { public static fun matchParentSize (Lcom/google/accompanist/pager/PagerScope;Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; } -public final class com/google/accompanist/pager/PagerState { +public final class com/google/accompanist/pager/PagerState : androidx/compose/foundation/gestures/ScrollableState { public static final field Companion Lcom/google/accompanist/pager/PagerState$Companion; public fun (IIF)V public synthetic fun (IIFILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun animateScrollToPage (IFFLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun animateScrollToPage$default (Lcom/google/accompanist/pager/PagerState;IFFLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun dispatchRawDelta (F)F public final fun getCurrentPage ()I public final fun getCurrentPageOffset ()F public final fun getPageCount ()I + public fun isScrollInProgress ()Z + public fun scroll (Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun scrollToPage (IFLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun scrollToPage$default (Lcom/google/accompanist/pager/PagerState;IFLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun setPageCount (I)V diff --git a/pager/src/main/java/com/google/accompanist/pager/Pager.kt b/pager/src/main/java/com/google/accompanist/pager/Pager.kt index e1ca34c38..a46bd02c8 100644 --- a/pager/src/main/java/com/google/accompanist/pager/Pager.kt +++ b/pager/src/main/java/com/google/accompanist/pager/Pager.kt @@ -20,9 +20,8 @@ package com.google.accompanist.pager import android.util.Log import androidx.annotation.IntRange -import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable @@ -30,14 +29,12 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.key import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.ParentDataModifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.ScrollAxisRange import androidx.compose.ui.semantics.horizontalScrollAxisRange import androidx.compose.ui.semantics.scrollBy @@ -45,8 +42,6 @@ import androidx.compose.ui.semantics.selectableGroup import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection -import kotlinx.coroutines.launch import kotlin.math.roundToInt /** @@ -102,46 +97,32 @@ fun HorizontalPager( ) { require(offscreenLimit >= 1) { "offscreenLimit is required to be >= 1" } - val reverseScroll = LocalLayoutDirection.current == LayoutDirection.Rtl - - val density = LocalDensity.current - val decay = remember(density) { splineBasedDecay(density) } - - val coroutineScope = rememberCoroutineScope() - val semantics = Modifier.semantics { if (state.pageCount > 0) { horizontalScrollAxisRange = ScrollAxisRange( value = { (state.currentPage + state.currentPageOffset) * state.pageSize }, maxValue = { state.lastPageIndex.toFloat() * state.pageSize }, - reverseScrolling = reverseScroll ) // Hook up scroll actions to our state scrollBy { x: Float, _ -> - coroutineScope.launch { - state.draggableState.drag { dragBy(x) } - } - true + state.dispatchRawDelta(x) != 0f } // Treat this as a selectable group selectableGroup() } } - val draggable = Modifier.draggable( - state = state.draggableState, - startDragImmediately = true, - onDragStopped = { velocity -> - launch { state.performFling(velocity, decay) } - }, + val scrollable = Modifier.scrollable( orientation = Orientation.Horizontal, - reverseDirection = reverseScroll, + flingBehavior = state.flingBehavior, + state = state ) Layout( modifier = modifier .then(semantics) - .then(draggable), + .then(scrollable) + .clipToBounds(), content = { val firstPage = (state.currentPage - offscreenLimit).coerceAtLeast(0) val lastPage = (state.currentPage + offscreenLimit).coerceAtMost(state.lastPageIndex) diff --git a/pager/src/main/java/com/google/accompanist/pager/PagerState.kt b/pager/src/main/java/com/google/accompanist/pager/PagerState.kt index 8a4c5babe..2870ec8bc 100644 --- a/pager/src/main/java/com/google/accompanist/pager/PagerState.kt +++ b/pager/src/main/java/com/google/accompanist/pager/PagerState.kt @@ -27,8 +27,12 @@ import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDecay import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.core.exponentialDecay import androidx.compose.animation.core.spring -import androidx.compose.foundation.gestures.DraggableState +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -85,12 +89,18 @@ class PagerState( @IntRange(from = 0) pageCount: Int, @IntRange(from = 0) currentPage: Int = 0, @FloatRange(from = 0.0, to = 1.0) currentPageOffset: Float = 0f, -) { +) : ScrollableState { private var _pageCount by mutableStateOf(pageCount) private var _currentPage by mutableStateOf(currentPage) private val _currentPageOffset = mutableStateOf(currentPageOffset) internal var pageSize by mutableStateOf(0) + /** + * The ScrollableController instance. We keep it as we need to call stopAnimation on it once + * we reached the end of the list. + */ + private val scrollableState = ScrollableState { onScroll(it) } + init { require(pageCount >= 0) { "pageCount must be >= 0" } requireCurrentPage(currentPage, "currentPage") @@ -160,9 +170,9 @@ class PagerState( if (page == currentPage) return - // We don't specifically use the DragScope's dragBy, but + // We don't specifically use the ScrollScope's scrollBy, but // we do want to use it's mutex - draggableState.drag { + scroll { animateToPage( page = page.coerceIn(0, lastPageIndex), pageOffset = pageOffset.coerceIn(0f, 1f), @@ -189,9 +199,9 @@ class PagerState( requireCurrentPage(page, "page") requireCurrentPageOffset(pageOffset, "pageOffset") - // We don't specifically use the DragScope's dragBy, but + // We don't specifically use the ScrollScope's scrollBy(), but // we do want to use it's mutex - draggableState.drag { + scroll { currentPage = page currentPageOffset = pageOffset } @@ -236,8 +246,10 @@ class PagerState( else -> 0f } - internal val draggableState = DraggableState { delta -> + private fun onScroll(delta: Float): Float { dragByOffset(delta / pageSize.coerceAtLeast(1)) + // FIXME: should consume properly + return delta } private fun dragByOffset(deltaOffset: Float) { @@ -277,13 +289,16 @@ class PagerState( } } - /** - * TODO make this public? - */ - internal suspend fun performFling( + internal val flingBehavior: FlingBehavior = object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + return fling(initialVelocity) + } + } + + private suspend fun ScrollScope.fling( initialVelocity: Float, - animationSpec: DecayAnimationSpec, - ) = draggableState.drag { + animationSpec: DecayAnimationSpec = exponentialDecay(), + ): Float { // We calculate the target offset using pixels, rather than using the offset val targetOffset = animationSpec.calculateTargetValue( initialValue = currentPageOffset * pageSize, @@ -304,16 +319,15 @@ class PagerState( // Animate with the decay animation spec using the fling velocity val targetPage = when { - targetOffset > 0 -> { - (currentPage + 1).coerceAtMost(lastPageIndex) - } + targetOffset > 0 -> (currentPage + 1).coerceAtMost(lastPageIndex) else -> currentPage } - AnimationState( + val state = AnimationState( initialValue = currentPageOffset * pageSize, initialVelocity = initialVelocity * -1 - ).animateDecay(animationSpec) { + ) + state.animateDecay(animationSpec) { if (DebugLog) { Log.d( LogTag, @@ -323,7 +337,8 @@ class PagerState( } val coerced = value.coerceIn(0f, pageSize.toFloat()) - dragBy((currentPageOffset * pageSize) - coerced) + + scrollBy((currentPageOffset * pageSize) - coerced) val pastLeftBound = initialVelocity > 0 && (currentPage < targetPage || (currentPage == targetPage && currentPageOffset == 0f)) @@ -339,8 +354,12 @@ class PagerState( currentPageOffset = 0f } } + snapToNearestPage() + // TODO: work out why we need to invert velocity + return state.velocity * -1 } else { // Otherwise we animate to the next item, or spring-back depending on the offset + var finalVelocity: Float = initialVelocity animate( initialValue = currentPageOffset * pageSize, targetValue = pageSize * determineSpringBackOffset( @@ -349,12 +368,28 @@ class PagerState( ), initialVelocity = initialVelocity, animationSpec = spring() - ) { value, _ -> - dragBy((currentPageOffset * pageSize) - value) + ) { value, velocity -> + scrollBy((currentPageOffset * pageSize) - value) + finalVelocity = velocity } + snapToNearestPage() + // TODO: work out why we need to invert velocity + return finalVelocity * -1 } + } - snapToNearestPage() + override val isScrollInProgress: Boolean + get() = scrollableState.isScrollInProgress + + override fun dispatchRawDelta(delta: Float): Float { + return scrollableState.dispatchRawDelta(delta) + } + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit + ) { + scrollableState.scroll(scrollPriority, block) } override fun toString(): String = "PagerState(" +