1 /*
<lambda>null2 * Copyright 2019 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 @file:Suppress("DEPRECATION")
18
19 package androidx.compose.ui.platform
20
21 import android.annotation.SuppressLint
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Point
25 import android.graphics.Rect
26 import android.os.Build.VERSION.SDK_INT
27 import android.os.Build.VERSION_CODES.M
28 import android.os.Build.VERSION_CODES.N
29 import android.os.Build.VERSION_CODES.O
30 import android.os.Build.VERSION_CODES.Q
31 import android.os.Build.VERSION_CODES.S
32 import android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM
33 import android.os.Looper
34 import android.os.StrictMode
35 import android.os.SystemClock
36 import android.util.LongSparseArray
37 import android.util.SparseArray
38 import android.view.InputDevice
39 import android.view.KeyEvent as AndroidKeyEvent
40 import android.view.MotionEvent
41 import android.view.MotionEvent.ACTION_CANCEL
42 import android.view.MotionEvent.ACTION_DOWN
43 import android.view.MotionEvent.ACTION_HOVER_ENTER
44 import android.view.MotionEvent.ACTION_HOVER_EXIT
45 import android.view.MotionEvent.ACTION_HOVER_MOVE
46 import android.view.MotionEvent.ACTION_MOVE
47 import android.view.MotionEvent.ACTION_POINTER_DOWN
48 import android.view.MotionEvent.ACTION_POINTER_UP
49 import android.view.MotionEvent.ACTION_SCROLL
50 import android.view.MotionEvent.ACTION_UP
51 import android.view.MotionEvent.TOOL_TYPE_MOUSE
52 import android.view.ScrollCaptureTarget
53 import android.view.View
54 import android.view.ViewGroup
55 import android.view.ViewStructure
56 import android.view.ViewTreeObserver
57 import android.view.accessibility.AccessibilityNodeInfo
58 import android.view.animation.AnimationUtils
59 import android.view.autofill.AutofillManager as PlatformAndroidManager
60 import android.view.autofill.AutofillValue
61 import android.view.inputmethod.EditorInfo
62 import android.view.inputmethod.InputConnection
63 import android.view.translation.ViewTranslationCallback
64 import android.view.translation.ViewTranslationRequest
65 import android.view.translation.ViewTranslationResponse
66 import androidx.annotation.DoNotInline
67 import androidx.annotation.RequiresApi
68 import androidx.annotation.VisibleForTesting
69 import androidx.collection.MutableIntObjectMap
70 import androidx.collection.mutableIntObjectMapOf
71 import androidx.collection.mutableObjectListOf
72 import androidx.compose.runtime.derivedStateOf
73 import androidx.compose.runtime.getValue
74 import androidx.compose.runtime.mutableStateOf
75 import androidx.compose.runtime.referentialEqualityPolicy
76 import androidx.compose.runtime.setValue
77 import androidx.compose.runtime.snapshots.Snapshot
78 import androidx.compose.ui.ComposeUiFlags
79 import androidx.compose.ui.ComposeUiFlags.isAdaptiveRefreshRateEnabled
80 import androidx.compose.ui.ExperimentalComposeUiApi
81 import androidx.compose.ui.InternalComposeUiApi
82 import androidx.compose.ui.Modifier
83 import androidx.compose.ui.R
84 import androidx.compose.ui.SessionMutex
85 import androidx.compose.ui.autofill.AndroidAutofill
86 import androidx.compose.ui.autofill.AndroidAutofillManager
87 import androidx.compose.ui.autofill.Autofill
88 import androidx.compose.ui.autofill.AutofillCallback
89 import androidx.compose.ui.autofill.AutofillManager
90 import androidx.compose.ui.autofill.AutofillTree
91 import androidx.compose.ui.autofill.PlatformAutofillManagerImpl
92 import androidx.compose.ui.autofill.performAutofill
93 import androidx.compose.ui.autofill.populateViewStructure
94 import androidx.compose.ui.contentcapture.AndroidContentCaptureManager
95 import androidx.compose.ui.draganddrop.AndroidDragAndDropManager
96 import androidx.compose.ui.draganddrop.ComposeDragShadowBuilder
97 import androidx.compose.ui.draganddrop.DragAndDropTransferData
98 import androidx.compose.ui.focus.FocusDirection
99 import androidx.compose.ui.focus.FocusDirection.Companion.Down
100 import androidx.compose.ui.focus.FocusDirection.Companion.Enter
101 import androidx.compose.ui.focus.FocusDirection.Companion.Exit
102 import androidx.compose.ui.focus.FocusDirection.Companion.Left
103 import androidx.compose.ui.focus.FocusDirection.Companion.Next
104 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
105 import androidx.compose.ui.focus.FocusDirection.Companion.Right
106 import androidx.compose.ui.focus.FocusDirection.Companion.Up
107 import androidx.compose.ui.focus.FocusOwner
108 import androidx.compose.ui.focus.FocusOwnerImpl
109 import androidx.compose.ui.focus.FocusTargetNode
110 import androidx.compose.ui.focus.calculateBoundingRectRelativeTo
111 import androidx.compose.ui.focus.focusRect
112 import androidx.compose.ui.focus.is1dFocusSearch
113 import androidx.compose.ui.focus.isBetterCandidate
114 import androidx.compose.ui.focus.requestInteropFocus
115 import androidx.compose.ui.focus.toAndroidFocusDirection
116 import androidx.compose.ui.focus.toFocusDirection
117 import androidx.compose.ui.focus.toLayoutDirection
118 import androidx.compose.ui.geometry.Offset
119 import androidx.compose.ui.geometry.Size
120 import androidx.compose.ui.graphics.Canvas
121 import androidx.compose.ui.graphics.CanvasHolder
122 import androidx.compose.ui.graphics.GraphicsContext
123 import androidx.compose.ui.graphics.Matrix
124 import androidx.compose.ui.graphics.drawscope.DrawScope
125 import androidx.compose.ui.graphics.layer.GraphicsLayer
126 import androidx.compose.ui.graphics.setFrom
127 import androidx.compose.ui.graphics.toAndroidRect
128 import androidx.compose.ui.graphics.toComposeRect
129 import androidx.compose.ui.hapticfeedback.HapticFeedback
130 import androidx.compose.ui.hapticfeedback.PlatformHapticFeedback
131 import androidx.compose.ui.input.InputMode.Companion.Keyboard
132 import androidx.compose.ui.input.InputMode.Companion.Touch
133 import androidx.compose.ui.input.InputModeManager
134 import androidx.compose.ui.input.InputModeManagerImpl
135 import androidx.compose.ui.input.indirect.IndirectTouchEvent
136 import androidx.compose.ui.input.indirect.IndirectTouchEventType
137 import androidx.compose.ui.input.key.Key
138 import androidx.compose.ui.input.key.Key.Companion.Back
139 import androidx.compose.ui.input.key.Key.Companion.DirectionCenter
140 import androidx.compose.ui.input.key.Key.Companion.DirectionDown
141 import androidx.compose.ui.input.key.Key.Companion.DirectionLeft
142 import androidx.compose.ui.input.key.Key.Companion.DirectionRight
143 import androidx.compose.ui.input.key.Key.Companion.DirectionUp
144 import androidx.compose.ui.input.key.Key.Companion.Escape
145 import androidx.compose.ui.input.key.Key.Companion.NavigateNext
146 import androidx.compose.ui.input.key.Key.Companion.NavigatePrevious
147 import androidx.compose.ui.input.key.Key.Companion.NumPadEnter
148 import androidx.compose.ui.input.key.Key.Companion.PageDown
149 import androidx.compose.ui.input.key.Key.Companion.PageUp
150 import androidx.compose.ui.input.key.Key.Companion.Tab
151 import androidx.compose.ui.input.key.KeyEvent
152 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
153 import androidx.compose.ui.input.key.isShiftPressed
154 import androidx.compose.ui.input.key.key
155 import androidx.compose.ui.input.key.onKeyEvent
156 import androidx.compose.ui.input.key.type
157 import androidx.compose.ui.input.pointer.AndroidPointerIcon
158 import androidx.compose.ui.input.pointer.AndroidPointerIconType
159 import androidx.compose.ui.input.pointer.MatrixPositionCalculator
160 import androidx.compose.ui.input.pointer.MotionEventAdapter
161 import androidx.compose.ui.input.pointer.PointerIcon
162 import androidx.compose.ui.input.pointer.PointerIconService
163 import androidx.compose.ui.input.pointer.PointerInputEventProcessor
164 import androidx.compose.ui.input.pointer.PointerKeyboardModifiers
165 import androidx.compose.ui.input.pointer.ProcessResult
166 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
167 import androidx.compose.ui.input.rotary.RotaryScrollEvent
168 import androidx.compose.ui.input.rotary.onRotaryScrollEvent
169 import androidx.compose.ui.internal.checkPreconditionNotNull
170 import androidx.compose.ui.layout.LayoutCoordinates
171 import androidx.compose.ui.layout.Placeable
172 import androidx.compose.ui.layout.PlacementScope
173 import androidx.compose.ui.layout.RootMeasurePolicy
174 import androidx.compose.ui.layout.positionInRoot
175 import androidx.compose.ui.modifier.ModifierLocalManager
176 import androidx.compose.ui.node.InternalCoreApi
177 import androidx.compose.ui.node.LayoutNode
178 import androidx.compose.ui.node.LayoutNode.UsageByParent
179 import androidx.compose.ui.node.LayoutNodeDrawScope
180 import androidx.compose.ui.node.MeasureAndLayoutDelegate
181 import androidx.compose.ui.node.ModifierNodeElement
182 import androidx.compose.ui.node.Nodes
183 import androidx.compose.ui.node.OutOfFrameExecutor
184 import androidx.compose.ui.node.OwnedLayer
185 import androidx.compose.ui.node.Owner
186 import androidx.compose.ui.node.OwnerSnapshotObserver
187 import androidx.compose.ui.node.RootForTest
188 import androidx.compose.ui.node.visitSubtree
189 import androidx.compose.ui.platform.MotionEventVerifierApi29.isValidMotionEvent
190 import androidx.compose.ui.platform.coreshims.ContentCaptureSessionCompat
191 import androidx.compose.ui.platform.coreshims.ViewCompatShims
192 import androidx.compose.ui.relocation.BringIntoViewModifierNode
193 import androidx.compose.ui.scrollcapture.ScrollCapture
194 import androidx.compose.ui.semantics.EmptySemanticsElement
195 import androidx.compose.ui.semantics.EmptySemanticsModifier
196 import androidx.compose.ui.semantics.SemanticsOwner
197 import androidx.compose.ui.semantics.findClosestParentNode
198 import androidx.compose.ui.spatial.RectManager
199 import androidx.compose.ui.text.font.Font
200 import androidx.compose.ui.text.font.FontFamily
201 import androidx.compose.ui.text.font.createFontFamilyResolver
202 import androidx.compose.ui.text.input.PlatformTextInputService
203 import androidx.compose.ui.text.input.TextInputService
204 import androidx.compose.ui.text.input.TextInputServiceAndroid
205 import androidx.compose.ui.unit.Constraints
206 import androidx.compose.ui.unit.Density
207 import androidx.compose.ui.unit.IntOffset
208 import androidx.compose.ui.unit.LayoutDirection
209 import androidx.compose.ui.unit.round
210 import androidx.compose.ui.util.fastIsFinite
211 import androidx.compose.ui.util.fastLastOrNull
212 import androidx.compose.ui.util.fastRoundToInt
213 import androidx.compose.ui.util.trace
214 import androidx.compose.ui.viewinterop.AndroidViewHolder
215 import androidx.compose.ui.viewinterop.InteropView
216 import androidx.core.view.AccessibilityDelegateCompat
217 import androidx.core.view.InputDeviceCompat.SOURCE_CLASS_POINTER
218 import androidx.core.view.InputDeviceCompat.SOURCE_ROTARY_ENCODER
219 import androidx.core.view.MotionEventCompat.AXIS_SCROLL
220 import androidx.core.view.ViewCompat
221 import androidx.core.view.ViewConfigurationCompat.getScaledHorizontalScrollFactor
222 import androidx.core.view.ViewConfigurationCompat.getScaledVerticalScrollFactor
223 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
224 import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
225 import androidx.lifecycle.DefaultLifecycleObserver
226 import androidx.lifecycle.Lifecycle
227 import androidx.lifecycle.LifecycleOwner
228 import androidx.lifecycle.findViewTreeLifecycleOwner
229 import androidx.savedstate.SavedStateRegistryOwner
230 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
231 import java.lang.reflect.Method
232 import java.util.function.Consumer
233 import kotlin.coroutines.CoroutineContext
234
235 /** Allows tests to inject a custom [PlatformTextInputService]. */
236 internal var platformTextInputServiceInterceptor:
237 (PlatformTextInputService) -> PlatformTextInputService =
238 {
239 it
240 }
241
242 private const val ONE_FRAME_120_HERTZ_IN_MILLISECONDS = 8L
243
244 @Suppress("ViewConstructor", "VisibleForTests", "ConstPropertyName", "NullAnnotationGroup")
245 @OptIn(InternalComposeUiApi::class)
246 internal class AndroidComposeView(context: Context, coroutineContext: CoroutineContext) :
247 ViewGroup(context),
248 Owner,
249 ViewRootForTest,
250 MatrixPositionCalculator,
251 DefaultLifecycleObserver,
252 OutOfFrameExecutor {
253
254 /**
255 * Remembers the position of the last pointer input event that was down. This position will be
256 * used to calculate whether this view is considered scrollable via [canScrollHorizontally]/
257 * [canScrollVertically].
258 */
259 private var lastDownPointerPosition: Offset = Offset.Unspecified
260
261 /**
262 * Signal that AndroidComposeView's superclass constructors have finished running. If this is
263 * false, it's because the runtime's default uninitialized value is currently visible and
264 * AndroidComposeView's constructor hasn't started running yet. In this state other expected
265 * invariants do not hold, e.g. property delegates may not be initialized. View/ViewGroup have a
266 * history of calling non-final methods in their constructors that can lead to this case, e.g.
267 * [onRtlPropertiesChanged].
268 */
269 private var superclassInitComplete = true
270
271 override val sharedDrawScope = LayoutNodeDrawScope()
272
273 override val view: View
274 get() = this
275
276 override var density by mutableStateOf(Density(context), referentialEqualityPolicy())
277 private set
278
279 private lateinit var frameRateCategoryView: View
280
281 internal val isArrEnabled =
282 @OptIn(ExperimentalComposeUiApi::class) isAdaptiveRefreshRateEnabled &&
283 SDK_INT >= VANILLA_ICE_CREAM
284
285 private val rootSemanticsNode = EmptySemanticsModifier()
286 private val semanticsModifier = EmptySemanticsElement(rootSemanticsNode)
287 private val bringIntoViewNode =
288 object : ModifierNodeElement<BringIntoViewOnScreenResponderNode>() {
createnull289 override fun create() = BringIntoViewOnScreenResponderNode(this@AndroidComposeView)
290
291 override fun update(node: BringIntoViewOnScreenResponderNode) {
292 node.view = this@AndroidComposeView
293 }
294
inspectablePropertiesnull295 override fun InspectorInfo.inspectableProperties() {
296 name = "BringIntoViewOnScreen"
297 }
298
hashCodenull299 override fun hashCode(): Int = this@AndroidComposeView.hashCode()
300
301 override fun equals(other: Any?) = other === this
302 }
303
304 override val focusOwner: FocusOwner =
305 FocusOwnerImpl(
306 onRequestApplyChangesListener = ::registerOnEndApplyChangesListener,
307 onRequestFocusForOwner = ::onRequestFocusForOwner,
308 onMoveFocusInterop = ::onMoveFocusInChildren,
309 onClearFocusForOwner = ::onClearFocusForOwner,
310 onFocusRectInterop = ::onFetchFocusRect,
311 onLayoutDirection = ::layoutDirection
312 )
313
314 override fun getImportantForAutofill(): Int {
315 return View.IMPORTANT_FOR_AUTOFILL_YES
316 }
317
318 override var coroutineContext: CoroutineContext = coroutineContext
319 // In some rare cases, the CoroutineContext is cancelled (because the parent
320 // CompositionContext containing the CoroutineContext is no longer associated with this
321 // class). Changing this CoroutineContext to the new CompositionContext's CoroutineContext
322 // needs to cancel all Pointer Input Nodes relying on the old CoroutineContext.
323 // See [Wrapper.android.kt] for more details.
324 set(value) {
325 field = value
326
327 val headModifierNode = root.nodes.head
328
329 // Reset head Modifier.Node's pointer input handler (that is, the underlying
330 // coroutine used to run the handler for input pointer events).
331 if (headModifierNode is SuspendingPointerInputModifierNode) {
332 headModifierNode.resetPointerInputHandler()
333 }
334
335 // Reset all other Modifier.Node's pointer input handler in the chain.
<lambda>null336 headModifierNode.visitSubtree(Nodes.PointerInput) {
337 if (it is SuspendingPointerInputModifierNode) {
338 it.resetPointerInputHandler()
339 }
340 }
341 }
342
343 override val dragAndDropManager = AndroidDragAndDropManager(::startDrag)
344
345 private val _windowInfo: LazyWindowInfo = LazyWindowInfo()
346 override val windowInfo: WindowInfo
347 get() = _windowInfo
348
349 /**
350 * Because AndroidComposeView always accepts focus, we have to divert focus to another View if
351 * there is nothing focusable within. However, if there are only nonfocusable ComposeViews, then
352 * the redirection can recurse infinitely. This makes sure that if that happens, then it can
353 * bail when it is detected
354 */
355 private var processingRequestFocusForNextNonChildView = false
356
357 // When move focus is triggered by a key event, and move focus does not cause any focus change,
358 // we return the key event to the view system if focus search finds a suitable view which is not
359 // a compose sub-view. However if move focus is triggered programmatically, we have to manually
360 // implement this behavior because the view system does not have a moveFocus API.
onMoveFocusInChildrennull361 private fun onMoveFocusInChildren(focusDirection: FocusDirection): Boolean {
362 @OptIn(ExperimentalComposeUiApi::class)
363 if (!ComposeUiFlags.isViewFocusFixEnabled) {
364 // The view system does not have an API corresponding to Enter/Exit.
365 if (focusDirection == Enter || focusDirection == Exit) return false
366
367 val direction =
368 checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
369 val focusedRect = onFetchFocusRect()?.toAndroidRect()
370
371 val nextView =
372 FocusFinderCompat.instance.let {
373 if (focusedRect == null) {
374 it.findNextFocus(this, findFocus(), direction)
375 } else {
376 it.findNextFocusFromRect(this, focusedRect, direction)
377 }
378 }
379 return nextView?.requestInteropFocus(direction, focusedRect) ?: false
380 }
381 // The view system does not have an API corresponding to Enter/Exit.
382 if (focusDirection == Enter || focusDirection == Exit || !hasFocus()) return false
383
384 val androidViewsHandler = _androidViewsHandler ?: return false
385
386 val direction =
387 checkNotNull(focusDirection.toAndroidFocusDirection()) { "Invalid focus direction" }
388
389 val root = rootView as ViewGroup
390
391 val currentFocus = root.findFocus() ?: error("view hasFocus but root can't find it")
392
393 val focusFinder = FocusFinderCompat.instance
394 val nextView = focusFinder.findNextFocus(root, currentFocus, direction)
395 val focusedRect: Rect?
396 if (focusDirection.is1dFocusSearch() && androidViewsHandler.hasFocus()) {
397 focusedRect = null
398 } else {
399 focusedRect = onFetchFocusRect()?.toAndroidRect()
400 if (nextView != null && focusedRect != null) {
401 root.offsetDescendantRectToMyCoords(this, focusedRect)
402 root.offsetRectIntoDescendantCoords(nextView, focusedRect)
403 }
404 }
405
406 // is it part of the compose hierarchy?
407 if (nextView == null || nextView === currentFocus) {
408 return false
409 }
410
411 val focusedChild = androidViewsHandler.focusedChild
412 var nextParent = nextView.parent
413 while (nextParent != null && nextParent !== focusedChild) {
414 nextParent = nextParent.parent
415 }
416 if (nextParent == null) {
417 return false // not a part of the compose hierarchy
418 }
419 return nextView.requestInteropFocus(direction, focusedRect)
420 }
421
422 // If this root view is focused, we can get the focus rect from focusOwner. But if a sub-view
423 // has focus, the rect returned by focusOwner would be the bounds of the focus target
424 // surrounding the embedded view. For a more accurate focus rect, we use the bounds of the
425 // focused sub-view.
onFetchFocusRectnull426 private fun onFetchFocusRect(): androidx.compose.ui.geometry.Rect? =
427 if (isFocused) {
428 focusOwner.getFocusRect()
429 } else {
430 findFocus()?.calculateBoundingRectRelativeTo(this)
431 }
432
433 // TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
434 // that this common logic can be used by all owners.
435 private val keyInputModifier =
keyEventnull436 Modifier.onKeyEvent { keyEvent ->
437 val focusDirection = getFocusDirection(keyEvent)
438 if (focusDirection == null || keyEvent.type != KeyDown) return@onKeyEvent false
439
440 val androidDirection = focusDirection.toAndroidFocusDirection()
441
442 @OptIn(ExperimentalComposeUiApi::class)
443 if (ComposeUiFlags.isViewFocusFixEnabled) {
444 if (hasFocus() && androidDirection != null) {
445 // A child AndroidView is focused. See if the view has a child that should be
446 // focused next.
447 if (onMoveFocusInChildren(focusDirection)) return@onKeyEvent true
448 }
449 }
450 val focusedRect = onFetchFocusRect()
451
452 // Consume the key event if we moved focus or if focus search or requestFocus is
453 // cancelled.
454 val focusWasMovedOrCancelled =
455 focusOwner.focusSearch(focusDirection, focusedRect) {
456 it.requestFocus(focusDirection)
457 } ?: true
458 if (focusWasMovedOrCancelled) return@onKeyEvent true
459
460 // For 2D focus search, we don't need to wrap around, so we just return false. If there
461 // are
462 // items after this view that haven't been visited, they will be visited when the
463 // unconsumed key event triggers a focus search.
464 if (!focusDirection.is1dFocusSearch()) return@onKeyEvent false
465
466 // For 1D focus search, we use FocusFinder to find the next view that is not a child of
467 // this view. We don't return false because we don't want to re-visit sub-views. They
468 // will
469 // instead be visited when the AndroidView around them gets a moveFocus(Enter)).
470
471 if (androidDirection != null) {
472 val nextView = findNextNonChildView(androidDirection).takeIf { it != this }
473 if (nextView != null) {
474 val androidRect = checkNotNull(focusedRect?.toAndroidRect()) { "Invalid rect" }
475 val rootView = rootView as ViewGroup
476 rootView.offsetDescendantRectToMyCoords(this, androidRect)
477 rootView.offsetRectIntoDescendantCoords(nextView, androidRect)
478 if (nextView.requestInteropFocus(androidDirection, androidRect)) {
479 return@onKeyEvent true
480 }
481 }
482 }
483
484 // Focus finder couldn't find another view. We manually wrap around since focus remained
485 // on this view.
486 val clearedFocusSuccessfully =
487 focusOwner.clearFocus(
488 force = false,
489 refreshFocusEvents = true,
490 clearOwnerFocus = false,
491 focusDirection = focusDirection
492 )
493
494 // Consume the key event if clearFocus was cancelled.
495 if (!clearedFocusSuccessfully) return@onKeyEvent true
496
497 // Perform wrap-around focus search by running a focus search after clearing focus.
498 return@onKeyEvent focusOwner.focusSearch(focusDirection, null) {
499 it.requestFocus(focusDirection)
500 } ?: true
501 }
502
findNextNonChildViewnull503 private fun findNextNonChildView(direction: Int): View? {
504 var currentView: View? = this
505 val focusFinder = FocusFinderCompat.instance
506 while (currentView != null) {
507 currentView = focusFinder.findNextFocus(rootView as ViewGroup, currentView, direction)
508 if (currentView != null && !containsDescendant(currentView)) return currentView
509 }
510 return null
511 }
512
513 private val rotaryInputModifier =
<lambda>null514 Modifier.onRotaryScrollEvent {
515 // TODO(b/210748692): call focusManager.moveFocus() in response to rotary events.
516 false
517 }
518
519 private val canvasHolder = CanvasHolder()
520
521 override val viewConfiguration: ViewConfiguration =
522 AndroidViewConfiguration(android.view.ViewConfiguration.get(context))
523
524 override val root =
<lambda>null525 LayoutNode().also {
526 it.measurePolicy = RootMeasurePolicy
527 it.density = density
528 it.viewConfiguration = viewConfiguration
529 // Composed modifiers cannot be added here directly
530 it.modifier =
531 Modifier.then(semanticsModifier)
532 .then(rotaryInputModifier)
533 .then(keyInputModifier)
534 .then(focusOwner.modifier)
535 .then(dragAndDropManager.modifier)
536 .then(bringIntoViewNode)
537 }
538
539 override val layoutNodes: MutableIntObjectMap<LayoutNode> = mutableIntObjectMapOf()
540
541 override val rectManager = RectManager(layoutNodes)
542
543 override val rootForTest: RootForTest = this
544 internal var uncaughtExceptionHandler: RootForTest.UncaughtExceptionHandler? = null
545
546 override val semanticsOwner: SemanticsOwner =
547 SemanticsOwner(root, rootSemanticsNode, layoutNodes)
548 private val composeAccessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this)
549 internal var contentCaptureManager =
550 AndroidContentCaptureManager(
551 view = this,
552 onContentCaptureSession = ::getContentCaptureSessionCompat
553 )
554
555 /**
556 * Provide accessibility manager to the user. Use the Android version of accessibility manager.
557 */
558 override val accessibilityManager = AndroidAccessibilityManager(context)
559
560 /**
561 * Provide access to a GraphicsContext instance used to create GraphicsLayers for providing
562 * isolation boundaries for rendering portions of a Composition hierarchy as well as for
563 * achieving certain visual effects like masks and blurs
564 */
565 override val graphicsContext = GraphicsContext(this)
566
567 // Used by components that want to provide autofill semantic information.
568 // TODO: Replace with SemanticsTree: Temporary hack until we have a semantics tree implemented.
569 // TODO: Replace with SemanticsTree.
570 // This is a temporary hack until we have a semantics tree implemented.
571 override val autofillTree = AutofillTree()
572
573 // OwnedLayers that are dirty and should be redrawn.
574 private val dirtyLayers = mutableListOf<OwnedLayer>()
575
576 // OwnerLayers that invalidated themselves during their last draw. They will be redrawn
577 // during the next AndroidComposeView dispatchDraw pass.
578 private var postponedDirtyLayers: MutableList<OwnedLayer>? = null
579
580 private var isDrawingContent = false
581 private var isPendingInteropViewLayoutChangeDispatch = false
582
583 private val motionEventAdapter = MotionEventAdapter()
584 private val pointerInputEventProcessor = PointerInputEventProcessor(root)
585
586 /**
587 * Used for updating LocalConfiguration when configuration changes - consume LocalConfiguration
588 * instead of changing this observer if you are writing a component that adapts to configuration
589 * changes.
590 */
<lambda>null591 var configurationChangeObserver: (Configuration) -> Unit = {}
592
593 private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
594
595 internal val _autofillManager =
596 if (autofillSupported()) {
597 val platformAutofill = context.getSystemService(PlatformAndroidManager::class.java)
<lambda>null598 checkPreconditionNotNull(platformAutofill) { "Autofill service could not be located." }
599 AndroidAutofillManager(
600 platformAutofillManager = PlatformAutofillManagerImpl(platformAutofill),
601 semanticsOwner = semanticsOwner,
602 view = this,
603 rectManager = rectManager,
604 packageName = context.packageName
605 )
606 } else {
607 null
608 }
609
610 // Used as a CompositionLocal for performing autofill.
611 override val autofill: Autofill?
612 get() = _autofill
613
614 // Used as a CompositionLocal for performing semantic autofill.
615 override val autofillManager: AutofillManager?
616 get() = _autofillManager
617
618 private var observationClearRequested = false
619
620 /** Provide clipboard manager to the user. Use the Android version of clipboard manager. */
621 override val clipboardManager = AndroidClipboardManager(context)
622
623 override val clipboard = AndroidClipboard(clipboardManager)
624
commandnull625 override val snapshotObserver = OwnerSnapshotObserver { command ->
626 if (handler?.looper === Looper.myLooper()) {
627 command()
628 } else {
629 handler?.post(command)
630 }
631 }
632
633 @Suppress("UnnecessaryOptInAnnotation")
634 @OptIn(InternalCoreApi::class)
635 override var showLayoutBounds = false
636 get() {
637 return if (SDK_INT >= 30) Api30Impl.isShowingLayoutBounds(this) else field
638 }
639
640 private var _androidViewsHandler: AndroidViewsHandler? = null
641 internal val androidViewsHandler: AndroidViewsHandler
642 get() {
643 if (_androidViewsHandler == null) {
644 _androidViewsHandler = AndroidViewsHandler(context)
645 addView(_androidViewsHandler)
646 // Ensure that AndroidViewsHandler is measured and laid out after creation, so that
647 // it can report correct bounds on screen (for semantics, etc).
648 // Normally this is done by addView, but here we disabled it for optimization
649 // purposes.
650 requestLayout()
651 }
652 return _androidViewsHandler!!
653 }
654
655 private var viewLayersContainer: DrawChildContainer? = null
656
657 // The constraints being used by the last onMeasure. It is set to null in onLayout. It allows
658 // us to detect the case when the View was measured twice with different constraints within
659 // the same measure pass.
660 private var onMeasureConstraints: Constraints? = null
661
662 // Will be set to true when we were measured twice with different constraints during the last
663 // measure pass.
664 private var wasMeasuredWithMultipleConstraints = false
665
666 private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)
667
668 override val measureIteration: Long
669 get() = measureAndLayoutDelegate.measureIteration
670
671 override val hasPendingMeasureOrLayout
672 get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
673
674 private var globalPosition: IntOffset = IntOffset(Int.MAX_VALUE, Int.MAX_VALUE)
675
676 private val tmpPositionArray = intArrayOf(0, 0)
677 private val tmpMatrix = Matrix()
678 private val viewToWindowMatrix = Matrix()
679 private val windowToViewMatrix = Matrix()
680
681 @VisibleForTesting internal var lastMatrixRecalculationAnimationTime = -1L
682 private var forceUseMatrixCache = false
683
684 /**
685 * On some devices, the `getLocationOnScreen()` returns `(0, 0)` even when the Window is offset
686 * in special circumstances. This contains the screen coordinates of the containing Window the
687 * last time the [viewToWindowMatrix] and [windowToViewMatrix] were recalculated.
688 */
689 private var windowPosition = Offset.Infinite
690
691 // Used to track whether or not there was an exception while creating an MRenderNode
692 // so that we don't have to continue using try/catch after fails once.
693 private var isRenderNodeCompatible = true
694
695 private var _viewTreeOwners: ViewTreeOwners? by mutableStateOf(null)
696
697 // Having an extra derived state here (instead of directly using _viewTreeOwners) is a
698 // workaround for b/271579465 to avoid unnecessary extra recompositions when this is mutated
699 // before setContent is called.
700 /**
701 * Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to execute your code
702 * when the object will be created.
703 */
<lambda>null704 val viewTreeOwners: ViewTreeOwners? by derivedStateOf { _viewTreeOwners }
705
706 private var onViewTreeOwnersAvailable: ((ViewTreeOwners) -> Unit)? = null
707
708 // executed when the layout pass has been finished. as a result of it our view could be moved
709 // inside the window (we are interested not only in the event when our parent positioned us
710 // on a different position, but also in the position of each of the grandparents as all these
711 // positions add up to final global position)
712 private val globalLayoutListener =
<lambda>null713 ViewTreeObserver.OnGlobalLayoutListener { updatePositionCacheAndDispatch() }
714
715 // executed when a scrolling container like ScrollView of RecyclerView performed the scroll,
716 // this could affect our global position
717 private val scrollChangedListener =
<lambda>null718 ViewTreeObserver.OnScrollChangedListener { updatePositionCacheAndDispatch() }
719
720 // executed whenever the touch mode changes.
721 private val touchModeChangeListener =
touchModenull722 ViewTreeObserver.OnTouchModeChangeListener { touchMode ->
723 _inputModeManager.inputMode = if (touchMode) Touch else Keyboard
724 }
725
726 private val legacyTextInputServiceAndroid = TextInputServiceAndroid(view, this)
727
728 /**
729 * The legacy text input service. This is only used for new text input sessions if
730 * [textInputSessionMutex] is null.
731 */
732 @Deprecated("Use PlatformTextInputModifierNode instead.")
733 override val textInputService =
734 TextInputService(platformTextInputServiceInterceptor(legacyTextInputServiceAndroid))
735
736 private val textInputSessionMutex = SessionMutex<AndroidPlatformTextInputSession>()
737
738 override val softwareKeyboardController: SoftwareKeyboardController =
739 DelegatingSoftwareKeyboardController(textInputService)
740
741 override val placementScope: Placeable.PlacementScope
742 get() = PlacementScope(this)
743
textInputSessionnull744 override suspend fun textInputSession(
745 session: suspend PlatformTextInputSessionScope.() -> Nothing
746 ): Nothing =
747 textInputSessionMutex.withSessionCancellingPrevious(
748 sessionInitializer = {
749 AndroidPlatformTextInputSession(
750 view = this,
751 textInputService = textInputService,
752 coroutineScope = it
753 )
754 },
755 session = session
756 )
757
758 @Deprecated(
759 "fontLoader is deprecated, use fontFamilyResolver",
760 replaceWith = ReplaceWith("fontFamilyResolver")
761 )
762 @Suppress("DEPRECATION")
763 override val fontLoader: Font.ResourceLoader = AndroidFontResourceLoader(context)
764
765 // Backed by mutableStateOf so that the local provider recomposes when it changes
766 // FontFamily.Resolver is not guaranteed to be stable or immutable, hence referential check
767 override var fontFamilyResolver: FontFamily.Resolver by
768 mutableStateOf(createFontFamilyResolver(context), referentialEqualityPolicy())
769 private set
770
771 // keeps track of changes in font weight adjustment to update fontFamilyResolver
772 private var currentFontWeightAdjustment: Int =
773 context.resources.configuration.fontWeightAdjustmentCompat
774
775 private val Configuration.fontWeightAdjustmentCompat: Int
776 get() = if (SDK_INT >= S) fontWeightAdjustment else 0
777
778 // Backed by mutableStateOf so that the ambient provider recomposes when it changes
779 override var layoutDirection by
780 mutableStateOf(
781 // We don't use the attached View's layout direction here since that layout direction
782 // may not
783 // be resolved since composables may be composed without attaching to the RootViewImpl.
784 // In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
785 // configuration) as a default layout direction.
786 toLayoutDirection(context.resources.configuration.layoutDirection)
787 ?: LayoutDirection.Ltr
788 )
789 private set
790
791 /** Provide haptic feedback to the user. Use the Android version of haptic feedback. */
792 override val hapticFeedBack: HapticFeedback = PlatformHapticFeedback(this)
793
794 /** Provide an instance of [InputModeManager] which is available as a CompositionLocal. */
795 private val _inputModeManager =
796 InputModeManagerImpl(
797 initialInputMode = if (isInTouchMode) Touch else Keyboard,
<lambda>null798 onRequestInputModeChange = {
799 when (it) {
800 // Android doesn't support programmatically switching to touch mode, so we
801 // don't do anything, but just return true if we are already in touch mode.
802 Touch -> isInTouchMode
803
804 // If we are already in keyboard mode, we return true, otherwise, we call
805 // requestFocusFromTouch, which puts the system in non-touch mode.
806 Keyboard -> if (isInTouchMode) requestFocusFromTouch() else true
807 else -> false
808 }
809 }
810 )
811 override val inputModeManager: InputModeManager
812 get() = _inputModeManager
813
814 override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
815
816 /**
817 * Provide textToolbar to the user, for text-related operation. Use the Android version of
818 * floating toolbar(post-M) and primary toolbar(pre-M).
819 */
820 override val textToolbar: TextToolbar = AndroidTextToolbar(this)
821
822 /**
823 * When the first event for a mouse is ACTION_DOWN, an ACTION_HOVER_ENTER is never sent. This
824 * means that we won't receive an `Enter` event for the first mouse. In order to prevent this
825 * problem, we track whether or not the previous event was with the mouse inside and if not, we
826 * can create a simulated mouse enter event to force an enter.
827 */
828 private var previousMotionEvent: MotionEvent? = null
829
830 /** The time of the last layout. This is used to send a synthetic MotionEvent. */
831 private var relayoutTime = 0L
832
833 /**
834 * A cache for OwnedLayers. Recreating ViewLayers is expensive, so we avoid it as much as
835 * possible. This also helps a little with RenderNodeLayers as well.
836 */
837 private val layerCache = WeakCache<OwnedLayer>()
838
839 /** List of lambdas to be called when [onEndApplyChanges] is called. */
840 private val endApplyChangesListeners = mutableObjectListOf<(() -> Unit)?>()
841
842 private var currentFrameRate = 0f
843 private var currentFrameRateCategory = 0f
844
845 /**
846 * Runnable used to update the pointer position after layout. If another pointer event comes in
847 * before this runs, this Runnable will be removed and not executed.
848 */
849 private val resendMotionEventRunnable =
850 object : Runnable {
runnull851 override fun run() {
852 removeCallbacks(this)
853 val lastMotionEvent = previousMotionEvent
854 if (lastMotionEvent != null) {
855 val wasMouseEvent = lastMotionEvent.getToolType(0) == TOOL_TYPE_MOUSE
856 val action = lastMotionEvent.actionMasked
857 val resend =
858 if (wasMouseEvent) {
859 action != ACTION_HOVER_EXIT && action != ACTION_UP
860 } else {
861 action != ACTION_UP
862 }
863 if (resend) {
864 val newAction =
865 if (action == ACTION_HOVER_MOVE || action == ACTION_HOVER_ENTER) {
866 ACTION_HOVER_MOVE
867 } else {
868 ACTION_MOVE
869 }
870 sendSimulatedEvent(
871 lastMotionEvent,
872 newAction,
873 relayoutTime,
874 forceHover = false
875 )
876 }
877 }
878 }
879 }
880
881 /**
882 * If an [ACTION_HOVER_EXIT] event is received, it could be because an [ACTION_DOWN] is coming
883 * from a mouse or stylus. We can't know for certain until the next event is sent. This message
884 * is posted after receiving the [ACTION_HOVER_EXIT] to send the event if nothing else is
885 * received before that.
886 */
<lambda>null887 private val sendHoverExitEvent = Runnable {
888 hoverExitReceived = false
889 val lastEvent = previousMotionEvent!!
890 check(lastEvent.actionMasked == ACTION_HOVER_EXIT) {
891 "The ACTION_HOVER_EXIT event was not cleared."
892 }
893 sendMotionEvent(lastEvent)
894 }
895
896 /** Set to `true` when [sendHoverExitEvent] has been posted. */
897 private var hoverExitReceived = false
898
899 /** Callback for [measureAndLayout] to update the pointer position 150ms after layout. */
<lambda>null900 private val resendMotionEventOnLayout: () -> Unit = {
901 val lastEvent = previousMotionEvent
902 if (lastEvent != null) {
903 when (lastEvent.actionMasked) {
904 // We currently only care about hover events being updated when layout changes
905 ACTION_HOVER_ENTER,
906 ACTION_HOVER_MOVE -> {
907 relayoutTime = SystemClock.uptimeMillis()
908 post(resendMotionEventRunnable)
909 }
910 }
911 }
912 }
913
914 private val matrixToWindow =
915 if (SDK_INT < Q) CalculateMatrixToWindowApi21(tmpMatrix) else CalculateMatrixToWindowApi29()
916
917 /**
918 * Keyboard modifiers state might be changed when window is not focused, so window doesn't
919 * receive any key events. This flag is set when window focus changes. Then we can rely on it
920 * when handling the first movementEvent to get the actual keyboard modifiers state from it.
921 * After window gains focus, the first motionEvent.metaState (after focus gained) is used to
922 * update windowInfo.keyboardModifiers. See [onWindowFocusChanged] and [sendMotionEvent]
923 */
924 private var keyboardModifiersRequireUpdate = false
925
<lambda>null926 init {
927 addOnAttachStateChangeListener(contentCaptureManager)
928 setWillNotDraw(false)
929 isFocusable = true
930 if (SDK_INT >= O) {
931 AndroidComposeViewVerificationHelperMethodsO.focusable(
932 this,
933 focusable = View.FOCUSABLE,
934 defaultFocusHighlightEnabled = false
935 )
936 }
937 isFocusableInTouchMode = true
938 clipChildren = false
939 ViewCompat.setAccessibilityDelegate(this, composeAccessibilityDelegate)
940 ViewRootForTest.onViewCreatedCallback?.invoke(this)
941 setOnDragListener(dragAndDropManager)
942 root.attach(this)
943
944 // Support for this feature in Compose is tracked here: b/207654434
945 if (SDK_INT >= Q) AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
946
947 if (isArrEnabled) {
948 frameRateCategoryView =
949 View(context).apply {
950 layoutParams = LayoutParams(1, 1)
951 // hide this View from layout inspector
952 setTag(R.id.hide_in_inspector_tag, true)
953 }
954 addView(frameRateCategoryView)
955 }
956 }
957
958 /**
959 * Since this view has its own concept of internal focus, it needs to report that to the view
960 * system for accurate focus searching and so ViewRootImpl will scroll correctly.
961 */
getFocusedRectnull962 override fun getFocusedRect(rect: Rect) {
963 onFetchFocusRect()?.run {
964 rect.left = left.fastRoundToInt()
965 rect.top = top.fastRoundToInt()
966 rect.right = right.fastRoundToInt()
967 rect.bottom = bottom.fastRoundToInt()
968 } ?: super.getFocusedRect(rect)
969 }
970
971 /**
972 * Avoid crash by not traversing assist structure. Autofill assistStructure will be dispatched
973 * via `dispatchProvideAutofillStructure` from Android 8 and on. See b/251152083 and b/320768586
974 * more details.
975 */
dispatchProvideStructurenull976 override fun dispatchProvideStructure(structure: ViewStructure) {
977 if (SDK_INT in 23..27) {
978 AndroidComposeViewAssistHelperMethodsO.setClassName(structure, view)
979 } else {
980 super.dispatchProvideStructure(structure)
981 }
982 }
983
984 private val scrollCapture = if (SDK_INT >= 31) ScrollCapture() else null
985 internal val scrollCaptureInProgress: Boolean
986 get() =
987 if (SDK_INT >= 31) {
988 scrollCapture?.scrollCaptureInProgress ?: false
989 } else {
990 false
991 }
992
onScrollCaptureSearchnull993 override fun onScrollCaptureSearch(
994 localVisibleRect: Rect,
995 windowOffset: Point,
996 targets: Consumer<ScrollCaptureTarget>
997 ) {
998 if (SDK_INT >= 31) {
999 scrollCapture?.onScrollCaptureSearch(
1000 view = this,
1001 semanticsOwner = semanticsOwner,
1002 coroutineContext = coroutineContext,
1003 targets = targets
1004 )
1005 }
1006 }
1007
onResumenull1008 override fun onResume(owner: LifecycleOwner) {
1009 // Refresh in onResume in case the value has changed.
1010 if (SDK_INT < 30) {
1011 showLayoutBounds = getIsShowingLayoutBounds()
1012 }
1013 }
1014
focusSearchnull1015 override fun focusSearch(focused: View?, direction: Int): View? {
1016 // do not propagate search if a measurement is happening
1017 if (focused == null || measureAndLayoutDelegate.duringMeasureLayout) {
1018 return super.focusSearch(focused, direction)
1019 }
1020
1021 // Find the next subview if any using FocusFinder.
1022 val nextView = FocusFinderCompat.instance.findNextFocus(this, focused, direction)
1023
1024 // Find the next composable using FocusOwner.
1025 val focusedBounds =
1026 if (focused === this) {
1027 focusOwner.getFocusRect() ?: focused.calculateBoundingRectRelativeTo(this)
1028 } else {
1029 focused.calculateBoundingRectRelativeTo(this)
1030 }
1031 val focusDirection = toFocusDirection(direction) ?: Down
1032 var focusTarget: FocusTargetNode? = null
1033 val searchResult =
1034 focusOwner.focusSearch(focusDirection, focusedBounds) {
1035 focusTarget = it
1036 true
1037 }
1038
1039 return when {
1040 searchResult == null -> focused // Focus Search Cancelled.
1041 focusTarget == null -> nextView ?: focused // No compose focus item
1042 nextView == null -> this // No found View, so go to the found Compose focus item
1043 focusDirection.is1dFocusSearch() -> super.focusSearch(focused, direction)
1044 isBetterCandidate(
1045 focusTarget!!.focusRect(),
1046 nextView.calculateBoundingRectRelativeTo(this),
1047 focusedBounds,
1048 focusDirection
1049 ) -> this // Compose focus is better than View focus
1050 else -> nextView // View focus is better than Compose focus
1051 }
1052 }
1053
requestFocusnull1054 override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
1055 @OptIn(ExperimentalComposeUiApi::class)
1056 if (!ComposeUiFlags.isViewFocusFixEnabled) {
1057 // This view is already focused.
1058 if (isFocused) return true
1059
1060 // If the root has focus, it means a sub-view is focused,
1061 // and is trying to move focus within itself.
1062 if (focusOwner.rootState.hasFocus) {
1063 return super.requestFocus(direction, previouslyFocusedRect)
1064 }
1065
1066 val focusDirection = toFocusDirection(direction) ?: Enter
1067 return focusOwner.focusSearch(
1068 focusDirection = focusDirection,
1069 focusedRect = previouslyFocusedRect?.toComposeRect()
1070 ) {
1071 it.requestFocus(focusDirection)
1072 } == true
1073 }
1074 // This view is already focused.
1075 if (isFocused) return true
1076
1077 // There is nothing focusable and we've looped around all Views back to this one, so
1078 // we just return false to indicate that nothing can be focused.
1079 if (processingRequestFocusForNextNonChildView) return false
1080
1081 // If there is currently a focusRequest() in operation, a clearFocus() due to AndroidView
1082 // may cause a recursive requestFocus(). This prevents that reentrant call.
1083 if (focusOwner.focusTransactionManager.ongoingTransaction) return false
1084
1085 val focusDirection = toFocusDirection(direction) ?: Enter
1086
1087 // If the root has focus, it means a sub-view is focused,
1088 // and is trying to move focus within itself.
1089 if (hasFocus() && onMoveFocusInChildren(focusDirection)) return true
1090
1091 var foundFocusable = false
1092 val focusSearchResult =
1093 focusOwner.focusSearch(
1094 focusDirection = focusDirection,
1095 focusedRect = previouslyFocusedRect?.toComposeRect()
1096 ) {
1097 foundFocusable = true
1098 it.requestFocus(focusDirection)
1099 }
1100 if (focusSearchResult == null) {
1101 return false // The focus search was canceled
1102 }
1103 if (focusSearchResult) {
1104 return true // We found something to focus on
1105 }
1106 if (foundFocusable) {
1107 return false // The requestFocus() from within the focusSearch was canceled
1108 }
1109
1110 if (previouslyFocusedRect != null && !hasFocus()) {
1111 // try searching, ignoring the previously focused rect. We've had a request to focus on
1112 // this specific View
1113 val altFocus =
1114 focusOwner.focusSearch(focusDirection = focusDirection, focusedRect = null) {
1115 it.requestFocus(focusDirection)
1116 }
1117 if (altFocus == true) {
1118 // found alternative focus
1119 return true
1120 }
1121 }
1122
1123 // We advertised ourselves as focusable, but we aren't. Try to just move the focus to the
1124 // next item.
1125 val nextFocusedView = findNextNonChildView(direction)
1126
1127 // Can crash if we return false when we've advertised ourselves as focusable and we aren't
1128 // b/369256395
1129 if (nextFocusedView == null || nextFocusedView === this) {
1130 // There is no next View, so just return true so we don't cause a crash
1131 return true
1132 }
1133
1134 // Try to focus on the next focusable View
1135 processingRequestFocusForNextNonChildView = true
1136 val requestFocusResult = nextFocusedView.requestFocus(direction)
1137 processingRequestFocusForNextNonChildView = false
1138 return requestFocusResult
1139 }
1140
onRequestFocusForOwnernull1141 private fun onRequestFocusForOwner(
1142 focusDirection: FocusDirection?,
1143 previouslyFocusedRect: androidx.compose.ui.geometry.Rect?
1144 ): Boolean {
1145 // We don't request focus if the view is already focused, or if an embedded view is focused,
1146 // because this would cause the embedded view to lose focus.
1147 if (isFocused || hasFocus()) return true
1148
1149 return super.requestFocus(
1150 focusDirection?.toAndroidFocusDirection() ?: FOCUS_DOWN,
1151 @Suppress("DEPRECATION") previouslyFocusedRect?.toAndroidRect()
1152 )
1153 }
1154
onClearFocusForOwnernull1155 private fun onClearFocusForOwner() {
1156 @OptIn(ExperimentalComposeUiApi::class)
1157 if (isFocused || (!ComposeUiFlags.isViewFocusFixEnabled && hasFocus())) {
1158 super.clearFocus()
1159 } else if (hasFocus()) {
1160 // Call clearFocus() on the child that has focus
1161 findFocus()?.clearFocus()
1162 super.clearFocus()
1163 }
1164 }
1165
onFocusChangednull1166 override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
1167 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
1168 if (!gainFocus && !hasFocus()) {
1169 focusOwner.releaseFocus()
1170 }
1171 }
1172
onWindowFocusChangednull1173 override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
1174 _windowInfo.isWindowFocused = hasWindowFocus
1175 keyboardModifiersRequireUpdate = true
1176 super.onWindowFocusChanged(hasWindowFocus)
1177
1178 if (hasWindowFocus && SDK_INT < 30) {
1179 // Refresh in onResume in case the value has changed from the quick settings tile, in
1180 // which case the activity won't be paused/resumed (b/225937688).
1181 getIsShowingLayoutBounds().also { newShowLayoutBounds ->
1182 if (showLayoutBounds != newShowLayoutBounds) {
1183 showLayoutBounds = newShowLayoutBounds
1184 // Unlike in onResume, getting window focus doesn't automatically trigger a new
1185 // draw pass, so we have to do that manually.
1186 invalidateDescendants()
1187 }
1188 }
1189 }
1190 }
1191
1192 /** This function is used by the testing framework to send key events. */
sendKeyEventnull1193 override fun sendKeyEvent(keyEvent: KeyEvent): Boolean =
1194 // First dispatch the key event to mimic the event being intercepted before it is sent to
1195 // the soft keyboard.
1196 focusOwner.dispatchInterceptedSoftKeyboardEvent(keyEvent) ||
1197 // Next, send the key event to the Soft Keyboard.
1198 // TODO(b/272600716): Send the key event to the IME.
1199
1200 // Finally, dispatch the key event to onPreKeyEvent/onKeyEvent listeners.
1201 focusOwner.dispatchKeyEvent(keyEvent)
1202
1203 /** This function is used by the testing framework to send indirect touch events. */
1204 @OptIn(ExperimentalComposeUiApi::class)
1205 override fun sendIndirectTouchEvent(indirectTouchEvent: IndirectTouchEvent): Boolean {
1206 return focusOwner.dispatchIndirectTouchEvent(indirectTouchEvent)
1207 }
1208
dispatchKeyEventnull1209 override fun dispatchKeyEvent(event: AndroidKeyEvent): Boolean =
1210 if (isFocused) {
1211 // Focus lies within the Compose hierarchy, so we dispatch the key event to the
1212 // appropriate place.
1213 _windowInfo.keyboardModifiers = PointerKeyboardModifiers(event.metaState)
1214 // If the event is not consumed, use the default implementation.
1215 focusOwner.dispatchKeyEvent(KeyEvent(event)) || super.dispatchKeyEvent(event)
1216 } else {
1217 // This Owner has a focused child view, which is a view interoperability use case,
1218 // so we use the default ViewGroup behavior which will route tke key event to the
1219 // focused child view.
1220 focusOwner.dispatchKeyEvent(
1221 keyEvent = KeyEvent(event),
<lambda>null1222 onFocusedItem = {
1223 // TODO(b/320510084): Add tests to verify that embedded views receive key
1224 // events.
1225 super.dispatchKeyEvent(event)
1226 }
1227 )
1228 }
1229
dispatchKeyEventPreImenull1230 override fun dispatchKeyEventPreIme(event: AndroidKeyEvent): Boolean {
1231 return (isFocused && focusOwner.dispatchInterceptedSoftKeyboardEvent(KeyEvent(event))) ||
1232 // If this view is not focused, and it received a key event, it means this is a view
1233 // interoperability use case and we need to route the event to the embedded child view.
1234 // Also, if this event wasn't consumed by the compose hierarchy, we need to send it back
1235 // to the parent view. Both these cases are handles by the default view implementation.
1236 super.dispatchKeyEventPreIme(event)
1237 }
1238
1239 /**
1240 * This function is used by the delegate file to enable accessibility frameworks for testing.
1241 */
forceAccessibilityForTestingnull1242 override fun forceAccessibilityForTesting(enable: Boolean) {
1243 composeAccessibilityDelegate.accessibilityForceEnabledForTesting = enable
1244 }
1245
1246 /**
1247 * This function is used by the delegate file to set the time interval between sending
1248 * accessibility events in milliseconds.
1249 */
setAccessibilityEventBatchIntervalMillisnull1250 override fun setAccessibilityEventBatchIntervalMillis(intervalMillis: Long) {
1251 composeAccessibilityDelegate.SendRecurringAccessibilityEventsIntervalMillis = intervalMillis
1252 }
1253
onPreAttachnull1254 override fun onPreAttach(node: LayoutNode) {
1255 layoutNodes[node.semanticsId] = node
1256 }
1257
onPostAttachnull1258 override fun onPostAttach(node: LayoutNode) {
1259 @OptIn(ExperimentalComposeUiApi::class)
1260 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1261 _autofillManager?.onPostAttach(node)
1262 }
1263 }
1264
onDetachnull1265 override fun onDetach(node: LayoutNode) {
1266 layoutNodes.remove(node.semanticsId)
1267 measureAndLayoutDelegate.onNodeDetached(node)
1268 requestClearInvalidObservations()
1269 @OptIn(ExperimentalComposeUiApi::class)
1270 if (ComposeUiFlags.isRectTrackingEnabled) {
1271 rectManager.remove(node)
1272 }
1273 @OptIn(ExperimentalComposeUiApi::class)
1274 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1275 _autofillManager?.onDetach(node)
1276 }
1277 }
1278
requestAutofillnull1279 override fun requestAutofill(node: LayoutNode) {
1280 @OptIn(ExperimentalComposeUiApi::class)
1281 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1282 _autofillManager?.requestAutofill(node)
1283 }
1284 }
1285
requestClearInvalidObservationsnull1286 fun requestClearInvalidObservations() {
1287 observationClearRequested = true
1288 }
1289
onEndApplyChangesnull1290 override fun onEndApplyChanges() {
1291 if (observationClearRequested) {
1292 snapshotObserver.clearInvalidObservations()
1293 observationClearRequested = false
1294 }
1295 val childAndroidViews = _androidViewsHandler
1296 if (childAndroidViews != null) {
1297 clearChildInvalidObservations(childAndroidViews)
1298 }
1299 @OptIn(ExperimentalComposeUiApi::class)
1300 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1301 _autofillManager?.onEndApplyChanges()
1302 }
1303 // Listeners can add more items to the list and we want to ensure that they
1304 // are executed after being added, so loop until the list is empty
1305 while (endApplyChangesListeners.isNotEmpty() && endApplyChangesListeners[0] != null) {
1306 val size = endApplyChangesListeners.size
1307 for (i in 0 until size) {
1308 val listener = endApplyChangesListeners[i]
1309 // null out the item so that if the listener is re-added then we execute it again.
1310 endApplyChangesListeners[i] = null
1311 listener?.invoke()
1312 }
1313 // Remove all the items that were visited. Removing items shifts all items after
1314 // to the front of the list, so removing in a chunk is cheaper than removing one-by-one
1315 endApplyChangesListeners.removeRange(0, size)
1316 }
1317 }
1318
registerOnEndApplyChangesListenernull1319 override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
1320 if (listener !in endApplyChangesListeners) {
1321 endApplyChangesListeners += listener
1322 }
1323 }
1324
startDragnull1325 private fun startDrag(
1326 transferData: DragAndDropTransferData,
1327 decorationSize: Size,
1328 drawDragDecoration: DrawScope.() -> Unit,
1329 ): Boolean {
1330 val density =
1331 with(context.resources) {
1332 Density(density = displayMetrics.density, fontScale = configuration.fontScale)
1333 }
1334 val shadowBuilder =
1335 ComposeDragShadowBuilder(
1336 density = density,
1337 decorationSize = decorationSize,
1338 drawDragDecoration = drawDragDecoration,
1339 )
1340 @Suppress("DEPRECATION")
1341 return if (SDK_INT >= N) {
1342 AndroidComposeViewStartDragAndDropN.startDragAndDrop(
1343 view = this,
1344 transferData = transferData,
1345 dragShadowBuilder = shadowBuilder,
1346 )
1347 } else {
1348 startDrag(
1349 transferData.clipData,
1350 shadowBuilder,
1351 transferData.localState,
1352 transferData.flags,
1353 )
1354 }
1355 }
1356
clearChildInvalidObservationsnull1357 private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
1358 for (i in 0 until viewGroup.childCount) {
1359 val child = viewGroup.getChildAt(i)
1360 if (child is AndroidComposeView) {
1361 child.onEndApplyChanges()
1362 } else if (child is ViewGroup) {
1363 clearChildInvalidObservations(child)
1364 }
1365 }
1366 }
1367
addExtraDataToAccessibilityNodeInfoHelpernull1368 private fun addExtraDataToAccessibilityNodeInfoHelper(
1369 virtualViewId: Int,
1370 info: AccessibilityNodeInfo,
1371 extraDataKey: String
1372 ) {
1373 // This extra is just for testing: needed a way to retrieve `traversalBefore` and
1374 // `traversalAfter` from a non-sealed instance of an ANI
1375 when (extraDataKey) {
1376 composeAccessibilityDelegate.ExtraDataTestTraversalBeforeVal -> {
1377 composeAccessibilityDelegate.idToBeforeMap.getOrDefault(virtualViewId, -1).let {
1378 if (it != -1) {
1379 info.extras.putInt(extraDataKey, it)
1380 }
1381 }
1382 }
1383 composeAccessibilityDelegate.ExtraDataTestTraversalAfterVal -> {
1384 composeAccessibilityDelegate.idToAfterMap.getOrDefault(virtualViewId, -1).let {
1385 if (it != -1) {
1386 info.extras.putInt(extraDataKey, it)
1387 }
1388 }
1389 }
1390 else -> {}
1391 }
1392 }
1393
addViewnull1394 override fun addView(child: View?) = addView(child, -1)
1395
1396 override fun addView(child: View?, index: Int) =
1397 addView(child, index, child!!.layoutParams ?: generateDefaultLayoutParams())
1398
1399 override fun addView(child: View?, width: Int, height: Int) =
1400 addView(
1401 child,
1402 -1,
1403 generateDefaultLayoutParams().also {
1404 it.width = width
1405 it.height = height
1406 }
1407 )
1408
addViewnull1409 override fun addView(child: View?, params: LayoutParams?) = addView(child, -1, params)
1410
1411 /**
1412 * Directly adding _real_ [View]s to this view is not supported for external consumers, so we
1413 * can use the non-layout-invalidating [addViewInLayout] for when we need to add utility
1414 * container views, such as [viewLayersContainer].
1415 */
1416 override fun addView(child: View?, index: Int, params: LayoutParams?) {
1417 addViewInLayout(child, index, params, /* preventRequestLayout= */ true)
1418 }
1419
1420 /**
1421 * Called to inform the owner that a new Android [View] was [attached][Owner.onPreAttach] to the
1422 * hierarchy.
1423 */
addAndroidViewnull1424 fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
1425 androidViewsHandler.holderToLayoutNode[view] = layoutNode
1426 androidViewsHandler.addView(view)
1427 androidViewsHandler.layoutNodeToHolder[layoutNode] = view
1428 // Fetching AccessibilityNodeInfo from a View which is not set to
1429 // IMPORTANT_FOR_ACCESSIBILITY_YES will return null.
1430 view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES)
1431 val thisView = this
1432 ViewCompat.setAccessibilityDelegate(
1433 view,
1434 object : AccessibilityDelegateCompat() {
1435 override fun onInitializeAccessibilityNodeInfo(
1436 host: View,
1437 info: AccessibilityNodeInfoCompat
1438 ) {
1439 super.onInitializeAccessibilityNodeInfo(host, info)
1440
1441 // Prevent TalkBack from trying to focus the AndroidViewHolder.
1442 // This also prevents UIAutomator from finding nodes, so don't
1443 // do it if there are no enabled a11y services (which implies that
1444 // UIAutomator is the one requesting an AccessibilityNodeInfo).
1445 if (composeAccessibilityDelegate.isEnabled) {
1446 info.isVisibleToUser = false
1447 }
1448
1449 var parentId =
1450 layoutNode
1451 .findClosestParentNode { it.nodes.has(Nodes.Semantics) }
1452 ?.semanticsId
1453 if (
1454 parentId == null || parentId == semanticsOwner.unmergedRootSemanticsNode.id
1455 ) {
1456 parentId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
1457 }
1458 info.setParent(thisView, parentId)
1459 val semanticsId = layoutNode.semanticsId
1460
1461 val beforeId =
1462 composeAccessibilityDelegate.idToBeforeMap.getOrDefault(semanticsId, -1)
1463 if (beforeId != -1) {
1464 val beforeView = androidViewsHandler.semanticsIdToView(beforeId)
1465 if (beforeView != null) {
1466 // If the node that should come before this one is a view, we want to
1467 // pass in the "before" view itself, which is retrieved
1468 // from `androidViewsHandler.idToViewMap`.
1469 info.setTraversalBefore(beforeView)
1470 } else {
1471 // Otherwise, we'll just set the "before" value by passing in
1472 // the semanticsId.
1473 info.setTraversalBefore(thisView, beforeId)
1474 }
1475 addExtraDataToAccessibilityNodeInfoHelper(
1476 semanticsId,
1477 info.unwrap(),
1478 composeAccessibilityDelegate.ExtraDataTestTraversalBeforeVal
1479 )
1480 }
1481
1482 val afterId =
1483 composeAccessibilityDelegate.idToAfterMap.getOrDefault(semanticsId, -1)
1484 if (afterId != -1) {
1485 val afterView = androidViewsHandler.semanticsIdToView(afterId)
1486 if (afterView != null) {
1487 info.setTraversalAfter(afterView)
1488 } else {
1489 info.setTraversalAfter(thisView, afterId)
1490 }
1491 addExtraDataToAccessibilityNodeInfoHelper(
1492 semanticsId,
1493 info.unwrap(),
1494 composeAccessibilityDelegate.ExtraDataTestTraversalAfterVal
1495 )
1496 }
1497 }
1498 }
1499 )
1500 }
1501
1502 /**
1503 * Called to inform the owner that an Android [View] was [detached][Owner.onDetach] from the
1504 * hierarchy.
1505 */
removeAndroidViewnull1506 fun removeAndroidView(view: AndroidViewHolder) {
1507 androidViewsHandler.removeViewInLayout(view)
1508 androidViewsHandler.layoutNodeToHolder.remove(
1509 androidViewsHandler.holderToLayoutNode.remove(view)
1510 )
1511 view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO)
1512 }
1513
1514 /** Called to ask the owner to draw a child Android [View] to [canvas]. */
drawAndroidViewnull1515 fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
1516 androidViewsHandler.drawView(view, canvas)
1517 }
1518
scheduleMeasureAndLayoutnull1519 private fun scheduleMeasureAndLayout(nodeToRemeasure: LayoutNode? = null) {
1520 if (!isLayoutRequested && isAttachedToWindow) {
1521 if (nodeToRemeasure != null) {
1522 // if [nodeToRemeasure] can potentially resize the root we should call
1523 // requestLayout() so our parent View can react on this change on the same frame.
1524 // if instead we just call invalidate() and remeasure inside dispatchDraw()
1525 // this will cause inconsistency as the Compose content will already have the
1526 // new size, but the View hierarchy will react only on the next frame.
1527 var node = nodeToRemeasure
1528 while (
1529 node != null &&
1530 node.measuredByParent == UsageByParent.InMeasureBlock &&
1531 node.childSizeCanAffectParentSize()
1532 ) {
1533 node = node.parent
1534 }
1535 if (node === root) {
1536 requestLayout()
1537 return
1538 }
1539 }
1540 if (width == 0 || height == 0) {
1541 // if the view has no size calling invalidate() will be skipped
1542 requestLayout()
1543 } else {
1544 invalidate()
1545 }
1546 }
1547 }
1548
LayoutNodenull1549 private fun LayoutNode.childSizeCanAffectParentSize(): Boolean {
1550 // if the view was measured twice with different constraints last time it means the
1551 // constraints we have could be not the final constraints and in fact our parent
1552 // ViewGroup can remeasure us with different constraints if we call requestLayout().
1553 return wasMeasuredWithMultipleConstraints ||
1554 // when parent's [hasFixedInnerContentConstraints] is true the child size change
1555 // can't affect parent size as the size is fixed. for example it happens when parent
1556 // has Modifier.fillMaxSize() set on it.
1557 parent?.hasFixedInnerContentConstraints == false
1558 }
1559
measureAndLayoutnull1560 override fun measureAndLayout(sendPointerUpdate: Boolean) {
1561 // only run the logic when we have something pending
1562 if (
1563 measureAndLayoutDelegate.hasPendingMeasureOrLayout ||
1564 measureAndLayoutDelegate.hasPendingOnPositionedCallbacks
1565 ) {
1566 trace("AndroidOwner:measureAndLayout") {
1567 val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
1568 val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
1569 if (rootNodeResized) {
1570 requestLayout()
1571 }
1572 measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
1573 dispatchPendingInteropLayoutCallbacks()
1574 }
1575 }
1576 }
1577
measureAndLayoutnull1578 override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
1579 trace("AndroidOwner:measureAndLayout") {
1580 measureAndLayoutDelegate.measureAndLayout(layoutNode, constraints)
1581 // only dispatch the callbacks if we don't have other nodes to process as otherwise
1582 // we will have one more measureAndLayout() pass anyway in the same frame.
1583 // it allows us to not traverse the hierarchy twice.
1584 if (!measureAndLayoutDelegate.hasPendingMeasureOrLayout) {
1585 measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
1586 dispatchPendingInteropLayoutCallbacks()
1587 }
1588 @OptIn(ExperimentalComposeUiApi::class)
1589 if (ComposeUiFlags.isRectTrackingEnabled) {
1590 rectManager.dispatchCallbacks()
1591 }
1592 }
1593 }
1594
dispatchPendingInteropLayoutCallbacksnull1595 private fun dispatchPendingInteropLayoutCallbacks() {
1596 if (isPendingInteropViewLayoutChangeDispatch) {
1597 viewTreeObserver.dispatchOnGlobalLayout()
1598 isPendingInteropViewLayoutChangeDispatch = false
1599 }
1600 }
1601
forceMeasureTheSubtreenull1602 override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
1603 measureAndLayoutDelegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
1604 }
1605
onRequestMeasurenull1606 override fun onRequestMeasure(
1607 layoutNode: LayoutNode,
1608 affectsLookahead: Boolean,
1609 forceRequest: Boolean,
1610 scheduleMeasureAndLayout: Boolean
1611 ) {
1612 if (affectsLookahead) {
1613 if (
1614 measureAndLayoutDelegate.requestLookaheadRemeasure(layoutNode, forceRequest) &&
1615 scheduleMeasureAndLayout
1616 ) {
1617 scheduleMeasureAndLayout(layoutNode)
1618 }
1619 } else if (
1620 measureAndLayoutDelegate.requestRemeasure(layoutNode, forceRequest) &&
1621 scheduleMeasureAndLayout
1622 ) {
1623 scheduleMeasureAndLayout(layoutNode)
1624 }
1625 }
1626
onRequestRelayoutnull1627 override fun onRequestRelayout(
1628 layoutNode: LayoutNode,
1629 affectsLookahead: Boolean,
1630 forceRequest: Boolean
1631 ) {
1632 if (affectsLookahead) {
1633 if (measureAndLayoutDelegate.requestLookaheadRelayout(layoutNode, forceRequest)) {
1634 scheduleMeasureAndLayout()
1635 }
1636 } else {
1637 if (measureAndLayoutDelegate.requestRelayout(layoutNode, forceRequest)) {
1638 scheduleMeasureAndLayout()
1639 }
1640 }
1641 }
1642
requestOnPositionedCallbacknull1643 override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
1644 measureAndLayoutDelegate.requestOnPositionedCallback(layoutNode)
1645 scheduleMeasureAndLayout()
1646 }
1647
measureAndLayoutForTestnull1648 override fun measureAndLayoutForTest() {
1649 measureAndLayout()
1650 }
1651
setUncaughtExceptionHandlernull1652 override fun setUncaughtExceptionHandler(handler: RootForTest.UncaughtExceptionHandler?) {
1653 uncaughtExceptionHandler = handler
1654 measureAndLayoutDelegate.uncaughtExceptionHandler = handler
1655 }
1656
onMeasurenull1657 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
1658 trace("AndroidOwner:onMeasure") {
1659 if (!isAttachedToWindow) {
1660 invalidateLayoutNodeMeasurement(root)
1661 }
1662 val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
1663 val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
1664
1665 val constraints =
1666 Constraints.fitPrioritizingHeight(
1667 minWidth = minWidth,
1668 maxWidth = maxWidth,
1669 minHeight = minHeight,
1670 maxHeight = maxHeight
1671 )
1672 if (onMeasureConstraints == null) {
1673 // first onMeasure after last onLayout
1674 onMeasureConstraints = constraints
1675 wasMeasuredWithMultipleConstraints = false
1676 } else if (onMeasureConstraints != constraints) {
1677 // we were remeasured twice with different constraints after last onLayout
1678 wasMeasuredWithMultipleConstraints = true
1679 }
1680 measureAndLayoutDelegate.updateRootConstraints(constraints)
1681 measureAndLayoutDelegate.measureOnly()
1682 setMeasuredDimension(root.width, root.height)
1683
1684 if (_androidViewsHandler != null) {
1685 androidViewsHandler.measure(
1686 MeasureSpec.makeMeasureSpec(root.width, MeasureSpec.EXACTLY),
1687 MeasureSpec.makeMeasureSpec(root.height, MeasureSpec.EXACTLY)
1688 )
1689 }
1690 }
1691 }
1692
1693 @Suppress("NOTHING_TO_INLINE")
component1null1694 private inline operator fun ULong.component1() = (this shr 32).toInt()
1695
1696 @Suppress("NOTHING_TO_INLINE")
1697 private inline operator fun ULong.component2() = (this and 0xFFFFFFFFUL).toInt()
1698
1699 private fun pack(a: Int, b: Int) = (a.toULong() shl 32 or b.toULong())
1700
1701 private fun convertMeasureSpec(measureSpec: Int): ULong {
1702 val mode = MeasureSpec.getMode(measureSpec)
1703 val size = MeasureSpec.getSize(measureSpec)
1704 return when (mode) {
1705 MeasureSpec.EXACTLY -> pack(size, size)
1706 MeasureSpec.UNSPECIFIED -> pack(0, Constraints.Infinity)
1707 MeasureSpec.AT_MOST -> pack(0, size)
1708 else -> throw IllegalStateException()
1709 }
1710 }
1711
onLayoutnull1712 override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
1713 lastMatrixRecalculationAnimationTime = 0L // reset it so that we're sure to have a new value
1714 measureAndLayoutDelegate.measureAndLayout(resendMotionEventOnLayout)
1715 onMeasureConstraints = null
1716 // we postpone onPositioned callbacks until onLayout as LayoutCoordinates
1717 // are currently wrong if you try to get the global(activity) coordinates -
1718 // View is not yet laid out.
1719 updatePositionCacheAndDispatch()
1720 if (_androidViewsHandler != null) {
1721 // Even if we laid out during onMeasure, we want to set the bounds of the
1722 // AndroidViewsHandler for accessibility and for Views making assumptions based on
1723 // the size of their ancestors. Usually the Views in the hierarchy will not
1724 // be relaid out, as they have not requested layout in the meantime.
1725 // However, there is also chance for the AndroidViewsHandler and the children to be
1726 // isLayoutRequested at this point, in case the Views hierarchy receives forceLayout().
1727 // In case of a forceLayout(), calling layout here will traverse the entire subtree
1728 // and replace the Views at the same position, which is needed to clean up their
1729 // layout state, which otherwise might cause further requestLayout()s to be blocked.
1730 androidViewsHandler.layout(0, 0, r - l, b - t)
1731 }
1732 }
1733
updatePositionCacheAndDispatchnull1734 private fun updatePositionCacheAndDispatch() {
1735 var positionChanged = false
1736 getLocationOnScreen(tmpPositionArray)
1737 val (globalX, globalY) = globalPosition
1738 if (
1739 globalX != tmpPositionArray[0] ||
1740 globalY != tmpPositionArray[1] ||
1741 // -1 means it has never been set, 0 means it has been "reset". We only want to
1742 // catch the "never been set" case
1743 lastMatrixRecalculationAnimationTime < 0L
1744 ) {
1745 globalPosition = IntOffset(tmpPositionArray[0], tmpPositionArray[1])
1746 if (globalX != Int.MAX_VALUE && globalY != Int.MAX_VALUE) {
1747 positionChanged = true
1748 root.layoutDelegate.measurePassDelegate.notifyChildrenUsingCoordinatesWhilePlacing()
1749 }
1750 }
1751 recalculateWindowPosition()
1752 rectManager.updateOffsets(globalPosition, windowPosition.round(), viewToWindowMatrix)
1753 measureAndLayoutDelegate.dispatchOnPositionedCallbacks(forceDispatch = positionChanged)
1754 @OptIn(ExperimentalComposeUiApi::class)
1755 if (ComposeUiFlags.isRectTrackingEnabled) {
1756 rectManager.dispatchCallbacks()
1757 }
1758 }
1759
onDrawnull1760 override fun onDraw(canvas: android.graphics.Canvas) {}
1761
createLayernull1762 override fun createLayer(
1763 drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
1764 invalidateParentLayer: () -> Unit,
1765 explicitLayer: GraphicsLayer?,
1766 forceUseOldLayers: Boolean
1767 ): OwnedLayer {
1768 if (explicitLayer != null) {
1769 return GraphicsLayerOwnerLayer(
1770 graphicsLayer = explicitLayer,
1771 context = null,
1772 ownerView = this,
1773 drawBlock = drawBlock,
1774 invalidateParentLayer = invalidateParentLayer
1775 )
1776 }
1777 if (!forceUseOldLayers) {
1778 // First try the layer cache
1779 val layer = layerCache.pop()
1780 if (layer !== null) {
1781 layer.reuseLayer(drawBlock, invalidateParentLayer)
1782 return layer
1783 }
1784
1785 // Prior to M ViewLayer implementation might be doing extra drawing in order
1786 // to support the software rendering. This extra drawing is breaking some of tests
1787 // and we can't fully migrate to it until we figure out how to solve it.
1788 if (SDK_INT >= M) {
1789 return GraphicsLayerOwnerLayer(
1790 graphicsLayer = graphicsContext.createGraphicsLayer(),
1791 context = graphicsContext,
1792 ownerView = this,
1793 drawBlock = drawBlock,
1794 invalidateParentLayer = invalidateParentLayer
1795 )
1796 }
1797 }
1798
1799 // RenderNode is supported on Q+ for certain, but may also be supported on M-O.
1800 // We can't be confident that RenderNode is supported, so we try and fail over to
1801 // the ViewLayer implementation. We'll try even on on P devices, but it will fail
1802 // until ART allows things on the unsupported list on P.
1803 if (isHardwareAccelerated && SDK_INT >= M && isRenderNodeCompatible) {
1804 try {
1805 return RenderNodeLayer(this, drawBlock, invalidateParentLayer)
1806 } catch (_: Throwable) {
1807 isRenderNodeCompatible = false
1808 }
1809 }
1810 if (viewLayersContainer == null) {
1811 if (!ViewLayer.hasRetrievedMethod) {
1812 // Test to see if updateDisplayList() can be called. If this fails then
1813 // ViewLayer.shouldUseDispatchDraw will be true.
1814 ViewLayer.updateDisplayList(View(context))
1815 }
1816 viewLayersContainer =
1817 if (ViewLayer.shouldUseDispatchDraw) {
1818 DrawChildContainer(context)
1819 } else {
1820 ViewLayerContainer(context)
1821 }
1822 addView(viewLayersContainer)
1823 }
1824 return ViewLayer(this, viewLayersContainer!!, drawBlock, invalidateParentLayer)
1825 }
1826
1827 /**
1828 * Return [layer] to the layer cache. It can be reused in [createLayer] after this. Returns
1829 * `true` if it was recycled or `false` if it will be discarded.
1830 */
recyclenull1831 internal fun recycle(layer: OwnedLayer): Boolean {
1832 val cacheValue =
1833 viewLayersContainer == null ||
1834 ViewLayer.shouldUseDispatchDraw ||
1835 SDK_INT >= M // L throws during RenderThread when reusing the Views.
1836 if (cacheValue) {
1837 layerCache.push(layer)
1838 }
1839 dirtyLayers -= layer
1840 return cacheValue
1841 }
1842
onSemanticsChangenull1843 override fun onSemanticsChange() {
1844 composeAccessibilityDelegate.onSemanticsChange()
1845 contentCaptureManager.onSemanticsChange()
1846 }
1847
onLayoutChangenull1848 override fun onLayoutChange(layoutNode: LayoutNode) {
1849 composeAccessibilityDelegate.onLayoutChange(layoutNode)
1850 contentCaptureManager.onLayoutChange()
1851 }
1852
onLayoutNodeDeactivatednull1853 override fun onLayoutNodeDeactivated(layoutNode: LayoutNode) {
1854 @OptIn(ExperimentalComposeUiApi::class)
1855 if (ComposeUiFlags.isRectTrackingEnabled) {
1856 rectManager.remove(layoutNode)
1857 }
1858 @OptIn(ExperimentalComposeUiApi::class)
1859 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1860 _autofillManager?.onLayoutNodeDeactivated(layoutNode)
1861 }
1862 }
1863
onPreLayoutNodeReusednull1864 override fun onPreLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
1865 // Keep the mapping up to date when the semanticsId changes
1866 layoutNodes.remove(oldSemanticsId)
1867 layoutNodes[layoutNode.semanticsId] = layoutNode
1868 }
1869
onPostLayoutNodeReusednull1870 override fun onPostLayoutNodeReused(layoutNode: LayoutNode, oldSemanticsId: Int) {
1871 @OptIn(ExperimentalComposeUiApi::class)
1872 if (autofillSupported() && ComposeUiFlags.isSemanticAutofillEnabled) {
1873 _autofillManager?.onPostLayoutNodeReused(layoutNode, oldSemanticsId)
1874 }
1875 }
1876
onInteropViewLayoutChangenull1877 override fun onInteropViewLayoutChange(view: InteropView) {
1878 isPendingInteropViewLayoutChangeDispatch = true
1879 }
1880
registerOnLayoutCompletedListenernull1881 override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
1882 measureAndLayoutDelegate.registerOnLayoutCompletedListener(listener)
1883 scheduleMeasureAndLayout()
1884 }
1885
getFocusDirectionnull1886 override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
1887 return when (keyEvent.key) {
1888 NavigatePrevious -> Previous
1889 NavigateNext -> Next
1890 Tab -> if (keyEvent.isShiftPressed) Previous else Next
1891 DirectionRight -> Right
1892 DirectionLeft -> Left
1893 // For the initial key input of a new composable, both up/down and page up/down will
1894 // trigger the composable to get focus (so the composable can handle key events to
1895 // move focus or scroll content). Remember, composables can't receive key events without
1896 // focus.
1897 DirectionUp,
1898 PageUp -> Up
1899 DirectionDown,
1900 PageDown -> Down
1901 DirectionCenter,
1902 Key.Enter,
1903 NumPadEnter -> Enter
1904 Back,
1905 Escape -> Exit
1906 else -> null
1907 }
1908 }
1909
dispatchDrawnull1910 override fun dispatchDraw(canvas: android.graphics.Canvas) {
1911 if (!isAttachedToWindow) {
1912 invalidateLayers(root)
1913 }
1914 measureAndLayout()
1915 Snapshot.notifyObjectsInitialized()
1916
1917 isDrawingContent = true
1918 // we don't have to observe here because the root has a layer modifier
1919 // that will observe all children. The AndroidComposeView has only the
1920 // root, so it doesn't have to invalidate itself based on model changes.
1921 try {
1922 canvasHolder.drawInto(canvas) {
1923 root.draw(
1924 canvas = this,
1925 graphicsLayer = null // the root node will provide the root graphics layer
1926 )
1927 }
1928
1929 if (dirtyLayers.isNotEmpty()) {
1930 for (i in 0 until dirtyLayers.size) {
1931 val layer = dirtyLayers[i]
1932 layer.updateDisplayList()
1933 }
1934 }
1935
1936 if (ViewLayer.shouldUseDispatchDraw) {
1937 // We must update the display list of all children using dispatchDraw()
1938 // instead of updateDisplayList(). But since we don't want to actually draw
1939 // the contents, we will clip out everything from the canvas.
1940 val saveCount = canvas.save()
1941 canvas.clipRect(0f, 0f, 0f, 0f)
1942
1943 super.dispatchDraw(canvas)
1944 canvas.restoreToCount(saveCount)
1945 }
1946
1947 dirtyLayers.clear()
1948 isDrawingContent = false
1949 } catch (t: Throwable) {
1950 uncaughtExceptionHandler?.onUncaughtException(t) ?: throw t
1951 }
1952
1953 // updateDisplayList operations performed above (during root.draw and during the explicit
1954 // layer.updateDisplayList() calls) can result in the same layers being invalidated. These
1955 // layers have been added to postponedDirtyLayers and will be redrawn during the next
1956 // dispatchDraw.
1957 if (postponedDirtyLayers != null) {
1958 val postponed = postponedDirtyLayers!!
1959 dirtyLayers.addAll(postponed)
1960 postponed.clear()
1961 }
1962
1963 // Used to handle frame rate information
1964 if (isArrEnabled) {
1965 super.setRequestedFrameRate(currentFrameRate)
1966 frameRateCategoryView.requestedFrameRate = currentFrameRateCategory
1967
1968 if (!currentFrameRateCategory.isNaN()) {
1969 frameRateCategoryView.invalidate()
1970 drawChild(canvas, frameRateCategoryView, drawingTime)
1971 }
1972
1973 currentFrameRate = Float.NaN
1974 currentFrameRateCategory = Float.NaN
1975 }
1976 }
1977
notifyLayerIsDirtynull1978 internal fun notifyLayerIsDirty(layer: OwnedLayer, isDirty: Boolean) {
1979 if (!isDirty) {
1980 // It is correct to remove the layer here regardless of this if, but for performance
1981 // we are hackily not doing the removal here in order to just do clear() a bit later.
1982 if (!isDrawingContent) {
1983 dirtyLayers.remove(layer)
1984 postponedDirtyLayers?.remove(layer)
1985 }
1986 } else if (!isDrawingContent) {
1987 dirtyLayers += layer
1988 } else {
1989 val postponed =
1990 postponedDirtyLayers
1991 ?: mutableListOf<OwnedLayer>().also { postponedDirtyLayers = it }
1992 postponed += layer
1993 }
1994 }
1995
1996 /**
1997 * The callback to be executed when [viewTreeOwners] is created and not-null anymore. Note that
1998 * this callback will be fired inline when it is already available
1999 */
setOnViewTreeOwnersAvailablenull2000 fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit) {
2001 val viewTreeOwners = viewTreeOwners
2002 if (viewTreeOwners != null) {
2003 callback(viewTreeOwners)
2004 }
2005 if (!isAttachedToWindow) {
2006 onViewTreeOwnersAvailable = callback
2007 }
2008 }
2009
2010 // TODO(mnuzen): combine both event loops into one larger one
boundsUpdatesContentCaptureEventLoopnull2011 suspend fun boundsUpdatesContentCaptureEventLoop() {
2012 contentCaptureManager.boundsUpdatesEventLoop()
2013 }
2014
boundsUpdatesAccessibilityEventLoopnull2015 suspend fun boundsUpdatesAccessibilityEventLoop() {
2016 composeAccessibilityDelegate.boundsUpdatesEventLoop()
2017 }
2018
2019 /** Walks the entire LayoutNode sub-hierarchy and marks all nodes as needing measurement. */
invalidateLayoutNodeMeasurementnull2020 private fun invalidateLayoutNodeMeasurement(node: LayoutNode) {
2021 measureAndLayoutDelegate.requestRemeasure(node)
2022 node.forEachChild { invalidateLayoutNodeMeasurement(it) }
2023 }
2024
2025 /** Walks the entire LayoutNode sub-hierarchy and marks all layers as needing to be redrawn. */
invalidateLayersnull2026 private fun invalidateLayers(node: LayoutNode) {
2027 node.invalidateLayers()
2028 node.forEachChild { invalidateLayers(it) }
2029 }
2030
invalidateDescendantsnull2031 override fun invalidateDescendants() {
2032 invalidateLayers(root)
2033 }
2034
onAttachedToWindownull2035 override fun onAttachedToWindow() {
2036 super.onAttachedToWindow()
2037 if (SDK_INT < 30) {
2038 showLayoutBounds = getIsShowingLayoutBounds()
2039 }
2040 addNotificationForSysPropsChange(this)
2041 _windowInfo.isWindowFocused = hasWindowFocus()
2042 _windowInfo.setOnInitializeContainerSize { calculateWindowSize(this) }
2043 updateWindowMetrics()
2044 invalidateLayoutNodeMeasurement(root)
2045 invalidateLayers(root)
2046 snapshotObserver.startObserving()
2047 ifDebug {
2048 if (autofillSupported()) {
2049 // TODO(b/333102566): Use _semanticAutofill after switching to the newer Autofill
2050 // system.
2051 _autofill?.let { AutofillCallback.register(it) }
2052 }
2053 }
2054
2055 val lifecycleOwner = findViewTreeLifecycleOwner()
2056 val savedStateRegistryOwner = findViewTreeSavedStateRegistryOwner()
2057
2058 val oldViewTreeOwners = viewTreeOwners
2059 // We need to change the ViewTreeOwner if there isn't one yet (null)
2060 // or if either the lifecycleOwner or savedStateRegistryOwner has changed.
2061 val resetViewTreeOwner =
2062 oldViewTreeOwners == null ||
2063 ((lifecycleOwner != null && savedStateRegistryOwner != null) &&
2064 (lifecycleOwner !== oldViewTreeOwners.lifecycleOwner ||
2065 savedStateRegistryOwner !== oldViewTreeOwners.lifecycleOwner))
2066 if (resetViewTreeOwner) {
2067 if (lifecycleOwner == null) {
2068 throw IllegalStateException(
2069 "Composed into the View which doesn't propagate ViewTreeLifecycleOwner!"
2070 )
2071 }
2072 if (savedStateRegistryOwner == null) {
2073 throw IllegalStateException(
2074 "Composed into the View which doesn't propagate" +
2075 "ViewTreeSavedStateRegistryOwner!"
2076 )
2077 }
2078 oldViewTreeOwners?.lifecycleOwner?.lifecycle?.removeObserver(this)
2079 lifecycleOwner.lifecycle.addObserver(this)
2080 val viewTreeOwners =
2081 ViewTreeOwners(
2082 lifecycleOwner = lifecycleOwner,
2083 savedStateRegistryOwner = savedStateRegistryOwner
2084 )
2085 _viewTreeOwners = viewTreeOwners
2086 onViewTreeOwnersAvailable?.invoke(viewTreeOwners)
2087 onViewTreeOwnersAvailable = null
2088 }
2089
2090 _inputModeManager.inputMode = if (isInTouchMode) Touch else Keyboard
2091
2092 val lifecycle =
2093 checkPreconditionNotNull(viewTreeOwners?.lifecycleOwner?.lifecycle) {
2094 "No lifecycle owner exists"
2095 }
2096 lifecycle.addObserver(this)
2097 lifecycle.addObserver(contentCaptureManager)
2098 viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
2099 viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
2100 viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
2101
2102 if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(this)
2103 _autofillManager?.let {
2104 focusOwner.listeners += it
2105 semanticsOwner.listeners += it
2106 }
2107 }
2108
onDetachedFromWindownull2109 override fun onDetachedFromWindow() {
2110 super.onDetachedFromWindow()
2111 if (isArrEnabled) {
2112 removeView(frameRateCategoryView)
2113 }
2114
2115 removeNotificationForSysPropsChange(this)
2116 snapshotObserver.stopObserving()
2117 _windowInfo.setOnInitializeContainerSize(null)
2118 val lifecycle =
2119 checkPreconditionNotNull(viewTreeOwners?.lifecycleOwner?.lifecycle) {
2120 "No lifecycle owner exists"
2121 }
2122 lifecycle.removeObserver(contentCaptureManager)
2123 lifecycle.removeObserver(this)
2124 ifDebug {
2125 if (autofillSupported()) {
2126 // TODO(b/333102566): Use _semanticAutofill after switching to the newer Autofill
2127 // system.
2128 _autofill?.let { AutofillCallback.unregister(it) }
2129 }
2130 }
2131 viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
2132 viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
2133 viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
2134
2135 if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
2136 _autofillManager?.let {
2137 semanticsOwner.listeners -= it
2138 focusOwner.listeners -= it
2139 }
2140 }
2141
onProvideAutofillVirtualStructurenull2142 override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
2143 if (autofillSupported() && structure != null) {
2144 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
2145 _autofillManager?.populateViewStructure(structure)
2146 }
2147 _autofill?.populateViewStructure(structure)
2148 }
2149 }
2150
autofillnull2151 override fun autofill(values: SparseArray<AutofillValue>) {
2152 if (autofillSupported()) {
2153 if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
2154 _autofillManager?.performAutofill(values)
2155 }
2156 _autofill?.performAutofill(values)
2157 }
2158 }
2159
2160 @RequiresApi(S)
onCreateVirtualViewTranslationRequestsnull2161 override fun onCreateVirtualViewTranslationRequests(
2162 virtualIds: LongArray,
2163 supportedFormats: IntArray,
2164 requestsCollector: Consumer<ViewTranslationRequest?>
2165 ) {
2166 contentCaptureManager.onCreateVirtualViewTranslationRequests(
2167 virtualIds,
2168 supportedFormats,
2169 requestsCollector
2170 )
2171 }
2172
2173 @RequiresApi(S)
onVirtualViewTranslationResponsesnull2174 override fun onVirtualViewTranslationResponses(
2175 response: LongSparseArray<ViewTranslationResponse?>
2176 ) {
2177 contentCaptureManager.onVirtualViewTranslationResponses(contentCaptureManager, response)
2178 }
2179
dispatchGenericMotionEventnull2180 override fun dispatchGenericMotionEvent(motionEvent: MotionEvent): Boolean {
2181 if (hoverExitReceived) {
2182 removeCallbacks(sendHoverExitEvent)
2183 // Ignore ACTION_HOVER_EXIT if it is directly followed by an ACTION_SCROLL.
2184 // Note: In some versions of Android Studio with screen mirroring, studio will
2185 // incorrectly add an ACTION_HOVER_EXIT during a scroll event which causes
2186 // issues (b/314269723), so we ignore the exit in that case.
2187 if (motionEvent.actionMasked == ACTION_SCROLL) {
2188 hoverExitReceived = false
2189 } else {
2190 sendHoverExitEvent.run()
2191 }
2192 }
2193 if (isBadMotionEvent(motionEvent) || !isAttachedToWindow) {
2194 return super.dispatchGenericMotionEvent(motionEvent)
2195 }
2196
2197 return when (motionEvent.actionMasked) {
2198 ACTION_SCROLL ->
2199 if (motionEvent.isFromSource(SOURCE_ROTARY_ENCODER)) {
2200 handleRotaryEvent(motionEvent)
2201 } else {
2202 handleMotionEvent(motionEvent).dispatchedToAPointerInputModifier
2203 }
2204 else -> {
2205 @OptIn(ExperimentalComposeUiApi::class)
2206 if (!motionEvent.isFromSource(SOURCE_CLASS_POINTER)) {
2207 val indirectTouchEvent =
2208 IndirectTouchEvent(
2209 position = Offset(motionEvent.x, motionEvent.y),
2210 eventTimeMillis = motionEvent.eventTime,
2211 type = convertActionToIndirectTouchEventType(motionEvent.actionMasked),
2212 )
2213 val handled =
2214 focusOwner.dispatchIndirectTouchEvent(indirectTouchEvent) {
2215 super.dispatchGenericMotionEvent(motionEvent)
2216 }
2217
2218 if (handled) return true
2219 }
2220
2221 // If focus owner did not handle, rely on ViewGroup to handle.
2222 super.dispatchGenericMotionEvent(motionEvent)
2223 }
2224 }
2225 }
2226
2227 @OptIn(ExperimentalComposeUiApi::class)
convertActionToIndirectTouchEventTypenull2228 private fun convertActionToIndirectTouchEventType(actionMasked: Int): IndirectTouchEventType {
2229 return when (actionMasked) {
2230 ACTION_UP -> IndirectTouchEventType.Release
2231 ACTION_DOWN -> IndirectTouchEventType.Press
2232 ACTION_MOVE -> IndirectTouchEventType.Move
2233 else -> IndirectTouchEventType.Unknown
2234 }
2235 }
2236
2237 // TODO(shepshapard): Test this method.
2238 @OptIn(ExperimentalComposeUiApi::class)
dispatchTouchEventnull2239 override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
2240 if (ComposeUiFlags.isHitPathTrackerLoggingEnabled) {
2241 println("POINTER_INPUT_DEBUG_LOG_TAG AndroidComposeView.dispatchTouchEvent()")
2242 println("POINTER_INPUT_DEBUG_LOG_TAG \t\tmotionEvent: $motionEvent")
2243 }
2244
2245 if (hoverExitReceived) {
2246 // Go ahead and send ACTION_HOVER_EXIT if this isn't an ACTION_DOWN for the same
2247 // pointer
2248 removeCallbacks(sendHoverExitEvent)
2249 val lastEvent = previousMotionEvent!!
2250 if (
2251 motionEvent.actionMasked != ACTION_DOWN || hasChangedDevices(motionEvent, lastEvent)
2252 ) {
2253 sendHoverExitEvent.run()
2254 } else {
2255 hoverExitReceived = false
2256 }
2257 }
2258 if (isBadMotionEvent(motionEvent) || !isAttachedToWindow) {
2259 return false // Bad MotionEvent. Don't handle it.
2260 }
2261
2262 if (motionEvent.actionMasked == ACTION_MOVE && !isPositionChanged(motionEvent)) {
2263 // There was no movement from previous MotionEvent, so we don't need to dispatch this.
2264 // This could be a scroll event or some other non-touch event that results in an
2265 // ACTION_MOVE without any movement.
2266 return false
2267 }
2268
2269 val processResult = handleMotionEvent(motionEvent)
2270
2271 if (processResult.anyMovementConsumed) {
2272 parent.requestDisallowInterceptTouchEvent(true)
2273 }
2274
2275 return processResult.dispatchedToAPointerInputModifier
2276 }
2277
handleRotaryEventnull2278 private fun handleRotaryEvent(event: MotionEvent): Boolean {
2279 val config = android.view.ViewConfiguration.get(context)
2280 val axisValue = -event.getAxisValue(AXIS_SCROLL)
2281 val rotaryEvent =
2282 RotaryScrollEvent(
2283 verticalScrollPixels = axisValue * getScaledVerticalScrollFactor(config, context),
2284 horizontalScrollPixels =
2285 axisValue * getScaledHorizontalScrollFactor(config, context),
2286 uptimeMillis = event.eventTime,
2287 inputDeviceId = event.deviceId
2288 )
2289 return focusOwner.dispatchRotaryEvent(rotaryEvent) {
2290 super.dispatchGenericMotionEvent(event)
2291 }
2292 }
2293
handleMotionEventnull2294 private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {
2295 removeCallbacks(resendMotionEventRunnable)
2296 try {
2297 recalculateWindowPosition(motionEvent)
2298 forceUseMatrixCache = true
2299 measureAndLayout(sendPointerUpdate = false)
2300 val result =
2301 trace("AndroidOwner:onTouch") {
2302 val action = motionEvent.actionMasked
2303 val lastEvent = previousMotionEvent
2304
2305 val wasMouseEvent = lastEvent?.getToolType(0) == TOOL_TYPE_MOUSE
2306 if (lastEvent != null && hasChangedDevices(motionEvent, lastEvent)) {
2307 if (isDevicePressEvent(lastEvent)) {
2308 // Send a cancel event
2309 pointerInputEventProcessor.processCancel()
2310 } else if (lastEvent.actionMasked != ACTION_HOVER_EXIT && wasMouseEvent) {
2311 // The mouse cursor disappeared without sending an ACTION_HOVER_EXIT, so
2312 // we have to send that event.
2313 sendSimulatedEvent(lastEvent, ACTION_HOVER_EXIT, lastEvent.eventTime)
2314 }
2315 }
2316
2317 val isMouseEvent = motionEvent.getToolType(0) == TOOL_TYPE_MOUSE
2318
2319 if (
2320 !wasMouseEvent &&
2321 isMouseEvent &&
2322 action != ACTION_CANCEL &&
2323 action != ACTION_HOVER_ENTER &&
2324 isInBounds(motionEvent)
2325 ) {
2326 // We didn't previously have an enter event and we're getting our first
2327 // mouse event. Send a simulated enter event so that we have a consistent
2328 // enter/exit.
2329 sendSimulatedEvent(motionEvent, ACTION_HOVER_ENTER, motionEvent.eventTime)
2330 }
2331 lastEvent?.recycle()
2332
2333 // If the previous MotionEvent was an ACTION_HOVER_EXIT, we need to check if it
2334 // was a synthetic MotionEvent generated by the platform for an ACTION_DOWN
2335 // event
2336 // or not.
2337 //
2338 // If it was synthetic, we do nothing, because we want to keep the existing
2339 // cache
2340 // of "Hit" Modifier.Node(s) from the previous hover events, so we can reuse
2341 // them
2342 // once an ACTION_UP event is triggered and we return to the same hover state
2343 // (cache improves performance for this frequent event sequence with a mouse).
2344 //
2345 // If it was NOT synthetic, we end the event stream in MotionEventAdapter and
2346 // clear
2347 // the hit cache used in PointerInputEventProcessor (specifically, the
2348 // HitPathTracker cache inside PointerInputEventProcessor), so events in this
2349 // new
2350 // stream do not trigger Modifier.Node(s) hit by the previous stream.
2351 if (previousMotionEvent?.action == ACTION_HOVER_EXIT) {
2352 val previousEventDefaultPointerId =
2353 previousMotionEvent?.getPointerId(0) ?: -1
2354
2355 // New ACTION_HOVER_ENTER, so this should be considered a new stream
2356 if (
2357 motionEvent.action == ACTION_HOVER_ENTER && motionEvent.historySize == 0
2358 ) {
2359 if (previousEventDefaultPointerId >= 0) {
2360 motionEventAdapter.endStream(previousEventDefaultPointerId)
2361 }
2362 } else if (
2363 motionEvent.action == ACTION_DOWN && motionEvent.historySize == 0
2364 ) {
2365 val previousX = previousMotionEvent?.x ?: Float.NaN
2366 val previousY = previousMotionEvent?.y ?: Float.NaN
2367
2368 val currentX = motionEvent.x
2369 val currentY = motionEvent.y
2370
2371 val previousAndCurrentCoordinatesDoNotMatch =
2372 (previousX != currentX || previousY != currentY)
2373
2374 val previousEventTime = previousMotionEvent?.eventTime ?: -1L
2375
2376 val previousAndCurrentEventTimesDoNotMatch =
2377 previousEventTime != motionEvent.eventTime
2378
2379 // A synthetically created Hover Exit event will always have the same x,
2380 // y, and timestamp as the down event it proceeds.
2381 val previousHoverEventWasNotSyntheticallyProducedFromADownEvent =
2382 previousAndCurrentCoordinatesDoNotMatch ||
2383 previousAndCurrentEventTimesDoNotMatch
2384
2385 if (previousHoverEventWasNotSyntheticallyProducedFromADownEvent) {
2386 // This should be considered a new stream, and we should
2387 // reset everything.
2388 if (previousEventDefaultPointerId >= 0) {
2389 motionEventAdapter.endStream(previousEventDefaultPointerId)
2390 }
2391 pointerInputEventProcessor.clearPreviouslyHitModifierNodes()
2392 }
2393 }
2394 }
2395
2396 previousMotionEvent = MotionEvent.obtainNoHistory(motionEvent)
2397
2398 sendMotionEvent(motionEvent)
2399 }
2400 return result
2401 } finally {
2402 forceUseMatrixCache = false
2403 }
2404 }
2405
hasChangedDevicesnull2406 private fun hasChangedDevices(event: MotionEvent, lastEvent: MotionEvent): Boolean {
2407 return lastEvent.source != event.source || lastEvent.getToolType(0) != event.getToolType(0)
2408 }
2409
isDevicePressEventnull2410 private fun isDevicePressEvent(event: MotionEvent): Boolean {
2411 if (event.buttonState != 0) {
2412 return true
2413 }
2414 return when (event.actionMasked) {
2415 ACTION_POINTER_UP, // means that there is at least one remaining pointer
2416 ACTION_DOWN,
2417 ACTION_MOVE -> true
2418 // ACTION_SCROLL, // We've already checked for buttonState, so it must not be
2419 // down
2420 // ACTION_HOVER_ENTER,
2421 // ACTION_HOVER_MOVE,
2422 // ACTION_HOVER_EXIT,
2423 // ACTION_UP,
2424 // ACTION_CANCEL,
2425 else -> false
2426 }
2427 }
2428
2429 @OptIn(InternalCoreApi::class)
sendMotionEventnull2430 private fun sendMotionEvent(motionEvent: MotionEvent): ProcessResult {
2431 if (keyboardModifiersRequireUpdate) {
2432 keyboardModifiersRequireUpdate = false
2433 _windowInfo.keyboardModifiers = PointerKeyboardModifiers(motionEvent.metaState)
2434 }
2435 val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent, this)
2436 return if (pointerInputEvent != null) {
2437 // Cache the last position of the last pointer to go down so we can check if
2438 // it's in a scrollable region in canScroll{Vertically|Horizontally}. Those
2439 // methods use semantics data, and because semantics coordinates are local to
2440 // this view, the pointer _position_, not _positionOnScreen_, is the offset that
2441 // needs to be cached.
2442 pointerInputEvent.pointers
2443 .fastLastOrNull { it.down }
2444 ?.position
2445 ?.let { lastDownPointerPosition = it }
2446
2447 val result =
2448 pointerInputEventProcessor.process(pointerInputEvent, this, isInBounds(motionEvent))
2449 // Clear the MotionEvent reference after dispatching it.
2450 pointerInputEvent.motionEvent = null
2451 val action = motionEvent.actionMasked
2452 if (
2453 (action == ACTION_DOWN || action == ACTION_POINTER_DOWN) &&
2454 !result.dispatchedToAPointerInputModifier
2455 ) {
2456 // We aren't handling the pointer, so the event stream has ended for us.
2457 // The next time we receive a pointer event, it should be considered a new
2458 // pointer.
2459 motionEventAdapter.endStream(motionEvent.getPointerId(motionEvent.actionIndex))
2460 }
2461 result
2462 } else {
2463 pointerInputEventProcessor.processCancel()
2464 ProcessResult(dispatchedToAPointerInputModifier = false, anyMovementConsumed = false)
2465 }
2466 }
2467
2468 @OptIn(InternalCoreApi::class)
sendSimulatedEventnull2469 private fun sendSimulatedEvent(
2470 motionEvent: MotionEvent,
2471 action: Int,
2472 eventTime: Long,
2473 forceHover: Boolean = true
2474 ) {
2475 // don't send any events for pointers that are "up" unless they support hover
2476 val upIndex =
2477 when (motionEvent.actionMasked) {
2478 ACTION_UP ->
2479 if (action == ACTION_HOVER_ENTER || action == ACTION_HOVER_EXIT) -1 else 0
2480 ACTION_POINTER_UP -> motionEvent.actionIndex
2481 else -> -1
2482 }
2483 val pointerCount = motionEvent.pointerCount - if (upIndex >= 0) 1 else 0
2484 if (pointerCount == 0) {
2485 return
2486 }
2487 val pointerProperties = Array(pointerCount) { MotionEvent.PointerProperties() }
2488 val pointerCoords = Array(pointerCount) { MotionEvent.PointerCoords() }
2489 for (i in 0 until pointerCount) {
2490 val sourceIndex = i + if (upIndex < 0 || i < upIndex) 0 else 1
2491 motionEvent.getPointerProperties(sourceIndex, pointerProperties[i])
2492 val coords = pointerCoords[i]
2493 motionEvent.getPointerCoords(sourceIndex, coords)
2494 val localPosition = Offset(coords.x, coords.y)
2495 val screenPosition = localToScreen(localPosition)
2496 coords.x = screenPosition.x
2497 coords.y = screenPosition.y
2498 }
2499 val buttonState = if (forceHover) 0 else motionEvent.buttonState
2500
2501 val downTime =
2502 if (motionEvent.downTime == motionEvent.eventTime) {
2503 eventTime
2504 } else {
2505 motionEvent.downTime
2506 }
2507 val event =
2508 MotionEvent.obtain(
2509 /* downTime */ downTime,
2510 /* eventTime */ eventTime,
2511 /* action */ action,
2512 /* pointerCount */ pointerCount,
2513 /* pointerProperties */ pointerProperties,
2514 /* pointerCoords */ pointerCoords,
2515 /* metaState */ motionEvent.metaState,
2516 /* buttonState */ buttonState,
2517 /* xPrecision */ motionEvent.xPrecision,
2518 /* yPrecision */ motionEvent.yPrecision,
2519 /* deviceId */ motionEvent.deviceId,
2520 /* edgeFlags */ motionEvent.edgeFlags,
2521 /* source */ motionEvent.source,
2522 /* flags */ motionEvent.flags
2523 )
2524 val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(event, this)!!
2525
2526 pointerInputEventProcessor.process(pointerInputEvent, this, true)
2527 event.recycle()
2528 }
2529
2530 /**
2531 * This method is required to correctly support swipe-to-dismiss layouts on WearOS, which search
2532 * their children for scrollable views to determine whether or not to intercept touch events – a
2533 * sort of simplified nested scrolling mechanism.
2534 *
2535 * Because a composition may contain many scrollable and non-scrollable areas, and this method
2536 * doesn't know which part of the view the caller cares about, it uses the
2537 * [lastDownPointerPosition] as the location to check.
2538 */
canScrollHorizontallynull2539 override fun canScrollHorizontally(direction: Int): Boolean =
2540 composeAccessibilityDelegate.canScroll(vertical = false, direction, lastDownPointerPosition)
2541
2542 /** See [canScrollHorizontally]. */
2543 override fun canScrollVertically(direction: Int): Boolean =
2544 composeAccessibilityDelegate.canScroll(vertical = true, direction, lastDownPointerPosition)
2545
2546 private fun isInBounds(motionEvent: MotionEvent): Boolean {
2547 val x = motionEvent.x
2548 val y = motionEvent.y
2549 return (x in 0f..width.toFloat() && y in 0f..height.toFloat())
2550 }
2551
localToScreennull2552 override fun localToScreen(localPosition: Offset): Offset {
2553 recalculateWindowPosition()
2554 val local = viewToWindowMatrix.map(localPosition)
2555 return Offset(local.x + windowPosition.x, local.y + windowPosition.y)
2556 }
2557
localToScreennull2558 override fun localToScreen(localTransform: Matrix) {
2559 recalculateWindowPosition()
2560 localTransform.timesAssign(viewToWindowMatrix)
2561 localTransform.preTranslate(windowPosition.x, windowPosition.y, tmpMatrix)
2562 }
2563
screenToLocalnull2564 override fun screenToLocal(positionOnScreen: Offset): Offset {
2565 recalculateWindowPosition()
2566 val x = positionOnScreen.x - windowPosition.x
2567 val y = positionOnScreen.y - windowPosition.y
2568 return windowToViewMatrix.map(Offset(x, y))
2569 }
2570
recalculateWindowPositionnull2571 private fun recalculateWindowPosition() {
2572 if (!forceUseMatrixCache) {
2573 val animationTime = AnimationUtils.currentAnimationTimeMillis()
2574 if (animationTime != lastMatrixRecalculationAnimationTime) {
2575 lastMatrixRecalculationAnimationTime = animationTime
2576 recalculateWindowViewTransforms()
2577 var viewParent = parent
2578 var view: View = this
2579 while (viewParent is ViewGroup) {
2580 view = viewParent
2581 viewParent = view.parent
2582 }
2583 view.getLocationOnScreen(tmpPositionArray)
2584 val screenX = tmpPositionArray[0].toFloat()
2585 val screenY = tmpPositionArray[1].toFloat()
2586 view.getLocationInWindow(tmpPositionArray)
2587 val windowX = tmpPositionArray[0].toFloat()
2588 val windowY = tmpPositionArray[1].toFloat()
2589 windowPosition = Offset(screenX - windowX, screenY - windowY)
2590 }
2591 }
2592 }
2593
2594 /**
2595 * Recalculates the window position based on the [motionEvent]'s coordinates and screen
2596 * coordinates. Some devices give false positions for [getLocationOnScreen] in some unusual
2597 * circumstances, so a different mechanism must be used to determine the actual position.
2598 */
recalculateWindowPositionnull2599 private fun recalculateWindowPosition(motionEvent: MotionEvent) {
2600 lastMatrixRecalculationAnimationTime = AnimationUtils.currentAnimationTimeMillis()
2601 recalculateWindowViewTransforms()
2602 val positionInWindow = viewToWindowMatrix.map(Offset(motionEvent.x, motionEvent.y))
2603
2604 windowPosition =
2605 Offset(motionEvent.rawX - positionInWindow.x, motionEvent.rawY - positionInWindow.y)
2606 }
2607
recalculateWindowViewTransformsnull2608 private fun recalculateWindowViewTransforms() {
2609 matrixToWindow.calculateMatrixToWindow(this, viewToWindowMatrix)
2610 viewToWindowMatrix.invertTo(windowToViewMatrix)
2611 }
2612
updateWindowMetricsnull2613 private fun updateWindowMetrics() {
2614 _windowInfo.updateContainerSizeIfObserved { calculateWindowSize(this) }
2615 }
2616
onCheckIsTextEditornull2617 override fun onCheckIsTextEditor(): Boolean {
2618 val parentSession =
2619 textInputSessionMutex.currentSession
2620 ?: return legacyTextInputServiceAndroid.isEditorFocused()
2621 // Don't bring this up before the ?: – establishTextInputSession has been called, but
2622 // startInputMethod has not, we're not a text editor until the session is cancelled or
2623 // startInputMethod is called.
2624 return parentSession.isReadyForConnection
2625 }
2626
onCreateInputConnectionnull2627 override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
2628 val parentSession =
2629 textInputSessionMutex.currentSession
2630 ?: return legacyTextInputServiceAndroid.createInputConnection(outAttrs)
2631 // Don't bring this up before the ?: - if this returns null, we SHOULD NOT fall back to
2632 // the legacy input system.
2633 return parentSession.createInputConnection(outAttrs)
2634 }
2635
calculateLocalPositionnull2636 override fun calculateLocalPosition(positionInWindow: Offset): Offset {
2637 recalculateWindowPosition()
2638 return windowToViewMatrix.map(positionInWindow)
2639 }
2640
calculatePositionInWindownull2641 override fun calculatePositionInWindow(localPosition: Offset): Offset {
2642 recalculateWindowPosition()
2643 return viewToWindowMatrix.map(localPosition)
2644 }
2645
onConfigurationChangednull2646 override fun onConfigurationChanged(newConfig: Configuration) {
2647 super.onConfigurationChanged(newConfig)
2648 density = Density(context)
2649 updateWindowMetrics()
2650 if (newConfig.fontWeightAdjustmentCompat != currentFontWeightAdjustment) {
2651 currentFontWeightAdjustment = newConfig.fontWeightAdjustmentCompat
2652 fontFamilyResolver = createFontFamilyResolver(context)
2653 }
2654 configurationChangeObserver(newConfig)
2655 }
2656
onRtlPropertiesChangednull2657 override fun onRtlPropertiesChanged(layoutDirection: Int) {
2658 // This method can be called while View's constructor is running
2659 // by way of resolving padding in response to initScrollbars.
2660 // If we get such a call, don't try to write to a property delegate
2661 // that hasn't been initialized yet.
2662 if (superclassInitComplete) {
2663 this.layoutDirection = toLayoutDirection(layoutDirection) ?: LayoutDirection.Ltr
2664 }
2665 }
2666
autofillSupportednull2667 private fun autofillSupported() = SDK_INT >= O
2668
2669 public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
2670 if (hoverExitReceived) {
2671 // Go ahead and send it now
2672 removeCallbacks(sendHoverExitEvent)
2673 sendHoverExitEvent.run()
2674 }
2675 if (isBadMotionEvent(event) || !isAttachedToWindow) {
2676 return false // Bad MotionEvent. Don't handle it.
2677 }
2678
2679 // Always call accessibilityDelegate dispatchHoverEvent (since accessibilityDelegate's
2680 // dispatchHoverEvent only runs if touch exploration is enabled)
2681 composeAccessibilityDelegate.dispatchHoverEvent(event)
2682
2683 when (event.actionMasked) {
2684 ACTION_HOVER_EXIT -> {
2685 if (isInBounds(event)) {
2686 if (event.getToolType(0) == TOOL_TYPE_MOUSE && event.buttonState != 0) {
2687 // We know that this is caused by a mouse button press, so we can ignore it
2688 return false
2689 }
2690
2691 // This may be caused by a press (e.g. stylus pressed on the screen), but
2692 // we can't be sure until the ACTION_DOWN is received. Let's delay this
2693 // message and see if the ACTION_DOWN comes.
2694 previousMotionEvent?.recycle()
2695 previousMotionEvent = MotionEvent.obtainNoHistory(event)
2696 hoverExitReceived = true
2697 // There are cases where the hover exit will incorrectly trigger because this
2698 // post is called right before the end of the frame and the new frame checks for
2699 // a press/down event (which hasn't occurred yet). Therefore, we delay the post
2700 // call a small amount to account for that.
2701 postDelayed(sendHoverExitEvent, ONE_FRAME_120_HERTZ_IN_MILLISECONDS)
2702 return false
2703 }
2704 }
2705 ACTION_HOVER_MOVE ->
2706 // Check if we're receiving this when we've already handled it elsewhere
2707 if (!isPositionChanged(event)) {
2708 return false
2709 }
2710 }
2711 val result = handleMotionEvent(event)
2712 return result.dispatchedToAPointerInputModifier
2713 }
2714
isBadMotionEventnull2715 private fun isBadMotionEvent(event: MotionEvent): Boolean {
2716 var eventInvalid =
2717 !event.x.fastIsFinite() ||
2718 !event.y.fastIsFinite() ||
2719 !event.rawX.fastIsFinite() ||
2720 !event.rawY.fastIsFinite()
2721
2722 if (!eventInvalid) {
2723 // First event x,y is checked above if block, so we can skip index 0.
2724 for (index in 1 until event.pointerCount) {
2725 eventInvalid =
2726 !event.getX(index).fastIsFinite() ||
2727 !event.getY(index).fastIsFinite() ||
2728 (SDK_INT >= Q && !isValidMotionEvent(event, index))
2729
2730 if (eventInvalid) break
2731 }
2732 }
2733
2734 return eventInvalid
2735 }
2736
isPositionChangednull2737 private fun isPositionChanged(event: MotionEvent): Boolean {
2738 if (event.pointerCount != 1) {
2739 return true
2740 }
2741 val lastEvent = previousMotionEvent
2742 return lastEvent == null ||
2743 lastEvent.pointerCount != event.pointerCount ||
2744 event.rawX != lastEvent.rawX ||
2745 event.rawY != lastEvent.rawY
2746 }
2747
findViewByAccessibilityIdRootedAtCurrentViewnull2748 private fun findViewByAccessibilityIdRootedAtCurrentView(
2749 accessibilityId: Int,
2750 currentView: View
2751 ): View? {
2752 if (SDK_INT < Q) {
2753 val getAccessibilityViewIdMethod =
2754 Class.forName("android.view.View").getDeclaredMethod("getAccessibilityViewId")
2755 getAccessibilityViewIdMethod.isAccessible = true
2756 if (getAccessibilityViewIdMethod.invoke(currentView) == accessibilityId) {
2757 return currentView
2758 }
2759 if (currentView is ViewGroup) {
2760 for (i in 0 until currentView.childCount) {
2761 val foundView =
2762 findViewByAccessibilityIdRootedAtCurrentView(
2763 accessibilityId,
2764 currentView.getChildAt(i)
2765 )
2766 if (foundView != null) {
2767 return foundView
2768 }
2769 }
2770 }
2771 }
2772 return null
2773 }
2774
2775 @RequiresApi(N)
onResolvePointerIconnull2776 override fun onResolvePointerIcon(
2777 event: MotionEvent,
2778 pointerIndex: Int
2779 ): android.view.PointerIcon {
2780 val toolType = event.getToolType(pointerIndex)
2781 if (
2782 !event.isFromSource(InputDevice.SOURCE_MOUSE) &&
2783 event.isFromSource(InputDevice.SOURCE_STYLUS) &&
2784 (toolType == MotionEvent.TOOL_TYPE_STYLUS ||
2785 toolType == MotionEvent.TOOL_TYPE_ERASER)
2786 ) {
2787 val icon = pointerIconService.getStylusHoverIcon()
2788 if (icon != null) {
2789 return AndroidComposeViewVerificationHelperMethodsN.toAndroidPointerIcon(
2790 context,
2791 icon
2792 )
2793 }
2794 }
2795 return super.onResolvePointerIcon(event, pointerIndex)
2796 }
2797
2798 override val pointerIconService: PointerIconService =
2799 object : PointerIconService {
2800 private var currentMouseCursorIcon: PointerIcon = PointerIcon.Default
2801 private var currentStylusHoverIcon: PointerIcon? = null
2802
getIconnull2803 override fun getIcon(): PointerIcon {
2804 return currentMouseCursorIcon
2805 }
2806
setIconnull2807 override fun setIcon(value: PointerIcon?) {
2808 currentMouseCursorIcon = value ?: PointerIcon.Default
2809 if (SDK_INT >= N) {
2810 AndroidComposeViewVerificationHelperMethodsN.setPointerIcon(
2811 this@AndroidComposeView,
2812 currentMouseCursorIcon
2813 )
2814 }
2815 }
2816
getStylusHoverIconnull2817 override fun getStylusHoverIcon(): PointerIcon? {
2818 return currentStylusHoverIcon
2819 }
2820
setStylusHoverIconnull2821 override fun setStylusHoverIcon(value: PointerIcon?) {
2822 currentStylusHoverIcon = value
2823 }
2824 }
2825
2826 /**
2827 * This overrides an @hide method in ViewGroup. Because of the @hide, the override keyword
2828 * cannot be used, but the override works anyway because the ViewGroup method is not final. In
2829 * Android P and earlier, the call path is
2830 * AccessibilityInteractionController#findViewByAccessibilityId ->
2831 * View#findViewByAccessibilityId -> ViewGroup#findViewByAccessibilityIdTraversal. In Android Q
2832 * and later, AccessibilityInteractionController#findViewByAccessibilityId uses
2833 * AccessibilityNodeIdManager and findViewByAccessibilityIdTraversal is only used by autofill.
2834 */
2835 @Suppress("BanHideTag")
findViewByAccessibilityIdTraversalnull2836 fun findViewByAccessibilityIdTraversal(accessibilityId: Int): View? {
2837 try {
2838 // AccessibilityInteractionController#findViewByAccessibilityId doesn't call this
2839 // method in Android Q and later. Ideally, we should only define this method in
2840 // Android P and earlier, but since we don't have a way to do so, we can simply
2841 // invoke the hidden parent method after Android P. If in new android, the hidden method
2842 // ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
2843 // simply return null here because there will be no call to this method.
2844 return if (SDK_INT >= Q) {
2845 val findViewByAccessibilityIdTraversalMethod =
2846 Class.forName("android.view.View")
2847 .getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
2848 findViewByAccessibilityIdTraversalMethod.isAccessible = true
2849 findViewByAccessibilityIdTraversalMethod.invoke(this, accessibilityId) as? View
2850 } else {
2851 findViewByAccessibilityIdRootedAtCurrentView(accessibilityId, this)
2852 }
2853 } catch (e: NoSuchMethodException) {
2854 return null
2855 }
2856 }
2857
2858 override val isLifecycleInResumedState: Boolean
2859 get() = viewTreeOwners?.lifecycleOwner?.lifecycle?.currentState == Lifecycle.State.RESUMED
2860
shouldDelayChildPressedStatenull2861 override fun shouldDelayChildPressedState(): Boolean = false
2862
2863 // Track sensitive composable visible in this view
2864 private var sensitiveComponentCount = 0
2865
2866 override fun incrementSensitiveComponentCount() {
2867 if (SDK_INT >= 35) {
2868 if (sensitiveComponentCount == 0) {
2869 AndroidComposeViewSensitiveContent35.setContentSensitivity(view, true)
2870 }
2871 sensitiveComponentCount += 1
2872 }
2873 }
2874
decrementSensitiveComponentCountnull2875 override fun decrementSensitiveComponentCount() {
2876 if (SDK_INT >= 35) {
2877 if (sensitiveComponentCount == 1) {
2878 AndroidComposeViewSensitiveContent35.setContentSensitivity(view, false)
2879 }
2880 sensitiveComponentCount -= 1
2881 }
2882 }
2883
2884 override val outOfFrameExecutor
2885 get() = if (isAttachedToWindow) this else null
2886
schedulenull2887 override fun schedule(block: () -> Unit) {
2888 val handler =
2889 requireNotNull(handler) {
2890 "schedule is called when outOfFrameExecutor is not available (view is detached)"
2891 }
2892 handler.postAtFrontOfQueue { trace("AndroidOwner:outOfFrameExecutor", block) }
2893 }
2894
2895 @RequiresApi(VANILLA_ICE_CREAM)
setRequestedFrameRatenull2896 override fun setRequestedFrameRate(frameRate: Float) {
2897 if (isArrEnabled) {
2898 if (frameRate > 0) {
2899 if (currentFrameRate.isNaN() || frameRate > currentFrameRate) {
2900 currentFrameRate = frameRate // set frame rate
2901 }
2902 } else if (frameRate < 0) {
2903 if (currentFrameRateCategory.isNaN() || frameRate < currentFrameRateCategory) {
2904 currentFrameRateCategory = frameRate // set frame rate category
2905 }
2906 }
2907 } else {
2908 super.setRequestedFrameRate(frameRate)
2909 }
2910 }
2911
2912 @RequiresApi(VANILLA_ICE_CREAM)
voteFrameRatenull2913 override fun voteFrameRate(frameRate: Float) {
2914 if (isArrEnabled) {
2915 requestedFrameRate = frameRate
2916 }
2917 }
2918
2919 @OptIn(ExperimentalComposeUiApi::class)
dispatchOnScrollChangednull2920 override fun dispatchOnScrollChanged(delta: Offset) {
2921 // TODO(levima) b/402138549: Use viewTreeObserver.dispatchOnScrollChanged()
2922 dispatchOnScrollChanged(viewTreeObserver)
2923 }
2924
2925 companion object {
2926 private var systemPropertiesClass: Class<*>? = null
2927 private var getBooleanMethod: Method? = null
2928 private var addChangeCallbackMethod: Method? = null
2929 private val composeViews = mutableObjectListOf<AndroidComposeView>()
2930 private var systemPropertiesChangedRunnable: Runnable? = null
2931 private var dispatchOnScrollChangedMethod: Method? = null
2932
2933 @Suppress("BanUncheckedReflection")
getIsShowingLayoutBoundsnull2934 private fun getIsShowingLayoutBounds(): Boolean =
2935 try {
2936 if (systemPropertiesClass == null) {
2937 systemPropertiesClass = Class.forName("android.os.SystemProperties")
2938 }
2939 if (getBooleanMethod == null) {
2940 getBooleanMethod =
2941 systemPropertiesClass?.getDeclaredMethod(
2942 "getBoolean",
2943 String::class.java,
2944 Boolean::class.java
2945 )
2946 }
2947 getBooleanMethod?.invoke(null, "debug.layout", false) as? Boolean == true
2948 } catch (_: Exception) {
2949 false
2950 }
2951
2952 @Suppress("BanUncheckedReflection")
addNotificationForSysPropsChangenull2953 private fun addNotificationForSysPropsChange(composeView: AndroidComposeView) {
2954 if (SDK_INT > 28) {
2955 // Removing the callback is prohibited on newer versions, so we should only add one
2956 // callback and use it for all AndroidComposeViews
2957 if (systemPropertiesChangedRunnable == null) {
2958 val runnable = Runnable {
2959 synchronized(composeViews) {
2960 if (SDK_INT < 30) {
2961 composeViews.forEach {
2962 val oldValue = it.showLayoutBounds
2963 it.showLayoutBounds = getIsShowingLayoutBounds()
2964 if (oldValue != it.showLayoutBounds) {
2965 it.invalidateDescendants()
2966 }
2967 }
2968 } else {
2969 composeViews.forEach { it.invalidateDescendants() }
2970 }
2971 }
2972 }
2973 systemPropertiesChangedRunnable = runnable
2974 val origPolicy = StrictMode.getVmPolicy()
2975 try {
2976 if (systemPropertiesClass == null) {
2977 systemPropertiesClass = Class.forName("android.os.SystemProperties")
2978 }
2979 if (addChangeCallbackMethod == null) {
2980 StrictMode.setVmPolicy(StrictMode.VmPolicy.LAX)
2981 addChangeCallbackMethod =
2982 systemPropertiesClass?.getDeclaredMethod(
2983 "addChangeCallback",
2984 Runnable::class.java
2985 )
2986 }
2987 addChangeCallbackMethod?.invoke(null, runnable)
2988 } catch (_: Throwable) {} finally {
2989 StrictMode.setVmPolicy(origPolicy)
2990 }
2991 }
2992 synchronized(composeViews) { composeViews += composeView }
2993 }
2994 }
2995
removeNotificationForSysPropsChangenull2996 private fun removeNotificationForSysPropsChange(composeView: AndroidComposeView) {
2997 if (SDK_INT > 28) {
2998 synchronized(composeViews) { composeViews -= composeView }
2999 }
3000 }
3001
3002 // Back compat implementation
3003 @SuppressLint("BanUncheckedReflection") // suppress for now, the API is available in MIN_SDK
dispatchOnScrollChangednull3004 fun dispatchOnScrollChanged(viewTreeObserver: ViewTreeObserver) {
3005 try {
3006 if (dispatchOnScrollChangedMethod == null) {
3007 dispatchOnScrollChangedMethod =
3008 viewTreeObserver.javaClass
3009 .getDeclaredMethod("dispatchOnScrollChanged")
3010 .also { it.isAccessible = true }
3011 }
3012 dispatchOnScrollChangedMethod?.invoke(viewTreeObserver)
3013 } catch (_: Exception) {}
3014 }
3015 }
3016
3017 /** Combines objects populated via ViewTree*Owner */
3018 class ViewTreeOwners(
3019 /** The [LifecycleOwner] associated with this owner. */
3020 val lifecycleOwner: LifecycleOwner,
3021 /** The [SavedStateRegistryOwner] associated with this owner. */
3022 val savedStateRegistryOwner: SavedStateRegistryOwner
3023 )
3024 }
3025
3026 @RequiresApi(S)
3027 private object AndroidComposeViewTranslationCallback : ViewTranslationCallback {
onShowTranslationnull3028 override fun onShowTranslation(view: View): Boolean {
3029 val androidComposeView = view as AndroidComposeView
3030 androidComposeView.contentCaptureManager.onShowTranslation()
3031 return true
3032 }
3033
onHideTranslationnull3034 override fun onHideTranslation(view: View): Boolean {
3035 val androidComposeView = view as AndroidComposeView
3036 androidComposeView.contentCaptureManager.onHideTranslation()
3037 return true
3038 }
3039
onClearTranslationnull3040 override fun onClearTranslation(view: View): Boolean {
3041 val androidComposeView = view as AndroidComposeView
3042 androidComposeView.contentCaptureManager.onClearTranslation()
3043 return true
3044 }
3045 }
3046
3047 /**
3048 * These classes are here to ensure that the classes that use this API will get verified and can be
3049 * AOT compiled. It is expected that this class will soft-fail verification, but the classes which
3050 * use this method will pass.
3051 */
3052 @RequiresApi(O)
3053 private object AndroidComposeViewVerificationHelperMethodsO {
3054 @RequiresApi(O)
3055 @DoNotInline
focusablenull3056 fun focusable(view: View, focusable: Int, defaultFocusHighlightEnabled: Boolean) {
3057 view.focusable = focusable
3058 // not to add the default focus highlight to the whole compose view
3059 view.defaultFocusHighlightEnabled = defaultFocusHighlightEnabled
3060 }
3061 }
3062
3063 @RequiresApi(M)
3064 private object AndroidComposeViewAssistHelperMethodsO {
3065 @RequiresApi(M)
3066 @DoNotInline
setClassNamenull3067 fun setClassName(structure: ViewStructure, view: View) {
3068 structure.setClassName(view.accessibilityClassName.toString())
3069 }
3070 }
3071
3072 @RequiresApi(N)
3073 private object AndroidComposeViewVerificationHelperMethodsN {
3074 @RequiresApi(N)
toAndroidPointerIconnull3075 fun toAndroidPointerIcon(context: Context, icon: PointerIcon?): android.view.PointerIcon =
3076 when (icon) {
3077 is AndroidPointerIcon -> icon.pointerIcon
3078 is AndroidPointerIconType -> android.view.PointerIcon.getSystemIcon(context, icon.type)
3079 else ->
3080 android.view.PointerIcon.getSystemIcon(
3081 context,
3082 android.view.PointerIcon.TYPE_DEFAULT
3083 )
3084 }
3085
3086 @DoNotInline
3087 @RequiresApi(N)
setPointerIconnull3088 fun setPointerIcon(view: View, icon: PointerIcon?) {
3089 val iconToSet = toAndroidPointerIcon(view.context, icon)
3090
3091 if (view.pointerIcon != iconToSet) {
3092 view.pointerIcon = iconToSet
3093 }
3094 }
3095 }
3096
3097 @RequiresApi(Q)
3098 private object AndroidComposeViewForceDarkModeQ {
3099 @DoNotInline
3100 @RequiresApi(Q)
disallowForceDarknull3101 fun disallowForceDark(view: View) {
3102 view.isForceDarkAllowed = false
3103 }
3104 }
3105
3106 @RequiresApi(S)
3107 internal object AndroidComposeViewTranslationCallbackS {
3108 @DoNotInline
3109 @RequiresApi(S)
setViewTranslationCallbacknull3110 fun setViewTranslationCallback(view: View) {
3111 view.setViewTranslationCallback(AndroidComposeViewTranslationCallback)
3112 }
3113
3114 @DoNotInline
3115 @RequiresApi(S)
clearViewTranslationCallbacknull3116 fun clearViewTranslationCallback(view: View) {
3117 view.clearViewTranslationCallback()
3118 }
3119 }
3120
3121 /** Sets this [Matrix] to be the result of this * [other] */
Matrixnull3122 private fun Matrix.preTransform(other: Matrix) {
3123 val v00 = dot(other, 0, this, 0)
3124 val v01 = dot(other, 0, this, 1)
3125 val v02 = dot(other, 0, this, 2)
3126 val v03 = dot(other, 0, this, 3)
3127 val v10 = dot(other, 1, this, 0)
3128 val v11 = dot(other, 1, this, 1)
3129 val v12 = dot(other, 1, this, 2)
3130 val v13 = dot(other, 1, this, 3)
3131 val v20 = dot(other, 2, this, 0)
3132 val v21 = dot(other, 2, this, 1)
3133 val v22 = dot(other, 2, this, 2)
3134 val v23 = dot(other, 2, this, 3)
3135 val v30 = dot(other, 3, this, 0)
3136 val v31 = dot(other, 3, this, 1)
3137 val v32 = dot(other, 3, this, 2)
3138 val v33 = dot(other, 3, this, 3)
3139 this[0, 0] = v00
3140 this[0, 1] = v01
3141 this[0, 2] = v02
3142 this[0, 3] = v03
3143 this[1, 0] = v10
3144 this[1, 1] = v11
3145 this[1, 2] = v12
3146 this[1, 3] = v13
3147 this[2, 0] = v20
3148 this[2, 1] = v21
3149 this[2, 2] = v22
3150 this[2, 3] = v23
3151 this[3, 0] = v30
3152 this[3, 1] = v31
3153 this[3, 2] = v32
3154 this[3, 3] = v33
3155 }
3156
3157 /** Like [android.graphics.Matrix.preTranslate], for a Compose [Matrix] */
Matrixnull3158 private fun Matrix.preTranslate(x: Float, y: Float, tmpMatrix: Matrix) {
3159 tmpMatrix.reset()
3160 tmpMatrix.translate(x, y)
3161 preTransform(tmpMatrix)
3162 }
3163
3164 // Taken from Matrix.kt
dotnull3165 private fun dot(m1: Matrix, row: Int, m2: Matrix, column: Int): Float {
3166 return m1[row, 0] * m2[0, column] +
3167 m1[row, 1] * m2[1, column] +
3168 m1[row, 2] * m2[2, column] +
3169 m1[row, 3] * m2[3, column]
3170 }
3171
3172 private interface CalculateMatrixToWindow {
3173 /**
3174 * Calculates the matrix from [view] to screen coordinates and returns the value in [matrix].
3175 */
calculateMatrixToWindownull3176 fun calculateMatrixToWindow(view: View, matrix: Matrix)
3177 }
3178
3179 @RequiresApi(35)
3180 private object AndroidComposeViewSensitiveContent35 {
3181 @DoNotInline
3182 @RequiresApi(35)
3183 fun setContentSensitivity(view: View, isSensitiveContent: Boolean) {
3184 if (isSensitiveContent) {
3185 view.setContentSensitivity(View.CONTENT_SENSITIVITY_SENSITIVE)
3186 } else {
3187 view.setContentSensitivity(View.CONTENT_SENSITIVITY_AUTO)
3188 }
3189 }
3190 }
3191
3192 @RequiresApi(Q)
3193 private class CalculateMatrixToWindowApi29 : CalculateMatrixToWindow {
3194 private val tmpMatrix = android.graphics.Matrix()
3195 private val tmpPosition = IntArray(2)
3196
3197 @DoNotInline
calculateMatrixToWindownull3198 override fun calculateMatrixToWindow(view: View, matrix: Matrix) {
3199 tmpMatrix.reset()
3200 view.transformMatrixToGlobal(tmpMatrix)
3201 var parent = view.parent
3202 var root = view
3203 while (parent is View) {
3204 root = parent
3205 parent = root.parent
3206 }
3207 root.getLocationOnScreen(tmpPosition)
3208 val (screenX, screenY) = tmpPosition
3209 root.getLocationInWindow(tmpPosition)
3210 val (windowX, windowY) = tmpPosition
3211 tmpMatrix.postTranslate((windowX - screenX).toFloat(), (windowY - screenY).toFloat())
3212 matrix.setFrom(tmpMatrix)
3213 }
3214 }
3215
3216 private class CalculateMatrixToWindowApi21(private val tmpMatrix: Matrix) :
3217 CalculateMatrixToWindow {
3218 private val tmpLocation = IntArray(2)
3219
calculateMatrixToWindownull3220 override fun calculateMatrixToWindow(view: View, matrix: Matrix) {
3221 matrix.reset()
3222 transformMatrixToWindow(view, matrix)
3223 }
3224
transformMatrixToWindownull3225 private fun transformMatrixToWindow(view: View, matrix: Matrix) {
3226 val parentView = view.parent
3227 if (parentView is View) {
3228 transformMatrixToWindow(parentView, matrix)
3229 matrix.preTranslate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
3230 matrix.preTranslate(view.left.toFloat(), view.top.toFloat())
3231 } else {
3232 val pos = tmpLocation
3233 view.getLocationInWindow(pos)
3234 matrix.preTranslate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
3235 matrix.preTranslate(pos[0].toFloat(), pos[1].toFloat())
3236 }
3237
3238 val viewMatrix = view.matrix
3239 if (!viewMatrix.isIdentity) {
3240 matrix.preConcat(viewMatrix)
3241 }
3242 }
3243
3244 /**
3245 * Like [android.graphics.Matrix.preConcat], for a Compose [Matrix] that accepts an [other]
3246 * [android.graphics.Matrix].
3247 */
Matrixnull3248 private fun Matrix.preConcat(other: android.graphics.Matrix) {
3249 tmpMatrix.setFrom(other)
3250 preTransform(tmpMatrix)
3251 }
3252
3253 /** Like [android.graphics.Matrix.preTranslate], for a Compose [Matrix] */
Matrixnull3254 private fun Matrix.preTranslate(x: Float, y: Float) {
3255 preTranslate(x, y, tmpMatrix)
3256 }
3257 }
3258
3259 @RequiresApi(29)
3260 private object MotionEventVerifierApi29 {
3261 @DoNotInline
isValidMotionEventnull3262 fun isValidMotionEvent(event: MotionEvent, index: Int): Boolean {
3263 return event.getRawX(index).fastIsFinite() && event.getRawY(index).fastIsFinite()
3264 }
3265 }
3266
3267 @RequiresApi(N)
3268 private object AndroidComposeViewStartDragAndDropN {
3269 @DoNotInline
3270 @RequiresApi(N)
startDragAndDropnull3271 fun startDragAndDrop(
3272 view: View,
3273 transferData: DragAndDropTransferData,
3274 dragShadowBuilder: ComposeDragShadowBuilder
3275 ): Boolean =
3276 view.startDragAndDrop(
3277 transferData.clipData,
3278 dragShadowBuilder,
3279 transferData.localState,
3280 transferData.flags,
3281 )
3282 }
3283
3284 private fun View.containsDescendant(other: View): Boolean {
3285 if (other == this) return false
3286 var viewParent = other.parent
3287 while (viewParent != null) {
3288 if (viewParent === this) return true
3289 viewParent = viewParent.parent
3290 }
3291 return false
3292 }
3293
Viewnull3294 private fun View.getContentCaptureSessionCompat(): ContentCaptureSessionCompat? {
3295 ViewCompatShims.setImportantForContentCapture(
3296 this,
3297 ViewCompatShims.IMPORTANT_FOR_CONTENT_CAPTURE_YES
3298 )
3299 return ViewCompatShims.getContentCaptureSession(this)
3300 }
3301
3302 private class BringIntoViewOnScreenResponderNode(var view: ViewGroup) :
3303 Modifier.Node(), BringIntoViewModifierNode {
bringIntoViewnull3304 override suspend fun bringIntoView(
3305 childCoordinates: LayoutCoordinates,
3306 boundsProvider: () -> androidx.compose.ui.geometry.Rect?
3307 ) {
3308 val childOffset = childCoordinates.positionInRoot()
3309 val rootRect = boundsProvider()?.translate(childOffset)
3310 if (rootRect != null) {
3311 view.requestRectangleOnScreen(rootRect.toAndroidRect(), false)
3312 }
3313 }
3314 }
3315
3316 /** Split out to avoid class verification errors. This class will only be loaded when SDK >= 30. */
3317 @RequiresApi(30)
3318 private object Api30Impl {
isShowingLayoutBoundsnull3319 @DoNotInline fun isShowingLayoutBounds(view: View) = view.isShowingLayoutBounds
3320 }
3321