blob: a88c4805f3c43949bed07d5033c5f36bd86af0aa [file] [log] [blame]
/*
* Copyright 2020 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.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.InternalComposeUiApi
import androidx.compose.ui.node.InternalCoreApi
import androidx.compose.ui.node.Owner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewTreeLifecycleOwner
/**
* Base class for custom [android.view.View]s implemented using Jetpack Compose UI.
* Subclasses should implement the [Content] function with the appropriate content.
* Calls to [addView] and its variants and overloads will fail with [IllegalStateException].
*
* This [android.view.View] requires that the window it is attached to contains a
* [ViewTreeLifecycleOwner]. This [androidx.lifecycle.LifecycleOwner] is used to
* [dispose][androidx.compose.runtime.Composition.dispose] of the underlying composition
* when the host [Lifecycle] is destroyed, permitting the view to be attached and
* detached repeatedly while preserving the composition. Call [disposeComposition]
* to dispose of the underlying composition earlier, or if the view is never initially
* attached to a window. (The requirement to dispose of the composition explicitly
* in the event that the view is never (re)attached is temporary.)
*/
abstract class AbstractComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
init {
clipChildren = false
clipToPadding = false
}
private var composition: Composition? = null
private var parentContext: CompositionContext? = null
set(value) {
if (field !== value) {
field = value
val old = composition
if (old !== null) {
old.dispose()
composition = null
// Recreate the composition now if we are attached.
if (isAttachedToWindow) {
ensureCompositionCreated()
}
}
}
}
/**
* Set the [CompositionContext] that should be the parent of this view's composition.
* If [parent] is `null` it will be determined automatically from the window the view is
* attached to.
*/
fun setParentCompositionContext(parent: CompositionContext?) {
parentContext = parent
}
// Leaking `this` during init is generally dangerous, but we know that the implementation of
// this particular ViewCompositionStrategy is not going to do something harmful with it.
@Suppress("LeakingThis")
private var disposeViewCompositionStrategy: (() -> Unit)? =
ViewCompositionStrategy.DisposeOnDetachedFromWindow.installFor(this)
/**
* Set the strategy for managing disposal of this View's internal composition.
* Defaults to [ViewCompositionStrategy.DisposeOnDetachedFromWindow].
*
* This View's composition is a live resource that must be disposed to ensure that
* long-lived references to it do not persist
*
* See [ViewCompositionStrategy] for more information.
*/
fun setViewCompositionStrategy(strategy: ViewCompositionStrategy) {
disposeViewCompositionStrategy?.invoke()
disposeViewCompositionStrategy = strategy.installFor(this)
}
/**
* If `true`, this View's composition will be created when it becomes attached to a
* window for the first time. Defaults to `true`.
*
* Subclasses may choose to override this property to prevent this eager initial composition
* in cases where the view's content is not yet ready. Initial composition will still occur
* when this view is first measured.
*/
protected open val shouldCreateCompositionOnAttachedToWindow: Boolean
get() = true
/**
* Enables the display of visual layout bounds for the Compose UI content of this view.
* This is typically managed
*/
@OptIn(InternalCoreApi::class)
@InternalComposeUiApi
var showLayoutBounds: Boolean = false
set(value) {
field = value
getChildAt(0)?.let {
(it as Owner).showLayoutBounds = value
}
}
/**
* The Jetpack Compose UI content for this view.
* Subclasses must implement this method to provide content. Initial composition will
* occur when the view becomes attached to a window or when [createComposition] is called,
* whichever comes first.
*/
@Composable
abstract fun Content()
/**
* Perform initial composition for this view.
* Once this method is called or the view becomes attached to a window,
* either [disposeComposition] must be called or the [ViewTreeLifecycleOwner] must
* reach the [Lifecycle.State.DESTROYED] state for the composition to be cleaned up
* properly. (This restriction is temporary.)
*
* If this method is called when the composition has already been created it has no effect.
*
* This method should only be called if this view [isAttachedToWindow] or if a parent
* [CompositionContext] has been [set][setParentCompositionContext] explicitly.
*/
fun createComposition() {
check(parentContext != null || isAttachedToWindow) {
"createComposition requires either a parent reference or the View to be attached" +
"to a window. Attach the View or call setParentCompositionReference."
}
ensureCompositionCreated()
}
private var creatingComposition = false
private fun checkAddView() {
if (!creatingComposition) {
throw UnsupportedOperationException(
"Cannot add views to " +
"${javaClass.simpleName}; only Compose content is supported"
)
}
}
@Suppress("DEPRECATION") // Still using ViewGroup.setContent for now
private fun ensureCompositionCreated() {
if (composition == null) {
try {
creatingComposition = true
composition = setContent(
parentContext ?: findViewTreeCompositionContext() ?: windowRecomposer
) {
Content()
}
} finally {
creatingComposition = false
}
}
}
/**
* Dispose of the underlying composition and [requestLayout].
* A new composition will be created if [createComposition] is called or when needed to
* lay out this view.
*/
fun disposeComposition() {
composition?.dispose()
composition = null
requestLayout()
}
/**
* `true` if this View is host to an active Compose UI composition.
* An active composition may consume resources.
*/
val hasComposition: Boolean get() = composition != null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (shouldCreateCompositionOnAttachedToWindow) {
ensureCompositionCreated()
}
}
final override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
ensureCompositionCreated()
val child = getChildAt(0)
if (child == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
val width = maxOf(0, MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight)
val height = maxOf(0, MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom)
child.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)),
)
setMeasuredDimension(
child.measuredWidth + paddingLeft + paddingRight,
child.measuredHeight + paddingTop + paddingBottom
)
}
final override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
getChildAt(0)?.layout(
paddingLeft,
paddingTop,
right - left - paddingRight,
bottom - top - paddingBottom
)
}
override fun onRtlPropertiesChanged(layoutDirection: Int) {
// Force the single child for our composition to have the same LayoutDirection
// that we do. We will get onRtlPropertiesChanged eagerly as the value changes,
// but the composition child view won't until it measures. This can be too late
// to catch the composition pass for that frame, so propagate it eagerly.
getChildAt(0)?.layoutDirection = layoutDirection
}
// Below: enforce restrictions on adding child views to this ViewGroup
override fun addView(child: View?) {
checkAddView()
super.addView(child)
}
override fun addView(child: View?, index: Int) {
checkAddView()
super.addView(child, index)
}
override fun addView(child: View?, width: Int, height: Int) {
checkAddView()
super.addView(child, width, height)
}
override fun addView(child: View?, params: LayoutParams?) {
checkAddView()
super.addView(child, params)
}
override fun addView(child: View?, index: Int, params: LayoutParams?) {
checkAddView()
super.addView(child, index, params)
}
override fun addViewInLayout(child: View?, index: Int, params: LayoutParams?): Boolean {
checkAddView()
return super.addViewInLayout(child, index, params)
}
override fun addViewInLayout(
child: View?,
index: Int,
params: LayoutParams?,
preventRequestLayout: Boolean
): Boolean {
checkAddView()
return super.addViewInLayout(child, index, params, preventRequestLayout)
}
}
/**
* A [android.view.View] that can host Jetpack Compose UI content.
* Use [setContent] to supply the content composable function for the view.
*
* This [android.view.View] requires that the window it is attached to contains a
* [ViewTreeLifecycleOwner]. This [androidx.lifecycle.LifecycleOwner] is used to
* [dispose][androidx.compose.runtime.Composition.dispose] of the underlying composition
* when the host [Lifecycle] is destroyed, permitting the view to be attached and
* detached repeatedly while preserving the composition. Call [disposeComposition]
* to dispose of the underlying composition earlier, or if the view is never initially
* attached to a window. (The requirement to dispose of the composition explicitly
* in the event that the view is never (re)attached is temporary.)
*/
class ComposeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {
private val content = mutableStateOf<(@Composable () -> Unit)?>(null)
@Suppress("RedundantVisibilityModifier")
protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
@Composable
override fun Content() {
content.value?.invoke()
}
/**
* Set the Jetpack Compose UI content for this view.
* Initial composition will occur when the view becomes attached to a window or when
* [createComposition] is called, whichever comes first.
*/
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
}