1 /* <lambda>null2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.foundation.gestures 18 19 import androidx.compose.animation.core.AnimationState 20 import androidx.compose.animation.core.DecayAnimationSpec 21 import androidx.compose.animation.core.animate 22 import androidx.compose.animation.core.animateDecay 23 import androidx.compose.animation.rememberSplineBasedDecay 24 import androidx.compose.animation.splineBasedDecay 25 import androidx.compose.foundation.ComposeFoundationFlags.isOnScrollChangedCallbackEnabled 26 import androidx.compose.foundation.ExperimentalFoundationApi 27 import androidx.compose.foundation.FocusedBoundsObserverNode 28 import androidx.compose.foundation.LocalOverscrollFactory 29 import androidx.compose.foundation.MutatePriority 30 import androidx.compose.foundation.OverscrollEffect 31 import androidx.compose.foundation.gestures.Orientation.Horizontal 32 import androidx.compose.foundation.gestures.Orientation.Vertical 33 import androidx.compose.foundation.interaction.MutableInteractionSource 34 import androidx.compose.foundation.internal.PlatformOptimizedCancellationException 35 import androidx.compose.foundation.relocation.BringIntoViewResponderNode 36 import androidx.compose.foundation.rememberOverscrollEffect 37 import androidx.compose.foundation.rememberPlatformOverscrollEffect 38 import androidx.compose.runtime.Composable 39 import androidx.compose.runtime.Stable 40 import androidx.compose.runtime.remember 41 import androidx.compose.ui.Modifier 42 import androidx.compose.ui.MotionDurationScale 43 import androidx.compose.ui.focus.FocusTargetModifierNode 44 import androidx.compose.ui.focus.Focusability 45 import androidx.compose.ui.geometry.Offset 46 import androidx.compose.ui.input.key.Key 47 import androidx.compose.ui.input.key.KeyEvent 48 import androidx.compose.ui.input.key.KeyEventType 49 import androidx.compose.ui.input.key.KeyInputModifierNode 50 import androidx.compose.ui.input.key.isCtrlPressed 51 import androidx.compose.ui.input.key.key 52 import androidx.compose.ui.input.key.type 53 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection 54 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher 55 import androidx.compose.ui.input.nestedscroll.NestedScrollSource 56 import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.SideEffect 57 import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput 58 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode 59 import androidx.compose.ui.input.pointer.PointerEvent 60 import androidx.compose.ui.input.pointer.PointerEventPass 61 import androidx.compose.ui.input.pointer.PointerEventType 62 import androidx.compose.ui.input.pointer.PointerInputChange 63 import androidx.compose.ui.input.pointer.PointerType 64 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode 65 import androidx.compose.ui.node.DelegatableNode 66 import androidx.compose.ui.node.ModifierNodeElement 67 import androidx.compose.ui.node.SemanticsModifierNode 68 import androidx.compose.ui.node.TraversableNode 69 import androidx.compose.ui.node.dispatchOnScrollChanged 70 import androidx.compose.ui.node.invalidateSemantics 71 import androidx.compose.ui.node.requireDensity 72 import androidx.compose.ui.platform.InspectorInfo 73 import androidx.compose.ui.platform.LocalLayoutDirection 74 import androidx.compose.ui.semantics.SemanticsPropertyReceiver 75 import androidx.compose.ui.semantics.scrollBy 76 import androidx.compose.ui.semantics.scrollByOffset 77 import androidx.compose.ui.unit.Density 78 import androidx.compose.ui.unit.IntSize 79 import androidx.compose.ui.unit.LayoutDirection 80 import androidx.compose.ui.unit.Velocity 81 import androidx.compose.ui.util.fastAny 82 import kotlin.math.abs 83 import kotlin.math.absoluteValue 84 import kotlinx.coroutines.CancellationException 85 import kotlinx.coroutines.launch 86 import kotlinx.coroutines.withContext 87 88 /** 89 * Configure touch scrolling and flinging for the UI element in a single [Orientation]. 90 * 91 * Users should update their state themselves using default [ScrollableState] and its 92 * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect 93 * their own state in UI when using this component. 94 * 95 * If you don't need to have fling or nested scroll support, but want to make component simply 96 * draggable, consider using [draggable]. 97 * 98 * @sample androidx.compose.foundation.samples.ScrollableSample 99 * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be 100 * interpreted by the user land logic and contains useful information about on-going events. 101 * @param orientation orientation of the scrolling 102 * @param enabled whether or not scrolling in enabled 103 * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave 104 * like bottom to top and left to right will behave like right to left. 105 * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If 106 * `null`, default from [ScrollableDefaults.flingBehavior] will be used. 107 * @param interactionSource [MutableInteractionSource] that will be used to emit drag events when 108 * this scrollable is being dragged. 109 */ 110 @Stable 111 fun Modifier.scrollable( 112 state: ScrollableState, 113 orientation: Orientation, 114 enabled: Boolean = true, 115 reverseDirection: Boolean = false, 116 flingBehavior: FlingBehavior? = null, 117 interactionSource: MutableInteractionSource? = null 118 ): Modifier = 119 scrollable( 120 state = state, 121 orientation = orientation, 122 enabled = enabled, 123 reverseDirection = reverseDirection, 124 flingBehavior = flingBehavior, 125 interactionSource = interactionSource, 126 overscrollEffect = null 127 ) 128 129 /** 130 * Configure touch scrolling and flinging for the UI element in a single [Orientation]. 131 * 132 * Users should update their state themselves using default [ScrollableState] and its 133 * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect 134 * their own state in UI when using this component. 135 * 136 * If you don't need to have fling or nested scroll support, but want to make component simply 137 * draggable, consider using [draggable]. 138 * 139 * This overload provides the access to [OverscrollEffect] that defines the behaviour of the over 140 * scrolling logic. Use [androidx.compose.foundation.rememberOverscrollEffect] to create an instance 141 * of the current provided overscroll implementation. 142 * 143 * @sample androidx.compose.foundation.samples.ScrollableSample 144 * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be 145 * interpreted by the user land logic and contains useful information about on-going events. 146 * @param orientation orientation of the scrolling 147 * @param overscrollEffect effect to which the deltas will be fed when the scrollable have some 148 * scrolling delta left. Pass `null` for no overscroll. If you pass an effect you should also 149 * apply [androidx.compose.foundation.overscroll] modifier. 150 * @param enabled whether or not scrolling in enabled 151 * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave 152 * like bottom to top and left to right will behave like right to left. 153 * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If 154 * `null`, default from [ScrollableDefaults.flingBehavior] will be used. 155 * @param interactionSource [MutableInteractionSource] that will be used to emit drag events when 156 * this scrollable is being dragged. 157 * @param bringIntoViewSpec The configuration that this scrollable should use to perform scrolling 158 * when scroll requests are received from the focus system. If null is provided the system will 159 * use the behavior provided by [LocalBringIntoViewSpec] which by default has a platform dependent 160 * implementation. 161 */ 162 @Stable 163 fun Modifier.scrollable( 164 state: ScrollableState, 165 orientation: Orientation, 166 overscrollEffect: OverscrollEffect?, 167 enabled: Boolean = true, 168 reverseDirection: Boolean = false, 169 flingBehavior: FlingBehavior? = null, 170 interactionSource: MutableInteractionSource? = null, 171 bringIntoViewSpec: BringIntoViewSpec? = null 172 ) = 173 this then 174 ScrollableElement( 175 state, 176 orientation, 177 overscrollEffect, 178 enabled, 179 reverseDirection, 180 flingBehavior, 181 interactionSource, 182 bringIntoViewSpec 183 ) 184 185 private class ScrollableElement( 186 val state: ScrollableState, 187 val orientation: Orientation, 188 val overscrollEffect: OverscrollEffect?, 189 val enabled: Boolean, 190 val reverseDirection: Boolean, 191 val flingBehavior: FlingBehavior?, 192 val interactionSource: MutableInteractionSource?, 193 val bringIntoViewSpec: BringIntoViewSpec? 194 ) : ModifierNodeElement<ScrollableNode>() { 195 override fun create(): ScrollableNode { 196 return ScrollableNode( 197 state, 198 overscrollEffect, 199 flingBehavior, 200 orientation, 201 enabled, 202 reverseDirection, 203 interactionSource, 204 bringIntoViewSpec 205 ) 206 } 207 208 override fun update(node: ScrollableNode) { 209 node.update( 210 state, 211 orientation, 212 overscrollEffect, 213 enabled, 214 reverseDirection, 215 flingBehavior, 216 interactionSource, 217 bringIntoViewSpec 218 ) 219 } 220 221 override fun hashCode(): Int { 222 var result = state.hashCode() 223 result = 31 * result + orientation.hashCode() 224 result = 31 * result + overscrollEffect.hashCode() 225 result = 31 * result + enabled.hashCode() 226 result = 31 * result + reverseDirection.hashCode() 227 result = 31 * result + flingBehavior.hashCode() 228 result = 31 * result + interactionSource.hashCode() 229 result = 31 * result + bringIntoViewSpec.hashCode() 230 return result 231 } 232 233 override fun equals(other: Any?): Boolean { 234 if (this === other) return true 235 236 if (other !is ScrollableElement) return false 237 238 if (state != other.state) return false 239 if (orientation != other.orientation) return false 240 if (overscrollEffect != other.overscrollEffect) return false 241 if (enabled != other.enabled) return false 242 if (reverseDirection != other.reverseDirection) return false 243 if (flingBehavior != other.flingBehavior) return false 244 if (interactionSource != other.interactionSource) return false 245 if (bringIntoViewSpec != other.bringIntoViewSpec) return false 246 247 return true 248 } 249 250 override fun InspectorInfo.inspectableProperties() { 251 name = "scrollable" 252 properties["orientation"] = orientation 253 properties["state"] = state 254 properties["overscrollEffect"] = overscrollEffect 255 properties["enabled"] = enabled 256 properties["reverseDirection"] = reverseDirection 257 properties["flingBehavior"] = flingBehavior 258 properties["interactionSource"] = interactionSource 259 properties["bringIntoViewSpec"] = bringIntoViewSpec 260 } 261 } 262 263 internal class ScrollableNode( 264 state: ScrollableState, 265 private var overscrollEffect: OverscrollEffect?, 266 private var flingBehavior: FlingBehavior?, 267 orientation: Orientation, 268 enabled: Boolean, 269 reverseDirection: Boolean, 270 interactionSource: MutableInteractionSource?, 271 bringIntoViewSpec: BringIntoViewSpec? 272 ) : 273 DragGestureNode( 274 canDrag = CanDragCalculation, 275 enabled = enabled, 276 interactionSource = interactionSource, 277 orientationLock = orientation 278 ), 279 KeyInputModifierNode, 280 SemanticsModifierNode, 281 CompositionLocalConsumerModifierNode, 282 OnScrollChangedDispatcher { 283 284 override val shouldAutoInvalidate: Boolean = false 285 286 private val nestedScrollDispatcher = NestedScrollDispatcher() 287 288 private val scrollableContainerNode = delegate(ScrollableContainerNode(enabled)) 289 290 // Place holder fling behavior, we'll initialize it when the density is available. 291 // TODO: It should differ between platforms, move it under expect/actual 292 private val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) 293 294 private val scrollingLogic = 295 ScrollingLogic( 296 scrollableState = state, 297 orientation = orientation, 298 overscrollEffect = overscrollEffect, 299 reverseDirection = reverseDirection, 300 flingBehavior = flingBehavior ?: defaultFlingBehavior, 301 nestedScrollDispatcher = nestedScrollDispatcher, 302 onScrollChangedDispatcher = this, <lambda>null303 isScrollableNodeAttached = { isAttached } 304 ) 305 306 private val nestedScrollConnection = 307 ScrollableNestedScrollConnection(enabled = enabled, scrollingLogic = scrollingLogic) 308 309 private val contentInViewNode = 310 delegate( 311 ContentInViewNode(orientation, scrollingLogic, reverseDirection, bringIntoViewSpec) 312 ) 313 314 private var scrollByAction: ((x: Float, y: Float) -> Boolean)? = null 315 private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null 316 317 private var mouseWheelScrollingLogic: MouseWheelScrollingLogic? = null 318 319 init { 320 /** Nested scrolling */ 321 delegate(nestedScrollModifierNode(nestedScrollConnection, nestedScrollDispatcher)) 322 323 /** Focus scrolling */ 324 delegate(FocusTargetModifierNode(focusability = Focusability.Never)) 325 delegate(BringIntoViewResponderNode(contentInViewNode)) <lambda>null326 delegate(FocusedBoundsObserverNode { contentInViewNode.onFocusBoundsChanged(it) }) 327 } 328 dispatchScrollDeltaInfonull329 override fun dispatchScrollDeltaInfo(delta: Offset) { 330 if (!isAttached) return 331 dispatchOnScrollChanged(delta) 332 } 333 dragnull334 override suspend fun drag( 335 forEachDelta: suspend ((dragDelta: DragEvent.DragDelta) -> Unit) -> Unit 336 ) { 337 with(scrollingLogic) { 338 scroll(scrollPriority = MutatePriority.UserInput) { 339 forEachDelta { 340 scrollByWithOverscroll(it.delta.singleAxisOffset(), source = UserInput) 341 } 342 } 343 } 344 } 345 onDragStartednull346 override fun onDragStarted(startedPosition: Offset) {} 347 onDragStoppednull348 override fun onDragStopped(velocity: Velocity) { 349 nestedScrollDispatcher.coroutineScope.launch { 350 scrollingLogic.onScrollStopped(velocity, isMouseWheel = false) 351 } 352 } 353 onWheelScrollStoppednull354 private fun onWheelScrollStopped(velocity: Velocity) { 355 nestedScrollDispatcher.coroutineScope.launch { 356 scrollingLogic.onScrollStopped(velocity, isMouseWheel = true) 357 } 358 } 359 startDragImmediatelynull360 override fun startDragImmediately(): Boolean { 361 return scrollingLogic.shouldScrollImmediately() 362 } 363 ensureMouseWheelScrollNodeInitializednull364 private fun ensureMouseWheelScrollNodeInitialized() { 365 if (mouseWheelScrollingLogic == null) { 366 mouseWheelScrollingLogic = 367 MouseWheelScrollingLogic( 368 scrollingLogic = scrollingLogic, 369 mouseWheelScrollConfig = platformScrollConfig(), 370 onScrollStopped = ::onWheelScrollStopped, 371 density = requireDensity() 372 ) 373 } 374 375 mouseWheelScrollingLogic?.startReceivingMouseWheelEvents(coroutineScope) 376 } 377 updatenull378 fun update( 379 state: ScrollableState, 380 orientation: Orientation, 381 overscrollEffect: OverscrollEffect?, 382 enabled: Boolean, 383 reverseDirection: Boolean, 384 flingBehavior: FlingBehavior?, 385 interactionSource: MutableInteractionSource?, 386 bringIntoViewSpec: BringIntoViewSpec? 387 ) { 388 var shouldInvalidateSemantics = false 389 if (this.enabled != enabled) { // enabled changed 390 nestedScrollConnection.enabled = enabled 391 scrollableContainerNode.update(enabled) 392 shouldInvalidateSemantics = true 393 } 394 // a new fling behavior was set, change the resolved one. 395 val resolvedFlingBehavior = flingBehavior ?: defaultFlingBehavior 396 397 val resetPointerInputHandling = 398 scrollingLogic.update( 399 scrollableState = state, 400 orientation = orientation, 401 overscrollEffect = overscrollEffect, 402 reverseDirection = reverseDirection, 403 flingBehavior = resolvedFlingBehavior, 404 nestedScrollDispatcher = nestedScrollDispatcher 405 ) 406 contentInViewNode.update(orientation, reverseDirection, bringIntoViewSpec) 407 408 this.overscrollEffect = overscrollEffect 409 this.flingBehavior = flingBehavior 410 411 // update DragGestureNode 412 update( 413 canDrag = CanDragCalculation, 414 enabled = enabled, 415 interactionSource = interactionSource, 416 orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal, 417 shouldResetPointerInputHandling = resetPointerInputHandling 418 ) 419 420 if (shouldInvalidateSemantics) { 421 clearScrollSemanticsActions() 422 invalidateSemantics() 423 } 424 } 425 onAttachnull426 override fun onAttach() { 427 updateDefaultFlingBehavior() 428 mouseWheelScrollingLogic?.updateDensity(requireDensity()) 429 } 430 updateDefaultFlingBehaviornull431 private fun updateDefaultFlingBehavior() { 432 if (!isAttached) return 433 val density = requireDensity() 434 defaultFlingBehavior.updateDensity(density) 435 } 436 onDensityChangenull437 override fun onDensityChange() { 438 onCancelPointerInput() 439 updateDefaultFlingBehavior() 440 mouseWheelScrollingLogic?.updateDensity(requireDensity()) 441 } 442 443 // Key handler for Page up/down scrolling behavior. onKeyEventnull444 override fun onKeyEvent(event: KeyEvent): Boolean { 445 return if ( 446 enabled && 447 (event.key == Key.PageDown || event.key == Key.PageUp) && 448 (event.type == KeyEventType.KeyDown) && 449 (!event.isCtrlPressed) 450 ) { 451 452 val scrollAmount: Offset = 453 if (scrollingLogic.isVertical()) { 454 val viewportHeight = contentInViewNode.viewportSize.height 455 456 val yAmount = 457 if (event.key == Key.PageUp) { 458 viewportHeight.toFloat() 459 } else { 460 -viewportHeight.toFloat() 461 } 462 463 Offset(0f, yAmount) 464 } else { 465 val viewportWidth = contentInViewNode.viewportSize.width 466 467 val xAmount = 468 if (event.key == Key.PageUp) { 469 viewportWidth.toFloat() 470 } else { 471 -viewportWidth.toFloat() 472 } 473 474 Offset(xAmount, 0f) 475 } 476 477 // A coroutine is launched for every individual scroll event in the 478 // larger scroll gesture. If we see degradation in the future (that is, 479 // a fast scroll gesture on a slow device causes UI jank [not seen up to 480 // this point), we can switch to a more efficient solution where we 481 // lazily launch one coroutine (with the first event) and use a Channel 482 // to communicate the scroll amount to the UI thread. 483 coroutineScope.launch { 484 scrollingLogic.scroll(scrollPriority = MutatePriority.UserInput) { 485 scrollBy(offset = scrollAmount, source = UserInput) 486 } 487 } 488 true 489 } else { 490 false 491 } 492 } 493 onPreKeyEventnull494 override fun onPreKeyEvent(event: KeyEvent) = false 495 496 // Forward all PointerInputModifierNode method calls to `mmouseWheelScrollNode.pointerInputNode` 497 // See explanation in `MouseWheelScrollNode.pointerInputNode` 498 499 override fun onPointerEvent( 500 pointerEvent: PointerEvent, 501 pass: PointerEventPass, 502 bounds: IntSize 503 ) { 504 if (pointerEvent.changes.fastAny { canDrag.invoke(it) }) { 505 super.onPointerEvent(pointerEvent, pass, bounds) 506 } 507 if (enabled) { 508 if (pass == PointerEventPass.Initial && pointerEvent.type == PointerEventType.Scroll) { 509 ensureMouseWheelScrollNodeInitialized() 510 } 511 mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) 512 } 513 } 514 applySemanticsnull515 override fun SemanticsPropertyReceiver.applySemantics() { 516 if (enabled && (scrollByAction == null || scrollByOffsetAction == null)) { 517 setScrollSemanticsActions() 518 } 519 520 scrollByAction?.let { scrollBy(action = it) } 521 522 scrollByOffsetAction?.let { scrollByOffset(action = it) } 523 } 524 setScrollSemanticsActionsnull525 private fun setScrollSemanticsActions() { 526 scrollByAction = { x, y -> 527 coroutineScope.launch { scrollingLogic.semanticsScrollBy(Offset(x, y)) } 528 true 529 } 530 531 scrollByOffsetAction = { offset -> scrollingLogic.semanticsScrollBy(offset) } 532 } 533 clearScrollSemanticsActionsnull534 private fun clearScrollSemanticsActions() { 535 scrollByAction = null 536 scrollByOffsetAction = null 537 } 538 } 539 540 /** Contains the default values used by [scrollable] */ 541 object ScrollableDefaults { 542 543 /** Create and remember default [FlingBehavior] that will represent natural fling curve. */ 544 // TODO: It should differ between platforms, move it under expect/actual 545 @Composable flingBehaviornull546 fun flingBehavior(): FlingBehavior { 547 val flingSpec = rememberSplineBasedDecay<Float>() 548 return remember(flingSpec) { DefaultFlingBehavior(flingSpec) } 549 } 550 551 /** 552 * Returns a remembered [OverscrollEffect] created from the current value of 553 * [LocalOverscrollFactory]. 554 * 555 * This API has been deprpecated, and replaced with [rememberOverscrollEffect] 556 */ 557 @Deprecated( 558 "This API has been replaced with rememberOverscrollEffect, which queries theme provided OverscrollFactory values instead of the 'platform default' without customization.", 559 replaceWith = 560 ReplaceWith( 561 "rememberOverscrollEffect()", 562 "androidx.compose.foundation.rememberOverscrollEffect" 563 ) 564 ) 565 @Composable overscrollEffectnull566 fun overscrollEffect(): OverscrollEffect { 567 return rememberPlatformOverscrollEffect() ?: NoOpOverscrollEffect 568 } 569 570 private object NoOpOverscrollEffect : OverscrollEffect { applyToScrollnull571 override fun applyToScroll( 572 delta: Offset, 573 source: NestedScrollSource, 574 performScroll: (Offset) -> Offset 575 ): Offset = performScroll(delta) 576 577 override suspend fun applyToFling( 578 velocity: Velocity, 579 performFling: suspend (Velocity) -> Velocity 580 ) { 581 performFling(velocity) 582 } 583 584 override val isInProgress: Boolean 585 get() = false 586 587 override val node: DelegatableNode 588 get() = object : Modifier.Node() {} 589 } 590 591 /** 592 * Used to determine the value of `reverseDirection` parameter of [Modifier.scrollable] in 593 * scrollable layouts. 594 * 595 * @param layoutDirection current layout direction (e.g. from [LocalLayoutDirection]) 596 * @param orientation orientation of scroll 597 * @param reverseScrolling whether scrolling direction should be reversed 598 * @return `true` if scroll direction should be reversed, `false` otherwise. 599 */ reverseDirectionnull600 fun reverseDirection( 601 layoutDirection: LayoutDirection, 602 orientation: Orientation, 603 reverseScrolling: Boolean 604 ): Boolean { 605 // A finger moves with the content, not with the viewport. Therefore, 606 // always reverse once to have "natural" gesture that goes reversed to layout 607 var reverseDirection = !reverseScrolling 608 // But if rtl and horizontal, things move the other way around 609 val isRtl = layoutDirection == LayoutDirection.Rtl 610 if (isRtl && orientation != Orientation.Vertical) { 611 reverseDirection = !reverseDirection 612 } 613 return reverseDirection 614 } 615 } 616 617 internal interface ScrollConfig { 618 619 /** Enables animated transition of scroll on mouse wheel events. */ 620 val isSmoothScrollingEnabled: Boolean 621 get() = true 622 isPreciseWheelScrollnull623 fun isPreciseWheelScroll(event: PointerEvent): Boolean = false 624 625 fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset 626 } 627 628 internal expect fun CompositionLocalConsumerModifierNode.platformScrollConfig(): ScrollConfig 629 630 // TODO: provide public way to drag by mouse (especially requested for Pager) 631 private val CanDragCalculation: (PointerInputChange) -> Boolean = { change -> 632 change.type != PointerType.Mouse 633 } 634 635 /** 636 * Holds all scrolling related logic: controls nested scrolling, flinging, overscroll and delta 637 * dispatching. 638 */ 639 internal class ScrollingLogic( 640 var scrollableState: ScrollableState, 641 private var overscrollEffect: OverscrollEffect?, 642 private var flingBehavior: FlingBehavior, 643 private var orientation: Orientation, 644 private var reverseDirection: Boolean, 645 private var nestedScrollDispatcher: NestedScrollDispatcher, 646 private var onScrollChangedDispatcher: OnScrollChangedDispatcher, 647 private val isScrollableNodeAttached: () -> Boolean 648 ) { 649 // specifies if this scrollable node is currently flinging 650 var isFlinging = false 651 private set 652 Floatnull653 fun Float.toOffset(): Offset = 654 when { 655 this == 0f -> Offset.Zero 656 orientation == Horizontal -> Offset(this, 0f) 657 else -> Offset(0f, this) 658 } 659 Offsetnull660 fun Offset.singleAxisOffset(): Offset = 661 if (orientation == Horizontal) copy(y = 0f) else copy(x = 0f) 662 663 fun Offset.toFloat(): Float = if (orientation == Horizontal) this.x else this.y 664 665 fun Float.toVelocity(): Velocity = 666 when { 667 this == 0f -> Velocity.Zero 668 orientation == Horizontal -> Velocity(this, 0f) 669 else -> Velocity(0f, this) 670 } 671 toFloatnull672 private fun Velocity.toFloat(): Float = if (orientation == Horizontal) this.x else this.y 673 674 private fun Velocity.singleAxisVelocity(): Velocity = 675 if (orientation == Horizontal) copy(y = 0f) else copy(x = 0f) 676 677 private fun Velocity.update(newValue: Float): Velocity = 678 if (orientation == Horizontal) copy(x = newValue) else copy(y = newValue) 679 680 fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this 681 682 fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this 683 684 private var latestScrollSource = UserInput 685 private var outerStateScope = NoOpScrollScope 686 687 private val nestedScrollScope = 688 object : NestedScrollScope { 689 override fun scrollBy(offset: Offset, source: NestedScrollSource): Offset { 690 return with(outerStateScope) { performScroll(offset, source) } 691 } 692 693 override fun scrollByWithOverscroll( 694 offset: Offset, 695 source: NestedScrollSource 696 ): Offset { 697 latestScrollSource = source 698 val overscroll = overscrollEffect 699 return if (overscroll != null && shouldDispatchOverscroll) { 700 overscroll.applyToScroll(offset, latestScrollSource, performScrollForOverscroll) 701 } else { 702 with(outerStateScope) { performScroll(offset, source) } 703 } 704 } 705 } 706 deltanull707 private val performScrollForOverscroll: (Offset) -> Offset = { delta -> 708 with(outerStateScope) { performScroll(delta, latestScrollSource) } 709 } 710 711 @OptIn(ExperimentalFoundationApi::class) ScrollScopenull712 private fun ScrollScope.performScroll(delta: Offset, source: NestedScrollSource): Offset { 713 val consumedByPreScroll = nestedScrollDispatcher.dispatchPreScroll(delta, source) 714 715 val scrollAvailableAfterPreScroll = delta - consumedByPreScroll 716 717 val singleAxisDeltaForSelfScroll = 718 scrollAvailableAfterPreScroll.singleAxisOffset().reverseIfNeeded().toFloat() 719 720 // Consume on a single axis. 721 val consumedBySelfScroll = 722 scrollBy(singleAxisDeltaForSelfScroll).toOffset().reverseIfNeeded() 723 724 // Trigger on scroll changed callback 725 if (isOnScrollChangedCallbackEnabled) { 726 onScrollChangedDispatcher.dispatchScrollDeltaInfo(consumedBySelfScroll) 727 } 728 729 val deltaAvailableAfterScroll = scrollAvailableAfterPreScroll - consumedBySelfScroll 730 val consumedByPostScroll = 731 nestedScrollDispatcher.dispatchPostScroll( 732 consumedBySelfScroll, 733 deltaAvailableAfterScroll, 734 source 735 ) 736 return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll 737 } 738 739 private val shouldDispatchOverscroll 740 get() = scrollableState.canScrollForward || scrollableState.canScrollBackward 741 performRawScrollnull742 fun performRawScroll(scroll: Offset): Offset { 743 return if (scrollableState.isScrollInProgress) { 744 Offset.Zero 745 } else { 746 dispatchRawDelta(scroll) 747 } 748 } 749 dispatchRawDeltanull750 private fun dispatchRawDelta(scroll: Offset): Offset { 751 return scrollableState 752 .dispatchRawDelta(scroll.toFloat().reverseIfNeeded()) 753 .reverseIfNeeded() 754 .toOffset() 755 } 756 onScrollStoppednull757 suspend fun onScrollStopped(initialVelocity: Velocity, isMouseWheel: Boolean) { 758 if (isMouseWheel && !flingBehavior.shouldBeTriggeredByMouseWheel) { 759 return 760 } 761 val availableVelocity = initialVelocity.singleAxisVelocity() 762 763 val performFling: suspend (Velocity) -> Velocity = { velocity -> 764 val preConsumedByParent = nestedScrollDispatcher.dispatchPreFling(velocity) 765 val available = velocity - preConsumedByParent 766 767 val velocityLeft = doFlingAnimation(available) 768 769 val consumedPost = 770 nestedScrollDispatcher.dispatchPostFling((available - velocityLeft), velocityLeft) 771 val totalLeft = velocityLeft - consumedPost 772 velocity - totalLeft 773 } 774 775 val overscroll = overscrollEffect 776 if (overscroll != null && shouldDispatchOverscroll) { 777 overscroll.applyToFling(availableVelocity, performFling) 778 } else { 779 performFling(availableVelocity) 780 } 781 } 782 783 // fling should be cancelled if we try to scroll more than we can or if this node 784 // is detached during a fling. shouldCancelFlingnull785 private fun shouldCancelFling(pixels: Float): Boolean { 786 // tries to scroll forward but cannot. 787 return (pixels > 0.0f && !scrollableState.canScrollForward) || 788 // tries to scroll backward but cannot. 789 (pixels < 0.0f && !scrollableState.canScrollBackward) || 790 // node is detached. 791 !isScrollableNodeAttached.invoke() 792 } 793 794 @OptIn(ExperimentalFoundationApi::class) doFlingAnimationnull795 suspend fun doFlingAnimation(available: Velocity): Velocity { 796 var result: Velocity = available 797 isFlinging = true 798 try { 799 scroll(scrollPriority = MutatePriority.Default) { 800 val nestedScrollScope = this 801 val reverseScope = 802 object : ScrollScope { 803 override fun scrollBy(pixels: Float): Float { 804 // Fling has hit the bounds or node left composition, 805 // cancel it to allow continuation. This will conclude this node's 806 // fling, 807 // allowing the onPostFling signal to be called 808 // with the leftover velocity from the fling animation. Any nested 809 // scroll 810 // node above will be able to pick up the left over velocity and 811 // continue 812 // the fling. 813 if (pixels.absoluteValue != 0.0f && shouldCancelFling(pixels)) { 814 throw FlingCancellationException() 815 } 816 817 return nestedScrollScope 818 .scrollByWithOverscroll( 819 offset = pixels.toOffset().reverseIfNeeded(), 820 source = SideEffect 821 ) 822 .toFloat() 823 .reverseIfNeeded() 824 } 825 } 826 with(reverseScope) { 827 with(flingBehavior) { 828 result = 829 result.update( 830 performFling(available.toFloat().reverseIfNeeded()) 831 .reverseIfNeeded() 832 ) 833 } 834 } 835 } 836 } finally { 837 isFlinging = false 838 } 839 840 return result 841 } 842 shouldScrollImmediatelynull843 fun shouldScrollImmediately(): Boolean { 844 return scrollableState.isScrollInProgress || overscrollEffect?.isInProgress ?: false 845 } 846 847 /** Opens a scrolling session with nested scrolling and overscroll support. */ scrollnull848 suspend fun scroll( 849 scrollPriority: MutatePriority = MutatePriority.Default, 850 block: suspend NestedScrollScope.() -> Unit 851 ) { 852 scrollableState.scroll(scrollPriority) { 853 outerStateScope = this 854 block.invoke(nestedScrollScope) 855 } 856 } 857 858 /** @return true if the pointer input should be reset */ updatenull859 fun update( 860 scrollableState: ScrollableState, 861 orientation: Orientation, 862 overscrollEffect: OverscrollEffect?, 863 reverseDirection: Boolean, 864 flingBehavior: FlingBehavior, 865 nestedScrollDispatcher: NestedScrollDispatcher 866 ): Boolean { 867 var resetPointerInputHandling = false 868 if (this.scrollableState != scrollableState) { 869 this.scrollableState = scrollableState 870 resetPointerInputHandling = true 871 } 872 this.overscrollEffect = overscrollEffect 873 if (this.orientation != orientation) { 874 this.orientation = orientation 875 resetPointerInputHandling = true 876 } 877 if (this.reverseDirection != reverseDirection) { 878 this.reverseDirection = reverseDirection 879 resetPointerInputHandling = true 880 } 881 this.flingBehavior = flingBehavior 882 this.nestedScrollDispatcher = nestedScrollDispatcher 883 return resetPointerInputHandling 884 } 885 isVerticalnull886 fun isVertical(): Boolean = orientation == Vertical 887 } 888 889 private val NoOpScrollScope: ScrollScope = 890 object : ScrollScope { 891 override fun scrollBy(pixels: Float): Float = pixels 892 } 893 894 private class ScrollableNestedScrollConnection( 895 val scrollingLogic: ScrollingLogic, 896 var enabled: Boolean 897 ) : NestedScrollConnection { 898 onPostScrollnull899 override fun onPostScroll( 900 consumed: Offset, 901 available: Offset, 902 source: NestedScrollSource 903 ): Offset = 904 if (enabled) { 905 scrollingLogic.performRawScroll(available) 906 } else { 907 Offset.Zero 908 } 909 910 @OptIn(ExperimentalFoundationApi::class) onPostFlingnull911 override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { 912 return if (enabled) { 913 val velocityLeft = 914 if (scrollingLogic.isFlinging) { 915 Velocity.Zero 916 } else { 917 scrollingLogic.doFlingAnimation(available) 918 } 919 available - velocityLeft 920 } else { 921 Velocity.Zero 922 } 923 } 924 } 925 926 /** Compatibility interface for default fling behaviors that depends on [Density]. */ 927 internal interface ScrollableDefaultFlingBehavior : FlingBehavior { 928 /** 929 * Update the internal parameters of FlingBehavior in accordance with the new 930 * [androidx.compose.ui.unit.Density] value. 931 * 932 * @param density new density value. 933 */ updateDensitynull934 fun updateDensity(density: Density) = Unit 935 } 936 937 /** 938 * TODO: Move it to public interface Currently, default [FlingBehavior] is not triggered at all to 939 * avoid unexpected effects during regular scrolling. However, custom one must be triggered 940 * because it's used not only for "inertia", but also for snapping in 941 * [androidx.compose.foundation.pager.Pager] or 942 * [androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior]. 943 */ 944 private val FlingBehavior.shouldBeTriggeredByMouseWheel 945 // TODO: Figure out more precise condition to trigger fling by mouse wheel. 946 get() = false // this !is ScrollableDefaultFlingBehavior 947 948 internal class DefaultFlingBehavior( 949 private var flingDecay: DecayAnimationSpec<Float>, 950 private val motionDurationScale: MotionDurationScale = DefaultScrollMotionDurationScale 951 ) : ScrollableDefaultFlingBehavior { 952 953 // For Testing 954 var lastAnimationCycleCount = 0 955 956 override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { 957 lastAnimationCycleCount = 0 958 // come up with the better threshold, but we need it since spline curve gives us NaNs 959 return withContext(motionDurationScale) { 960 if (abs(initialVelocity) > 1f) { 961 var velocityLeft = initialVelocity 962 var lastValue = 0f 963 val animationState = 964 AnimationState( 965 initialValue = 0f, 966 initialVelocity = initialVelocity, 967 ) 968 try { 969 animationState.animateDecay(flingDecay) { 970 val delta = value - lastValue 971 val consumed = scrollBy(delta) 972 lastValue = value 973 velocityLeft = this.velocity 974 // avoid rounding errors and stop if anything is unconsumed 975 if (abs(delta - consumed) > 0.5f) this.cancelAnimation() 976 lastAnimationCycleCount++ 977 } 978 } catch (exception: CancellationException) { 979 velocityLeft = animationState.velocity 980 } 981 velocityLeft 982 } else { 983 initialVelocity 984 } 985 } 986 } 987 988 override fun updateDensity(density: Density) { 989 flingDecay = splineBasedDecay(density) 990 } 991 } 992 993 private const val DefaultScrollMotionDurationScaleFactor = 1f 994 internal val DefaultScrollMotionDurationScale = 995 object : MotionDurationScale { 996 override val scaleFactor: Float 997 get() = DefaultScrollMotionDurationScaleFactor 998 } 999 1000 /** 1001 * (b/311181532): This could not be flattened so we moved it to TraversableNode, but ideally 1002 * ScrollabeNode should be the one to be travesable. 1003 */ 1004 internal class ScrollableContainerNode(enabled: Boolean) : Modifier.Node(), TraversableNode { 1005 override val traverseKey: Any = TraverseKey 1006 1007 var enabled: Boolean = enabled 1008 private set 1009 1010 companion object TraverseKey 1011 updatenull1012 fun update(enabled: Boolean) { 1013 this.enabled = enabled 1014 } 1015 } 1016 1017 private val UnityDensity = 1018 object : Density { 1019 override val density: Float 1020 get() = 1f 1021 1022 override val fontScale: Float 1023 get() = 1f 1024 } 1025 1026 /** A scroll scope for nested scrolling and overscroll support. */ 1027 internal interface NestedScrollScope { scrollBynull1028 fun scrollBy(offset: Offset, source: NestedScrollSource): Offset 1029 1030 fun scrollByWithOverscroll(offset: Offset, source: NestedScrollSource): Offset 1031 } 1032 1033 /** 1034 * Scroll deltas originating from the semantics system. Should be dispatched as an animation driven 1035 * event. 1036 */ 1037 private suspend fun ScrollingLogic.semanticsScrollBy(offset: Offset): Offset { 1038 var previousValue = 0f 1039 scroll(scrollPriority = MutatePriority.Default) { 1040 animate(0f, offset.toFloat()) { currentValue, _ -> 1041 val delta = currentValue - previousValue 1042 val consumed = 1043 scrollBy(offset = delta.reverseIfNeeded().toOffset(), source = UserInput) 1044 .toFloat() 1045 .reverseIfNeeded() 1046 previousValue += consumed 1047 } 1048 } 1049 return previousValue.toOffset() 1050 } 1051 1052 internal class FlingCancellationException : 1053 PlatformOptimizedCancellationException("The fling animation was cancelled") 1054 1055 internal interface OnScrollChangedDispatcher { dispatchScrollDeltaInfonull1056 fun dispatchScrollDeltaInfo(delta: Offset) 1057 } 1058