1 /* <lambda>null2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.xr.compose.platform 18 19 import android.os.Handler 20 import android.os.Looper 21 import androidx.compose.runtime.snapshots.SnapshotStateObserver 22 import androidx.lifecycle.DefaultLifecycleObserver 23 import androidx.lifecycle.LifecycleOwner 24 import androidx.xr.compose.subspace.node.SubspaceLayoutNode 25 import androidx.xr.compose.subspace.node.SubspaceOwner 26 import androidx.xr.compose.unit.VolumeConstraints 27 import androidx.xr.runtime.math.Pose 28 import androidx.xr.scenecore.PanelEntity 29 30 /** 31 * An implementation of the [SubspaceOwner] interface, bridging the Compose layout and rendering 32 * phases. 33 * 34 * Compose decouples layout computation from rendering. This necessitates two distinct trees that 35 * [SubspaceOwner] defines and owns: 36 * 1. The layout tree: Created by [SubspaceLayoutNode], this represents the app's defined hierarchy 37 * and is used for layout calculations. 38 * 2. The rendering tree: A flat tree rooted at the [SubspaceOwner]. This is used for rendering. The 39 * hierarchy is necessary to ensure [SpatialElement] instances are visible on a screen (detached 40 * [SpatialElement] instances are always hidden). The positions of [SpatialElement] are updated 41 * post-layout computation, during the placement phase. 42 * 43 * This class draws inspiration from the [androidx/compose/ui/platform/AndroidComposeView]. 44 */ 45 internal class AndroidComposeSpatialElement : 46 SpatialElement(), SubspaceOwner, DefaultLifecycleObserver { 47 override val root: SubspaceLayoutNode = SubspaceLayoutNode() 48 49 private val handler by lazy { Handler(Looper.getMainLooper()) } 50 private val snapshotStateObserver: SnapshotStateObserver = SnapshotStateObserver { 51 if (handler.looper === Looper.myLooper()) { 52 it() 53 } else { 54 handler.post(it) 55 } 56 } 57 58 internal var wrappedComposition: WrappedComposition? = null 59 60 /** 61 * Callback that is registered in [setOnSubspaceAvailable] to be executed when this element is 62 * attached a subspace. 63 */ 64 private var onSubspaceAvailable: ((LifecycleOwner) -> Unit)? = null 65 66 private var windowLeashLayoutNode: SubspaceLayoutNode? = null 67 68 /** 69 * Whether a layout request has been made. If a layout request is made while a layout is in 70 * progress, the new request will be handled after the current layout is complete. 71 */ 72 private var isLayoutRequested = false 73 74 /** 75 * Tracks whether a layout is currently in progress to avoid recursively triggering a layout. 76 */ 77 private var isLayoutInProgress = false 78 79 internal var rootVolumeConstraints: VolumeConstraints = VolumeConstraints.Unbounded 80 81 init { 82 root.attach(this) 83 } 84 85 /** 86 * Registers the [callback] to be executed when this element is attached to a 87 * [spatialComposeScene]. 88 * 89 * Note that the [callback] will be invoked immediately if [spatialComposeScene] is already 90 * available. 91 */ 92 internal fun setOnSubspaceAvailable(callback: (LifecycleOwner) -> Unit) { 93 if (spatialComposeScene != null) { 94 callback(spatialComposeScene!!) 95 } else { 96 onSubspaceAvailable = callback 97 } 98 } 99 100 override fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) { 101 super.onAttachedToSubspace(spatialComposeScene) 102 103 spatialComposeScene.lifecycle.addObserver(this) 104 snapshotStateObserver.start() 105 onSubspaceAvailable?.invoke(spatialComposeScene) 106 onSubspaceAvailable = null 107 } 108 109 override fun onDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) { 110 super.onDetachedFromSubspace(spatialComposeScene) 111 112 spatialComposeScene.lifecycle.removeObserver(this) 113 snapshotStateObserver.stop() 114 snapshotStateObserver.clear() 115 } 116 117 override fun onAttach(node: SubspaceLayoutNode) { 118 node.coreEntity?.entity.let { entity -> 119 if (entity is PanelEntity && entity.isMainPanelEntity) { 120 check(windowLeashLayoutNode == null) { 121 "Cannot add $node as there is already another SubspaceLayoutNode for the Window Leash Node" 122 } 123 windowLeashLayoutNode = node 124 } 125 } 126 } 127 128 override fun onDetach(node: SubspaceLayoutNode) { 129 node.coreEntity?.entity.let { entity -> 130 if (entity is PanelEntity && entity.isMainPanelEntity) { 131 windowLeashLayoutNode = null 132 } 133 } 134 } 135 136 override fun onResume(owner: LifecycleOwner) { 137 super.onResume(owner) 138 // TODO: "Refresh the layout hierarchy." <- Can we just call refreshLayout() here? 139 } 140 141 override fun onDestroy(owner: LifecycleOwner) { 142 super.onDestroy(owner) 143 root.detach() 144 } 145 146 // TODO: Consider adding stricter control over how this is called here, or at call sites, if it 147 // becomes too easy to generate superfluous layouts. 148 override fun requestRelayout() { 149 refreshLayout() 150 } 151 152 // TODO: Add unit tests. 153 private fun refreshLayout() { 154 if (isLayoutInProgress) { 155 isLayoutRequested = true 156 return 157 } 158 159 isLayoutRequested = false 160 isLayoutInProgress = true 161 162 snapshotStateObserver.observeReads(this, onLayoutStateValueChanged) { 163 val measureResults = root.measurableLayout.measure(rootVolumeConstraints) 164 (measureResults as SubspaceLayoutNode.MeasurableLayout).placeAt(Pose.Identity) 165 } 166 167 Logger.log("AndroidComposeSpatialElement") { root.debugTreeToString() } 168 Logger.log("AndroidComposeSpatialElement") { root.debugEntityTreeToString() } 169 170 isLayoutInProgress = false 171 if (isLayoutRequested) { 172 refreshLayout() 173 } 174 } 175 176 public companion object { 177 private val onLayoutStateValueChanged: (AndroidComposeSpatialElement) -> Unit = { 178 it.requestRelayout() 179 } 180 } 181 } 182