| /* |
| * 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.ui.core |
| |
| import android.annotation.SuppressLint |
| import android.annotation.TargetApi |
| import android.content.Context |
| import android.content.res.Configuration |
| import android.os.Build |
| import android.os.Looper |
| import android.os.Parcelable |
| import android.util.Log |
| import android.util.SparseArray |
| import android.view.MotionEvent |
| import android.view.View |
| import android.view.ViewGroup |
| import android.view.ViewStructure |
| import android.view.accessibility.AccessibilityEvent |
| import android.view.accessibility.AccessibilityManager |
| import android.view.autofill.AutofillValue |
| import android.view.inputmethod.EditorInfo |
| import android.view.inputmethod.InputConnection |
| import androidx.annotation.RestrictTo |
| import androidx.core.view.ViewCompat |
| import androidx.core.view.accessibility.AccessibilityNodeProviderCompat |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.LifecycleOwner |
| import androidx.lifecycle.LifecycleRegistry |
| import androidx.lifecycle.ViewTreeLifecycleOwner |
| import androidx.ui.autofill.AndroidAutofill |
| import androidx.ui.autofill.Autofill |
| import androidx.ui.autofill.AutofillTree |
| import androidx.ui.autofill.performAutofill |
| import androidx.ui.autofill.populateViewStructure |
| import androidx.ui.autofill.registerCallback |
| import androidx.ui.autofill.unregisterCallback |
| import androidx.ui.core.Owner.Companion.enableExtraAssertions |
| import androidx.ui.core.clipboard.AndroidClipboardManager |
| import androidx.ui.core.clipboard.ClipboardManager |
| import androidx.ui.core.focus.FocusModifierImpl |
| import androidx.ui.core.hapticfeedback.AndroidHapticFeedback |
| import androidx.ui.core.hapticfeedback.HapticFeedback |
| import androidx.ui.core.pointerinput.MotionEventAdapter |
| import androidx.ui.core.pointerinput.PointerInputEventProcessor |
| import androidx.ui.core.pointerinput.ProcessResult |
| import androidx.ui.core.semantics.SemanticsConfiguration |
| import androidx.ui.core.semantics.SemanticsNode |
| import androidx.ui.core.semantics.SemanticsOwner |
| import androidx.ui.core.semantics.getAllSemanticsNodesToMap |
| import androidx.ui.core.semantics.getOrNull |
| import androidx.ui.core.text.AndroidFontResourceLoader |
| import androidx.ui.core.texttoolbar.AndroidTextToolbar |
| import androidx.ui.core.texttoolbar.TextToolbar |
| import androidx.ui.focus.FocusDetailedState.Active |
| import androidx.ui.focus.FocusDetailedState.Inactive |
| import androidx.ui.geometry.Size |
| import androidx.ui.graphics.Canvas |
| import androidx.ui.input.TextInputService |
| import androidx.ui.input.TextInputServiceAndroid |
| import androidx.ui.savedinstancestate.UiSavedStateRegistry |
| import androidx.ui.semantics.SemanticsProperties |
| import androidx.ui.text.font.Font |
| import androidx.ui.unit.Density |
| import androidx.ui.unit.IntPx |
| import androidx.ui.unit.IntPxPosition |
| import androidx.ui.unit.Px |
| import androidx.ui.unit.PxSize |
| import androidx.ui.unit.ipx |
| import androidx.ui.unit.max |
| import androidx.ui.util.trace |
| import org.jetbrains.annotations.TestOnly |
| import java.lang.reflect.Method |
| |
| /*** |
| * This function creates an instance of [AndroidOwner] |
| * |
| * @param context Context to use to create a View |
| * @param lifecycleOwner Current [LifecycleOwner]. When it is not provided we will try to get the |
| * owner using [ViewTreeLifecycleOwner] when we will be attached. |
| */ |
| fun createOwner( |
| context: Context, |
| lifecycleOwner: LifecycleOwner? = null |
| ): AndroidOwner = AndroidComposeView(context, lifecycleOwner).also { |
| AndroidOwner.onAndroidOwnerCreatedCallback?.invoke(it) |
| } |
| |
| @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| internal class AndroidComposeView constructor( |
| context: Context, |
| lifecycleOwner: LifecycleOwner? |
| ) : ViewGroup(context), AndroidOwner { |
| |
| override val view: View = this |
| private val accessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this) |
| |
| override var density = Density(context) |
| private set |
| |
| private val focusModifier: FocusModifierImpl = FocusModifierImpl(Inactive) |
| |
| override val root = LayoutNode().also { |
| it.measureBlocks = RootMeasureBlocks |
| it.layoutDirection = |
| context.applicationContext.resources.configuration.localeLayoutDirection |
| it.modifier = Modifier.drawLayer(clipToBounds = false) + focusModifier |
| } |
| |
| // LayoutNodes that need measure and layout |
| private val relayoutNodes = DepthSortedSet<LayoutNode>(enableExtraAssertions) |
| |
| private inner class SemanticsNodeCopy( |
| semanticsNode: SemanticsNode |
| ) { |
| val config = semanticsNode.config |
| val children: MutableSet<Int> = mutableSetOf() |
| |
| init { |
| for (child in semanticsNode.children) { |
| children.add(child.id) |
| } |
| } |
| } |
| override val semanticsOwner: SemanticsOwner = SemanticsOwner(root) |
| private var semanticsNodes: MutableMap<Int, SemanticsNodeCopy> = mutableMapOf() |
| private var semanticsRoot = SemanticsNodeCopy(semanticsOwner.rootSemanticsNode) |
| |
| // 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. |
| internal val dirtyLayers = mutableListOf<OwnedLayer>() |
| |
| var ref: Ref<AndroidComposeView>? = null |
| set(value) { |
| field = value |
| if (value != null) { |
| value.value = this |
| } |
| } |
| |
| private val motionEventAdapter = MotionEventAdapter() |
| private val pointerInputEventProcessor = PointerInputEventProcessor(root) |
| |
| var constraints = Constraints.fixed(width = IntPx.Zero, height = IntPx.Zero) |
| // TODO(mount): reinstate when coroutines are supported by IR compiler |
| // private val ownerScope = CoroutineScope(Dispatchers.Main.immediate + Job()) |
| |
| // Used for updating the ConfigurationAmbient when configuration changes - consume the |
| // configuration ambient instead of changing this observer if you are writing a component that |
| // adapts to configuration changes. |
| override var configurationChangeObserver: () -> Unit = {} |
| |
| private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null |
| |
| // Used as an ambient for performing autofill. |
| override val autofill: Autofill? get() = _autofill |
| |
| override var measureIteration: Long = 1L |
| get() { |
| require(duringMeasureLayout) { |
| "measureIteration should be only used during the measure/layout pass" |
| } |
| return field |
| } |
| private set |
| |
| override fun dispatchWindowFocusChanged(hasFocus: Boolean) { |
| if (hasFocus) { |
| focusModifier.focusDetailedState = Active |
| // TODO(b/152535715): propagate focus to children based on child focusability. |
| } else { |
| // If this view lost focus, clear focus from the children. For now we clear focus |
| // from the children by requesting focus on the parent. |
| // TODO(b/151335411): use clearFocus() instead. |
| focusModifier.apply { |
| requestFocus() |
| focusDetailedState = Inactive |
| } |
| } |
| } |
| |
| /** |
| * Flag to indicate that we're currently measuring. |
| */ |
| private var duringMeasureLayout = false |
| /** |
| * Stores the list of [LayoutNode]s scheduled to be remeasured in the next measure/layout pass. |
| * We were unable to mark them as needsRemeasure=true previously as this request happened |
| * during the previous measure/layout pass and they were already measured as part of it. |
| * See [onRequestMeasure] for more details. |
| */ |
| private val postponedMeasureRequests = mutableListOf<LayoutNode>() |
| |
| private val modelObserver = ModelObserver { command -> |
| if (handler.looper === Looper.myLooper()) { |
| command() |
| } else { |
| handler.post(command) |
| } |
| } |
| |
| private val onCommitAffectingMeasure: (LayoutNode) -> Unit = { layoutNode -> |
| onRequestMeasure(layoutNode) |
| } |
| |
| private val onCommitAffectingLayout: (LayoutNode) -> Unit = { layoutNode -> |
| requestRelayout(layoutNode) |
| } |
| |
| internal val onCommitAffectingLayer: (OwnedLayer) -> Unit = { layer -> |
| layer.invalidate() |
| } |
| |
| private val onCommitAffectingLayerParams: (OwnedLayer) -> Unit = { layer -> |
| handler.postAtFrontOfQueue { |
| updateLayerProperties(layer) |
| } |
| } |
| |
| private val onPositionedDispatcher = OnPositionedDispatcher() |
| |
| override var showLayoutBounds = false |
| |
| private val consistencyChecker: LayoutTreeConsistencyChecker? = |
| if (enableExtraAssertions) { |
| LayoutTreeConsistencyChecker( |
| root, |
| { duringMeasureLayout }, |
| relayoutNodes, |
| postponedMeasureRequests |
| ) |
| } else { |
| null |
| } |
| |
| override fun pauseModelReadObserveration(block: () -> Unit) = |
| modelObserver.pauseObservingReads(block) |
| |
| init { |
| setWillNotDraw(false) |
| isFocusable = true |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| focusable = View.FOCUSABLE |
| // not to add the default focus highlight to the whole compose view |
| defaultFocusHighlightEnabled = false |
| } |
| isFocusableInTouchMode = true |
| clipChildren = false |
| root.isPlaced = true |
| ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate) |
| } |
| |
| override fun onInvalidate(drawNode: DrawNode) { |
| invalidate(drawNode) |
| } |
| |
| override fun onInvalidate(layoutNode: LayoutNode) { |
| invalidate(layoutNode) |
| } |
| |
| private fun invalidate(node: ComponentNode) { |
| val layer = node.containingLayoutNode?.innerLayoutNodeWrapper?.findLayer() |
| if (layer == null) { |
| invalidate() |
| } else { |
| layer.invalidate() |
| } |
| } |
| |
| override fun onRequestMeasure(layoutNode: LayoutNode) { |
| trace("AndroidOwner:onRequestMeasure") { |
| layoutNode.requireOwner() |
| if (enableExtraAssertions) { |
| Log.d("AndroidOwner", "onRequestMeasure on $layoutNode") |
| } |
| if (layoutNode.isMeasuring) { |
| // we're already measuring it, let's swallow. example when it happens: we compose |
| // DataNode inside WithConstraints, this calls onRequestMeasure on DataNode's |
| // parent, but this parent is WithConstraints which is currently measuring. |
| return |
| } |
| if (layoutNode.needsRemeasure) { |
| // requestMeasure has already been called for this node |
| return |
| } |
| if (layoutNode.isLayingOut) { |
| // requestMeasure is currently laying out and it is incorrect to request remeasure |
| // now, let's postpone it. |
| layoutNode.markRemeasureRequested() |
| postponedMeasureRequests.add(layoutNode) |
| consistencyChecker?.assertConsistent() |
| return |
| } |
| |
| // find root of layout request: |
| var layout = layoutNode |
| while (layout.affectsParentSize && layout.parentLayoutNode != null) { |
| val parent = layout.parentLayoutNode!! |
| if (parent.isMeasuring || parent.isLayingOut) { |
| if (!layout.needsRemeasure) { |
| layout.markRemeasureRequested() |
| // parent is currently measuring and we set needsRemeasure to true so if |
| // the parent didn't yet try to measure the node it will remeasure it. |
| // if the parent didn't plan to measure during this pass then needsRemeasure |
| // stay 'true' and we will manually call 'onRequestMeasure' for all |
| // the not-measured nodes in 'postponedMeasureRequests'. |
| postponedMeasureRequests.add(layout) |
| } |
| consistencyChecker?.assertConsistent() |
| return |
| } else { |
| layout.markRemeasureRequested() |
| if (parent.needsRemeasure) { |
| // don't need to do anything else since the parent is already scheduled |
| // for a remeasuring |
| consistencyChecker?.assertConsistent() |
| return |
| } |
| layout = parent |
| } |
| } |
| layout.markRemeasureRequested() |
| |
| requestRelayout(layout.parentLayoutNode ?: layout) |
| } |
| } |
| |
| private fun requestRelayout(layoutNode: LayoutNode) { |
| if (layoutNode.needsRelayout || (layoutNode.needsRemeasure && layoutNode !== root) || |
| layoutNode.isLayingOut || layoutNode.isMeasuring |
| ) { |
| // don't need to do anything else since the parent is already scheduled |
| // for a relayout (measure will trigger relayout), or is laying out right now |
| consistencyChecker?.assertConsistent() |
| return |
| } |
| layoutNode.requireOwner() |
| var nodeToRelayout = layoutNode |
| |
| // mark alignments as dirty first |
| if (!layoutNode.alignmentLinesRequired) { |
| // Mark parents alignment lines as dirty, for cases when we needed alignment lines |
| // at some point, but currently they are not queried anymore. If they are actively |
| // queried, they will be made dirty below in this method. |
| var layout: LayoutNode? = layoutNode |
| while (layout != null && !layout.dirtyAlignmentLines) { |
| layout.dirtyAlignmentLines = true |
| layout = layout.parentLayoutNode |
| } |
| } else { |
| var layout = layoutNode |
| while (layout != layoutNode.alignmentLinesQueryOwner && |
| // and relayout or remeasure(includes relayout) is not scheduled already |
| !(layout.needsRelayout || layout.needsRemeasure || layoutNode.isLayingOut || |
| layoutNode.isMeasuring) |
| ) { |
| layout.markRelayoutRequested() |
| layout.dirtyAlignmentLines = true |
| if (layout.parentLayoutNode == null) break |
| layout = layout.parentLayoutNode!! |
| } |
| layout.dirtyAlignmentLines = true |
| nodeToRelayout = layout |
| } |
| |
| nodeToRelayout.markRelayoutRequested() |
| relayoutNodes.add(nodeToRelayout) |
| if (!duringMeasureLayout) { |
| if (nodeToRelayout == root || constraints.isZero) { |
| requestLayout() |
| } else { |
| // Invalidate and catch measureAndLayout() in the dispatchDraw() |
| onInvalidate(nodeToRelayout) |
| } |
| } |
| consistencyChecker?.assertConsistent() |
| } |
| |
| private fun LayoutNode.markRemeasureRequested() { |
| needsRemeasure = true |
| if (needsRelayout) { |
| // cancel needsRelayout as remeasure includes relayout |
| needsRelayout = false |
| } |
| } |
| |
| private fun LayoutNode.markRelayoutRequested() { |
| // remeasure includes relayout so we are ok |
| if (!needsRemeasure) { |
| needsRelayout = true |
| } |
| } |
| |
| override fun onAttach(node: ComponentNode) { |
| } |
| |
| override fun onDetach(node: ComponentNode) { |
| when (node) { |
| is LayoutNode -> { |
| relayoutNodes.remove(node) |
| } |
| } |
| modelObserver.clear(node) |
| } |
| |
| private val androidViewsHandler by lazy(LazyThreadSafetyMode.NONE) { |
| AndroidViewsHandler(context).also { addView(it) } |
| } |
| private val viewLayersContainer by lazy(LazyThreadSafetyMode.NONE) { |
| ViewLayerContainer(context).also { addView(it) } |
| } |
| |
| override fun addAndroidView(view: View, layoutNode: LayoutNode) { |
| androidViewsHandler.addView(view) |
| androidViewsHandler.layoutNode[view] = layoutNode |
| } |
| |
| override fun removeAndroidView(view: View) { |
| androidViewsHandler.removeView(view) |
| androidViewsHandler.layoutNode.remove(view) |
| } |
| |
| /** |
| * Iterates through all LayoutNodes that have requested layout and measures and lays them out |
| */ |
| override fun measureAndLayout() { |
| trace("AndroidOwner:measureAndLayout") { |
| if (relayoutNodes.isNotEmpty()) { |
| duringMeasureLayout = true |
| relayoutNodes.popEach { layoutNode -> |
| measureIteration++ |
| if (layoutNode === root) { |
| // it is the root node - the only top node from relayoutNodes |
| // which needs to be remeasured. |
| layoutNode.measure(constraints, layoutNode.layoutDirection) |
| } |
| require(!layoutNode.needsRemeasure) { |
| "$layoutNode shouldn't require remeasure. relayoutNodes " + |
| "consists of the top nodes of the affected subtrees" |
| } |
| if (layoutNode.needsRelayout) { |
| layoutNode.layout() |
| onPositionedDispatcher.onNodePositioned(layoutNode) |
| consistencyChecker?.assertConsistent() |
| } |
| // execute postponed `onRequestMeasure` |
| if (postponedMeasureRequests.isNotEmpty()) { |
| postponedMeasureRequests.forEach { |
| // if it was detached or already measured by the parent then skip it |
| if (it.isAttached() && it.needsRemeasure) { |
| it.needsRemeasure = false |
| onRequestMeasure(it) |
| } |
| } |
| postponedMeasureRequests.clear() |
| } |
| } |
| duringMeasureLayout = false |
| onPositionedDispatcher.dispatch() |
| consistencyChecker?.assertConsistent() |
| } |
| } |
| } |
| |
| override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
| savedStateDelegate.stopWaitingForStateRestoration() |
| trace("AndroidOwner:onMeasure") { |
| val targetWidth = convertMeasureSpec(widthMeasureSpec) |
| val targetHeight = convertMeasureSpec(heightMeasureSpec) |
| |
| this.constraints = Constraints( |
| targetWidth.min, targetWidth.max, |
| targetHeight.min, targetHeight.max |
| ) |
| |
| relayoutNodes.add(root) |
| onPositionedDispatcher.disableDispatching { |
| // we want to 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. |
| measureAndLayout() |
| } |
| setMeasuredDimension(root.width.value, root.height.value) |
| } |
| } |
| |
| override fun observeLayoutModelReads(node: LayoutNode, block: () -> Unit) { |
| modelObserver.observeReads(node, onCommitAffectingLayout, block) |
| } |
| |
| override fun observeMeasureModelReads(node: LayoutNode, block: () -> Unit) { |
| modelObserver.observeReads(node, onCommitAffectingMeasure, block) |
| } |
| |
| fun observeLayerModelReads(layer: OwnedLayer, block: () -> Unit) { |
| modelObserver.observeReads(layer, onCommitAffectingLayer, block) |
| } |
| |
| override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { |
| onPositionedDispatcher.dispatch() |
| } |
| |
| override fun onDraw(canvas: android.graphics.Canvas) { |
| } |
| |
| override fun callDraw( |
| canvas: Canvas, |
| node: ComponentNode, |
| parentSize: PxSize |
| ) { |
| trace("AndroidOwner:callDraw") { |
| when (node) { |
| is DrawNode -> { |
| val onPaintWithChildren = node.onPaintWithChildren |
| if (onPaintWithChildren != null) { |
| val ownerData = node.ownerData |
| val receiver: DrawScopeImpl |
| val size = Size(parentSize.width.value, parentSize.height.value) |
| if (ownerData == null) { |
| receiver = DrawScopeImpl(node, canvas, size, density) |
| node.ownerData = receiver |
| } else { |
| receiver = ownerData as DrawScopeImpl |
| receiver.childDrawn = false |
| receiver.canvas = canvas |
| receiver.size = size |
| receiver.currentDensity = density |
| } |
| onPaintWithChildren(receiver, canvas, parentSize) |
| if (!receiver.childDrawn) { |
| with(receiver) { |
| with(density) { |
| drawContent() |
| } |
| } |
| } |
| } else { |
| val onPaint = node.onPaint!! |
| density.onPaint(canvas, parentSize) |
| } |
| node.needsPaint = false |
| } |
| is LayoutNode -> { |
| if (node.isPlaced) { |
| require(!node.needsRemeasure) { "$node is not measured, draw requested" } |
| require(!node.needsRelayout) { "$node is not laid out, draw requested" } |
| node.draw(canvas) |
| } |
| } |
| else -> node.visitChildren { |
| callDraw(canvas, it, parentSize) |
| } |
| } |
| } |
| } |
| |
| override fun createLayer( |
| drawLayerModifier: DrawLayerModifier, |
| drawBlock: (Canvas) -> Unit, |
| invalidateParentLayer: () -> Unit |
| ): OwnedLayer { |
| val layer = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || isInEditMode()) { |
| ViewLayer( |
| this, viewLayersContainer, drawLayerModifier, drawBlock, |
| invalidateParentLayer |
| ) |
| } else { |
| RenderNodeLayer(this, drawLayerModifier, drawBlock, invalidateParentLayer) |
| } |
| |
| updateLayerProperties(layer) |
| |
| return layer |
| } |
| |
| private fun updateLayerProperties(layer: OwnedLayer) { |
| modelObserver.observeReads(layer, onCommitAffectingLayerParams) { |
| layer.updateLayerProperties() |
| } |
| } |
| |
| private fun checkForSemanticsChanges() { |
| val newSemanticsNodes = semanticsOwner.getAllSemanticsNodesToMap() |
| |
| // Structural change |
| sendSemanticsStructureChangeEvents(semanticsOwner.rootSemanticsNode, semanticsRoot) |
| |
| // Property change |
| for (id in newSemanticsNodes.keys) { |
| if (semanticsNodes.contains(id)) { |
| // We do doing this search because the new configuration is set as a whole, so we |
| // can't indicate which property is changed when setting the new configuration. |
| var newNode = newSemanticsNodes[id] |
| var oldNode = semanticsNodes[id] |
| for (entry in newNode!!.config) { |
| if (entry.value == oldNode!!.config.getOrNull(entry.key)) { |
| continue |
| } |
| when (entry.key) { |
| // we are in aosp, so can't use the state description yet. |
| SemanticsProperties.AccessibilityValue, |
| SemanticsProperties.AccessibilityLabel -> |
| accessibilityDelegate.sendEventForVirtualView( |
| semanticsNodeIdToAccessibilityVirtualNodeId(id), |
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, |
| AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION, |
| entry.value as CharSequence |
| ) |
| else -> { |
| // TODO(b/151840490) send the correct events when property changes |
| } |
| } |
| } |
| } |
| } |
| |
| // Update the cache |
| semanticsNodes.clear() |
| for (entry in newSemanticsNodes.entries) { |
| semanticsNodes[entry.key] = SemanticsNodeCopy(entry.value) |
| } |
| semanticsRoot = SemanticsNodeCopy(semanticsOwner.rootSemanticsNode) |
| } |
| |
| private fun sendSemanticsStructureChangeEvents( |
| newNode: SemanticsNode, |
| oldNode: SemanticsNodeCopy |
| ) { |
| var newChildren: MutableSet<Int> = mutableSetOf() |
| |
| // If any child is added, clear the subtree rooted at this node and return. |
| for (child in newNode.children) { |
| if (!oldNode.children.contains(child.id)) { |
| accessibilityDelegate.sendEventForVirtualView( |
| semanticsNodeIdToAccessibilityVirtualNodeId(newNode.id), |
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, |
| AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE, |
| null |
| ) |
| return |
| } |
| newChildren.add(child.id) |
| } |
| |
| // If any child is deleted, clear the subtree rooted at this node and return. |
| for (child in oldNode.children) { |
| if (!newChildren.contains(child)) { |
| accessibilityDelegate.sendEventForVirtualView( |
| semanticsNodeIdToAccessibilityVirtualNodeId(newNode.id), |
| AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, |
| AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE, |
| null |
| ) |
| return |
| } |
| } |
| |
| for (child in newNode.children) { |
| sendSemanticsStructureChangeEvents(child, semanticsNodes[child.id]!!) |
| } |
| } |
| |
| private fun semanticsNodeIdToAccessibilityVirtualNodeId(id: Int): Int { |
| if (id == semanticsOwner.rootSemanticsNode.id) { |
| return AccessibilityNodeProviderCompat.HOST_VIEW_ID |
| } |
| return id |
| } |
| |
| override fun dispatchDraw(canvas: android.graphics.Canvas) { |
| // TODO(b/154633012): move this to a proper place |
| if ((context.getSystemService(Context.ACCESSIBILITY_SERVICE) |
| as AccessibilityManager).isEnabled) { |
| checkForSemanticsChanges() |
| } |
| |
| measureAndLayout() |
| val uiCanvas = Canvas(canvas) |
| // 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. |
| root.draw(uiCanvas) |
| |
| if (dirtyLayers.isNotEmpty()) { |
| for (i in 0 until dirtyLayers.size) { |
| val layer = dirtyLayers[i] |
| layer.updateDisplayList() |
| } |
| dirtyLayers.clear() |
| } |
| } |
| |
| override var lifecycleOwner: LifecycleOwner? = lifecycleOwner |
| private set |
| |
| private var onLifecycleAvailable: ((LifecycleOwner) -> Unit)? = null |
| |
| override fun setOnLifecycleOwnerAvailable(callback: (LifecycleOwner) -> Unit) { |
| require(lifecycleOwner == null) { "LifecycleOwner is already available" } |
| onLifecycleAvailable = callback |
| } |
| |
| // Workaround for the cases when we don't have a real LifecycleOwner, this happens when |
| // ViewTreeLifecycleOwner.get(this) returned null: |
| // 1) we are in AppCompatActivity and there is a bug for(should be fixed soon) |
| // 2) we are in a regular Activity. once we fix bug in AppCompatActivity we stop support it. |
| private val viewLifecycleOwner = object : LifecycleOwner { |
| val lifecycleRegistry = LifecycleRegistry(this) |
| override fun getLifecycle() = lifecycleRegistry |
| } |
| |
| override fun onAttachedToWindow() { |
| super.onAttachedToWindow() |
| showLayoutBounds = getIsShowingLayoutBounds() |
| modelObserver.enableModelUpdatesObserving(true) |
| ifDebug { if (autofillSupported()) _autofill?.registerCallback() } |
| root.attach(this) |
| if (lifecycleOwner == null) { |
| lifecycleOwner = ViewTreeLifecycleOwner.get(this) ?: viewLifecycleOwner |
| } |
| onLifecycleAvailable?.invoke(lifecycleOwner!!) |
| onLifecycleAvailable = null |
| viewLifecycleOwner.lifecycleRegistry.currentState = Lifecycle.State.RESUMED |
| } |
| |
| override fun onDetachedFromWindow() { |
| super.onDetachedFromWindow() |
| modelObserver.enableModelUpdatesObserving(false) |
| ifDebug { if (autofillSupported()) _autofill?.unregisterCallback() } |
| root.detach() |
| viewLifecycleOwner.lifecycleRegistry.currentState = Lifecycle.State.CREATED |
| } |
| |
| 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) |
| } |
| |
| // TODO(shepshapard): Test this method. |
| override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean { |
| val processResult = trace("AndroidOwner:onTouch") { |
| val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent) |
| if (pointerInputEvent != null) { |
| pointerInputEventProcessor.process(pointerInputEvent) |
| } else { |
| pointerInputEventProcessor.processCancel() |
| ProcessResult( |
| dispatchedToAPointerInputModifier = false, |
| anyMovementConsumed = false |
| ) |
| } |
| } |
| |
| if (processResult.anyMovementConsumed) { |
| parent.requestDisallowInterceptTouchEvent(true) |
| } |
| |
| return processResult.dispatchedToAPointerInputModifier |
| } |
| |
| private fun convertMeasureSpec(measureSpec: Int): ConstraintRange { |
| val mode = MeasureSpec.getMode(measureSpec) |
| val size = IntPx(MeasureSpec.getSize(measureSpec)) |
| return when (mode) { |
| MeasureSpec.EXACTLY -> ConstraintRange(size, size) |
| MeasureSpec.UNSPECIFIED -> ConstraintRange(IntPx.Zero, IntPx.Infinity) |
| MeasureSpec.AT_MOST -> ConstraintRange(IntPx.Zero, size) |
| else -> throw IllegalStateException() |
| } |
| } |
| |
| private val textInputServiceAndroid = TextInputServiceAndroid(this) |
| |
| override val textInputService = TextInputService(textInputServiceAndroid) |
| |
| override val fontLoader: Font.ResourceLoader = AndroidFontResourceLoader(context) |
| |
| /** |
| * Provide haptic feedback to the user. Use the Android version of haptic feedback. |
| */ |
| override val hapticFeedBack: HapticFeedback = |
| AndroidHapticFeedback(this) |
| |
| /** |
| * Provide clipboard manager to the user. Use the Android version of clipboard manager. |
| */ |
| override val clipboardManager: ClipboardManager = AndroidClipboardManager(context) |
| |
| /** |
| * 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) |
| |
| override fun onCheckIsTextEditor(): Boolean = textInputServiceAndroid.isEditorFocused() |
| |
| override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? = |
| textInputServiceAndroid.createInputConnection(outAttrs) |
| |
| override fun calculatePosition(): IntPxPosition { |
| val positionArray = intArrayOf(0, 0) |
| getLocationOnScreen(positionArray) |
| return IntPxPosition(positionArray[0].ipx, positionArray[1].ipx) |
| } |
| |
| override fun onConfigurationChanged(newConfig: Configuration?) { |
| super.onConfigurationChanged(newConfig) |
| density = Density(context) |
| configurationChangeObserver() |
| } |
| |
| private val savedStateDelegate = SavedStateDelegate { |
| // When AndroidComposeView is composed into some ViewGroup we just add ourself as a child |
| // for this ViewGroup. And we don't have any id on AndroidComposeView as we can't make it |
| // unique, but we require this parent ViewGroup to have an unique id for the saved |
| // instance state mechanism to work (similarly to how it works without Compose). |
| // When we composed into Activity our parent is the ViewGroup with android.R.id.content. |
| (parent as? View)?.id ?: View.NO_ID |
| } |
| |
| /** |
| * The current instance of [UiSavedStateRegistry]. If it's null you can wait for it to became |
| * available using [setOnSavedStateRegistryAvailable]. |
| */ |
| override val savedStateRegistry: UiSavedStateRegistry? |
| get() = savedStateDelegate.savedStateRegistry |
| |
| /** |
| * Allows other components to be notified when the [UiSavedStateRegistry] became available. |
| */ |
| override fun setOnSavedStateRegistryAvailable(callback: (UiSavedStateRegistry) -> Unit) { |
| savedStateDelegate.setOnSaveRegistryAvailable(callback) |
| } |
| |
| override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) { |
| val superState = super.onSaveInstanceState()!! |
| savedStateDelegate.dispatchSaveInstanceState(container, superState) |
| } |
| |
| override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) { |
| val superState = savedStateDelegate.dispatchRestoreInstanceState(container) |
| onRestoreInstanceState(superState) |
| } |
| |
| private inner class DrawScopeImpl( |
| private val drawNode: DrawNode, |
| var canvas: Canvas, |
| override var size: Size, |
| var currentDensity: Density |
| ) : Canvas by canvas, Density by currentDensity, ContentDrawScope { |
| internal var childDrawn = false |
| // Draw composable does not support Rtl and will be removed soon anyway. |
| // The only place where Draw is in use is Table which will be updated anyway b/150276337. |
| override val layoutDirection: LayoutDirection = LayoutDirection.Ltr |
| |
| override fun drawContent() { |
| if (childDrawn) { |
| throw IllegalStateException("Cannot call drawContent() twice within Draw element") |
| } |
| childDrawn = true |
| val pxSize = PxSize(Px(size.width), Px(size.height)) |
| drawNode.visitChildren { child -> |
| callDraw(canvas, child, pxSize) |
| } |
| } |
| } |
| |
| private fun autofillSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O |
| |
| public override fun dispatchHoverEvent(event: MotionEvent): Boolean { |
| return accessibilityDelegate.dispatchHoverEvent(event) |
| } |
| |
| companion object { |
| private var systemPropertiesClass: Class<*>? = null |
| private var getBooleanMethod: Method? = null |
| |
| // TODO(mount): replace with ViewCompat.isShowingLayoutBounds() when it becomes available. |
| @SuppressLint("PrivateApi") |
| private fun getIsShowingLayoutBounds(): Boolean { |
| try { |
| if (systemPropertiesClass == null) { |
| systemPropertiesClass = Class.forName("android.os.SystemProperties") |
| getBooleanMethod = systemPropertiesClass?.getDeclaredMethod( |
| "getBoolean", |
| String::class.java, |
| Boolean::class.java |
| ) |
| } |
| |
| return getBooleanMethod?.invoke(null, "debug.layout", false) as? Boolean ?: false |
| } catch (e: Exception) { |
| return false |
| } |
| } |
| |
| private val RootMeasureBlocks = object : LayoutNode.MeasureBlocks { |
| override fun measure( |
| measureScope: MeasureScope, |
| measurables: List<Measurable>, |
| constraints: Constraints, |
| layoutDirection: LayoutDirection |
| ): MeasureScope.MeasureResult { |
| return when { |
| measurables.isEmpty() -> measureScope.layout(IntPx.Zero, IntPx.Zero) {} |
| measurables.size == 1 -> { |
| val placeable = measurables[0].measure(constraints, layoutDirection) |
| measureScope.layout(placeable.width, placeable.height) { |
| placeable.place(IntPx.Zero, IntPx.Zero) |
| } |
| } |
| else -> { |
| val placeables = measurables.map { |
| it.measure(constraints, layoutDirection) |
| } |
| var maxWidth = IntPx.Zero |
| var maxHeight = IntPx.Zero |
| placeables.forEach { placeable -> |
| maxWidth = max(placeable.width, maxWidth) |
| maxHeight = max(placeable.height, maxHeight) |
| } |
| measureScope.layout(maxWidth, maxHeight) { |
| placeables.forEach { placeable -> |
| placeable.place(IntPx.Zero, IntPx.Zero) |
| } |
| } |
| } |
| } |
| } |
| |
| override fun minIntrinsicWidth( |
| intrinsicMeasureScope: IntrinsicMeasureScope, |
| measurables: List<IntrinsicMeasurable>, |
| h: IntPx, |
| layoutDirection: LayoutDirection |
| ) = error("Undefined intrinsics block and it is required") |
| |
| override fun minIntrinsicHeight( |
| intrinsicMeasureScope: IntrinsicMeasureScope, |
| measurables: List<IntrinsicMeasurable>, |
| w: IntPx, |
| layoutDirection: LayoutDirection |
| ) = error("Undefined intrinsics block and it is required") |
| |
| override fun maxIntrinsicWidth( |
| intrinsicMeasureScope: IntrinsicMeasureScope, |
| measurables: List<IntrinsicMeasurable>, |
| h: IntPx, |
| layoutDirection: LayoutDirection |
| ) = error("Undefined intrinsics block and it is required") |
| |
| override fun maxIntrinsicHeight( |
| intrinsicMeasureScope: IntrinsicMeasureScope, |
| measurables: List<IntrinsicMeasurable>, |
| w: IntPx, |
| layoutDirection: LayoutDirection |
| ) = error("Undefined intrinsics block and it is required") |
| } |
| } |
| } |
| |
| /** |
| * Interface to be implemented by [Owner]s able to handle Android View specific functionality. |
| */ |
| interface AndroidOwner : Owner { |
| |
| /** |
| * The view backing this Owner. |
| */ |
| val view: View |
| |
| /** |
| * Called to inform the owner that a new Android [View] was [attached][Owner.onAttach] |
| * to the hierarchy. |
| */ |
| fun addAndroidView(view: View, layoutNode: LayoutNode) |
| |
| /** |
| * Called to inform the owner that an Android [View] was [detached][Owner.onDetach] |
| * from the hierarchy. |
| */ |
| fun removeAndroidView(view: View) |
| |
| /** @suppress */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| companion object { |
| /** |
| * Called after an [AndroidOwner] is created. Used by AndroidComposeTestRule to keep |
| * track of all attached [AndroidComposeView]s. Not to be set or used by any other |
| * component. |
| */ |
| var onAndroidOwnerCreatedCallback: ((AndroidOwner) -> Unit)? = null |
| @TestOnly |
| set |
| } |
| } |
| |
| private class ConstraintRange(val min: IntPx, val max: IntPx) |
| |
| /** |
| * Return the layout direction set by the [Locale]. |
| * |
| * A convenience getter that translates [Configuration.getLayoutDirection] result into |
| * [LayoutDirection] instance. |
| */ |
| val Configuration.localeLayoutDirection: LayoutDirection |
| get() = when (layoutDirection) { |
| android.util.LayoutDirection.LTR -> LayoutDirection.Ltr |
| android.util.LayoutDirection.RTL -> LayoutDirection.Rtl |
| // API doc says Configuration#getLayoutDirection only returns LTR or RTL. |
| // Fallback to LTR for unexpected return value. |
| else -> LayoutDirection.Ltr |
| } |