Skip to content

Commit

Permalink
Try out using Modifier.scrollable
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbanes committed Mar 14, 2021
1 parent b99a197 commit 282181a
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 50 deletions.
5 changes: 4 additions & 1 deletion pager/api/pager.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (IIF)V
public synthetic fun <init> (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
Expand Down
35 changes: 8 additions & 27 deletions pager/src/main/java/com/google/accompanist/pager/Pager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,28 @@ 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
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
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

/**
Expand Down Expand Up @@ -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<Float>(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)
Expand Down
79 changes: 57 additions & 22 deletions pager/src/main/java/com/google/accompanist/pager/PagerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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),
Expand All @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Float>,
) = draggableState.drag {
animationSpec: DecayAnimationSpec<Float> = exponentialDecay(),
): Float {
// We calculate the target offset using pixels, rather than using the offset
val targetOffset = animationSpec.calculateTargetValue(
initialValue = currentPageOffset * pageSize,
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -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(
Expand All @@ -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(" +
Expand Down

0 comments on commit 282181a

Please sign in to comment.