[go: nahoru, domu]

blob: 895164e5714648cf74d15b9c8fc20b782a9509d5 [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.ui.core
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.Recomposer
import androidx.compose.runtime.emptyContent
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
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.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) {
private var composition: Composition? = null
/**
* 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()
private object DisposedComposition : Composition {
override fun setContent(content: () -> Unit) {
// No-op
}
override fun dispose() {
// No-op
}
}
/**
* 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.
* If it is called after the composition is [disposed][disposeComposition] it will throw
* [IllegalStateException].
*/
fun createComposition() {
check(composition !== DisposedComposition) {
"Cannot create composition - composition was already disposed"
}
ensureCompositionCreated()
}
private var creatingComposition = false
private fun checkAddView() {
if (!creatingComposition) {
throw UnsupportedOperationException("Cannot add views to " +
"${javaClass.simpleName}; only Compose content is supported")
}
}
private fun ensureCompositionCreated() {
if (composition == null) {
// TODO: Cannot use try/catch here until b/161894067 is fixed.
creatingComposition = true
composition = setContent(Recomposer.current()) {
Content()
}
creatingComposition = false
}
}
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)
}
/**
* Dispose of the underlying composition.
* The result of this call is permanent; once disposed a ComposeView cannot be used again
* and will remain empty.
*/
fun disposeComposition() {
composition?.dispose()
composition = DisposedComposition
}
/**
* `true` if [disposeComposition] has been called, either explicitly or by the host window's
* [ViewTreeLifecycleOwner] being destroyed.
*/
val isDisposed: Boolean get() = composition === DisposedComposition
private var lastLifecycle: Lifecycle? = null
private val lifecycleObserver = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
disposeComposition()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val newLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
"ViewTreeLifecycleOwner is not present in this window. Use ComponentActivity, " +
"FragmentActivity or AppCompatActivity to configure ViewTreeLifecycleOwner " +
"automatically, or call ViewTreeLifecycleOwner.set() for this View or an " +
"ancestor in the same window."
}
val newLifecycle = newLifecycleOwner.lifecycle
if (newLifecycle !== lastLifecycle) {
lastLifecycle?.removeObserver(lifecycleObserver)
lastLifecycle = newLifecycle
newLifecycle.addObserver(lifecycleObserver)
}
ensureCompositionCreated()
}
final override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val child = checkNotNull(getChildAt(0)) { "Composition view not present for measure!" }
child.measure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(child.measuredWidthAndState, child.measuredHeightAndState)
}
final override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
val child = checkNotNull(getChildAt(0)) { "Composition view not present for layout!" }
child.layout(0, 0, right - left, bottom - top)
}
}
/**
* 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.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) {
// Note: the call to emptyContent() below instead of a literal {} works around
// https://youtrack.jetbrains.com/issue/KT-17467, which causes the compiler to emit classes
// named `content` and `Content` (from the Content method's composable update scope)
// which causes compilation problems on case-insensitive filesystems.
@Suppress("RemoveExplicitTypeArguments")
private val content = mutableStateOf<@Composable () -> Unit>(emptyContent())
@Composable
override fun Content() {
content.value()
}
/**
* 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) {
this.content.value = content
}
}