[go: nahoru, domu]

blob: d7435c5459eb4d7e379e3bf869491323d9dedb5c [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.viewinterop
import android.content.Context
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout
import androidx.annotation.LayoutRes
import androidx.compose.Composable
import androidx.compose.ExperimentalComposeApi
import androidx.compose.currentComposer
import androidx.ui.core.Modifier
import androidx.ui.core.materialize
import androidx.compose.emit
import androidx.compose.snapshots.SnapshotStateObserver
import androidx.ui.core.ContextAmbient
import androidx.ui.node.UiApplier
/**
* Composes an Android [View] obtained from [viewBlock]. The [viewBlock] block will be called
* exactly once to obtain the [View] to be composed, and it is also guaranteed to be invoked on
* the UI thread. Therefore, in addition to creating the [viewBlock], the block can also be used
* to perform one-off initializations and [View] constant properties' setting.
* The [update] block can be run multiple times (on the UI thread as well) due to recomposition,
* and it is the right place to set [View] properties depending on state. When state changes,
* the block will be reexecuted to set the new properties. Note the block will also be ran once
* right after the [viewBlock] block completes.
*
* @sample androidx.ui.core.samples.AndroidViewSample
*
* @param viewBlock The block creating the [View] to be composed.
* @param modifier The modifier to be applied to the layout.
* @param update The callback to be invoked after the layout is inflated.
*/
@Composable
fun <T : View> AndroidView(
viewBlock: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {
val context = ContextAmbient.current
val materialized = currentComposer.materialize(modifier)
emit<AndroidViewHolder<T>, UiApplier>(
ctor = { AndroidViewHolder(context) },
update = {
set(viewBlock) { this.viewBlock = it }
set(materialized) { this.modifier = it }
set(update) { this.update = update }
}
)
}
/**
* Composes an Android [View] given a layout resource [resId]. The method handles the inflation
* of the [View] and will call the [postInflationCallback] after this happens. Note that the
* callback will always be invoked on the main thread.
*
* @param resId The id of the layout resource to be inflated.
* @param modifier The modifier to be applied to the layout.
* @param postInflationCallback The callback to be invoked after the layout is inflated.
*/
@Composable
@Deprecated(
"This AndroidView overload will be removed. Please use the non-deprecated one.",
ReplaceWith(
"AndroidView({ context ->\n" +
"val view = LayoutInflater.from(context)" +
".inflate(resId, FrameLayout(context), false\n" +
"view.let(postInflationCallback)\n" +
"view\n" +
"})",
"android.view.LayoutInflater"
)
)
fun AndroidView(
@LayoutRes resId: Int,
modifier: Modifier = Modifier,
postInflationCallback: (View) -> Unit = { _ -> }
) = AndroidView(
{ context ->
LayoutInflater.from(context)
// The fake FrameLayout is needed for layout params creation.
.inflate(resId, FrameLayout(context), false)
.apply(postInflationCallback)
},
modifier
)
/**
* Composes an Android [View].
*
* @param view The [View] to compose.
* @param modifier The [Modifier] to be applied to the [view].
*/
@Composable
@Deprecated(
"This AndroidView overload will be removed. Please use the non-deprecated one.",
ReplaceWith(
"AndroidView({ view })"
)
)
fun AndroidView(view: View, modifier: Modifier = Modifier) = AndroidView({ view }, modifier)
/**
* An empty update block used by [AndroidView].
*/
val NoOpUpdate: View.() -> Unit = {}
// Opt in snapshot observing APIs.
@OptIn(ExperimentalComposeApi::class)
// Open to be mockable in tests.
internal open class AndroidViewHolder<T : View>(context: Context) : ViewGroup(context) {
internal var view: T? = null
set(value) {
if (value !== field) {
field = value
removeAllViews()
if (value != null) {
addView(value)
runUpdate()
}
}
}
internal var viewBlock: ((Context) -> T)? = null
set(value) {
// Only run the lambda once.
if (field == null) {
field = value
if (value != null) {
view = value(context)
}
}
}
internal var modifier: Modifier = Modifier
set(value) {
if (value !== field) {
field = value
onModifierChanged?.invoke(value)
}
}
internal var onModifierChanged: ((Modifier) -> Unit)? = null
internal var update: (T) -> Unit = NoOpUpdate
set(value) {
if (value !== field) {
field = value
runUpdate()
}
}
@OptIn(ExperimentalComposeApi::class)
private val snapshotObserver = SnapshotStateObserver { command ->
if (handler.looper === Looper.myLooper()) {
command()
} else {
handler.post(command)
}
}
private val onCommitAffectingUpdate: (AndroidViewHolder<T>) -> Unit = {
handler.post(runUpdate)
}
@OptIn(ExperimentalComposeApi::class)
private val runUpdate: () -> Unit = {
if (update !== NoOpUpdate) {
snapshotObserver.observeReads(this, onCommitAffectingUpdate) {
view?.apply(update)
}
}
}
internal var onRequestDisallowInterceptTouchEvent: ((Boolean) -> Unit)? = null
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
view?.measure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
view?.layout(0, 0, r - l, b - t)
}
override fun getLayoutParams(): LayoutParams? {
return view?.layoutParams ?: LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
onRequestDisallowInterceptTouchEvent?.invoke(disallowIntercept)
super.requestDisallowInterceptTouchEvent(disallowIntercept)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
snapshotObserver.enableStateUpdatesObserving(true)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
snapshotObserver.enableStateUpdatesObserving(false)
snapshotObserver.clear(this)
}
}