LayerWrapper.kt
/*
* 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.node
import androidx.compose.ui.DrawLayerModifier
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.layout.globalPosition
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
internal class LayerWrapper(
wrapped: LayoutNodeWrapper,
modifier: DrawLayerModifier
) : DelegatingLayoutNodeWrapper<DrawLayerModifier>(wrapped, modifier), (Canvas) -> Unit {
private var _layer: OwnedLayer? = null
// Do not invalidate itself on position change.
override val invalidateLayerOnBoundsChange get() = false
override var modifier: DrawLayerModifier
get() = super.modifier
set(value) {
super.modifier = value
_layer?.modifier = value
}
private val invalidateParentLayer: () -> Unit = {
wrappedBy?.invalidateLayer()
}
/**
* True when the last drawing of this layer didn't draw the real content as the LayoutNode
* containing this layer was not placed by the parent.
*/
internal var lastDrawingWasSkipped = false
private set
val layer: OwnedLayer
get() = _layer!!
// TODO (njawad): This cache matrix is not thread safe
private var _matrixCache: Matrix? = null
private val matrixCache: Matrix
get() = _matrixCache ?: Matrix().also { _matrixCache = it }
override fun performMeasure(constraints: Constraints): Placeable {
val placeable = super.performMeasure(constraints)
layer.resize(measuredSize)
return placeable
}
override fun placeAt(position: IntOffset) {
super.placeAt(position)
layer.move(position)
}
override fun draw(canvas: Canvas) {
layer.drawLayer(canvas)
}
override fun attach() {
super.attach()
_layer = layoutNode.requireOwner().createLayer(
modifier,
this,
invalidateParentLayer
)
invalidateParentLayer()
}
override fun detach() {
super.detach()
// The layer has been removed and we need to invalidate the containing layer. We've lost
// which layer contained this one, but all layers in this modifier chain will be invalidated
// in onModifierChanged(). Therefore the only possible layer that won't automatically be
// invalidated is the parent's layer. We'll invalidate it here:
@OptIn(ExperimentalLayoutNodeApi::class)
layoutNode.parent?.invalidateLayer()
_layer?.destroy()
_layer = null
}
override fun invalidateLayer() {
_layer?.invalidate()
}
override fun fromParentPosition(position: Offset): Offset {
val inverse = matrixCache
layer.getMatrix(inverse)
inverse.invert()
val targetPosition = inverse.map(position)
return super.fromParentPosition(targetPosition)
}
override fun toParentPosition(position: Offset): Offset {
val matrix = matrixCache
val targetPosition = matrix.map(position)
return super.toParentPosition(targetPosition)
}
override fun rectInParent(bounds: MutableRect) {
if (modifier.clip) {
bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
if (bounds.isEmpty) {
return
}
}
val matrix = matrixCache
layer.getMatrix(matrix)
matrix.map(bounds)
return super.rectInParent(bounds)
}
override fun hitTest(
pointerPositionRelativeToScreen: Offset,
hitPointerInputFilters: MutableList<PointerInputFilter>
) {
if (modifier.clip) {
val l = globalPosition.x
val t = globalPosition.y
val r = l + width
val b = t + height
val localBoundsRelativeToScreen = Rect(l, t, r, b)
if (!localBoundsRelativeToScreen.contains(pointerPositionRelativeToScreen)) {
// If we should clip pointer input hit testing to our bounds, and the pointer is
// not in our bounds, then return false now.
return
}
}
// If we are here, either we aren't clipping to bounds or we are and the pointer was in
// bounds.
super.hitTest(
pointerPositionRelativeToScreen,
hitPointerInputFilters
)
}
override fun onModifierChanged() {
_layer?.invalidate()
}
@ExperimentalLayoutNodeApi
override fun invoke(canvas: Canvas) {
if (layoutNode.isPlaced) {
require(layoutNode.layoutState == LayoutNode.LayoutState.Ready) {
"Layer is redrawn for LayoutNode in state ${layoutNode.layoutState} [$layoutNode]"
}
wrapped.draw(canvas)
lastDrawingWasSkipped = false
} else {
// The invalidation is requested even for nodes which are not placed. As we are not
// going to display them we skip the drawing. It is safe to just draw nothing as the
// layer will be invalidated again when the node will be finally placed.
lastDrawingWasSkipped = true
}
}
}