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.ui.viewinterop
18
19 import android.content.Context
20 import android.graphics.Rect
21 import android.graphics.Region
22 import android.os.Build
23 import android.os.Build.VERSION.SDK_INT
24 import android.view.View
25 import android.view.ViewGroup
26 import android.view.ViewParent
27 import androidx.compose.runtime.ComposeNodeLifecycleCallback
28 import androidx.compose.runtime.CompositionContext
29 import androidx.compose.ui.ComposeUiFlags
30 import androidx.compose.ui.ExperimentalComposeUiApi
31 import androidx.compose.ui.InternalComposeUiApi
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.draw.drawBehind
34 import androidx.compose.ui.geometry.Offset
35 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
36 import androidx.compose.ui.graphics.graphicsLayer
37 import androidx.compose.ui.graphics.nativeCanvas
38 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
39 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
40 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
41 import androidx.compose.ui.input.nestedscroll.nestedScroll
42 import androidx.compose.ui.input.pointer.pointerInteropFilter
43 import androidx.compose.ui.internal.checkPrecondition
44 import androidx.compose.ui.layout.IntrinsicMeasurable
45 import androidx.compose.ui.layout.IntrinsicMeasureScope
46 import androidx.compose.ui.layout.Measurable
47 import androidx.compose.ui.layout.MeasurePolicy
48 import androidx.compose.ui.layout.MeasureResult
49 import androidx.compose.ui.layout.MeasureScope
50 import androidx.compose.ui.layout.findRootCoordinates
51 import androidx.compose.ui.layout.onGloballyPositioned
52 import androidx.compose.ui.layout.positionInRoot
53 import androidx.compose.ui.node.LayoutNode
54 import androidx.compose.ui.node.Owner
55 import androidx.compose.ui.node.OwnerScope
56 import androidx.compose.ui.node.OwnerSnapshotObserver
57 import androidx.compose.ui.platform.AndroidComposeView
58 import androidx.compose.ui.platform.composeToViewOffset
59 import androidx.compose.ui.platform.compositionContext
60 import androidx.compose.ui.semantics.semantics
61 import androidx.compose.ui.unit.Constraints
62 import androidx.compose.ui.unit.Density
63 import androidx.compose.ui.unit.IntSize
64 import androidx.compose.ui.unit.Velocity
65 import androidx.compose.ui.unit.round
66 import androidx.compose.ui.util.fastCoerceAtLeast
67 import androidx.compose.ui.util.fastRoundToInt
68 import androidx.core.graphics.Insets
69 import androidx.core.view.NestedScrollingParent3
70 import androidx.core.view.NestedScrollingParentHelper
71 import androidx.core.view.OnApplyWindowInsetsListener
72 import androidx.core.view.ViewCompat
73 import androidx.core.view.WindowInsetsAnimationCompat
74 import androidx.core.view.WindowInsetsAnimationCompat.BoundsCompat
75 import androidx.core.view.WindowInsetsCompat
76 import androidx.lifecycle.LifecycleOwner
77 import androidx.lifecycle.setViewTreeLifecycleOwner
78 import androidx.savedstate.SavedStateRegistryOwner
79 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
80 import kotlinx.coroutines.launch
81
82 /**
83 * A base class used to host a [View] inside Compose. This API is not designed to be used directly,
84 * but rather using the [AndroidView] and `AndroidViewBinding` APIs, which are built on top of
85 * [AndroidViewHolder].
86 *
87 * @param view The view hosted by this holder.
88 * @param owner The [Owner] of the composition that this holder lives in.
89 */
90 internal open class AndroidViewHolder(
91 context: Context,
92 parentContext: CompositionContext?,
93 private val compositeKeyHash: Int,
94 private val dispatcher: NestedScrollDispatcher,
95 val view: View,
96 private val owner: Owner,
97 ) :
98 ViewGroup(context),
99 NestedScrollingParent3,
100 ComposeNodeLifecycleCallback,
101 OwnerScope,
102 OnApplyWindowInsetsListener {
103
104 init {
105 // Any [Abstract]ComposeViews that are descendants of this view will host
106 // subcompositions of the host composition.
107 // UiApplier doesn't supply this, only AndroidView.
108 parentContext?.let { compositionContext = it }
109 // We save state ourselves, depending on composition.
110 isSaveFromParentEnabled = false
111
112 @Suppress("LeakingThis") addView(view)
113 ViewCompat.setWindowInsetsAnimationCallback(
114 this,
115 object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
116 override fun onStart(
117 animation: WindowInsetsAnimationCompat,
118 bounds: WindowInsetsAnimationCompat.BoundsCompat
119 ): WindowInsetsAnimationCompat.BoundsCompat = insetBounds(bounds)
120
121 override fun onProgress(
122 insets: WindowInsetsCompat,
123 runningAnimations: MutableList<WindowInsetsAnimationCompat>
124 ): WindowInsetsCompat = insetToLayoutPosition(insets)
125 }
126 )
127 ViewCompat.setOnApplyWindowInsetsListener(this, this)
128 }
129
130 // Keep nullable to match the `expect` declaration of InteropViewFactoryHolder
131 @Suppress("RedundantNullableReturnType") fun getInteropView(): InteropView? = view
132
133 /** The update logic of the [View]. */
134 var update: () -> Unit = {}
135 protected set(value) {
136 field = value
137 hasUpdateBlock = true
138 runUpdate()
139 }
140
141 private var hasUpdateBlock = false
142
143 var reset: () -> Unit = {}
144 protected set
145
146 var release: () -> Unit = {}
147 protected set
148
149 /** The modifier of the `LayoutNode` corresponding to this [View]. */
150 var modifier: Modifier = Modifier
151 set(value) {
152 if (value !== field) {
153 field = value
154 onModifierChanged?.invoke(value)
155 }
156 }
157
158 internal var onModifierChanged: ((Modifier) -> Unit)? = null
159
160 /** The screen density of the layout. */
161 var density: Density = Density(1f)
162 set(value) {
163 if (value !== field) {
164 field = value
165 onDensityChanged?.invoke(value)
166 }
167 }
168
169 internal var onDensityChanged: ((Density) -> Unit)? = null
170
171 /** Sets the ViewTreeLifecycleOwner for this view. */
172 var lifecycleOwner: LifecycleOwner? = null
173 set(value) {
174 if (value !== field) {
175 field = value
176 setViewTreeLifecycleOwner(value)
177 }
178 }
179
180 /** Sets the ViewTreeSavedStateRegistryOwner for this view. */
181 var savedStateRegistryOwner: SavedStateRegistryOwner? = null
182 set(value) {
183 if (value !== field) {
184 field = value
185 setViewTreeSavedStateRegistryOwner(value)
186 }
187 }
188
189 private val position = IntArray(2)
190 private var size = IntSize.Zero
191 private var insets: WindowInsetsCompat? = null
192
193 /**
194 * The [OwnerSnapshotObserver] of this holder's [Owner]. Will be null when this view is not
195 * attached, since the observer is not valid unless the view is attached.
196 */
197 private val snapshotObserver: OwnerSnapshotObserver
198 get() {
199 checkPrecondition(isAttachedToWindow) {
200 "Expected AndroidViewHolder to be attached when observing reads."
201 }
202 return owner.snapshotObserver
203 }
204
205 private val runUpdate: () -> Unit = {
206 // If we're not attached, the observer isn't started, so don't bother running it.
207 // onAttachedToWindow will run an update the next time the view is attached.
208 // Also, the view will have no parent when the node is deactivated. when the node will
209 // be reactivated the update block will be re-executed.
210 if (hasUpdateBlock && isAttachedToWindow && view.parent === this) {
211 snapshotObserver.observeReads(this, OnCommitAffectingUpdate, update)
212 }
213 }
214
215 private val runInvalidate: () -> Unit = { layoutNode.invalidateLayer() }
216
217 internal var onRequestDisallowInterceptTouchEvent: ((Boolean) -> Unit)? = null
218
219 private val location = IntArray(2)
220
221 private var lastWidthMeasureSpec: Int = Unmeasured
222 private var lastHeightMeasureSpec: Int = Unmeasured
223
224 private val nestedScrollingParentHelper: NestedScrollingParentHelper =
225 NestedScrollingParentHelper(this)
226
227 private var isDrawing = false
228
229 override val isValidOwnerScope: Boolean
230 get() = isAttachedToWindow
231
232 override fun getAccessibilityClassName(): CharSequence {
233 return javaClass.name
234 }
235
236 override fun onReuse() {
237 // We reset at the same time we remove the view. So if the view was removed, we can just
238 // re-add it and it's ready to go. If it's already attached, we didn't reset it and need
239 // to do so for it to be reused correctly.
240 if (view.parent !== this) {
241 addView(view)
242 } else {
243 reset()
244 }
245 }
246
247 override fun onDeactivate() {
248 reset()
249 if (
250 @OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isRemoveFocusedViewFixEnabled &&
251 hasFocus() &&
252 isInTouchMode &&
253 SDK_INT > 28
254 ) {
255 // Removing a view that is focused results in focus being re-assigned to an existing
256 // view on screen. We don't want this behavior in touch mode.
257 // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
258 findFocus().clearFocus()
259 }
260 removeAllViewsInLayout()
261 }
262
263 override fun onRelease() {
264 release()
265 }
266
267 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
268 if (view.parent !== this) {
269 setMeasuredDimension(
270 MeasureSpec.getSize(widthMeasureSpec),
271 MeasureSpec.getSize(heightMeasureSpec)
272 )
273 return
274 }
275 if (view.visibility == GONE) {
276 setMeasuredDimension(0, 0)
277 return
278 }
279
280 view.measure(widthMeasureSpec, heightMeasureSpec)
281 setMeasuredDimension(view.measuredWidth, view.measuredHeight)
282 lastWidthMeasureSpec = widthMeasureSpec
283 lastHeightMeasureSpec = heightMeasureSpec
284 }
285
286 fun remeasure() {
287 if (lastWidthMeasureSpec == Unmeasured || lastHeightMeasureSpec == Unmeasured) {
288 // This should never happen: it means that the views handler was measured without
289 // the AndroidComposeView having been measured.
290 return
291 }
292 measure(lastWidthMeasureSpec, lastHeightMeasureSpec)
293 }
294
295 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
296 view.layout(0, 0, r - l, b - t)
297 }
298
299 override fun getLayoutParams(): LayoutParams? {
300 return view.layoutParams
301 ?: LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
302 }
303
304 override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
305 onRequestDisallowInterceptTouchEvent?.invoke(disallowIntercept)
306 super.requestDisallowInterceptTouchEvent(disallowIntercept)
307 }
308
309 override fun onAttachedToWindow() {
310 super.onAttachedToWindow()
311 runUpdate()
312 }
313
314 override fun onDetachedFromWindow() {
315 super.onDetachedFromWindow()
316 // remove all observations:
317 snapshotObserver.clear(this)
318 }
319
320 // When there is no hardware acceleration invalidates are intercepted using this method,
321 // otherwise using onDescendantInvalidated. Return null to avoid invalidating the
322 // AndroidComposeView or the handler.
323 @Suppress("OVERRIDE_DEPRECATION", "Deprecation")
324 override fun invalidateChildInParent(location: IntArray?, dirty: Rect?): ViewParent? {
325 super.invalidateChildInParent(location, dirty)
326 invalidateOrDefer()
327 return null
328 }
329
330 override fun onDescendantInvalidated(child: View, target: View) {
331 // We need to call super here in order to correctly update the dirty flags of the holder.
332 super.onDescendantInvalidated(child, target)
333 invalidateOrDefer()
334 }
335
336 fun invalidateOrDefer() {
337 if (isDrawing) {
338 // If an invalidation occurs while drawing invalidate until next frame to avoid
339 // redrawing multiple times during the same frame the same content.
340 view.postOnAnimation(runInvalidate)
341 } else {
342 // when not drawing, we can invalidate any time and not risk multiple draws, we don't
343 // defer to avoid waiting a full frame to draw content.
344 layoutNode.invalidateLayer()
345 }
346 }
347
348 override fun onWindowVisibilityChanged(visibility: Int) {
349 super.onWindowVisibilityChanged(visibility)
350 // On Lollipop, when the Window becomes visible, child Views need to be explicitly
351 // invalidated for some reason.
352 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M && visibility == View.VISIBLE) {
353 layoutNode.invalidateLayer()
354 }
355 }
356
357 // Always mark the region of the View to not be transparent to disable an optimisation which
358 // would otherwise cause certain buggy drawing scenarios. For example, Compose drawing on top
359 // of SurfaceViews included in Compose would sometimes not be displayed, as the drawing is
360 // not done by Views, therefore the area is not known as non-transparent to the View system.
361 override fun gatherTransparentRegion(region: Region?): Boolean {
362 if (region == null) return true
363 getLocationInWindow(location)
364 region.op(
365 location[0],
366 location[1],
367 location[0] + width,
368 location[1] + height,
369 Region.Op.DIFFERENCE
370 )
371 return true
372 }
373
374 /**
375 * A [LayoutNode] tree representation for this Android [View] holder. The [LayoutNode] will
376 * proxy the Compose core calls to the [View].
377 */
378 val layoutNode: LayoutNode = run {
379 // Prepare layout node that proxies measure and layout passes to the View.
380 val layoutNode = LayoutNode()
381
382 // there is an issue in how SurfaceViews being drawn into the new layers. this flag is
383 // a workaround until we find a better solution. it allows us to create an extra rendernode
384 // wrapping android views using the old implementation of layers, where we don't do
385 // layer persistence logic, as it causes SurfaceView flickering.
386 // we should find a better fix as part of b/348144529
387 layoutNode.forceUseOldLayers = true
388
389 @OptIn(InternalComposeUiApi::class)
390 layoutNode.interopViewFactoryHolder = this@AndroidViewHolder
391
392 val coreModifier =
393 Modifier.nestedScroll(NoOpScrollConnection, dispatcher)
394 .semantics(true) {}
395 .pointerInteropFilter(this)
396 // we don't normally need an extra layer here, it is a workaround for b/348144529
397 .graphicsLayer()
398 .drawBehind {
399 drawIntoCanvas { canvas ->
400 if (view.visibility != GONE) {
401 isDrawing = true
402 (layoutNode.owner as? AndroidComposeView)?.drawAndroidView(
403 this@AndroidViewHolder,
404 canvas.nativeCanvas
405 )
406 isDrawing = false
407 }
408 }
409 }
410 .onGloballyPositioned {
411 // The global position of this LayoutNode can change with it being replaced. For
412 // these cases, we need to inform the View.
413 layoutAccordingTo(layoutNode)
414 @OptIn(InternalComposeUiApi::class) owner.onInteropViewLayoutChange(this)
415 val previousX = position[0]
416 val previousY = position[1]
417 view.getLocationOnScreen(position)
418 val oldSize = size
419 size = it.size
420 val previouslyDispatchedInsets = insets
421 if (previouslyDispatchedInsets != null) {
422 if (
423 previousX != position[0] || previousY != position[1] || oldSize != size
424 ) {
425 // If we have previously been dispatched insets (no parents consumed
426 // the insets already), we need to dispatch the insets again when the
427 // view is moved as this could cause the insets (once we account for
428 // the layout position) to change.
429 insetToLayoutPosition(previouslyDispatchedInsets)
430 .toWindowInsets()
431 ?.let { translatedInsets ->
432 // Re-dispatch the insets - we do this instead of calling
433 // requestApplyInsets() as that schedules a full traversal of
434 // the
435 // view hierarchy - there is no need to do that when only the
436 // AndroidView moved. Other changes that cause insets to change
437 // will be dispatched as normal.
438 view.dispatchApplyWindowInsets(translatedInsets)
439 }
440 }
441 }
442 }
443 layoutNode.compositeKeyHash = compositeKeyHash
444 layoutNode.modifier = modifier.then(coreModifier)
445 onModifierChanged = { layoutNode.modifier = it.then(coreModifier) }
446
447 layoutNode.density = density
448 onDensityChanged = { layoutNode.density = it }
449
450 layoutNode.onAttach = { owner ->
451 (owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)
452 if (view.parent !== this) addView(view)
453 }
454 layoutNode.onDetach = { owner ->
455 @OptIn(ExperimentalComposeUiApi::class)
456 if (ComposeUiFlags.isViewFocusFixEnabled && hasFocus()) {
457 owner.focusOwner.clearFocus(true)
458 }
459 (owner as? AndroidComposeView)?.removeAndroidView(this)
460 removeAllViewsInLayout()
461 }
462
463 layoutNode.measurePolicy =
464 object : MeasurePolicy {
465 override fun MeasureScope.measure(
466 measurables: List<Measurable>,
467 constraints: Constraints
468 ): MeasureResult {
469 if (childCount == 0) {
470 return layout(constraints.minWidth, constraints.minHeight) {}
471 }
472
473 if (constraints.minWidth != 0) {
474 getChildAt(0).minimumWidth = constraints.minWidth
475 }
476 if (constraints.minHeight != 0) {
477 getChildAt(0).minimumHeight = constraints.minHeight
478 }
479
480 measure(
481 obtainMeasureSpec(
482 constraints.minWidth,
483 constraints.maxWidth,
484 layoutParams!!.width
485 ),
486 obtainMeasureSpec(
487 constraints.minHeight,
488 constraints.maxHeight,
489 layoutParams!!.height
490 )
491 )
492 return layout(measuredWidth, measuredHeight) { layoutAccordingTo(layoutNode) }
493 }
494
495 override fun IntrinsicMeasureScope.minIntrinsicWidth(
496 measurables: List<IntrinsicMeasurable>,
497 height: Int
498 ) = intrinsicWidth(height)
499
500 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
501 measurables: List<IntrinsicMeasurable>,
502 height: Int
503 ) = intrinsicWidth(height)
504
505 private fun intrinsicWidth(height: Int): Int {
506 measure(
507 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
508 obtainMeasureSpec(0, height, layoutParams!!.height)
509 )
510 return measuredWidth
511 }
512
513 override fun IntrinsicMeasureScope.minIntrinsicHeight(
514 measurables: List<IntrinsicMeasurable>,
515 width: Int
516 ) = intrinsicHeight(width)
517
518 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
519 measurables: List<IntrinsicMeasurable>,
520 width: Int
521 ) = intrinsicHeight(width)
522
523 private fun intrinsicHeight(width: Int): Int {
524 measure(
525 obtainMeasureSpec(0, width, layoutParams!!.width),
526 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
527 )
528 return measuredHeight
529 }
530 }
531 layoutNode
532 }
533
534 /**
535 * Intersects [Constraints] and [View] LayoutParams to obtain the suitable [View.MeasureSpec]
536 * for measuring the [View].
537 */
538 private fun obtainMeasureSpec(min: Int, max: Int, preferred: Int): Int =
539 when {
540 preferred >= 0 || min == max -> {
541 // Fixed size due to fixed size layout param or fixed constraints.
542 MeasureSpec.makeMeasureSpec(preferred.coerceIn(min, max), MeasureSpec.EXACTLY)
543 }
544 preferred == LayoutParams.WRAP_CONTENT && max != Constraints.Infinity -> {
545 // Wrap content layout param with finite max constraint. If max constraint is
546 // infinite,
547 // we will measure the child with UNSPECIFIED.
548 MeasureSpec.makeMeasureSpec(max, MeasureSpec.AT_MOST)
549 }
550 preferred == LayoutParams.MATCH_PARENT && max != Constraints.Infinity -> {
551 // Match parent layout param, so we force the child to fill the available space.
552 MeasureSpec.makeMeasureSpec(max, MeasureSpec.EXACTLY)
553 }
554 else -> {
555 // max constraint is infinite and layout param is WRAP_CONTENT or MATCH_PARENT.
556 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
557 }
558 }
559
560 // TODO: b/203141462 - consume whether the AndroidView() is inside a scrollable container, and
561 // use that to set this. In the meantime set true as the defensive default.
562 override fun shouldDelayChildPressedState(): Boolean = true
563
564 // NestedScrollingParent3
565 override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
566 return (axes and ViewCompat.SCROLL_AXIS_VERTICAL) != 0 ||
567 (axes and ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0
568 }
569
570 override fun getNestedScrollAxes(): Int {
571 return nestedScrollingParentHelper.nestedScrollAxes
572 }
573
574 override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
575 nestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes, type)
576 }
577
578 override fun onStopNestedScroll(target: View, type: Int) {
579 nestedScrollingParentHelper.onStopNestedScroll(target, type)
580 }
581
582 override fun onNestedScroll(
583 target: View,
584 dxConsumed: Int,
585 dyConsumed: Int,
586 dxUnconsumed: Int,
587 dyUnconsumed: Int,
588 type: Int,
589 consumed: IntArray
590 ) {
591 if (!isNestedScrollingEnabled) return
592 val consumedByParent =
593 dispatcher.dispatchPostScroll(
594 consumed = Offset(dxConsumed.toComposeOffset(), dyConsumed.toComposeOffset()),
595 available = Offset(dxUnconsumed.toComposeOffset(), dyUnconsumed.toComposeOffset()),
596 source = toNestedScrollSource(type)
597 )
598 consumed[0] = composeToViewOffset(consumedByParent.x)
599 consumed[1] = composeToViewOffset(consumedByParent.y)
600 }
601
602 override fun onNestedScroll(
603 target: View,
604 dxConsumed: Int,
605 dyConsumed: Int,
606 dxUnconsumed: Int,
607 dyUnconsumed: Int,
608 type: Int
609 ) {
610 if (!isNestedScrollingEnabled) return
611 dispatcher.dispatchPostScroll(
612 consumed = Offset(dxConsumed.toComposeOffset(), dyConsumed.toComposeOffset()),
613 available = Offset(dxUnconsumed.toComposeOffset(), dyUnconsumed.toComposeOffset()),
614 source = toNestedScrollSource(type)
615 )
616 }
617
618 override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
619 if (!isNestedScrollingEnabled) return
620 val consumedByParent =
621 dispatcher.dispatchPreScroll(
622 available = Offset(dx.toComposeOffset(), dy.toComposeOffset()),
623 source = toNestedScrollSource(type)
624 )
625 consumed[0] = composeToViewOffset(consumedByParent.x)
626 consumed[1] = composeToViewOffset(consumedByParent.y)
627 }
628
629 override fun onNestedFling(
630 target: View,
631 velocityX: Float,
632 velocityY: Float,
633 consumed: Boolean
634 ): Boolean {
635 if (!isNestedScrollingEnabled) return false
636 val viewVelocity = Velocity(velocityX.toComposeVelocity(), velocityY.toComposeVelocity())
637 dispatcher.coroutineScope.launch {
638 if (!consumed) {
639 dispatcher.dispatchPostFling(consumed = Velocity.Zero, available = viewVelocity)
640 } else {
641 dispatcher.dispatchPostFling(consumed = viewVelocity, available = Velocity.Zero)
642 }
643 }
644 return false
645 }
646
647 override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
648 if (!isNestedScrollingEnabled) return false
649 val toBeConsumed = Velocity(velocityX.toComposeVelocity(), velocityY.toComposeVelocity())
650 dispatcher.coroutineScope.launch { dispatcher.dispatchPreFling(toBeConsumed) }
651 return false
652 }
653
654 override fun isNestedScrollingEnabled(): Boolean {
655 return view.isNestedScrollingEnabled
656 }
657
658 override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
659 // Cache a copy of the last known insets
660 this.insets = WindowInsetsCompat(insets)
661 return insetToLayoutPosition(insets)
662 }
663
664 private fun insetToLayoutPosition(insets: WindowInsetsCompat): WindowInsetsCompat {
665 if (!insets.hasInsets()) {
666 return insets
667 }
668 return insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
669 }
670
671 private fun insetBounds(bounds: BoundsCompat): BoundsCompat =
672 insetValue(bounds) { l, t, r, b ->
673 BoundsCompat(bounds.lowerBound.inset(l, t, r, b), bounds.upperBound.inset(l, t, r, b))
674 }
675
676 private inline fun <T> insetValue(value: T, block: (l: Int, t: Int, r: Int, b: Int) -> T): T {
677 val coordinates = layoutNode.innerCoordinator
678 if (!coordinates.isAttached) {
679 return value
680 }
681 val topLeft = coordinates.positionInRoot().round()
682 val left = topLeft.x.fastCoerceAtLeast(0)
683 val top = topLeft.y.fastCoerceAtLeast(0)
684 val (rootWidth, rootHeight) = coordinates.findRootCoordinates().size
685 val (width, height) = coordinates.size
686 val bottomRight = coordinates.localToRoot(Offset(width.toFloat(), height.toFloat())).round()
687 val right = (rootWidth - bottomRight.x).fastCoerceAtLeast(0)
688 val bottom = (rootHeight - bottomRight.y).fastCoerceAtLeast(0)
689
690 return if (left == 0 && top == 0 && right == 0 && bottom == 0) {
691 value
692 } else {
693 block(left, top, right, bottom)
694 }
695 }
696
697 private fun Insets.inset(left: Int, top: Int, right: Int, bottom: Int): Insets {
698 return Insets.of(
699 (this.left - left).fastCoerceAtLeast(0),
700 (this.top - top).fastCoerceAtLeast(0),
701 (this.right - right).fastCoerceAtLeast(0),
702 (this.bottom - bottom).fastCoerceAtLeast(0)
703 )
704 }
705
706 companion object {
707 private val OnCommitAffectingUpdate: (AndroidViewHolder) -> Unit = {
708 it.handler.post(it.runUpdate)
709 }
710 }
711 }
712
layoutAccordingTonull713 private fun View.layoutAccordingTo(layoutNode: LayoutNode) {
714 val position = layoutNode.coordinates.positionInRoot()
715 val x = position.x.fastRoundToInt()
716 val y = position.y.fastRoundToInt()
717 layout(x, y, x + measuredWidth, y + measuredHeight)
718 }
719
720 private const val Unmeasured = Int.MIN_VALUE
721
722 /**
723 * No-op Connection required by nested scroll modifier. This is No-op because we don't want to
724 * influence nested scrolling with it and it is required by [Modifier.nestedScroll].
725 */
726 private val NoOpScrollConnection = object : NestedScrollConnection {}
727
toComposeOffsetnull728 private fun Int.toComposeOffset() = toFloat() * -1
729
730 private fun Float.toComposeVelocity(): Float = this * -1f
731
732 private fun toNestedScrollSource(type: Int): NestedScrollSource =
733 when (type) {
734 ViewCompat.TYPE_TOUCH -> NestedScrollSource.UserInput
735 else -> NestedScrollSource.SideEffect
736 }
737