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 15, 2021
1 parent 27f9312 commit eeb7513
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 59 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
34 changes: 10 additions & 24 deletions pager/src/main/java/com/google/accompanist/pager/Pager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,21 @@ 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
Expand All @@ -46,7 +44,6 @@ 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 +99,35 @@ 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 reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl

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,
reverseDirection = reverseDirection,
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
102 changes: 68 additions & 34 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,21 @@ 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)

private val globalPosition: Float
get() = currentPage + currentPageOffset

/**
* 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)

init {
require(pageCount >= 0) { "pageCount must be >= 0" }
requireCurrentPage(currentPage, "currentPage")
Expand Down Expand Up @@ -160,9 +173,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 +202,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 All @@ -212,7 +225,7 @@ class PagerState(
initialVelocity: Float = 0f,
) {
animate(
initialValue = currentPage + currentPageOffset,
initialValue = globalPosition,
targetValue = page + pageOffset,
initialVelocity = initialVelocity,
animationSpec = animationSpec
Expand All @@ -224,20 +237,21 @@ class PagerState(
}

private fun determineSpringBackOffset(
velocity: Float,
offset: Float = currentPageOffset,
): Float = when {
// If the offset exceeds the scroll threshold (in either direction), we want to
// move to the next/previous item
offset < ScrollThreshold -> 0f
offset > 1 - ScrollThreshold -> 1f
// Otherwise we look at the velocity for scroll direction
velocity < 0 -> 1f
// Otherwise we go back to 0f
else -> 0f
}

internal val draggableState = DraggableState { delta ->
private fun onScroll(delta: Float): Float {
val current = globalPosition
dragByOffset(delta / pageSize.coerceAtLeast(1))
val new = globalPosition
return (new - current) * pageSize
}

private fun dragByOffset(deltaOffset: Float) {
Expand Down Expand Up @@ -277,17 +291,21 @@ 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, scrollBy = ::scrollBy)
}
}

internal suspend fun fling(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,
) = draggableState.drag {
animationSpec: DecayAnimationSpec<Float> = exponentialDecay(),
scrollBy: (Float) -> Float,
): Float {
// We calculate the target offset using pixels, rather than using the offset
val targetOffset = animationSpec.calculateTargetValue(
initialValue = currentPageOffset * pageSize,
initialVelocity = initialVelocity * -1
initialVelocity = initialVelocity
) / pageSize

if (DebugLog) {
Expand All @@ -298,21 +316,21 @@ class PagerState(
)
}

var lastVelocity: Float = initialVelocity

// If the animation can naturally end outside of current page bounds, we will
// animate with decay.
if (targetOffset.absoluteValue >= 1) {
// 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(
initialValue = currentPageOffset * pageSize,
initialVelocity = initialVelocity * -1
initialVelocity = initialVelocity
).animateDecay(animationSpec) {
if (DebugLog) {
Log.d(
Expand All @@ -322,39 +340,55 @@ class PagerState(
)
}

// Keep track of velocity
lastVelocity = velocity

val coerced = value.coerceIn(0f, pageSize.toFloat())
dragBy((currentPageOffset * pageSize) - coerced)
val unconsumed = scrollBy(coerced - (currentPageOffset * pageSize))

val pastLeftBound = initialVelocity > 0 &&
val pastLeftBound = initialVelocity < 0 &&
(currentPage < targetPage || (currentPage == targetPage && currentPageOffset == 0f))

val pastRightBound = initialVelocity < 0 &&
val pastRightBound = initialVelocity > 0 &&
(currentPage > targetPage || (currentPage == targetPage && currentPageOffset > 0f))

if (pastLeftBound || pastRightBound) {
if (unconsumed > 0.5f || pastLeftBound || pastRightBound) {
// If we reach the bounds of the allowed offset, cancel the animation
cancelAnimation()

currentPage = targetPage
currentPageOffset = 0f
}
}
} else {
// Otherwise we animate to the next item, or spring-back depending on the offset
animate(
initialValue = currentPageOffset * pageSize,
targetValue = pageSize * determineSpringBackOffset(
velocity = initialVelocity * -1,
offset = targetOffset
),
initialValue = globalPosition * pageSize,
targetValue = (currentPage + determineSpringBackOffset(targetOffset)) * pageSize,
initialVelocity = initialVelocity,
animationSpec = spring()
) { value, _ ->
dragBy((currentPageOffset * pageSize) - value)
) { value, velocity ->
scrollBy(value - (globalPosition * pageSize))
// Keep track of velocity
lastVelocity = velocity
}
}

snapToNearestPage()
return lastVelocity
}

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 eeb7513

Please sign in to comment.