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 11, 2021
1 parent 5a42d25 commit c2b73fd
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 113 deletions.
15 changes: 4 additions & 11 deletions pager/api/pager.api
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,26 @@ public final class dev/chrisbanes/accompanist/pager/Pager {
public abstract interface class dev/chrisbanes/accompanist/pager/PagerScope : androidx/compose/foundation/layout/BoxScope {
public abstract fun getCurrentPage ()I
public abstract fun getCurrentPageOffset ()F
public abstract fun getSelectionState ()Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
}

public final class dev/chrisbanes/accompanist/pager/PagerScope$DefaultImpls {
public static fun align (Ldev/chrisbanes/accompanist/pager/PagerScope;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Alignment;)Landroidx/compose/ui/Modifier;
public static fun matchParentSize (Ldev/chrisbanes/accompanist/pager/PagerScope;Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
}

public final class dev/chrisbanes/accompanist/pager/PagerState {
public final class dev/chrisbanes/accompanist/pager/PagerState : androidx/compose/foundation/gestures/ScrollableState {
public static final field $stable I
public static final field Companion Ldev/chrisbanes/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 (Ldev/chrisbanes/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 final fun getSelectionState ()Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
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 (Ldev/chrisbanes/accompanist/pager/PagerState;IFLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun setPageCount (I)V
Expand All @@ -37,14 +38,6 @@ public final class dev/chrisbanes/accompanist/pager/PagerState$Companion {
public final fun getSaver ()Landroidx/compose/runtime/saveable/Saver;
}

public final class dev/chrisbanes/accompanist/pager/PagerState$SelectionState : java/lang/Enum {
public static final field Dragging Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
public static final field Idle Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
public static final field Settling Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
public static fun valueOf (Ljava/lang/String;)Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
public static fun values ()[Ldev/chrisbanes/accompanist/pager/PagerState$SelectionState;
}

public final class dev/chrisbanes/accompanist/pager/PagerStateKt {
public static final fun rememberPagerState (IIFLandroidx/compose/runtime/Composer;II)Ldev/chrisbanes/accompanist/pager/PagerState;
}
Expand Down
63 changes: 6 additions & 57 deletions pager/src/main/java/dev/chrisbanes/accompanist/pager/Pager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,24 @@ package dev.chrisbanes.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.foundation.selection.selectableGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
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.composed
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.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,48 +93,14 @@ fun Pager(
) {
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 semantics = Modifier.composed {
val coroutineScope = rememberCoroutineScope()
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
}
// Treat this as a selectable group
selectableGroup()
}
}
}

val draggable = Modifier.draggable(
state = state.draggableState,
startDragImmediately = true,
onDragStarted = {
state.selectionState = PagerState.SelectionState.Dragging
},
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),
modifier = modifier.then(scrollable).clipToBounds(),
content = {
val firstPage = (state.currentPage - offscreenLimit).coerceAtLeast(0)
val lastPage = (state.currentPage + offscreenLimit).coerceAtMost(state.lastPageIndex)
Expand Down Expand Up @@ -220,11 +177,6 @@ interface PagerScope : BoxScope {
* Returns the current selected page offset
*/
val currentPageOffset: Float

/**
* Returns the current selection state
*/
val selectionState: PagerState.SelectionState
}

@ExperimentalPagerApi
Expand All @@ -237,7 +189,4 @@ private class PagerScopeImpl(

override val currentPageOffset: Float
get() = state.currentPageOffset

override val selectionState: PagerState.SelectionState
get() = state.selectionState
}
90 changes: 45 additions & 45 deletions pager/src/main/java/dev/chrisbanes/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.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -80,12 +84,18 @@ class PagerState(
pageCount: Int,
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) }

/**
* The number of pages to display.
*/
Expand Down Expand Up @@ -127,34 +137,6 @@ class PagerState(
)
}

/**
* Represents the current selection state of a [Pager].
* Usually read from [PagerState.selectionState].
*/
enum class SelectionState {
/**
* Indicates that the pager is in an idle, settled state. The current page
* is fully in view and no animation is in progress.
*/
Idle,

/**
* Indicates that the pager is currently being dragged by the user.
*/
Dragging,

/**
* Indicates that the pager is in the process of settling to a final position.
*/
Settling
}

/**
* The current selection state.
*/
var selectionState by mutableStateOf(SelectionState.Idle)
internal set

/**
* Animate (smooth scroll) to the given page.
*
Expand All @@ -174,14 +156,12 @@ class PagerState(

// We don't specifically use the DragScope's dragBy, but
// we do want to use it's mutex
draggableState.drag {
selectionState = SelectionState.Settling
scroll {
animateToPage(
page = page.coerceIn(0, lastPageIndex),
pageOffset = pageOffset.coerceIn(0f, 1f),
initialVelocity = initialVelocity,
)
selectionState = SelectionState.Idle
}
}

Expand All @@ -201,10 +181,9 @@ class PagerState(
) {
// We don't specifically use the DragScope's dragBy, but
// we do want to use it's mutex
draggableState.drag {
scroll {
currentPage = page
currentPageOffset = pageOffset
selectionState = SelectionState.Idle
}
}

Expand All @@ -214,7 +193,6 @@ class PagerState(
}
currentPage += currentPageOffset.roundToInt()
currentPageOffset = 0f
selectionState = SelectionState.Idle
}

private suspend fun animateToPage(
Expand Down Expand Up @@ -248,8 +226,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 @@ -289,15 +269,21 @@ class PagerState(
}
}

internal val flingBehavior: FlingBehavior = object : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
performFling2(initialVelocity)
// FIXME: calculate proper remaining velocity
return initialVelocity
}
}

/**
* TODO make this public?
*/
internal suspend fun performFling(
internal suspend fun ScrollScope.performFling2(
initialVelocity: Float,
animationSpec: DecayAnimationSpec<Float>,
) = draggableState.drag {
selectionState = SelectionState.Settling

animationSpec: DecayAnimationSpec<Float> = exponentialDecay(),
) {
// We calculate the target offset using pixels, rather than using the offset
val targetOffset = animationSpec.calculateTargetValue(
initialValue = currentPageOffset * pageSize,
Expand Down Expand Up @@ -337,7 +323,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 @@ -364,17 +351,30 @@ class PagerState(
initialVelocity = initialVelocity,
animationSpec = spring()
) { value, _ ->
dragBy((currentPageOffset * pageSize) - value)
scrollBy((currentPageOffset * pageSize) - value)
}
}

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(" +
"pageCount=$pageCount, " +
"currentPage=$currentPage, " +
"selectionState=$selectionState, " +
"currentPageOffset=$currentPageOffset" +
")"

Expand Down

0 comments on commit c2b73fd

Please sign in to comment.