1 /* <lambda>null2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.ui.platform 18 19 import android.content.Context 20 import android.os.IBinder 21 import android.util.AttributeSet 22 import android.view.View 23 import android.view.ViewGroup 24 import androidx.compose.runtime.Composable 25 import androidx.compose.runtime.Composition 26 import androidx.compose.runtime.CompositionContext 27 import androidx.compose.runtime.Recomposer 28 import androidx.compose.runtime.mutableStateOf 29 import androidx.compose.ui.InternalComposeUiApi 30 import androidx.compose.ui.UiComposable 31 import androidx.compose.ui.node.InternalCoreApi 32 import androidx.compose.ui.node.Owner 33 import androidx.lifecycle.Lifecycle 34 import androidx.lifecycle.LifecycleOwner 35 import androidx.lifecycle.findViewTreeLifecycleOwner 36 import androidx.savedstate.SavedStateRegistryOwner 37 import java.lang.ref.WeakReference 38 39 /** 40 * Base class for custom [android.view.View]s implemented using Jetpack Compose UI. Subclasses 41 * should implement the [Content] function with the appropriate content. Calls to [addView] and its 42 * variants and overloads will fail with [IllegalStateException]. 43 * 44 * By default, the composition is disposed according to [ViewCompositionStrategy.Default]. Call 45 * [disposeComposition] to dispose of the underlying composition earlier, or if the view is never 46 * initially attached to a window. (The requirement to dispose of the composition explicitly in the 47 * event that the view is never (re)attached is temporary.) 48 * 49 * [AbstractComposeView] only supports being added into view hierarchies propagating 50 * [LifecycleOwner] and [SavedStateRegistryOwner] via [androidx.lifecycle.setViewTreeLifecycleOwner] 51 * and [androidx.savedstate.setViewTreeSavedStateRegistryOwner]. In most cases you will already have 52 * it set up correctly as [androidx.activity.ComponentActivity], [androidx.fragment.app.Fragment] 53 * and [androidx.navigation.NavController] will provide the correct values. 54 */ 55 abstract class AbstractComposeView 56 @JvmOverloads 57 constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 58 ViewGroup(context, attrs, defStyleAttr) { 59 60 init { 61 clipChildren = false 62 clipToPadding = false 63 importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES 64 } 65 66 /** 67 * The first time we successfully locate this we'll save it here. If this View moves to the 68 * [android.view.ViewOverlay] we won't be able to find view tree dependencies; this happens when 69 * using transition APIs to animate views out in particular. 70 * 71 * We only ever set this when we're attached to a window. 72 */ 73 private var cachedViewTreeCompositionContext: WeakReference<CompositionContext>? = null 74 75 /** 76 * The [getWindowToken] of the window this view was last attached to. If we become attached to a 77 * new window we clear [cachedViewTreeCompositionContext] so that we might appeal to the 78 * (possibly lazily created) [windowRecomposer] if [findViewTreeCompositionContext] can't locate 79 * one instead of using the previous [cachedViewTreeCompositionContext]. 80 */ 81 private var previousAttachedWindowToken: IBinder? = null 82 set(value) { 83 if (field !== value) { 84 field = value 85 cachedViewTreeCompositionContext = null 86 } 87 } 88 89 private var composition: Composition? = null 90 91 /** 92 * The explicitly set [CompositionContext] to use as the parent of compositions created for this 93 * view. Set by [setParentCompositionContext]. 94 * 95 * If set to a non-null value [cachedViewTreeCompositionContext] will be cleared. 96 */ 97 private var parentContext: CompositionContext? = null 98 set(value) { 99 if (field !== value) { 100 field = value 101 if (value != null) { 102 cachedViewTreeCompositionContext = null 103 } 104 val old = composition 105 if (old !== null) { 106 old.dispose() 107 composition = null 108 109 // Recreate the composition now if we are attached. 110 if (isAttachedToWindow) { 111 ensureCompositionCreated() 112 } 113 } 114 } 115 } 116 117 /** 118 * Set the [CompositionContext] that should be the parent of this view's composition. If 119 * [parent] is `null` it will be determined automatically from the window the view is attached 120 * to. 121 */ 122 fun setParentCompositionContext(parent: CompositionContext?) { 123 parentContext = parent 124 } 125 126 // Leaking `this` during init is generally dangerous, but we know that the implementation of 127 // this particular ViewCompositionStrategy is not going to do something harmful with it. 128 @Suppress("LeakingThis") 129 private var disposeViewCompositionStrategy: (() -> Unit)? = 130 ViewCompositionStrategy.Default.installFor(this) 131 132 /** 133 * Set the strategy for managing disposal of this View's internal composition. Defaults to 134 * [ViewCompositionStrategy.Default]. 135 * 136 * This View's composition is a live resource that must be disposed to ensure that long-lived 137 * references to it do not persist 138 * 139 * See [ViewCompositionStrategy] for more information. 140 */ 141 fun setViewCompositionStrategy(strategy: ViewCompositionStrategy) { 142 disposeViewCompositionStrategy?.invoke() 143 disposeViewCompositionStrategy = strategy.installFor(this) 144 } 145 146 /** 147 * If `true`, this View's composition will be created when it becomes attached to a window for 148 * the first time. Defaults to `true`. 149 * 150 * Subclasses may choose to override this property to prevent this eager initial composition in 151 * cases where the view's content is not yet ready. Initial composition will still occur when 152 * this view is first measured. 153 */ 154 protected open val shouldCreateCompositionOnAttachedToWindow: Boolean 155 get() = true 156 157 /** 158 * Enables the display of visual layout bounds for the Compose UI content of this view. This is 159 * typically configured using the system developer setting for "Show layout bounds." 160 */ 161 @OptIn(InternalCoreApi::class) 162 @InternalComposeUiApi 163 @Suppress("GetterSetterNames") 164 @get:Suppress("GetterSetterNames") 165 var showLayoutBounds: Boolean = false 166 set(value) { 167 field = value 168 getChildAt(0)?.let { (it as Owner).showLayoutBounds = value } 169 } 170 171 /** 172 * The Jetpack Compose UI content for this view. Subclasses must implement this method to 173 * provide content. Initial composition will occur when the view becomes attached to a window or 174 * when [createComposition] is called, whichever comes first. 175 */ 176 @Composable @UiComposable abstract fun Content() 177 178 /** 179 * Perform initial composition for this view. Once this method is called or the view becomes 180 * attached to a window, either [disposeComposition] must be called or the [LifecycleOwner] 181 * returned by [findViewTreeLifecycleOwner] must reach the [Lifecycle.State.DESTROYED] state for 182 * the composition to be cleaned up properly. (This restriction is temporary.) 183 * 184 * If this method is called when the composition has already been created it has no effect. 185 * 186 * This method should only be called if this view [isAttachedToWindow] or if a parent 187 * [CompositionContext] has been [set][setParentCompositionContext] explicitly. 188 */ 189 fun createComposition() { 190 check(parentContext != null || isAttachedToWindow) { 191 "createComposition requires either a parent reference or the View to be attached" + 192 "to a window. Attach the View or call setParentCompositionReference." 193 } 194 ensureCompositionCreated() 195 } 196 197 private var creatingComposition = false 198 199 private fun checkAddView() { 200 if (!creatingComposition) { 201 throw UnsupportedOperationException( 202 "Cannot add views to " + 203 "${javaClass.simpleName}; only Compose content is supported" 204 ) 205 } 206 } 207 208 /** 209 * `true` if the [CompositionContext] can be considered to be "alive" for the purposes of 210 * locally caching it in case the view is placed into a ViewOverlay. [Recomposer]s that are in 211 * the [Recomposer.State.ShuttingDown] state or lower should not be cached or reusedif currently 212 * cached, as they will never recompose content. 213 */ 214 private val CompositionContext.isAlive: Boolean 215 get() = this !is Recomposer || currentState.value > Recomposer.State.ShuttingDown 216 217 /** 218 * Cache this [CompositionContext] in [cachedViewTreeCompositionContext] if it [isAlive] and 219 * return the [CompositionContext] itself either way. 220 */ 221 private fun CompositionContext.cacheIfAlive(): CompositionContext = also { context -> 222 context.takeIf { it.isAlive }?.let { cachedViewTreeCompositionContext = WeakReference(it) } 223 } 224 225 /** 226 * Determine the correct [CompositionContext] to use as the parent of this view's composition. 227 * This can result in caching a looked-up [CompositionContext] for use later. See 228 * [cachedViewTreeCompositionContext] for more details. 229 * 230 * If [cachedViewTreeCompositionContext] is available but [findViewTreeCompositionContext] 231 * cannot find a parent context, we will use the cached context if present before appealing to 232 * the [windowRecomposer], as [windowRecomposer] can lazily create a recomposer. If we're 233 * reattached to the same window and [findViewTreeCompositionContext] can't find the context 234 * that [windowRecomposer] would install, we might be in the [getOverlay] of some part of the 235 * view hierarchy to animate the disappearance of this and other views. We still need to be able 236 * to compose/recompose in this state without creating a brand new recomposer to do it, as well 237 * as still locate any view tree dependencies. 238 */ 239 private fun resolveParentCompositionContext() = 240 parentContext 241 ?: findViewTreeCompositionContext()?.cacheIfAlive() 242 ?: cachedViewTreeCompositionContext?.get()?.takeIf { it.isAlive } 243 ?: windowRecomposer.cacheIfAlive() 244 245 @Suppress("DEPRECATION") // Still using ViewGroup.setContent for now 246 private fun ensureCompositionCreated() { 247 if (composition == null) { 248 try { 249 creatingComposition = true 250 composition = setContent(resolveParentCompositionContext()) { Content() } 251 } finally { 252 creatingComposition = false 253 } 254 } 255 } 256 257 /** 258 * Dispose of the underlying composition and [requestLayout]. A new composition will be created 259 * if [createComposition] is called or when needed to lay out this view. 260 */ 261 fun disposeComposition() { 262 composition?.dispose() 263 composition = null 264 requestLayout() 265 } 266 267 /** 268 * `true` if this View is host to an active Compose UI composition. An active composition may 269 * consume resources. 270 */ 271 val hasComposition: Boolean 272 get() = composition != null 273 274 override fun onAttachedToWindow() { 275 super.onAttachedToWindow() 276 277 previousAttachedWindowToken = windowToken 278 279 if (shouldCreateCompositionOnAttachedToWindow) { 280 ensureCompositionCreated() 281 } 282 } 283 284 final override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 285 ensureCompositionCreated() 286 internalOnMeasure(widthMeasureSpec, heightMeasureSpec) 287 } 288 289 @Suppress("WrongCall") 290 internal open fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 291 val child = getChildAt(0) 292 if (child == null) { 293 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 294 return 295 } 296 297 val width = maxOf(0, MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight) 298 val height = maxOf(0, MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom) 299 child.measure( 300 MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)), 301 MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)), 302 ) 303 setMeasuredDimension( 304 child.measuredWidth + paddingLeft + paddingRight, 305 child.measuredHeight + paddingTop + paddingBottom 306 ) 307 } 308 309 final override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) = 310 internalOnLayout(changed, left, top, right, bottom) 311 312 internal open fun internalOnLayout( 313 changed: Boolean, 314 left: Int, 315 top: Int, 316 right: Int, 317 bottom: Int 318 ) { 319 getChildAt(0) 320 ?.layout( 321 paddingLeft, 322 paddingTop, 323 right - left - paddingRight, 324 bottom - top - paddingBottom 325 ) 326 } 327 328 override fun onRtlPropertiesChanged(layoutDirection: Int) { 329 // Force the single child for our composition to have the same LayoutDirection 330 // that we do. We will get onRtlPropertiesChanged eagerly as the value changes, 331 // but the composition child view won't until it measures. This can be too late 332 // to catch the composition pass for that frame, so propagate it eagerly. 333 getChildAt(0)?.layoutDirection = layoutDirection 334 } 335 336 // Transition group handling: 337 // Both the framework and androidx transition APIs use isTransitionGroup as a signal for 338 // determining view properties to capture during a transition. As AbstractComposeView uses 339 // a view subhierarchy to perform its work but operates as a single unit, mark instances as 340 // transition groups by default. 341 // This is implemented as overridden methods instead of setting isTransitionGroup = true in 342 // the constructor so that values set explicitly by xml inflation performed by the ViewGroup 343 // constructor will take precedence. As of this writing all known framework implementations 344 // use the public isTransitionGroup method rather than checking the internal ViewGroup flag 345 // to determine behavior, making this implementation a slight compatibility risk for a 346 // tradeoff of cleaner View-consumer API behavior without the overhead of performing an 347 // additional obtainStyledAttributes call to determine a value potentially overridden from xml. 348 349 private var isTransitionGroupSet = false 350 351 override fun isTransitionGroup(): Boolean = !isTransitionGroupSet || super.isTransitionGroup() 352 353 override fun setTransitionGroup(isTransitionGroup: Boolean) { 354 super.setTransitionGroup(isTransitionGroup) 355 isTransitionGroupSet = true 356 } 357 358 // Below: enforce restrictions on adding child views to this ViewGroup 359 360 override fun addView(child: View?) { 361 checkAddView() 362 super.addView(child) 363 } 364 365 override fun addView(child: View?, index: Int) { 366 checkAddView() 367 super.addView(child, index) 368 } 369 370 override fun addView(child: View?, width: Int, height: Int) { 371 checkAddView() 372 super.addView(child, width, height) 373 } 374 375 override fun addView(child: View?, params: LayoutParams?) { 376 checkAddView() 377 super.addView(child, params) 378 } 379 380 override fun addView(child: View?, index: Int, params: LayoutParams?) { 381 checkAddView() 382 super.addView(child, index, params) 383 } 384 385 override fun addViewInLayout(child: View?, index: Int, params: LayoutParams?): Boolean { 386 checkAddView() 387 return super.addViewInLayout(child, index, params) 388 } 389 390 override fun addViewInLayout( 391 child: View?, 392 index: Int, 393 params: LayoutParams?, 394 preventRequestLayout: Boolean 395 ): Boolean { 396 checkAddView() 397 return super.addViewInLayout(child, index, params, preventRequestLayout) 398 } 399 400 override fun shouldDelayChildPressedState(): Boolean = false 401 } 402 403 /** 404 * A [android.view.View] that can host Jetpack Compose UI content. Use [setContent] to supply the 405 * content composable function for the view. 406 * 407 * By default, the composition is disposed according to [ViewCompositionStrategy.Default]. Call 408 * [disposeComposition] to dispose of the underlying composition earlier, or if the view is never 409 * initially attached to a window. (The requirement to dispose of the composition explicitly in the 410 * event that the view is never (re)attached is temporary.) 411 * 412 * [ComposeView] only supports being added into view hierarchies propagating [LifecycleOwner] and 413 * [SavedStateRegistryOwner] via [androidx.lifecycle.setViewTreeLifecycleOwner] and 414 * [androidx.savedstate.setViewTreeSavedStateRegistryOwner]. In most cases you will already have it 415 * set up correctly as [androidx.activity.ComponentActivity], [androidx.fragment.app.Fragment] and 416 * [androidx.navigation.NavController] will provide the correct values. 417 */ 418 class ComposeView 419 @JvmOverloads 420 constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : 421 AbstractComposeView(context, attrs, defStyleAttr) { 422 423 private val content = mutableStateOf<(@Composable () -> Unit)?>(null) 424 425 @Suppress("RedundantVisibilityModifier") 426 protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false 427 private set 428 429 @Composable Contentnull430 override fun Content() { 431 content.value?.invoke() 432 } 433 getAccessibilityClassNamenull434 override fun getAccessibilityClassName(): CharSequence { 435 return javaClass.name 436 } 437 438 /** 439 * Set the Jetpack Compose UI content for this view. Initial composition will occur when the 440 * view becomes attached to a window or when [createComposition] is called, whichever comes 441 * first. 442 */ setContentnull443 fun setContent(content: @Composable () -> Unit) { 444 shouldCreateCompositionOnAttachedToWindow = true 445 this.content.value = content 446 if (isAttachedToWindow) { 447 createComposition() 448 } 449 } 450 } 451