[go: nahoru, domu]

blob: 0d2c362c040f9378adab388176be9755330dfd78 [file] [log] [blame]
/*
* Copyright 2019 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.
*/
@file:OptIn(ExperimentalComposeApi::class)
// TODO(b/160821157): Replace FocusDetailedState with FocusState2
@file:Suppress("DEPRECATION")
package androidx.ui.core
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.Log
import android.util.SparseArray
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewStructure
import android.view.ViewTreeObserver
import android.view.autofill.AutofillValue
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.compose.ExperimentalComposeApi
import androidx.compose.snapshots.SnapshotStateObserver
import androidx.core.os.HandlerCompat
import androidx.core.view.ViewCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.ViewTreeLifecycleOwner
import androidx.lifecycle.ViewTreeViewModelStoreOwner
import androidx.ui.autofill.AndroidAutofill
import androidx.ui.autofill.Autofill
import androidx.ui.autofill.AutofillTree
import androidx.ui.autofill.performAutofill
import androidx.ui.autofill.populateViewStructure
import androidx.ui.autofill.registerCallback
import androidx.ui.autofill.unregisterCallback
import androidx.ui.core.AndroidOwner.ViewTreeOwners
import androidx.ui.core.clipboard.AndroidClipboardManager
import androidx.ui.core.clipboard.ClipboardManager
import androidx.ui.core.focus.ExperimentalFocus
import androidx.ui.core.focus.FOCUS_TAG
import androidx.ui.core.focus.FocusDetailedState
import androidx.ui.core.focus.FocusModifier2
import androidx.ui.core.focus.FocusModifierImpl
import androidx.ui.core.focus.FocusState2.Active
import androidx.ui.core.focus.FocusState2.Inactive
import androidx.ui.core.hapticfeedback.AndroidHapticFeedback
import androidx.ui.core.hapticfeedback.HapticFeedback
import androidx.ui.core.keyinput.ExperimentalKeyInput
import androidx.ui.core.keyinput.KeyEvent2
import androidx.ui.core.keyinput.KeyEventAndroid
import androidx.ui.core.keyinput.KeyInputModifier
import androidx.ui.core.pointerinput.MotionEventAdapter
import androidx.ui.core.pointerinput.PointerInputEventProcessor
import androidx.ui.core.pointerinput.ProcessResult
import androidx.ui.core.semantics.SemanticsModifierCore
import androidx.ui.core.semantics.SemanticsOwner
import androidx.ui.core.text.AndroidFontResourceLoader
import androidx.ui.core.texttoolbar.AndroidTextToolbar
import androidx.ui.core.texttoolbar.TextToolbar
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.text.input.TextInputServiceAndroid
import androidx.compose.ui.text.input.textInputServiceFactory
import androidx.compose.runtime.savedinstancestate.UiSavedStateRegistry
import androidx.ui.core.LayoutNode.UsageByParent
import androidx.compose.ui.text.font.Font
import androidx.ui.unit.Density
import androidx.ui.unit.IntOffset
import androidx.ui.util.fastForEach
import androidx.ui.util.trace
import java.lang.reflect.Method
import kotlin.math.max
import android.view.KeyEvent as AndroidKeyEvent
/***
* This function creates an instance of [AndroidOwner]
*
* @param context Context to use to create a View
* @param lifecycleOwner Current [LifecycleOwner]. When it is not provided we will try to get the
* owner using [ViewTreeLifecycleOwner] when we will be attached.
*/
fun AndroidOwner(
context: Context,
lifecycleOwner: LifecycleOwner? = null,
viewModelStoreOwner: ViewModelStoreOwner? = null
): AndroidOwner = AndroidComposeView(
context,
lifecycleOwner,
viewModelStoreOwner
)
@OptIn(
ExperimentalFocus::class,
ExperimentalKeyInput::class,
ExperimentalLayoutNodeApi::class
)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
internal class AndroidComposeView constructor(
context: Context,
initialLifecycleOwner: LifecycleOwner?,
initialViewModelStoreOwner: ViewModelStoreOwner?
) : ViewGroup(context), AndroidOwner {
override val view: View = this
override var density = Density(context)
private set
private val semanticsModifier = SemanticsModifierCore(
id = SemanticsModifierCore.generateSemanticsId(),
mergeAllDescendants = false,
properties = {}
)
// TODO(b/160821157): Replace FocusDetailedState with FocusState2.
@Suppress("DEPRECATION")
private val focusModifier = FocusModifierImpl(FocusDetailedState.Inactive)
private val focusModifier2 = FocusModifier2(Inactive)
private val keyInputModifier = KeyInputModifier(null, null)
private val canvasHolder = CanvasHolder()
override val root = LayoutNode().also {
it.measureBlocks = RootMeasureBlocks
it.modifier = Modifier
.drawLayer() + semanticsModifier + focusModifier + focusModifier2 + keyInputModifier
}
override val semanticsOwner: SemanticsOwner = SemanticsOwner(root)
private val accessibilityDelegate = AndroidComposeViewAccessibilityDelegateCompat(this)
// Used by components that want to provide autofill semantic information.
// TODO: Replace with SemanticsTree: Temporary hack until we have a semantics tree implemented.
// TODO: Replace with SemanticsTree.
// This is a temporary hack until we have a semantics tree implemented.
override val autofillTree = AutofillTree()
// OwnedLayers that are dirty and should be redrawn.
internal val dirtyLayers = mutableListOf<OwnedLayer>()
private val motionEventAdapter = MotionEventAdapter()
private val pointerInputEventProcessor = PointerInputEventProcessor(root)
// TODO(mount): reinstate when coroutines are supported by IR compiler
// private val ownerScope = CoroutineScope(Dispatchers.Main.immediate + Job())
// Used for updating the ConfigurationAmbient when configuration changes - consume the
// configuration ambient instead of changing this observer if you are writing a component that
// adapts to configuration changes.
override var configurationChangeObserver: () -> Unit = {}
private val _autofill = if (autofillSupported()) AndroidAutofill(this, autofillTree) else null
// Used as an ambient for performing autofill.
override val autofill: Autofill? get() = _autofill
// TODO(b/160821157): Replace FocusDetailedState with FocusState2.
@Suppress("DEPRECATION")
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect)
Log.d(FOCUS_TAG, "Owner FocusChanged($gainFocus)")
if (gainFocus) {
// If the focus state is not Inactive, it indicates that the focus state is already
// set (possibly by dispatchWindowFocusChanged). So we don't update the state.
if (focusModifier.focusDetailedState == FocusDetailedState.Inactive) {
focusModifier.focusDetailedState = FocusDetailedState.Active
}
if (focusModifier2.focusState == Inactive) {
focusModifier2.focusState = Active
// TODO(b/152535715): propagate focus to children based on child focusability.
}
} else {
// If this view lost focus, clear focus from the children.
with(focusModifier) {
focusNode?.clearFocus(forcedClear = true)
focusDetailedState = FocusDetailedState.Inactive
}
with(focusModifier2) {
focusNode.clearFocus(forcedClear = true)
focusState = Inactive
}
}
}
override fun sendKeyEvent(keyEvent: KeyEvent2): Boolean {
return keyInputModifier.processKeyInput(keyEvent)
}
override fun dispatchKeyEvent(event: AndroidKeyEvent): Boolean {
return sendKeyEvent(KeyEventAndroid(event))
}
private val snapshotObserver = SnapshotStateObserver { command ->
if (handler.looper === Looper.myLooper()) {
command()
} else {
handler.post(command)
}
}
private val onCommitAffectingMeasure: (LayoutNode) -> Unit = { layoutNode ->
onRequestMeasure(layoutNode)
}
private val onCommitAffectingLayout: (LayoutNode) -> Unit = { layoutNode ->
if (measureAndLayoutDelegate.requestRelayout(layoutNode)) {
scheduleMeasureAndLayout()
}
}
internal val onCommitAffectingLayer: (OwnedLayer) -> Unit = { layer ->
layer.invalidate()
}
private val onCommitAffectingLayerParams: (OwnedLayer) -> Unit = { layer ->
handler.postAtFrontOfQueue {
updateLayerProperties(layer)
}
}
@OptIn(InternalCoreApi::class)
override var showLayoutBounds = false
override fun pauseModelReadObserveration(block: () -> Unit) =
@OptIn(ExperimentalComposeApi::class)
snapshotObserver.pauseObservingReads(block)
init {
setWillNotDraw(false)
isFocusable = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusable = View.FOCUSABLE
// not to add the default focus highlight to the whole compose view
defaultFocusHighlightEnabled = false
}
isFocusableInTouchMode = true
clipChildren = false
root.isPlaced = true
ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate)
AndroidOwner.onAndroidOwnerCreatedCallback?.invoke(this)
}
override fun onInvalidate(layoutNode: LayoutNode) {
invalidate(layoutNode)
}
private fun invalidate(node: LayoutNode) {
val layer = node.findLayer()
if (layer == null) {
invalidate()
} else {
layer.invalidate()
}
}
override fun onAttach(node: LayoutNode) {
}
override fun onDetach(node: LayoutNode) {
measureAndLayoutDelegate.onNodeDetached(node)
snapshotObserver.clear(node)
}
private var _androidViewsHandler: AndroidViewsHandler? = null
private val androidViewsHandler: AndroidViewsHandler
get() {
if (_androidViewsHandler == null) {
_androidViewsHandler = AndroidViewsHandler(context)
addView(_androidViewsHandler)
}
return _androidViewsHandler!!
}
private val viewLayersContainer by lazy(LazyThreadSafetyMode.NONE) {
ViewLayerContainer(context).also { addView(it) }
}
override fun addAndroidView(view: View, layoutNode: LayoutNode) {
androidViewsHandler.layoutNode[view] = layoutNode
androidViewsHandler.addView(view)
}
override fun removeAndroidView(view: View) {
androidViewsHandler.removeView(view)
androidViewsHandler.layoutNode.remove(view)
}
// [ Layout block start ]
// The constraints being used by the last onMeasure. It is set to null in onLayout. It allows
// us to detect the case when the View was measured twice with different constraints within
// the same measure pass.
private var onMeasureConstraints: Constraints? = null
// Will be set to true when we were measured twice with different constraints during the last
// measure pass.
private var wasMeasuredWithMultipleConstraints = false
private val measureAndLayoutDelegate = MeasureAndLayoutDelegate(root)
private var measureAndLayoutScheduled = false
private val measureAndLayoutHandler: Handler =
HandlerCompat.createAsync(Looper.getMainLooper()) {
measureAndLayoutScheduled = false
measureAndLayout()
true
}
private fun scheduleMeasureAndLayout(nodeToRemeasure: LayoutNode? = null) {
if (!isLayoutRequested) {
if (wasMeasuredWithMultipleConstraints && nodeToRemeasure != null) {
// if nodeToRemeasure can potentially resize the root and the view was measured
// twice with different constraints last time it means the constraints we have could
// be not the final constraints and in fact our parent ViewGroup can remeasure us
// with larger constraints if we call requestLayout()
var node = nodeToRemeasure
while (node != null && node.measuredByParent == UsageByParent.InMeasureBlock) {
node = node.parent
}
if (node === root) {
requestLayout()
return
}
}
if (!measureAndLayoutScheduled) {
measureAndLayoutScheduled = true
measureAndLayoutHandler.sendEmptyMessage(0)
}
}
}
override val measureIteration: Long get() = measureAndLayoutDelegate.measureIteration
override fun measureAndLayout() {
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
if (rootNodeResized) {
requestLayout()
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
override fun onRequestMeasure(layoutNode: LayoutNode) {
if (measureAndLayoutDelegate.requestRemeasure(layoutNode)) {
scheduleMeasureAndLayout(layoutNode)
}
}
override fun onRequestRelayout(layoutNode: LayoutNode) {
if (measureAndLayoutDelegate.requestRelayout(layoutNode)) {
scheduleMeasureAndLayout()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
savedStateDelegate.stopWaitingForStateRestoration()
trace("AndroidOwner:onMeasure") {
val (minWidth, maxWidth) = convertMeasureSpec(widthMeasureSpec)
val (minHeight, maxHeight) = convertMeasureSpec(heightMeasureSpec)
val constraints = Constraints(minWidth, maxWidth, minHeight, maxHeight)
if (onMeasureConstraints == null) {
// first onMeasure after last onLayout
onMeasureConstraints = constraints
wasMeasuredWithMultipleConstraints = false
} else if (onMeasureConstraints != constraints) {
// we were remeasured twice with different constraints after last onLayout
wasMeasuredWithMultipleConstraints = true
}
measureAndLayoutDelegate.updateRootParams(
constraints,
resources.configuration.localeLayoutDirection
)
measureAndLayoutDelegate.measureAndLayout()
setMeasuredDimension(root.width, root.height)
}
}
private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
MeasureSpec.EXACTLY -> size to size
MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
MeasureSpec.AT_MOST -> 0 to size
else -> throw IllegalStateException()
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
onMeasureConstraints = null
// we postpone onPositioned callbacks until onLayout as LayoutCoordinates
// are currently wrong if you try to get the global(activity) coordinates -
// View is not yet laid out.
dispatchOnPositioned()
if (_androidViewsHandler != null && androidViewsHandler.isLayoutRequested) {
// Even if we laid out during onMeasure, this can happen when the Views hierarchy
// receives forceLayout(). We need to relayout to clear the isLayoutRequested info
// on the Views, as otherwise further layout requests will be discarded.
androidViewsHandler.layout(0, 0, r - l, b - t)
}
}
private var globalPosition: IntOffset = IntOffset.Origin
private val tmpPositionArray = intArrayOf(0, 0)
private fun dispatchOnPositioned() {
var positionChanged = false
getLocationOnScreen(tmpPositionArray)
if (globalPosition.x != tmpPositionArray[0] || globalPosition.y != tmpPositionArray[1]) {
globalPosition = IntOffset(tmpPositionArray[0], tmpPositionArray[1])
positionChanged = true
}
measureAndLayoutDelegate.dispatchOnPositionedCallbacks(forceDispatch = positionChanged)
}
// [ Layout block end ]
override fun observeLayoutModelReads(node: LayoutNode, block: () -> Unit) {
snapshotObserver.observeReads(node, onCommitAffectingLayout, block)
}
override fun observeMeasureModelReads(node: LayoutNode, block: () -> Unit) {
snapshotObserver.observeReads(node, onCommitAffectingMeasure, block)
}
fun observeLayerModelReads(layer: OwnedLayer, block: () -> Unit) {
snapshotObserver.observeReads(layer, onCommitAffectingLayer, block)
}
override fun onDraw(canvas: android.graphics.Canvas) {
}
override fun createLayer(
drawLayerModifier: DrawLayerModifier,
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
): OwnedLayer {
val layer = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P || isInEditMode()) {
ViewLayer(
this, viewLayersContainer, drawLayerModifier, drawBlock,
invalidateParentLayer
)
} else {
RenderNodeLayer(this, drawLayerModifier, drawBlock, invalidateParentLayer)
}
updateLayerProperties(layer)
return layer
}
override fun onSemanticsChange() {
accessibilityDelegate.onSemanticsChange()
}
private fun updateLayerProperties(layer: OwnedLayer) {
snapshotObserver.observeReads(layer, onCommitAffectingLayerParams) {
layer.updateLayerProperties()
}
}
override fun dispatchDraw(canvas: android.graphics.Canvas) {
measureAndLayout()
// we don't have to observe here because the root has a layer modifier
// that will observe all children. The AndroidComposeView has only the
// root, so it doesn't have to invalidate itself based on model changes.
canvasHolder.drawInto(canvas) { root.draw(this) }
if (dirtyLayers.isNotEmpty()) {
for (i in 0 until dirtyLayers.size) {
val layer = dirtyLayers[i]
layer.updateDisplayList()
}
dirtyLayers.clear()
}
}
override var viewTreeOwners: ViewTreeOwners? =
if (initialLifecycleOwner != null && initialViewModelStoreOwner != null) {
ViewTreeOwners(
initialLifecycleOwner,
initialViewModelStoreOwner
)
} else {
null
}
private set
override fun setOnViewTreeOwnersAvailable(callback: (ViewTreeOwners) -> Unit) {
val viewTreeOwners = viewTreeOwners
if (viewTreeOwners != null) {
callback(viewTreeOwners)
} else {
onViewTreeOwnersAvailable = callback
}
}
private var onViewTreeOwnersAvailable: ((ViewTreeOwners) -> Unit)? = null
// executed when the layout pass has been finished. as a result of it our view could be moved
// inside the window (we are interested not only in the event when our parent positioned us
// on a different position, but also in the position of each of the grandparents as all these
// positions add up to final global position)
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
dispatchOnPositioned()
}
// executed when a scrolling container like ScrollView of RecyclerView performed the scroll,
// this could affect our global position
private val scrollChangedListener = ViewTreeObserver.OnScrollChangedListener {
dispatchOnPositioned()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
showLayoutBounds = getIsShowingLayoutBounds()
snapshotObserver.enableStateUpdatesObserving(true)
ifDebug { if (autofillSupported()) _autofill?.registerCallback() }
root.attach(this)
if (viewTreeOwners == null) {
val lifecycleOwner = ViewTreeLifecycleOwner.get(this) ?: throw IllegalStateException(
"Composed into the View which doesn't propagate ViewTreeLifecycleOwner!"
)
val viewModelStoreOwner =
ViewTreeViewModelStoreOwner.get(this) ?: throw IllegalStateException(
"Composed into the View which doesn't propagate ViewTreeViewModelStoreOwner!"
)
val viewTreeOwners = ViewTreeOwners(
lifecycleOwner = lifecycleOwner,
viewModelStoreOwner = viewModelStoreOwner
)
this.viewTreeOwners = viewTreeOwners
onViewTreeOwnersAvailable?.invoke(viewTreeOwners)
}
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
snapshotObserver.enableStateUpdatesObserving(false)
ifDebug { if (autofillSupported()) _autofill?.unregisterCallback() }
if (measureAndLayoutScheduled) {
measureAndLayoutHandler.removeMessages(0)
}
root.detach()
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
}
override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
if (autofillSupported() && structure != null) _autofill?.populateViewStructure(structure)
}
override fun autofill(values: SparseArray<AutofillValue>) {
if (autofillSupported()) _autofill?.performAutofill(values)
}
// TODO(shepshapard): Test this method.
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
measureAndLayout()
val processResult = trace("AndroidOwner:onTouch") {
val pointerInputEvent = motionEventAdapter.convertToPointerInputEvent(motionEvent)
if (pointerInputEvent != null) {
pointerInputEventProcessor.process(pointerInputEvent)
} else {
pointerInputEventProcessor.processCancel()
ProcessResult(
dispatchedToAPointerInputModifier = false,
anyMovementConsumed = false
)
}
}
if (processResult.anyMovementConsumed) {
parent.requestDisallowInterceptTouchEvent(true)
}
return processResult.dispatchedToAPointerInputModifier
}
private val textInputServiceAndroid = TextInputServiceAndroid(this)
override val textInputService =
@Suppress("DEPRECATION_ERROR")
textInputServiceFactory(textInputServiceAndroid)
override val fontLoader: Font.ResourceLoader = AndroidFontResourceLoader(context)
/**
* Provide haptic feedback to the user. Use the Android version of haptic feedback.
*/
override val hapticFeedBack: HapticFeedback =
AndroidHapticFeedback(this)
/**
* Provide clipboard manager to the user. Use the Android version of clipboard manager.
*/
override val clipboardManager: ClipboardManager = AndroidClipboardManager(context)
/**
* Provide textToolbar to the user, for text-related operation. Use the Android version of
* floating toolbar(post-M) and primary toolbar(pre-M).
*/
override val textToolbar: TextToolbar = AndroidTextToolbar(this)
override fun onCheckIsTextEditor(): Boolean = textInputServiceAndroid.isEditorFocused()
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? =
textInputServiceAndroid.createInputConnection(outAttrs)
override fun calculatePosition(): IntOffset = globalPosition
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
density = Density(context)
configurationChangeObserver()
}
private val savedStateDelegate = SavedStateDelegate {
// When AndroidComposeView is composed into some ViewGroup we just add ourself as a child
// for this ViewGroup. And we don't have any id on AndroidComposeView as we can't make it
// unique, but we require this parent ViewGroup to have an unique id for the saved
// instance state mechanism to work (similarly to how it works without Compose).
// When we composed into Activity our parent is the ViewGroup with android.R.id.content.
(parent as? View)?.id ?: View.NO_ID
}
/**
* The current instance of [UiSavedStateRegistry]. If it's null you can wait for it to became
* available using [setOnSavedStateRegistryAvailable].
*/
override val savedStateRegistry: UiSavedStateRegistry?
get() = savedStateDelegate.savedStateRegistry
/**
* Allows other components to be notified when the [UiSavedStateRegistry] became available.
*/
override fun setOnSavedStateRegistryAvailable(callback: (UiSavedStateRegistry) -> Unit) {
savedStateDelegate.setOnSaveRegistryAvailable(callback)
}
override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
val superState = super.onSaveInstanceState()!!
savedStateDelegate.dispatchSaveInstanceState(container, superState)
}
override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
val superState = savedStateDelegate.dispatchRestoreInstanceState(container)
onRestoreInstanceState(superState)
}
private fun autofillSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
public override fun dispatchHoverEvent(event: MotionEvent): Boolean {
return accessibilityDelegate.dispatchHoverEvent(event)
}
companion object {
private var systemPropertiesClass: Class<*>? = null
private var getBooleanMethod: Method? = null
// TODO(mount): replace with ViewCompat.isShowingLayoutBounds() when it becomes available.
@SuppressLint("PrivateApi")
private fun getIsShowingLayoutBounds(): Boolean {
try {
if (systemPropertiesClass == null) {
systemPropertiesClass = Class.forName("android.os.SystemProperties")
getBooleanMethod = systemPropertiesClass?.getDeclaredMethod(
"getBoolean",
String::class.java,
Boolean::class.java
)
}
return getBooleanMethod?.invoke(null, "debug.layout", false) as? Boolean ?: false
} catch (e: Exception) {
return false
}
}
private val RootMeasureBlocks = object : LayoutNode.MeasureBlocks {
override fun measure(
measureScope: MeasureScope,
measurables: List<Measurable>,
constraints: Constraints,
layoutDirection: LayoutDirection
): MeasureScope.MeasureResult {
return when {
measurables.isEmpty() -> measureScope.layout(0, 0) {}
measurables.size == 1 -> {
val placeable = measurables[0].measure(constraints, layoutDirection)
measureScope.layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
else -> {
val placeables = measurables.map {
it.measure(constraints, layoutDirection)
}
var maxWidth = 0
var maxHeight = 0
placeables.fastForEach { placeable ->
maxWidth = max(placeable.width, maxWidth)
maxHeight = max(placeable.height, maxHeight)
}
measureScope.layout(maxWidth, maxHeight) {
placeables.fastForEach { placeable ->
placeable.place(0, 0)
}
}
}
}
}
override fun minIntrinsicWidth(
intrinsicMeasureScope: IntrinsicMeasureScope,
measurables: List<IntrinsicMeasurable>,
h: Int,
layoutDirection: LayoutDirection
) = error("Undefined intrinsics block and it is required")
override fun minIntrinsicHeight(
intrinsicMeasureScope: IntrinsicMeasureScope,
measurables: List<IntrinsicMeasurable>,
w: Int,
layoutDirection: LayoutDirection
) = error("Undefined intrinsics block and it is required")
override fun maxIntrinsicWidth(
intrinsicMeasureScope: IntrinsicMeasureScope,
measurables: List<IntrinsicMeasurable>,
h: Int,
layoutDirection: LayoutDirection
) = error("Undefined intrinsics block and it is required")
override fun maxIntrinsicHeight(
intrinsicMeasureScope: IntrinsicMeasureScope,
measurables: List<IntrinsicMeasurable>,
w: Int,
layoutDirection: LayoutDirection
) = error("Undefined intrinsics block and it is required")
}
}
}
/**
* Return the layout direction set by the [Locale][java.util.Locale].
*
* A convenience getter that translates [Configuration.getLayoutDirection] result into
* [LayoutDirection] instance.
*/
val Configuration.localeLayoutDirection: LayoutDirection
get() = when (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
}