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