/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.platform
import android.view.KeyEvent as AndroidKeyEvent
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.util.SparseArray
import android.view.InputDevice
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_CANCEL
import android.view.MotionEvent.ACTION_DOWN
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
import android.view.MotionEvent.ACTION_HOVER_MOVE
import android.view.MotionEvent.ACTION_MOVE
import android.view.MotionEvent.ACTION_POINTER_DOWN
import android.view.MotionEvent.ACTION_POINTER_UP
import android.view.MotionEvent.ACTION_SCROLL
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.TOOL_TYPE_MOUSE
import android.view.View
import android.view.ViewGroup
import android.view.ViewStructure
import android.view.ViewTreeObserver
import android.view.animation.AnimationUtils
import android.view.autofill.AutofillValue
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AndroidAutofill
import androidx.compose.ui.autofill.Autofill
import androidx.compose.ui.autofill.AutofillCallback
import androidx.compose.ui.autofill.AutofillTree
import androidx.compose.ui.autofill.performAutofill
import androidx.compose.ui.autofill.populateViewStructure
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusDirection.Companion.Down
import androidx.compose.ui.focus.FocusDirection.Companion.In
import androidx.compose.ui.focus.FocusDirection.Companion.Left
import androidx.compose.ui.focus.FocusDirection.Companion.Next
import androidx.compose.ui.focus.FocusDirection.Companion.Out
import androidx.compose.ui.focus.FocusDirection.Companion.Previous
import androidx.compose.ui.focus.FocusDirection.Companion.Right
import androidx.compose.ui.focus.FocusDirection.Companion.Up
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusManagerImpl
import androidx.compose.ui.focus.focusRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.setFrom
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.PlatformHapticFeedback
import androidx.compose.ui.input.InputMode.Companion.Keyboard
import androidx.compose.ui.input.InputMode.Companion.Touch
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.InputModeManagerImpl
import androidx.compose.ui.input.key.Key.Companion.Back
import androidx.compose.ui.input.key.Key.Companion.DirectionCenter
import androidx.compose.ui.input.key.Key.Companion.DirectionDown
import androidx.compose.ui.input.key.Key.Companion.DirectionLeft
import androidx.compose.ui.input.key.Key.Companion.DirectionRight
import androidx.compose.ui.input.key.Key.Companion.DirectionUp
import androidx.compose.ui.input.key.Key.Companion.Enter
import androidx.compose.ui.input.key.Key.Companion.Escape
import androidx.compose.ui.input.key.Key.Companion.NumPadEnter
import androidx.compose.ui.input.key.Key.Companion.Tab
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
import androidx.compose.ui.input.key.KeyInputModifier
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.pointer.AndroidPointerIcon
import androidx.compose.ui.input.pointer.AndroidPointerIconType
import androidx.compose.ui.input.pointer.MotionEventAdapter
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.PointerIconDefaults
import androidx.compose.ui.input.pointer.PointerIconService
import androidx.compose.ui.input.pointer.PointerInputEventProcessor
import androidx.compose.ui.input.pointer.PositionCalculator
import androidx.compose.ui.input.pointer.ProcessResult
import androidx.compose.ui.input.rotary.RotaryScrollEvent
import androidx.compose.ui.input.rotary.onRotaryScrollEvent
import androidx.compose.ui.layout.RootMeasurePolicy
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.LayoutNode
import androidx.compose.ui.node.LayoutNode.UsageByParent
import androidx.compose.ui.node.LayoutNodeDrawScope
import androidx.compose.ui.node.MeasureAndLayoutDelegate
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.node.Owner
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextInputService
import androidx.compose.ui.text.input.TextInputServiceAndroid
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.trace
import androidx.compose.ui.viewinterop.AndroidViewHolder
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.InputDeviceCompat.SOURCE_ROTARY_ENCODER
import androidx.core.view.MotionEventCompat.AXIS_SCROLL
import androidx.core.view.ViewCompat
import androidx.core.view.ViewConfigurationCompat.getScaledHorizontalScrollFactor
import androidx.core.view.ViewConfigurationCompat.getScaledVerticalScrollFactor
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.ViewTreeSavedStateRegistryOwner
import java.lang.reflect.Method
import kotlin.math.roundToInt
@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(ExperimentalComposeUiApi::class)
internal class AndroidComposeView(context: Context) :
ViewGroup(context), Owner, ViewRootForTest, PositionCalculator, DefaultLifecycleObserver {
/**
* Remembers the position of the last pointer input event that was down. This position will be
* used to calculate whether this view is considered scrollable via [canScrollHorizontally]/
* [canScrollVertically].
*/
private var lastDownPointerPosition: Offset = Offset.Unspecified
/**
* Signal that AndroidComposeView's superclass constructors have finished running.
* If this is false, it's because the runtime's default uninitialized value is currently
* visible and AndroidComposeView's constructor hasn't started running yet. In this state
* other expected invariants do not hold, e.g. property delegates may not be initialized.
* View/ViewGroup have a history of calling non-final methods in their constructors that
* can lead to this case, e.g. [onRtlPropertiesChanged].
*/
private var superclassInitComplete = true
override val sharedDrawScope = LayoutNodeDrawScope()
override val view: View get() = this
override var density = Density(context)
private set
private val semanticsModifier = SemanticsModifierCore(
id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
)
private val _focusManager: FocusManagerImpl = FocusManagerImpl()
override val focusManager: FocusManager
get() = _focusManager
private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
override val windowInfo: WindowInfo
get() = _windowInfo
// TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
// that this common logic can be used by all owners.
private val keyInputModifier: KeyInputModifier = KeyInputModifier(
onKeyEvent = {
val focusDirection = getFocusDirection(it)
if (focusDirection == null || it.type != KeyDown) return@KeyInputModifier false
// Consume the key event if we moved focus.
focusManager.moveFocus(focusDirection)
},
onPreviewKeyEvent = null
)
private val rotaryInputModifier = Modifier.onRotaryScrollEvent {
// TODO(b/210748692): call focusManager.moveFocus() in response to rotary events.
false
}
private val canvasHolder = CanvasHolder()
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
it.modifier = Modifier
.then(semanticsModifier)
.then(rotaryInputModifier)
.then(_focusManager.modifier)
.then(keyInputModifier)
it.density = density
}
override val rootForTest: RootForTest = this
override val semanticsOwner: SemanticsOwner = SemanticsOwner(root)
private val accessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this)
// Used by components that want to provide autofill semantic information.
// TODO: Replace with SemanticsTree: Temporary hack until we have a semantics tree implemented.
// TODO: Replace with SemanticsTree.
// This is a temporary hack until we have a semantics tree implemented.
override val autofillTree = AutofillTree()
// OwnedLayers that are dirty and should be redrawn.
private val dirtyLayers = mutableListOf<OwnedLayer>()
// OwnerLayers that invalidated themselves during their last draw. They will be redrawn
// during the next AndroidComposeView dispatchDraw pass.
private var postponedDirtyLayers: MutableList<OwnedLayer>? = null
private var isDrawingContent = false
private val motionEventAdapter = MotionEventAdapter()
private val pointerInputEventProcessor = PointerInputEventProcessor(root)
// TODO(mount): reinstate when coroutines are supported by IR compiler
// private val ownerScope = CoroutineScope(Dispatchers.Main.immediate + Job())
/**
* Used for updating LocalConfiguration when configuration changes - consume LocalConfiguration
* instead of changing this observer if you are writing a component that adapts to
* configuration changes.
*/
var configurationChangeObserver: (Configuration) -> Unit = {}
private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
// Used as a CompositionLocal for performing autofill.
override val autofill: Autofill? get() = _autofill
private var observationClearRequested = false
/**
* Provide clipboard manager to the user. Use the Android version of clipboard manager.
*/
override val clipboardManager = AndroidClipboardManager(context)
/**
* Provide accessibility manager to the user. Use the Android version of accessibility manager.
*/
override val accessibilityManager = AndroidAccessibilityManager(context)
override val snapshotObserver = OwnerSnapshotObserver { command ->
if (handler?.looper === Looper.myLooper()) {
command()
} else {
handler?.post(command)
}
}
@OptIn(InternalCoreApi::class)
override var showLayoutBounds = false
private var _androidViewsHandler: AndroidViewsHandler? = null
internal val androidViewsHandler: AndroidViewsHandler
get() {
if (_androidViewsHandler == null) {
_androidViewsHandler = AndroidViewsHandler(context)
addView(_androidViewsHandler)
}
return _androidViewsHandler!!
}
private var viewLayersContainer: DrawChildContainer? = null
// The constraints being used by the last onMeasure. It is set to null in onLayout. It allows
// us to detect the case when the View was measured twice with different constraints within
// the same measure pass.
private var onMeasureConstraints: Constraints? = null
// Will be set to true when we were measured twice with different constraints during the last
// measure pass.
private var wasMeasuredWithMultipleConstraints = false
private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)
override val measureIteration: Long get() = measureAndLayoutDelegate.measureIteration
override val viewConfiguration: ViewConfiguration =
AndroidViewConfiguration(android.view.ViewConfiguration.get(context))
override val hasPendingMeasureOrLayout
get() = measureAndLayoutDelegate.hasPendingMeasureOrLayout
private var globalPosition: IntOffset = IntOffset.Zero
private val tmpPositionArray = intArrayOf(0, 0)
private val viewToWindowMatrix = Matrix()
private val windowToViewMatrix = Matrix()
private val tmpCalculationMatrix = Matrix()
@VisibleForTesting
internal var lastMatrixRecalculationAnimationTime = -1L
private var forceUseMatrixCache = false
/**
* On some devices, the `getLocationOnScreen()` returns `(0, 0)` even when the Window
* is offset in special circumstances. This contains the screen coordinates of the containing
* Window the last time the [viewToWindowMatrix] and [windowToViewMatrix] were recalculated.
*/
private var windowPosition = Offset.Infinite
// Used to track whether or not there was an exception while creating an MRenderNode
// so that we don't have to continue using try/catch after fails once.
private var isRenderNodeCompatible = true
/**
* Current [ViewTreeOwners]. Use [setOnViewTreeOwnersAvailable] if you want to
* execute your code when the object will be created.
*/
var viewTreeOwners: ViewTreeOwners? by mutableStateOf(null)
private set
private var onViewTreeOwnersAvailable: ((ViewTreeOwners) -> Unit)? = null
// executed when the layout pass has been finished. as a result of it our view could be moved
// inside the window (we are interested not only in the event when our parent positioned us
// on a different position, but also in the position of each of the grandparents as all these
// positions add up to final global position)
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
updatePositionCacheAndDispatch()
}
// executed when a scrolling container like ScrollView of RecyclerView performed the scroll,
// this could affect our global position
private val scrollChangedListener = ViewTreeObserver.OnScrollChangedListener {
updatePositionCacheAndDispatch()
}
// executed whenever the touch mode changes.
private val touchModeChangeListener = ViewTreeObserver.OnTouchModeChangeListener { touchMode ->
_inputModeManager.inputMode = if (touchMode) Touch else Keyboard
_focusManager.fetchUpdatedFocusProperties()
}
private val textInputServiceAndroid = TextInputServiceAndroid(this)
@OptIn(InternalComposeUiApi::class)
override val textInputService = textInputServiceFactory(textInputServiceAndroid)
@Suppress("DEPRECATION", "OverridingDeprecatedMember")
override val fontLoader: Font.ResourceLoader = AndroidFontResourceLoader(context)
override val fontFamilyResolver: FontFamily.Resolver = createFontFamilyResolver(context)
// Backed by mutableStateOf so that the ambient provider recomposes when it changes
override var layoutDirection by mutableStateOf(
context.resources.configuration.localeLayoutDirection
)
private set
/**
* Provide haptic feedback to the user. Use the Android version of haptic feedback.
*/
override val hapticFeedBack: HapticFeedback =
PlatformHapticFeedback(this)
/**
* Provide an instance of [InputModeManager] which is available as a CompositionLocal.
*/
private val _inputModeManager = InputModeManagerImpl(
initialInputMode = if (isInTouchMode) Touch else Keyboard,
onRequestInputModeChange = {
when (it) {
// Android doesn't support programmatically switching to touch mode, so we
// don't do anything, but just return true if we are already in touch mode.
Touch -> isInTouchMode
// If we are already in keyboard mode, we return true, otherwise, we call
// requestFocusFromTouch, which puts the system in non-touch mode.
Keyboard -> if (isInTouchMode) requestFocusFromTouch() else true
else -> false
}
}
)
override val inputModeManager: InputModeManager get() = _inputModeManager
/**
* Provide textToolbar to the user, for text-related operation. Use the Android version of
* floating toolbar(post-M) and primary toolbar(pre-M).
*/
override val textToolbar: TextToolbar = AndroidTextToolbar(this)
/**
* When the first event for a mouse is ACTION_DOWN, an ACTION_HOVER_ENTER is never sent.
* This means that we won't receive an `Enter` event for the first mouse. In order to prevent
* this problem, we track whether or not the previous event was with the mouse inside and
* if not, we can create a simulated mouse enter event to force an enter.
*/
private var previousMotionEvent: MotionEvent? = null
/**
* The time of the last layout. This is used to send a synthetic MotionEvent.
*/
private var relayoutTime = 0L
/**
* A cache for OwnedLayers. Recreating ViewLayers is expensive, so we avoid it as much
* as possible. This also helps a little with RenderNodeLayers as well.
*/
private val layerCache = WeakCache<OwnedLayer>()
/**
* List of lambdas to be called when [onEndApplyChanges] is called.
*/
private val endApplyChangesListeners = mutableVectorOf<() -> Unit>()
/**
* Runnable used to update the pointer position after layout. If
* another pointer event comes in before this runs, this Runnable will be removed and
* not executed.
*/
private val resendMotionEventRunnable = object : Runnable {
override fun run() {
removeCallbacks(this)
val lastMotionEvent = previousMotionEvent
if (lastMotionEvent != null) {
val wasMouseEvent = lastMotionEvent.getToolType(0) == TOOL_TYPE_MOUSE
val action = lastMotionEvent.actionMasked
val resend = if (wasMouseEvent) {
action != ACTION_HOVER_EXIT && action != ACTION_UP
} else {
action != ACTION_UP
}
if (resend) {
val newAction =
if (action == ACTION_HOVER_MOVE || action == ACTION_HOVER_ENTER) {
ACTION_HOVER_MOVE
} else {
ACTION_MOVE
}
sendSimulatedEvent(lastMotionEvent, newAction, relayoutTime, forceHover = false)
}
}
}
}
/**
* If an [ACTION_HOVER_EXIT] event is received, it could be because an [ACTION_DOWN] is coming
* from a mouse or stylus. We can't know for certain until the next event is sent. This message
* is posted after receiving the [ACTION_HOVER_EXIT] to send the event if nothing else is
* received before that.
*/
private val sendHoverExitEvent = Runnable {
hoverExitReceived = false
val lastEvent = previousMotionEvent!!
check(lastEvent.actionMasked == ACTION_HOVER_EXIT) {
"The ACTION_HOVER_EXIT event was not cleared."
}
sendMotionEvent(lastEvent)
}
/**
* Set to `true` when [sendHoverExitEvent] has been posted.
*/
private var hoverExitReceived = false
/**
* Callback for [measureAndLayout] to update the pointer position 150ms after layout.
*/
private val resendMotionEventOnLayout: () -> Unit = {
val lastEvent = previousMotionEvent
if (lastEvent != null) {
when (lastEvent.actionMasked) {
// We currently only care about hover events being updated when layout changes
ACTION_HOVER_ENTER, ACTION_HOVER_MOVE -> {
relayoutTime = SystemClock.uptimeMillis()
post(resendMotionEventRunnable)
}
}
}
}
init {
setWillNotDraw(false)
isFocusable = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AndroidComposeViewVerificationHelperMethodsO.focusable(
this,
focusable = View.FOCUSABLE,
defaultFocusHighlightEnabled = false
)
}
isFocusableInTouchMode = true
clipChildren = false
isTransitionGroup = true
ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)
ViewRootForTest.onViewCreatedCallback?.invoke(this)
root.attach(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Support for this feature in Compose is tracked here: b/207654434
AndroidComposeViewForceDarkModeQ.disallowForceDark(this)
}
}
/**
* Since this view has its own concept of internal focus, it needs to report that to the view
* system for accurate focus searching and so ViewRootImpl will scroll correctly.
*/
override fun getFocusedRect(rect: Rect) {
_focusManager.getActiveFocusModifier()?.focusRect()?.let {
rect.left = it.left.roundToInt()
rect.top = it.top.roundToInt()
rect.right = it.right.roundToInt()
rect.bottom = it.bottom.roundToInt()
} ?: super.getFocusedRect(rect)
}
override fun onResume(owner: LifecycleOwner) {
// Refresh in onResume in case the value has changed.
@OptIn(InternalCoreApi::class)
showLayoutBounds = getIsShowingLayoutBounds()
}
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
Log.d(FocusTag, "Owner FocusChanged($gainFocus)")
with(_focusManager) {
if (gainFocus) takeFocus() else releaseFocus()
}
}
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
_windowInfo.isWindowFocused = hasWindowFocus
super.onWindowFocusChanged(hasWindowFocus)
}
override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
return keyInputModifier.processKeyInput(keyEvent)
}
override fun dispatchKeyEvent(event: AndroidKeyEvent) =
if (isFocused) {
// Focus lies within the Compose hierarchy, so we dispatch the key event to the
// appropriate place.
sendKeyEvent(KeyEvent(event))
} else {
// This Owner has a focused child view, which is a view interop use case,
// so we use the default ViewGroup behavior which will route tke key event to the
// focused view.
super.dispatchKeyEvent(event)
}
override fun onAttach(node: LayoutNode) {
}
override fun onDetach(node: LayoutNode) {
measureAndLayoutDelegate.onNodeDetached(node)
requestClearInvalidObservations()
}
fun requestClearInvalidObservations() {
observationClearRequested = true
}
override fun onEndApplyChanges() {
if (observationClearRequested) {
snapshotObserver.clearInvalidObservations()
observationClearRequested = false
}
val childAndroidViews = _androidViewsHandler
if (childAndroidViews != null) {
clearChildInvalidObservations(childAndroidViews)
}
// Iterate through the whole list, even if listeners are added.
var i = 0
while (i < endApplyChangesListeners.size) {
val listener = endApplyChangesListeners[i]
listener()
i++
}
endApplyChangesListeners.clear()
}
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
endApplyChangesListeners += listener
}
private fun clearChildInvalidObservations(viewGroup: ViewGroup) {
for (i in 0 until viewGroup.childCount) {
val child = viewGroup.getChildAt(i)
if (child is AndroidComposeView) {
child.onEndApplyChanges()
} else if (child is ViewGroup) {
clearChildInvalidObservations(child)
}
}
}
/**
* Called to inform the owner that a new Android [View] was [attached][Owner.onAttach]
* to the hierarchy.
*/
fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
androidViewsHandler.holderToLayoutNode[view] = layoutNode
androidViewsHandler.addView(view)
androidViewsHandler.layoutNodeToHolder[layoutNode] = view
// Fetching AccessibilityNodeInfo from a View which is not set to
// IMPORTANT_FOR_ACCESSIBILITY_YES will return null.
ViewCompat.setImportantForAccessibility(
view,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
)
val thisView = this
ViewCompat.setAccessibilityDelegate(
view,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
var parentId = SemanticsNode(layoutNode.outerSemantics!!, false).parent!!.id
if (parentId == semanticsOwner.unmergedRootSemanticsNode.id) {
parentId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
}
info.setParent(thisView, parentId)
}
}
)
}
/**
* Called to inform the owner that an Android [View] was [detached][Owner.onDetach]
* from the hierarchy.
*/
fun removeAndroidView(view: AndroidViewHolder) {
androidViewsHandler.removeView(view)
androidViewsHandler.layoutNodeToHolder.remove(
androidViewsHandler.holderToLayoutNode.remove(view)
)
ViewCompat.setImportantForAccessibility(
view,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO
)
}
/**
* Called to ask the owner to draw a child Android [View] to [canvas].
*/
fun drawAndroidView(view: AndroidViewHolder, canvas: android.graphics.Canvas) {
androidViewsHandler.drawView(view, canvas)
}
private fun scheduleMeasureAndLayout(nodeToRemeasure: LayoutNode? = null) {
if (!isLayoutRequested && isAttachedToWindow) {
if (wasMeasuredWithMultipleConstraints && nodeToRemeasure != null) {
// if nodeToRemeasure can potentially resize the root and the view was measured
// twice with different constraints last time it means the constraints we have could
// be not the final constraints and in fact our parent ViewGroup can remeasure us
// with larger constraints if we call requestLayout()
var node = nodeToRemeasure
while (node != null && node.measuredByParent == UsageByParent.InMeasureBlock) {
node = node.parent
}
if (node === root) {
requestLayout()
return
}
}
if (width == 0 || height == 0) {
// if the view has no size calling invalidate() will be skipped
requestLayout()
} else {
invalidate()
}
}
}
override fun measureAndLayout(sendPointerUpdate: Boolean) {
val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
if (rootNodeResized) {
requestLayout()
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
measureAndLayoutDelegate.measureAndLayout(layoutNode, constraints)
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
override fun forceMeasureTheSubtree(layoutNode: LayoutNode) {
measureAndLayoutDelegate.forceMeasureTheSubtree(layoutNode)
}
override fun onRequestMeasure(layoutNode: LayoutNode, forceRequest: Boolean) {
if (measureAndLayoutDelegate.requestRemeasure(layoutNode, forceRequest)) {
scheduleMeasureAndLayout(layoutNode)
}
}
override fun onRequestRelayout(layoutNode: LayoutNode, forceRequest: Boolean) {
if (measureAndLayoutDelegate.requestRelayout(layoutNode, forceRequest)) {
scheduleMeasureAndLayout()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
trace("AndroidOwner:onMeasure") {
if (!isAttachedToWindow) {
invalidateLayoutNodeMeasurement(root)
}
val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
val constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
if (onMeasureConstraints == null) {
// first onMeasure after last onLayout
onMeasureConstraints = constraints
wasMeasuredWithMultipleConstraints = false
} else if (onMeasureConstraints != constraints) {
// we were remeasured twice with different constraints after last onLayout
wasMeasuredWithMultipleConstraints = true
}
measureAndLayoutDelegate.updateRootConstraints(constraints)
measureAndLayoutDelegate.measureAndLayout(resendMotionEventOnLayout)
setMeasuredDimension(root.width, root.height)
if (_androidViewsHandler != null) {
androidViewsHandler.measure(
MeasureSpec.makeMeasureSpec(root.width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(root.height, MeasureSpec.EXACTLY)
)
}
}
}
private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
MeasureSpec.EXACTLY -> size to size
MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
MeasureSpec.AT_MOST -> 0 to size
else -> throw IllegalStateException()
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
onMeasureConstraints = null
// we postpone onPositioned callbacks until onLayout as LayoutCoordinates
// are currently wrong if you try to get the global(activity) coordinates -
// View is not yet laid out.
updatePositionCacheAndDispatch()
if (_androidViewsHandler != null) {
// Even if we laid out during onMeasure, we want to set the bounds of the
// AndroidViewsHandler for accessibility and for Views making assumptions based on
// the size of their ancestors. Usually the Views in the hierarchy will not
// be relaid out, as they have not requested layout in the meantime.
// However, there is also chance for the AndroidViewsHandler and the children to be
// isLayoutRequested at this point, in case the Views hierarchy receives forceLayout().
// In case of a forceLayout(), calling layout here will traverse the entire subtree
// and replace the Views at the same position, which is needed to clean up their
// layout state, which otherwise might cause further requestLayout()s to be blocked.
androidViewsHandler.layout(0, 0, r - l, b - t)
}
}
private fun updatePositionCacheAndDispatch() {
var positionChanged = false
getLocationOnScreen(tmpPositionArray)
if (globalPosition.x != tmpPositionArray[0] || globalPosition.y != tmpPositionArray[1]) {
globalPosition = IntOffset(tmpPositionArray[0], tmpPositionArray[1])
positionChanged = true
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks(forceDispatch = positionChanged)
}
override fun onDraw(canvas: android.graphics.Canvas) {
}
override fun createLayer(
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
): OwnedLayer {
// First try the layer cache
val layer = layerCache.pop()
if (layer !== null) {
layer.reuseLayer(drawBlock, invalidateParentLayer)
return layer
}
// RenderNode is supported on Q+ for certain, but may also be supported on M-O.
// We can't be confident that RenderNode is supported, so we try and fail over to
// the ViewLayer implementation. We'll try even on on P devices, but it will fail
// until ART allows things on the unsupported list on P.
if (isHardwareAccelerated &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
isRenderNodeCompatible
) {
try {
return RenderNodeLayer(
this,
drawBlock,
invalidateParentLayer
)
} catch (_: Throwable) {
isRenderNodeCompatible = false
}
}
if (viewLayersContainer == null) {
if (!ViewLayer.hasRetrievedMethod) {
// Test to see if updateDisplayList() can be called. If this fails then
// ViewLayer.shouldUseDispatchDraw will be true.
ViewLayer.updateDisplayList(View(context))
}
viewLayersContainer = if (ViewLayer.shouldUseDispatchDraw) {
DrawChildContainer(context)
} else {
ViewLayerContainer(context)
}
addView(viewLayersContainer)
}
return ViewLayer(this, viewLayersContainer!!, drawBlock, invalidateParentLayer)
}
/**
* Return [layer] to the layer cache. It can be reused in [createLayer] after this.
* Returns `true` if it was recycled or `false` if it will be discarded.
*/
internal fun recycle(layer: OwnedLayer): Boolean {
// L throws during RenderThread when reusing the Views. The stack trace
// wasn't easy to decode, so this work-around keeps up to 10 Views active
// only for L. On other versions, it uses the WeakHashMap to retain as many
// as are convenient.
val cacheValue = viewLayersContainer == null || ViewLayer.shouldUseDispatchDraw ||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ||
layerCache.size < MaximumLayerCacheSize
if (cacheValue) {
layerCache.push(layer)
}
return cacheValue
}
override fun onSemanticsChange() {
accessibilityDelegate.onSemanticsChange()
}
override fun onLayoutChange(layoutNode: LayoutNode) {
accessibilityDelegate.onLayoutChange(layoutNode)
}
override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
return when (keyEvent.key) {
Tab -> if (keyEvent.isShiftPressed) Previous else Next
DirectionRight -> Right
DirectionLeft -> Left
DirectionUp -> Up
DirectionDown -> Down
DirectionCenter, Enter, NumPadEnter -> In
Back, Escape -> Out
else -> null
}
}
override fun dispatchDraw(canvas: android.graphics.Canvas) {
if (!isAttachedToWindow) {
invalidateLayers(root)
}
measureAndLayout()
isDrawingContent = true
// we don't have to observe here because the root has a layer modifier
// that will observe all children. The AndroidComposeView has only the
// root, so it doesn't have to invalidate itself based on model changes.
canvasHolder.drawInto(canvas) { root.draw(this) }
if (dirtyLayers.isNotEmpty()) {
for (i in 0 until dirtyLayers.size) {
val layer = dirtyLayers[i]
layer.updateDisplayList()
}
}
if (ViewLayer.shouldUseDispatchDraw) {
// We must update the display list of all children using dispatchDraw()
// instead of updateDisplayList(). But since we don't want to actually draw
// the contents, we will clip out everything from the canvas.
val saveCount = canvas.save()
canvas.clipRect(0f, 0f, 0f, 0f)
super.dispatchDraw(canvas)
canvas.restoreToCount(saveCount)
}
dirtyLayers.clear()
isDrawingContent = false
// updateDisplayList operations performed above (during root.draw and during the explicit
// layer.updateDisplayList() calls) can result in the same layers being invalidated. These
// layers have been added to postponedDirtyLayers and will be redrawn during the next
// dispatchDraw.
if (postponedDirtyLayers != null) {
val postponed = postponedDirtyLayers!!
dirtyLayers.addAll(postponed)
postponed.clear()
}
}
internal fun notifyLayerIsDirty(layer: OwnedLayer, isDirty: Boolean) {
if (!isDirty) {
// It is correct to remove the layer here regardless of this if, but for performance
// we are hackily not doing the removal here in order to just do clear() a bit later.
if (!isDrawingContent) require(dirtyLayers.remove(layer))
} else if (!isDrawingContent) {
dirtyLayers += layer
} else {
val postponed = postponedDirtyLayers
?: mutableListOf<OwnedLayer>().also { postponedDirtyLayers = it }
postponed += layer
}
}
/**
* The callback to be executed when [viewTreeOwners] is created and not-null anymore.
* Note that this callback will be fired inline when it is already available
*/
fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit) {
val viewTreeOwners = viewTreeOwners
if (viewTreeOwners != null) {
callback(viewTreeOwners)
}
if (!isAttachedToWindow) {
onViewTreeOwnersAvailable = callback
}
}
suspend fun boundsUpdatesEventLoop() {
accessibilityDelegate.boundsUpdatesEventLoop()
}
/**
* Android has an issue where calling showSoftwareKeyboard after calling
* hideSoftwareKeyboard, it results in keyboard flickering and sometimes the keyboard ends up
* being hidden even though the most recent call was to showKeyboard.
*
* This function starts a suspended function that listens for show/hide commands and only
* runs the latest command.
*/
suspend fun keyboardVisibilityEventLoop() {
textInputServiceAndroid.textInputCommandEventLoop()
}
/**
* Walks the entire LayoutNode sub-hierarchy and marks all nodes as needing measurement.
*/
private fun invalidateLayoutNodeMeasurement(node: LayoutNode) {
measureAndLayoutDelegate.requestRemeasure(node)
node._children.forEach { invalidateLayoutNodeMeasurement(it) }
}
/**
* Walks the entire LayoutNode sub-hierarchy and marks all layers as needing to be redrawn.
*/
private fun invalidateLayers(node: LayoutNode) {
node.invalidateLayers()
node._children.forEach { invalidateLayers(it) }
}
override fun invalidateDescendants() {
invalidateLayers(root)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
invalidateLayoutNodeMeasurement(root)
invalidateLayers(root)
snapshotObserver.startObserving()
ifDebug {
if (autofillSupported()) {
_autofill?.let { AutofillCallback.register(it) }
}
}
val lifecycleOwner = ViewTreeLifecycleOwner.get(this)
val savedStateRegistryOwner = ViewTreeSavedStateRegistryOwner.get(this)
val oldViewTreeOwners = viewTreeOwners
// We need to change the ViewTreeOwner if there isn't one yet (null)
// or if either the lifecycleOwner or savedStateRegistryOwner has changed.
val resetViewTreeOwner = oldViewTreeOwners == null ||
(
(lifecycleOwner != null && savedStateRegistryOwner != null) &&
(
lifecycleOwner !== oldViewTreeOwners.lifecycleOwner ||
savedStateRegistryOwner !== oldViewTreeOwners.lifecycleOwner
)
)
if (resetViewTreeOwner) {
if (lifecycleOwner == null) {
throw IllegalStateException(
"Composed into the View which doesn't propagate ViewTreeLifecycleOwner!"
)
}
if (savedStateRegistryOwner == null) {
throw IllegalStateException(
"Composed into the View which doesn't propagate" +
"ViewTreeSavedStateRegistryOwner!"
)
}
oldViewTreeOwners?.lifecycleOwner?.lifecycle?.removeObserver(this)
lifecycleOwner.lifecycle.addObserver(this)
val viewTreeOwners = ViewTreeOwners(
lifecycleOwner = lifecycleOwner,
savedStateRegistryOwner = savedStateRegistryOwner
)
this.viewTreeOwners = viewTreeOwners
onViewTreeOwnersAvailable?.invoke(viewTreeOwners)
onViewTreeOwnersAvailable = null
}
viewTreeOwners!!.lifecycleOwner.lifecycle.addObserver(this)
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
snapshotObserver.stopObserving()
viewTreeOwners?.lifecycleOwner?.lifecycle?.removeObserver(this)
ifDebug {
if (autofillSupported()) {
_autofill?.let { AutofillCallback.unregister(it) }
}
}
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
}
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
if (autofillSupported() && structure != null) _autofill?.populateViewStructure(structure)
}
override fun autofill(values: SparseArray<AutofillValue>) {
if (autofillSupported()) _autofill?.performAutofill(values)
}
override fun dispatchGenericMotionEvent(event: MotionEvent) = when (event.actionMasked) {
ACTION_SCROLL -> when {
event.isFromSource(SOURCE_ROTARY_ENCODER) -> handleRotaryEvent(event)
else -> handleMotionEvent(event).dispatchedToAPointerInputModifier
}
else -> super.dispatchGenericMotionEvent(event)
}
// TODO(shepshapard): Test this method.
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
if (hoverExitReceived) {
// Go ahead and send ACTION_HOVER_EXIT if this isn't an ACTION_DOWN for the same
// pointer
removeCallbacks(sendHoverExitEvent)
val lastEvent = previousMotionEvent!!
if (motionEvent.actionMasked != ACTION_DOWN ||
hasChangedDevices(motionEvent, lastEvent)
) {
sendHoverExitEvent.run()
} else {
hoverExitReceived = false
}
}
if (isBadMotionEvent(motionEvent)) {
return false // Bad MotionEvent. Don't handle it.
}
if (motionEvent.actionMasked == ACTION_MOVE && !isPositionChanged(motionEvent)) {
// There was no movement from previous MotionEvent, so we don't need to dispatch this.
// This could be a scroll event or some other non-touch event that results in an
// ACTION_MOVE without any movement.
return false
}
val processResult = handleMotionEvent(motionEvent)
if (processResult.anyMovementConsumed) {
parent.requestDisallowInterceptTouchEvent(true)
}
return processResult.dispatchedToAPointerInputModifier
}
private fun handleRotaryEvent(event: MotionEvent): Boolean {
val config = android.view.ViewConfiguration.get(context)
val axisValue = -event.getAxisValue(AXIS_SCROLL)
val rotaryEvent = RotaryScrollEvent(
verticalScrollPixels = axisValue * getScaledVerticalScrollFactor(config, context),
horizontalScrollPixels = axisValue * getScaledHorizontalScrollFactor(config, context),
uptimeMillis = event.eventTime
)
return _focusManager.getActiveFocusModifier()?.propagateRotaryEvent(rotaryEvent) ?: false
}
private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {
removeCallbacks(resendMotionEventRunnable)
try {
recalculateWindowPosition(motionEvent)
forceUseMatrixCache = true
measureAndLayout(sendPointerUpdate = false)
desiredPointerIcon = null
val result = trace("AndroidOwner:onTouch") {
val action = motionEvent.actionMasked
val lastEvent = previousMotionEvent
val wasMouseEvent = lastEvent?.getToolType(0) == TOOL_TYPE_MOUSE
if (lastEvent != null &&
hasChangedDevices(motionEvent, lastEvent)
) {
if (isDevicePressEvent(lastEvent)) {
// Send a cancel event
pointerInputEventProcessor.processCancel()
} else if (lastEvent.actionMasked != ACTION_HOVER_EXIT && wasMouseEvent) {
// The mouse cursor disappeared without sending an ACTION_HOVER_EXIT, so
// we have to send that event.
sendSimulatedEvent(lastEvent, ACTION_HOVER_EXIT, lastEvent.eventTime)
}
}
val isMouseEvent = motionEvent.getToolType(0) == TOOL_TYPE_MOUSE
if (!wasMouseEvent &&
isMouseEvent &&
action != ACTION_CANCEL &&
action != ACTION_HOVER_ENTER &&
isInBounds(motionEvent)
) {
// We didn't previously have an enter event and we're getting our first
// mouse event. Send a simulated enter event so that we have a consistent
// enter/exit.
sendSimulatedEvent(motionEvent, ACTION_HOVER_ENTER, motionEvent.eventTime)
}
lastEvent?.recycle()
previousMotionEvent = MotionEvent.obtainNoHistory(motionEvent)
sendMotionEvent(motionEvent)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
AndroidComposeViewVerificationHelperMethodsN.setPointerIcon(
this,
desiredPointerIcon
)
}
return result
} finally {
forceUseMatrixCache = false
}
}
private fun hasChangedDevices(event: MotionEvent, lastEvent: MotionEvent): Boolean {
return lastEvent.source != event.source ||
lastEvent.getToolType(0) != event.getToolType(0)
}
private fun isDevicePressEvent(event: MotionEvent): Boolean {
if (event.buttonState != 0) {
return true
}
return when (event.actionMasked) {
ACTION_POINTER_UP, // means that there is at least one remaining pointer
ACTION_DOWN,
ACTION_MOVE -> true
// ACTION_SCROLL, // We've already checked for buttonState, so it must not be down
// ACTION_HOVER_ENTER,
// ACTION_HOVER_MOVE,
// ACTION_HOVER_EXIT,
// ACTION_UP,
// ACTION_CANCEL,
else -> false
}
}
private fun sendMotionEvent(motionEvent: MotionEvent): ProcessResult {
val pointerInputEvent =
motionEventAdapter.convertToPointerInputEvent(motionEvent, this)
return if (pointerInputEvent != null) {
// Cache the last position of the last pointer to go down so we can check if
// it's in a scrollable region in canScroll{Vertically|Horizontally}. Those
// methods use semantics data, and because semantics coordinates are local to
// this view, the pointer _position_, not _positionOnScreen_, is the offset that
// needs to be cached.
pointerInputEvent.pointers.lastOrNull { it.down }?.position?.let {
lastDownPointerPosition = it
}
val result = pointerInputEventProcessor.process(
pointerInputEvent,
this,
isInBounds(motionEvent)
)
val action = motionEvent.actionMasked
if ((action == ACTION_DOWN || action == ACTION_POINTER_DOWN) &&
!result.dispatchedToAPointerInputModifier
) {
// We aren't handling the pointer, so the event stream has ended for us.
// The next time we receive a pointer event, it should be considered a new
// pointer.
motionEventAdapter.endStream(motionEvent.getPointerId(motionEvent.actionIndex))
}
result
} else {
pointerInputEventProcessor.processCancel()
ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
}
}
private fun sendSimulatedEvent(
motionEvent: MotionEvent,
action: Int,
eventTime: Long,
forceHover: Boolean = true
) {
val oldAction = motionEvent.actionMasked
// don't send any events for pointers that are "up" unless they support hover
val upIndex = when (oldAction) {
ACTION_UP -> if (action == ACTION_HOVER_ENTER || action == ACTION_HOVER_EXIT) -1 else 0
ACTION_POINTER_UP -> motionEvent.actionIndex
else -> -1
}
val pointerCount = motionEvent.pointerCount - if (upIndex >= 0) 1 else 0
if (pointerCount == 0) {
return
}
val pointerProperties = Array(pointerCount) { MotionEvent.PointerProperties() }
val pointerCoords = Array(pointerCount) { MotionEvent.PointerCoords() }
for (i in 0 until pointerCount) {
val sourceIndex = i + if (upIndex < 0 || i < upIndex) 0 else 1
motionEvent.getPointerProperties(sourceIndex, pointerProperties[i])
val coords = pointerCoords[i]
motionEvent.getPointerCoords(sourceIndex, coords)
val localPosition = Offset(coords.x, coords.y)
val screenPosition = localToScreen(localPosition)
coords.x = screenPosition.x
coords.y = screenPosition.y
}
val buttonState = if (forceHover) 0 else motionEvent.buttonState
val downTime = if (motionEvent.downTime == motionEvent.eventTime) {
eventTime
} else {
motionEvent.downTime
}
val event = MotionEvent.obtain(
/* downTime */ downTime,
/* eventTime */ eventTime,
/* action */ action,
/* pointerCount */ pointerCount,
/* pointerProperties */ pointerProperties,
/* pointerCoords */ pointerCoords,
/* metaState */ motionEvent.metaState,
/* buttonState */ buttonState,
/* xPrecision */ motionEvent.xPrecision,
/* yPrecision */ motionEvent.yPrecision,
/* deviceId */ motionEvent.deviceId,
/* edgeFlags */ motionEvent.edgeFlags,
/* source */ motionEvent.source,
/* flags */ motionEvent.flags
)
val pointerInputEvent =
motionEventAdapter.convertToPointerInputEvent(event, this)!!
pointerInputEventProcessor.process(
pointerInputEvent,
this,
true
)
event.recycle()
}
/**
* This method is required to correctly support swipe-to-dismiss layouts on WearOS, which search
* their children for scrollable views to determine whether or not to intercept touch events –
* a sort of simplified nested scrolling mechanism.
*
* Because a composition may contain many scrollable and non-scrollable areas, and this method
* doesn't know which part of the view the caller cares about, it uses the
* [lastDownPointerPosition] as the location to check.
*/
override fun canScrollHorizontally(direction: Int): Boolean =
accessibilityDelegate.canScroll(vertical = false, direction, lastDownPointerPosition)
/** See [canScrollHorizontally]. */
override fun canScrollVertically(direction: Int): Boolean =
accessibilityDelegate.canScroll(vertical = true, direction, lastDownPointerPosition)
private fun isInBounds(motionEvent: MotionEvent): Boolean {
val x = motionEvent.x
val y = motionEvent.y
return (x in 0f..width.toFloat() && y in 0f..height.toFloat())
}
override fun localToScreen(localPosition: Offset): Offset {
recalculateWindowPosition()
val local = viewToWindowMatrix.map(localPosition)
return Offset(
local.x + windowPosition.x,
local.y + windowPosition.y
)
}
override fun screenToLocal(positionOnScreen: Offset): Offset {
recalculateWindowPosition()
val x = positionOnScreen.x - windowPosition.x
val y = positionOnScreen.y - windowPosition.y
return windowToViewMatrix.map(Offset(x, y))
}
private fun recalculateWindowPosition() {
if (!forceUseMatrixCache) {
val animationTime = AnimationUtils.currentAnimationTimeMillis()
if (animationTime != lastMatrixRecalculationAnimationTime) {
lastMatrixRecalculationAnimationTime = animationTime
recalculateWindowViewTransforms()
var viewParent = parent
var view: View = this
while (viewParent is ViewGroup) {
view = viewParent
viewParent = view.parent
}
view.getLocationOnScreen(tmpPositionArray)
val screenX = tmpPositionArray[0].toFloat()
val screenY = tmpPositionArray[1].toFloat()
view.getLocationInWindow(tmpPositionArray)
val windowX = tmpPositionArray[0].toFloat()
val windowY = tmpPositionArray[1].toFloat()
windowPosition = Offset(screenX - windowX, screenY - windowY)
}
}
}
/**
* Recalculates the window position based on the [motionEvent]'s coordinates and
* screen coordinates. Some devices give false positions for [getLocationOnScreen] in
* some unusual circumstances, so a different mechanism must be used to determine the
* actual position.
*/
private fun recalculateWindowPosition(motionEvent: MotionEvent) {
lastMatrixRecalculationAnimationTime = AnimationUtils.currentAnimationTimeMillis()
recalculateWindowViewTransforms()
val positionInWindow = viewToWindowMatrix.map(Offset(motionEvent.x, motionEvent.y))
windowPosition = Offset(
motionEvent.rawX - positionInWindow.x,
motionEvent.rawY - positionInWindow.y
)
}
private fun recalculateWindowViewTransforms() {
viewToWindowMatrix.reset()
transformMatrixToWindow(this, viewToWindowMatrix)
viewToWindowMatrix.invertTo(windowToViewMatrix)
}
override fun onCheckIsTextEditor(): Boolean = textInputServiceAndroid.isEditorFocused()
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? =
textInputServiceAndroid.createInputConnection(outAttrs)
override fun calculateLocalPosition(positionInWindow: Offset): Offset {
recalculateWindowPosition()
return windowToViewMatrix.map(positionInWindow)
}
override fun calculatePositionInWindow(localPosition: Offset): Offset {
recalculateWindowPosition()
return viewToWindowMatrix.map(localPosition)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
density = Density(context)
configurationChangeObserver(newConfig)
}
override fun onRtlPropertiesChanged(layoutDirection: Int) {
// This method can be called while View's constructor is running
// by way of resolving padding in response to initScrollbars.
// If we get such a call, don't try to write to a property delegate
// that hasn't been initialized yet.
if (superclassInitComplete) {
layoutDirectionFromInt(layoutDirection).let {
this.layoutDirection = it
_focusManager.layoutDirection = it
}
}
}
private fun autofillSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
if (hoverExitReceived) {
// Go ahead and send it now
removeCallbacks(sendHoverExitEvent)
sendHoverExitEvent.run()
}
if (isBadMotionEvent(event)) {
return false // Bad MotionEvent. Don't handle it.
}
if (event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN) &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
) {
// Accessibility touch exploration
return accessibilityDelegate.dispatchHoverEvent(event)
}
when (event.actionMasked) {
ACTION_HOVER_EXIT -> {
if (isInBounds(event)) {
if (event.getToolType(0) != TOOL_TYPE_MOUSE) {
// This may be caused by a press (e.g. stylus pressed on the screen), but
// we can't be sure until the ACTION_DOWN is received. Let's delay this
// message and see if the ACTION_DOWN comes.
previousMotionEvent?.recycle()
previousMotionEvent = MotionEvent.obtainNoHistory(event)
hoverExitReceived = true
post(sendHoverExitEvent)
return false
} else if (event.buttonState != 0) {
// We know that this is caused by a button press, so we can ignore it
return false
}
}
}
ACTION_HOVER_MOVE ->
// Check if we're receiving this when we've already handled it elsewhere
if (!isPositionChanged(event)) {
return false
}
}
val result = handleMotionEvent(event)
return result.dispatchedToAPointerInputModifier
}
private fun isBadMotionEvent(event: MotionEvent): Boolean {
return event.x.isNaN() ||
event.y.isNaN() ||
event.rawX.isNaN() ||
event.rawY.isNaN()
}
private fun isPositionChanged(event: MotionEvent): Boolean {
if (event.pointerCount != 1) {
return true
}
val lastEvent = previousMotionEvent
return lastEvent == null || event.rawX != lastEvent.rawX || event.rawY != lastEvent.rawY
}
private fun findViewByAccessibilityIdRootedAtCurrentView(
accessibilityId: Int,
currentView: View
): View? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val getAccessibilityViewIdMethod = View::class.java
.getDeclaredMethod("getAccessibilityViewId")
getAccessibilityViewIdMethod.isAccessible = true
if (getAccessibilityViewIdMethod.invoke(currentView) == accessibilityId) {
return currentView
}
if (currentView is ViewGroup) {
for (i in 0 until currentView.childCount) {
val foundView = findViewByAccessibilityIdRootedAtCurrentView(
accessibilityId,
currentView.getChildAt(i)
)
if (foundView != null) {
return foundView
}
}
}
}
return null
}
private var desiredPointerIcon: PointerIcon? = null
override val pointerIconService: PointerIconService =
object : PointerIconService {
override var current: PointerIcon
get() = desiredPointerIcon ?: PointerIconDefaults.Default
set(value) {
desiredPointerIcon = value
}
}
/**
* This overrides an @hide method in ViewGroup. Because of the @hide, the override keyword
* cannot be used, but the override works anyway because the ViewGroup method is not final.
* In Android P and earlier, the call path is
* AccessibilityInteractionController#findViewByAccessibilityId ->
* View#findViewByAccessibilityId -> ViewGroup#findViewByAccessibilityIdTraversal. In Android
* Q and later, AccessibilityInteractionController#findViewByAccessibilityId uses
* AccessibilityNodeIdManager and findViewByAccessibilityIdTraversal is only used by autofill.
*/
@Suppress("unused")
fun findViewByAccessibilityIdTraversal(accessibilityId: Int): View? {
try {
// AccessibilityInteractionController#findViewByAccessibilityId doesn't call this
// method in Android Q and later. Ideally, we should only define this method in
// Android P and earlier, but since we don't have a way to do so, we can simply
// invoke the hidden parent method after Android P. If in new android, the hidden method
// ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
// simply return null here because there will be no call to this method.
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val findViewByAccessibilityIdTraversalMethod = View::class.java
.getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
findViewByAccessibilityIdTraversalMethod.isAccessible = true
findViewByAccessibilityIdTraversalMethod.invoke(this, accessibilityId) as? View
} else {
findViewByAccessibilityIdRootedAtCurrentView(accessibilityId, this)
}
} catch (e: NoSuchMethodException) {
return null
}
}
override val isLifecycleInResumedState: Boolean
get() = viewTreeOwners?.lifecycleOwner
?.lifecycle?.currentState == Lifecycle.State.RESUMED
private fun transformMatrixToWindow(view: View, matrix: Matrix) {
val parentView = view.parent
if (parentView is View) {
transformMatrixToWindow(parentView, matrix)
matrix.preTranslate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
matrix.preTranslate(view.left.toFloat(), view.top.toFloat())
} else {
view.getLocationInWindow(tmpPositionArray)
matrix.preTranslate(-view.scrollX.toFloat(), -view.scrollY.toFloat())
matrix.preTranslate(tmpPositionArray[0].toFloat(), tmpPositionArray[1].toFloat())
}
val viewMatrix = view.matrix
if (!viewMatrix.isIdentity) {
matrix.preConcat(viewMatrix)
}
}
/**
* Like [android.graphics.Matrix.preConcat], for a Compose [Matrix] that accepts an [other]
* [android.graphics.Matrix].
*/
private fun Matrix.preConcat(other: android.graphics.Matrix) {
tmpCalculationMatrix.setFrom(other)
preTransform(tmpCalculationMatrix)
}
/**
* Like [android.graphics.Matrix.preTranslate], for a Compose [Matrix]
*/
private fun Matrix.preTranslate(x: Float, y: Float) {
tmpCalculationMatrix.reset()
tmpCalculationMatrix.translate(x, y)
preTransform(tmpCalculationMatrix)
}
override fun shouldDelayChildPressedState(): Boolean = false
companion object {
private const val FocusTag = "Compose Focus"
private const val MaximumLayerCacheSize = 10
private var systemPropertiesClass: Class<*>? = null
private var getBooleanMethod: Method? = null
// TODO(mount): replace with ViewCompat.isShowingLayoutBounds() when it becomes available.
@SuppressLint("PrivateApi", "BanUncheckedReflection")
private fun getIsShowingLayoutBounds(): Boolean = try {
if (systemPropertiesClass == null) {
systemPropertiesClass = Class.forName("android.os.SystemProperties")
getBooleanMethod = systemPropertiesClass?.getDeclaredMethod(
"getBoolean",
String::class.java,
Boolean::class.java
)
}
getBooleanMethod?.invoke(null, "debug.layout", false) as? Boolean ?: false
} catch (e: Exception) {
false
}
}
/**
* Combines objects populated via ViewTree*Owner
*/
class ViewTreeOwners(
/**
* The [LifecycleOwner] associated with this owner.
*/
val lifecycleOwner: LifecycleOwner,
/**
* The [SavedStateRegistryOwner] associated with this owner.
*/
val savedStateRegistryOwner: SavedStateRegistryOwner
)
}
/**
* Return the layout direction set by the [Locale][java.util.Locale].
*
* A convenience getter that translates [Configuration.getLayoutDirection] result into
* [LayoutDirection] instance.
*/
internal val Configuration.localeLayoutDirection: LayoutDirection
// We don't use the attached View's layout direction here since that layout direction may not
// be resolved since the composables may be composed without attaching to the RootViewImpl.
// In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
// configuration) as a default layout direction.
get() = layoutDirectionFromInt(layoutDirection)
private fun layoutDirectionFromInt(layoutDirection: Int): LayoutDirection = when (layoutDirection) {
android.util.LayoutDirection.LTR -> LayoutDirection.Ltr
android.util.LayoutDirection.RTL -> LayoutDirection.Rtl
else -> LayoutDirection.Ltr
}
/** @suppress */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@InternalComposeUiApi // used by testing infra
var textInputServiceFactory: (PlatformTextInputService) -> TextInputService =
{ TextInputService(it) }
/**
* These classes are here to ensure that the classes that use this API will get verified and can be
* AOT compiled. It is expected that this class will soft-fail verification, but the classes
* which use this method will pass.
*/
@RequiresApi(Build.VERSION_CODES.O)
private object AndroidComposeViewVerificationHelperMethodsO {
@RequiresApi(Build.VERSION_CODES.O)
@DoNotInline
fun focusable(view: View, focusable: Int, defaultFocusHighlightEnabled: Boolean) {
view.focusable = focusable
// not to add the default focus highlight to the whole compose view
view.defaultFocusHighlightEnabled = defaultFocusHighlightEnabled
}
}
@RequiresApi(Build.VERSION_CODES.N)
private object AndroidComposeViewVerificationHelperMethodsN {
@DoNotInline
@RequiresApi(Build.VERSION_CODES.N)
fun setPointerIcon(view: View, icon: PointerIcon?) {
val iconToSet = when (icon) {
is AndroidPointerIcon ->
icon.pointerIcon
is AndroidPointerIconType ->
android.view.PointerIcon.getSystemIcon(view.context, icon.type)
else ->
android.view.PointerIcon.getSystemIcon(
view.context,
android.view.PointerIcon.TYPE_DEFAULT
)
}
if (view.pointerIcon != iconToSet) {
view.pointerIcon = iconToSet
}
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private object AndroidComposeViewForceDarkModeQ {
@DoNotInline
@RequiresApi(Build.VERSION_CODES.Q)
fun disallowForceDark(view: View) {
view.isForceDarkAllowed = false
}
}
/**
* Sets this [Matrix] to be the result of this * [other]
*/
private fun Matrix.preTransform(other: Matrix) {
val v00 = dot(other, 0, this, 0)
val v01 = dot(other, 0, this, 1)
val v02 = dot(other, 0, this, 2)
val v03 = dot(other, 0, this, 3)
val v10 = dot(other, 1, this, 0)
val v11 = dot(other, 1, this, 1)
val v12 = dot(other, 1, this, 2)
val v13 = dot(other, 1, this, 3)
val v20 = dot(other, 2, this, 0)
val v21 = dot(other, 2, this, 1)
val v22 = dot(other, 2, this, 2)
val v23 = dot(other, 2, this, 3)
val v30 = dot(other, 3, this, 0)
val v31 = dot(other, 3, this, 1)
val v32 = dot(other, 3, this, 2)
val v33 = dot(other, 3, this, 3)
this[0, 0] = v00
this[0, 1] = v01
this[0, 2] = v02
this[0, 3] = v03
this[1, 0] = v10
this[1, 1] = v11
this[1, 2] = v12
this[1, 3] = v13
this[2, 0] = v20
this[2, 1] = v21
this[2, 2] = v22
this[2, 3] = v23
this[3, 0] = v30
this[3, 1] = v31
this[3, 2] = v32
this[3, 3] = v33
}
// Taken from Matrix.kt
private fun dot(m1: Matrix, row: Int, m2: Matrix, column: Int): Float {
return m1[row, 0] * m2[0, column] +
m1[row, 1] * m2[1, column] +
m1[row, 2] * m2[2, column] +
m1[row, 3] * m2[3, column]
}