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