| /* |
| * Copyright 2020 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 androidx.compose.foundation.gestures |
| |
| import androidx.animation.AnimatedFloat |
| import androidx.animation.AnimationClockObservable |
| import androidx.animation.AnimationClockObserver |
| import androidx.animation.AnimationEndReason |
| import androidx.animation.AnimationSpec |
| import androidx.animation.Spring |
| import androidx.animation.SpringSpec |
| import androidx.compose.Composable |
| import androidx.compose.mutableStateOf |
| import androidx.compose.onDispose |
| import androidx.compose.remember |
| import androidx.ui.animation.asDisposableClock |
| import androidx.ui.core.AnimationClockAmbient |
| import androidx.ui.core.Direction |
| import androidx.ui.core.Modifier |
| import androidx.ui.core.composed |
| import androidx.ui.core.gesture.ScrollCallback |
| import androidx.ui.core.gesture.scrollGestureFilter |
| import androidx.ui.core.gesture.scrollorientationlocking.Orientation |
| import androidx.compose.foundation.animation.FlingConfig |
| import androidx.compose.foundation.animation.defaultFlingConfig |
| import androidx.compose.foundation.animation.fling |
| import androidx.ui.geometry.Offset |
| |
| /** |
| * Create and remember [ScrollableController] for [scrollable] with default [FlingConfig] and |
| * [AnimationClockObservable] |
| * |
| * @param consumeScrollDelta callback invoked when scrollable drag/fling/smooth scrolling occurs. |
| * The callback receives the delta in pixels. Callers should update their state in this lambda |
| * and return amount of delta consumed |
| */ |
| @Composable |
| fun rememberScrollableController( |
| consumeScrollDelta: (Float) -> Float |
| ): ScrollableController { |
| val clocks = AnimationClockAmbient.current.asDisposableClock() |
| val flingConfig = defaultFlingConfig() |
| return remember(clocks, flingConfig) { |
| ScrollableController(consumeScrollDelta, flingConfig, clocks) |
| } |
| } |
| |
| /** |
| * Controller to control the [scrollable] modifier with. Contains necessary information about the |
| * ongoing fling and provides smooth scrolling capabilities. |
| * |
| * @param consumeScrollDelta callback invoked when drag/fling/smooth scrolling occurs. The |
| * callback receives the delta in pixels. Callers should update their state in this lambda and |
| * return the amount of delta consumed |
| * @param flingConfig fling configuration to use for flinging |
| * @param animationClock animation clock to run flinging and smooth scrolling on |
| */ |
| class ScrollableController( |
| val consumeScrollDelta: (Float) -> Float, |
| val flingConfig: FlingConfig, |
| animationClock: AnimationClockObservable |
| ) { |
| /** |
| * Smooth scroll by [value] amount of pixels |
| * |
| * @param value delta to scroll by |
| * @param spec [AnimationSpec] to be used for this smooth scrolling |
| * @param onEnd lambda to be called when smooth scrolling has ended |
| */ |
| fun smoothScrollBy( |
| value: Float, |
| spec: AnimationSpec<Float> = SpringSpec(), |
| onEnd: (endReason: AnimationEndReason, finishValue: Float) -> Unit = { _, _ -> } |
| ) { |
| val to = animatedFloat.value + value |
| animatedFloat.animateTo(to, anim = spec, onEnd = onEnd) |
| } |
| |
| private val isAnimationRunningState = mutableStateOf(false) |
| |
| private val clocksProxy: AnimationClockObservable = object : AnimationClockObservable { |
| override fun subscribe(observer: AnimationClockObserver) { |
| isAnimationRunningState.value = true |
| animationClock.subscribe(observer) |
| } |
| |
| override fun unsubscribe(observer: AnimationClockObserver) { |
| isAnimationRunningState.value = false |
| animationClock.unsubscribe(observer) |
| } |
| } |
| |
| /** |
| * whether this [ScrollableController] is currently animating/flinging |
| */ |
| val isAnimationRunning |
| get() = isAnimationRunningState.value |
| |
| /** |
| * Stop any ongoing animation, smooth scrolling or fling |
| * |
| * Call this to stop receiving scrollable deltas in [consumeScrollDelta] |
| */ |
| fun stopAnimation() { |
| animatedFloat.stop() |
| } |
| |
| private val animatedFloat = |
| DeltaAnimatedFloat(0f, clocksProxy, consumeScrollDelta) |
| |
| /** |
| * current position for scrollable |
| */ |
| internal var value: Float |
| get() = animatedFloat.value |
| set(value) = animatedFloat.snapTo(value) |
| |
| internal fun fling(velocity: Float, onScrollEnd: (Float) -> Unit) { |
| animatedFloat.fling( |
| config = flingConfig, |
| startVelocity = velocity, |
| onAnimationEnd = { _, _, velocityLeft -> |
| onScrollEnd(velocityLeft) |
| }) |
| } |
| } |
| |
| /** |
| * Configure touch scrolling and flinging for the UI element in a single [Orientation]. |
| * |
| * Users should update their state via [ScrollableController.consumeScrollDelta] and reflect |
| * their own state in UI when using this component. |
| * |
| * [ScrollableController] is required for this modifier to work correctly. When constructing |
| * [ScrollableController], you must provide a [ScrollableController.consumeScrollDelta] lambda, |
| * which will be invoked whenever scroll happens (by gesture input, by smooth scrolling or |
| * flinging) with the delta in pixels. The amount of scrolling delta consumed must be returned |
| * from this lambda to ensure proper nested scrolling. |
| * |
| * @sample androidx.compose.foundation.samples.ScrollableSample |
| * |
| * @param orientation orientation of the scrolling |
| * @param controller [ScrollableController] object that is responsible for redirecting scroll |
| * deltas to [ScrollableController.consumeScrollDelta] callback and provides smooth scrolling |
| * capabilities |
| * @param enabled whether or not scrolling in enabled |
| * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will |
| * behave like bottom to top and left to right will behave like right to left. |
| * @param canScroll callback to indicate whether or not scroll is allowed for given [Direction] |
| * @param onScrollStarted callback to be invoked when scroll has started from the certain |
| * position on the screen |
| * @param onScrollStopped callback to be invoked when scroll stops with amount of velocity |
| * unconsumed provided |
| */ |
| fun Modifier.scrollable( |
| orientation: Orientation, |
| controller: ScrollableController, |
| enabled: Boolean = true, |
| reverseDirection: Boolean = false, |
| canScroll: (Direction) -> Boolean = { enabled }, |
| onScrollStarted: (startedPosition: Offset) -> Unit = {}, |
| onScrollStopped: (velocity: Float) -> Unit = {} |
| ): Modifier = composed { |
| onDispose { |
| controller.stopAnimation() |
| } |
| |
| scrollGestureFilter( |
| scrollCallback = object : ScrollCallback { |
| |
| override fun onStart(downPosition: Offset) { |
| if (enabled) { |
| controller.stopAnimation() |
| onScrollStarted(downPosition) |
| } |
| } |
| |
| override fun onScroll(scrollDistance: Float): Float { |
| if (!enabled) return 0f |
| controller.stopAnimation() |
| val toConsume = if (reverseDirection) scrollDistance * -1 else scrollDistance |
| val consumed = controller.consumeScrollDelta(toConsume) |
| controller.value = controller.value + consumed |
| return if (reverseDirection) consumed * -1 else consumed |
| } |
| |
| override fun onCancel() { |
| if (enabled) onScrollStopped(0f) |
| } |
| |
| override fun onStop(velocity: Float) { |
| if (enabled) { |
| controller.fling( |
| velocity = if (reverseDirection) velocity * -1 else velocity, |
| onScrollEnd = onScrollStopped |
| ) |
| } |
| } |
| }, |
| orientation = orientation, |
| canDrag = canScroll, |
| startDragImmediately = controller.isAnimationRunning |
| ) |
| } |
| |
| private class DeltaAnimatedFloat( |
| initial: Float, |
| clock: AnimationClockObservable, |
| private val onDelta: (Float) -> Float |
| ) : AnimatedFloat(clock, Spring.DefaultDisplacementThreshold) { |
| |
| override var value = initial |
| set(value) { |
| if (isRunning) { |
| val delta = value - field |
| onDelta(delta) |
| } |
| field = value |
| } |
| } |