[go: nahoru, domu]

blob: 2118ecb63778cb9c983a65a09a56d3a401c5f0da [file] [log] [blame]
/*
* Copyright 2018 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.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.animation.AnimationClockObservable
import androidx.animation.rootAnimationClockFactory
import androidx.annotation.MainThread
import androidx.ui.core.input.FocusManager
import androidx.ui.input.TextInputService
import androidx.compose.Composable
import androidx.compose.Composition
import androidx.compose.CompositionReference
import androidx.compose.FrameManager
import androidx.compose.NeverEqual
import androidx.compose.Providers
import androidx.compose.StructurallyEqual
import androidx.compose.ambientOf
import androidx.compose.remember
import androidx.compose.state
import androidx.compose.staticAmbientOf
import androidx.ui.autofill.Autofill
import androidx.ui.autofill.AutofillTree
import androidx.ui.core.hapticfeedback.HapticFeedback
import androidx.ui.core.selection.SelectionContainer
import androidx.ui.node.UiComposer
import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient
import androidx.ui.text.font.Font
import androidx.ui.unit.Density
import kotlinx.coroutines.Dispatchers
import org.jetbrains.annotations.TestOnly
import java.util.WeakHashMap
import kotlin.coroutines.CoroutineContext
private val TAG_COMPOSITION = "androidx.compose.Composition".hashCode()
private val ROOT_COMPONENTNODES = WeakHashMap<ComponentNode, Composition>()
private val ROOT_VIEWGROUPS = WeakHashMap<ViewGroup, Composition>()
/**
* Apply Code Changes will invoke the two functions before and after a code swap.
*
* This forces the whole view hierarchy to be redrawn to invoke any code change that was
* introduce in the code swap.
*
* All these are private as within JVMTI / JNI accessibility is mostly a formality.
*/
// NOTE(lmr): right now, this class only takes into account Emittables and Views composed using
// compose. In reality, there might be more (ie, Vectors), and we should figure out a more
// holistic way to capture those as well.
private class HotReloader {
companion object {
private var compositions = mutableListOf<Pair<Composition, @Composable() () -> Unit>>()
@TestOnly
fun clearRoots() {
ROOT_COMPONENTNODES.clear()
ROOT_VIEWGROUPS.clear()
}
// Called before Dex Code Swap
@Suppress("UNUSED_PARAMETER")
private fun saveStateAndDispose(context: Any) {
compositions.clear()
val componentNodes = ROOT_COMPONENTNODES.entries.toSet()
for ((_, composition) in componentNodes) {
compositions.add(composition to composition.composable)
}
for ((_, composition) in componentNodes) {
if (composition.isRoot) {
composition.dispose()
}
}
val viewRoots = ROOT_VIEWGROUPS.entries.toSet()
for ((_, composition) in viewRoots) {
compositions.add(composition to composition.composable)
}
for ((_, composition) in viewRoots) {
if (composition.isRoot) {
composition.dispose()
}
}
}
// Called after Dex Code Swap
@Suppress("UNUSED_PARAMETER")
private fun loadStateAndCompose(context: Any) {
for ((composition, composable) in compositions) {
composition.composable = composable
}
for ((composition, composable) in compositions) {
if (composition.isRoot) {
composition.compose(composable)
}
}
compositions.clear()
}
@TestOnly
internal fun simulateHotReload(context: Any) {
saveStateAndDispose(context)
loadStateAndCompose(context)
}
}
}
/**
* @suppress
*/
@TestOnly
fun simulateHotReload(context: Any) = HotReloader.simulateHotReload(context)
/**
* @suppress
*/
@TestOnly
fun clearRoots() = HotReloader.clearRoots()
internal fun findComposition(view: View): Composition? {
return view.getTag(TAG_COMPOSITION) as? Composition
}
internal fun storeComposition(view: View, composition: Composition) {
view.setTag(TAG_COMPOSITION, composition)
if (view is ViewGroup)
ROOT_VIEWGROUPS[view] = composition
}
internal fun removeRoot(view: View) {
view.setTag(TAG_COMPOSITION, null)
if (view is ViewGroup)
ROOT_VIEWGROUPS.remove(view)
}
internal fun findComposition(node: ComponentNode): Composition? {
return ROOT_COMPONENTNODES[node]
}
private fun storeComposition(node: ComponentNode, composition: Composition) {
ROOT_COMPONENTNODES[node] = composition
}
/**
* Composes the children of the view with the passed in [composable].
*
* @see setViewContent
* @see Composition.dispose
*/
// TODO: Remove this API when View/ComponentNode mixed trees work
fun ViewGroup.setViewContent(
parent: CompositionReference? = null,
composable: @Composable() () -> Unit
): Composition {
val composition = findComposition(this)
?: UiComposition(this, context, parent).also {
removeAllViews()
}
composition.compose(composable)
return composition
}
/**
* Sets the contentView of an activity to a FrameLayout, and composes the contents of the layout
* with the passed in [composable].
*
* @see setContent
* @see Activity.setContentView
*/
// TODO: Remove this API when View/ComponentNode mixed trees work
fun Activity.setViewContent(composable: @Composable() () -> Unit): Composition {
// TODO(lmr): add ambients here, or remove API entirely if we can
// If there is already a FrameLayout in the root, we assume we want to compose
// into it instead of create a new one. This allows for `setContent` to be
// called multiple times.
val root = window
.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ViewGroup
?: FrameLayout(this).also { setContentView(it) }
return root.setViewContent(null, composable)
}
/**
* @suppress
*/
@TestOnly
fun makeCompositionForTesting(
root: Any,
context: Context,
parent: CompositionReference? = null
): Composition = UiComposition(
root,
context,
parent
)
internal class UiComposition(
private val root: Any,
private val context: Context,
parent: CompositionReference? = null
) : Composition(
{ slots, recomposer ->
UiComposer(
context,
root,
slots,
recomposer
)
},
parent
) {
var enabled: Boolean = true
set(value) {
if (value != field) {
field = value
if (value && disabledComposition != null) {
compose(disabledComposition!!)
disabledComposition = null
}
}
}
private var disabledComposition: (@Composable() () -> Unit)? = null
override fun compose(content: @Composable() () -> Unit) {
if (!enabled) {
disabledComposition = content
} else {
super.compose(content)
}
}
init {
when (root) {
is ViewGroup -> storeComposition(root, this)
is ComponentNode -> storeComposition(root, this)
}
}
override fun dispose() {
super.dispose()
disabledComposition = null
when (root) {
is ViewGroup -> removeRoot(root)
is ComponentNode -> ROOT_COMPONENTNODES.remove(root)
}
}
}
// TODO(chuckj): This is a temporary work-around until subframes exist so that
// nextFrame() inside recompose() doesn't really start a new frame, but a new subframe
// instead.
@MainThread
fun subcomposeInto(
container: ComponentNode,
context: Context,
parent: CompositionReference? = null,
composable: @Composable() () -> Unit
): Composition {
val composition = findComposition(container)
?: UiComposition(container, context, parent)
composition.compose(composable)
return composition
}
/**
* Composes the given composable into the given activity. The composable will become the root view
* of the given activity.
*
* @param content Composable that will be the content of the activity.
*/
fun Activity.setContent(
content: @Composable() () -> Unit
): Composition {
FrameManager.ensureStarted()
val composeView: Owner = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? Owner
?: createOwner(this).also {
if (it is ViewGroup) {
setContentView(it)
}
}
// TODO(lmr): setup lifecycle-based dispose since we have Activity here
return doSetContent(composeView, this, content)
}
/**
* Disposes of a composition of the children of this view. This is a convenience method around
* [Composition.dispose].
*
* @see Composition.dispose
* @see setContent
*/
@Deprecated(
"disposing should be done with the Composition object returned by setContent",
replaceWith = ReplaceWith("Composition#dispose()")
)
fun ViewGroup.disposeComposition() {
findComposition(this)?.dispose()
}
/**
* Disposes of a composition that was started using [setContent]. This is a convenience method
* around [Composition.dispose].
*
* @see setContent
* @see Composition.dispose
*/
@Deprecated(
"disposing should be done with the Composition object returned by setContent",
replaceWith = ReplaceWith("Composition#dispose()")
)
fun Activity.disposeComposition() {
val view = window
.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ViewGroup
?: error("No root view found")
val composition = findComposition(view) ?: error("No composition found")
composition.dispose()
}
/**
* We want text/image selection to be enabled by default and disabled per widget. Therefore a root
* level [SelectionContainer] is installed at the root.
*/
@Composable
private fun WrapWithSelectionContainer(content: @Composable() () -> Unit) {
SelectionContainer(children = content)
}
/**
* Composes the given composable into the given view.
*
* Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to
* be able to save and restore the values used within the composition. See [View.setId].
* @param content Composable that will be the content of the view.
*/
fun ViewGroup.setContent(
content: @Composable() () -> Unit
): Composition {
val composeView =
if (childCount > 0) {
getChildAt(0) as? Owner
} else {
removeAllViews(); null
}
?: createOwner(context).also { if (it is ViewGroup) addView(it) }
return doSetContent(composeView, context, content)
}
private fun createComposeViewComposition(owner: Owner, context: Context): Composition {
val composition = UiComposition(owner.root, context, null)
// we will postpone (disable) the composition until [Owner] restores the state
if (owner.savedStateRegistry == null) {
composition.enabled = false
owner.setOnSavedStateRegistryAvailable {
composition.enabled = true
}
}
return composition
}
private fun doSetContent(
owner: Owner,
context: Context,
content: @Composable() () -> Unit
): Composition {
val composition = findComposition(owner.root)
?: createComposeViewComposition(owner, context)
composition.compose {
WrapWithAmbients(owner, context, Dispatchers.Main) {
WrapWithSelectionContainer(content)
}
}
return composition
}
@SuppressLint("UnnecessaryLambdaCreation")
@Composable
private fun WrapWithAmbients(
owner: Owner,
context: Context,
coroutineContext: CoroutineContext,
content: @Composable() () -> Unit
) {
// TODO(nona): Tie the focus manger lifecycle to Window, otherwise FocusManager won't work
// with nested AndroidComposeView case
val focusManager = remember { FocusManager() }
var configuration by state(NeverEqual) {
context.applicationContext.resources.configuration
}
// onConfigurationChange is the correct hook to update configuration, however it is
// possible that the configuration object itself may come from a wrapped
// context / themed activity, and may not actually reflect the system. So instead we
// use this hook to grab the applicationContext's configuration, which accurately
// reflects the state of the application / system.
owner.configurationChangeObserver = {
configuration = context.applicationContext.resources.configuration
}
// We don't use the attached View's layout direction here since that layout direction may not
// be resolved since the composables may be composed without attaching to the RootViewImpl.
// In Jetpack Compose, use the locale layout direction (i.e. layoutDirection came from
// configuration) as a default layout direction.
val layoutDirection = when (configuration.layoutDirection) {
android.util.LayoutDirection.LTR -> LayoutDirection.Ltr
android.util.LayoutDirection.RTL -> LayoutDirection.Rtl
// API doc says Configuration#getLayoutDirection only returns LTR or RTL.
// Fallback to LTR for unexpected return value.
else -> LayoutDirection.Ltr
}
val rootAnimationClock = remember { rootAnimationClockFactory() }
val savedStateRegistry = requireNotNull(owner.savedStateRegistry)
Providers(
ContextAmbient provides context,
CoroutineContextAmbient provides coroutineContext,
DensityAmbient provides Density(context),
FocusManagerAmbient provides focusManager,
TextInputServiceAmbient provides owner.textInputService,
FontLoaderAmbient provides owner.fontLoader,
HapticFeedBackAmbient provides owner.hapticFeedBack,
AutofillTreeAmbient provides owner.autofillTree,
ConfigurationAmbient provides configuration,
OwnerAmbient provides owner,
LayoutDirectionAmbient provides layoutDirection,
AnimationClockAmbient provides rootAnimationClock,
UiSavedStateRegistryAmbient provides savedStateRegistry,
children = content
)
}
val ContextAmbient = staticAmbientOf<Context>()
val DensityAmbient = ambientOf<Density>(StructurallyEqual)
val CoroutineContextAmbient = ambientOf<CoroutineContext>()
val ConfigurationAmbient = ambientOf<Configuration>(NeverEqual)
// TODO(b/139866476): The Owner should not be exposed via ambient
val OwnerAmbient = staticAmbientOf<Owner>()
val AutofillAmbient = ambientOf<Autofill?>()
// This will ultimately be replaced by Autofill Semantics (b/138604305).
val AutofillTreeAmbient = staticAmbientOf<AutofillTree>()
val LayoutDirectionAmbient = ambientOf<LayoutDirection>()
val FocusManagerAmbient = ambientOf<FocusManager>()
val TextInputServiceAmbient = staticAmbientOf<TextInputService?>()
val AnimationClockAmbient = staticAmbientOf<AnimationClockObservable>()
val FontLoaderAmbient = staticAmbientOf<Font.ResourceLoader>()
/**
* The ambient to provide haptic feedback to the user.
*/
val HapticFeedBackAmbient = staticAmbientOf<HapticFeedback>()