[go: nahoru, domu]

blob: f718f7b6639a902c87299092b6ece6c5b8a2c0c1 [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.compose.ui.platform
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.GraphicLayerInfo
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
/**
* RenderNode implementation of OwnedLayer.
*/
@RequiresApi(Build.VERSION_CODES.M)
internal class RenderNodeLayer(
val ownerView: AndroidComposeView,
drawBlock: (Canvas) -> Unit,
invalidateParentLayer: () -> Unit
) : OwnedLayer, GraphicLayerInfo {
private var drawBlock: ((Canvas) -> Unit)? = drawBlock
private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
/**
* True when the RenderNodeLayer has been invalidated and not yet drawn.
*/
private var isDirty = false
set(value) {
if (value != field) {
field = value
ownerView.notifyLayerIsDirty(this, value)
}
}
private val outlineResolver = OutlineResolver(ownerView.density)
private var isDestroyed = false
private var drawnWithZ = false
/**
* Optional paint used when the RenderNode is rendered on a software backed
* canvas and is somewhat transparent (i.e. alpha less than 1.0f)
*/
private var softwareLayerPaint: Paint? = null
private val matrixCache = LayerMatrixCache(getMatrix)
private val canvasHolder = CanvasHolder()
/**
* Local copy of the transform origin as GraphicsLayerModifier can be implemented
* as a model object. Update this field within [updateLayerProperties] and use it
* in [resize] or other methods
*/
private var transformOrigin: TransformOrigin = TransformOrigin.Center
private val renderNode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
RenderNodeApi29(ownerView)
} else {
RenderNodeApi23(ownerView)
}.apply { setHasOverlappingRendering(true) }
override val layerId: Long
get() = renderNode.uniqueId
override val ownerViewId: Long
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
UniqueDrawingIdApi29.getUniqueDrawingId(ownerView)
} else {
-1
}
@RequiresApi(29)
private object UniqueDrawingIdApi29 {
@JvmStatic
@androidx.annotation.DoNotInline
fun getUniqueDrawingId(view: View) = view.uniqueDrawingId
}
override fun updateLayerProperties(
scaleX: Float,
scaleY: Float,
alpha: Float,
translationX: Float,
translationY: Float,
shadowElevation: Float,
rotationX: Float,
rotationY: Float,
rotationZ: Float,
cameraDistance: Float,
transformOrigin: TransformOrigin,
shape: Shape,
clip: Boolean,
renderEffect: RenderEffect?,
ambientShadowColor: Color,
spotShadowColor: Color,
compositingStrategy: CompositingStrategy,
layoutDirection: LayoutDirection,
density: Density
) {
this.transformOrigin = transformOrigin
val wasClippingManually = renderNode.clipToOutline && !outlineResolver.outlineClipSupported
renderNode.scaleX = scaleX
renderNode.scaleY = scaleY
renderNode.alpha = alpha
renderNode.translationX = translationX
renderNode.translationY = translationY
renderNode.elevation = shadowElevation
renderNode.ambientShadowColor = ambientShadowColor.toArgb()
renderNode.spotShadowColor = spotShadowColor.toArgb()
renderNode.rotationZ = rotationZ
renderNode.rotationX = rotationX
renderNode.rotationY = rotationY
renderNode.cameraDistance = cameraDistance
renderNode.pivotX = transformOrigin.pivotFractionX * renderNode.width
renderNode.pivotY = transformOrigin.pivotFractionY * renderNode.height
renderNode.clipToOutline = clip && shape !== RectangleShape
renderNode.clipToBounds = clip && shape === RectangleShape
renderNode.renderEffect = renderEffect
renderNode.compositingStrategy = compositingStrategy
val shapeChanged = outlineResolver.update(
shape,
renderNode.alpha,
renderNode.clipToOutline,
renderNode.elevation,
layoutDirection,
density
)
renderNode.setOutline(outlineResolver.outline)
val isClippingManually = renderNode.clipToOutline && !outlineResolver.outlineClipSupported
if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) {
invalidate()
} else {
triggerRepaint()
}
if (!drawnWithZ && renderNode.elevation > 0f) {
invalidateParentLayer?.invoke()
}
matrixCache.invalidate()
}
override fun isInLayer(position: Offset): Boolean {
val x = position.x
val y = position.y
if (renderNode.clipToBounds) {
return 0f <= x && x < renderNode.width && 0f <= y && y < renderNode.height
}
if (renderNode.clipToOutline) {
return outlineResolver.isInOutline(position)
}
return true
}
override fun resize(size: IntSize) {
val width = size.width
val height = size.height
renderNode.pivotX = transformOrigin.pivotFractionX * width
renderNode.pivotY = transformOrigin.pivotFractionY * height
if (renderNode.setPosition(
renderNode.left,
renderNode.top,
renderNode.left + width,
renderNode.top + height
)
) {
outlineResolver.update(Size(width.toFloat(), height.toFloat()))
renderNode.setOutline(outlineResolver.outline)
invalidate()
matrixCache.invalidate()
}
}
override fun move(position: IntOffset) {
val oldLeft = renderNode.left
val oldTop = renderNode.top
val newLeft = position.x
val newTop = position.y
if (oldLeft != newLeft || oldTop != newTop) {
renderNode.offsetLeftAndRight(newLeft - oldLeft)
renderNode.offsetTopAndBottom(newTop - oldTop)
triggerRepaint()
matrixCache.invalidate()
}
}
override fun invalidate() {
if (!isDirty && !isDestroyed) {
ownerView.invalidate()
isDirty = true
}
}
/**
* This only triggers the system so that it knows that some kind of painting
* must happen without actually causing the layer to be invalidated and have
* to re-record its drawing.
*/
private fun triggerRepaint() {
// onDescendantInvalidated is only supported on O+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WrapperRenderNodeLayerHelperMethods.onDescendantInvalidated(ownerView)
} else {
ownerView.invalidate()
}
}
override fun drawLayer(canvas: Canvas) {
val androidCanvas = canvas.nativeCanvas
if (androidCanvas.isHardwareAccelerated) {
updateDisplayList()
drawnWithZ = renderNode.elevation > 0f
if (drawnWithZ) {
canvas.enableZ()
}
renderNode.drawInto(androidCanvas)
if (drawnWithZ) {
canvas.disableZ()
}
} else {
val left = renderNode.left.toFloat()
val top = renderNode.top.toFloat()
val right = renderNode.right.toFloat()
val bottom = renderNode.bottom.toFloat()
// If there is alpha applied, we must render into an offscreen buffer to
// properly blend the contents of this layer against the background content
if (renderNode.alpha < 1.0f) {
val paint = (softwareLayerPaint ?: Paint().also { softwareLayerPaint = it })
.apply { alpha = renderNode.alpha }
androidCanvas.saveLayer(
left,
top,
right,
bottom,
paint.asFrameworkPaint()
)
} else {
canvas.save()
}
// If we are software rendered we must translate the canvas based on the offset provided
// in the move call which operates directly on the RenderNode
canvas.translate(left, top)
canvas.concat(matrixCache.calculateMatrix(renderNode))
clipRenderNode(canvas)
drawBlock?.invoke(canvas)
canvas.restore()
isDirty = false
}
}
/**
* Manually clips the content of the RenderNodeLayer in the provided canvas.
* This is used only in software rendered use cases
*/
private fun clipRenderNode(canvas: Canvas) {
if (renderNode.clipToOutline || renderNode.clipToBounds) {
outlineResolver.clipToOutline(canvas)
}
}
override fun updateDisplayList() {
if (isDirty || !renderNode.hasDisplayList) {
isDirty = false
val clipPath = if (renderNode.clipToOutline && !outlineResolver.outlineClipSupported) {
outlineResolver.clipPath
} else {
null
}
drawBlock?.let {
renderNode.record(canvasHolder, clipPath, it)
}
}
}
override fun destroy() {
if (renderNode.hasDisplayList) {
renderNode.discardDisplayList()
}
drawBlock = null
invalidateParentLayer = null
isDestroyed = true
isDirty = false
ownerView.requestClearInvalidObservations()
ownerView.recycle(this)
}
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
return if (inverse) {
matrixCache.calculateInverseMatrix(renderNode)?.map(point) ?: Offset.Infinite
} else {
matrixCache.calculateMatrix(renderNode).map(point)
}
}
override fun mapBounds(rect: MutableRect, inverse: Boolean) {
if (inverse) {
val matrix = matrixCache.calculateInverseMatrix(renderNode)
if (matrix == null) {
rect.set(0f, 0f, 0f, 0f)
} else {
matrix.map(rect)
}
} else {
matrixCache.calculateMatrix(renderNode).map(rect)
}
}
override fun reuseLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit) {
isDirty = false
isDestroyed = false
drawnWithZ = false
transformOrigin = TransformOrigin.Center
this.drawBlock = drawBlock
this.invalidateParentLayer = invalidateParentLayer
}
override fun transform(matrix: Matrix) {
matrix.timesAssign(matrixCache.calculateMatrix(renderNode))
}
override fun inverseTransform(matrix: Matrix) {
val inverse = matrixCache.calculateInverseMatrix(renderNode)
if (inverse != null) {
matrix.timesAssign(inverse)
}
}
companion object {
private val getMatrix: (DeviceRenderNode, android.graphics.Matrix) -> Unit = { rn, matrix ->
rn.getMatrix(matrix)
}
}
}
/**
* This class is here to ensure that the classes that use this API will get verified and can be
* AOT compiled. It is expected that this class will soft-fail verification, but the classes
* which use this method will pass.
*/
@RequiresApi(Build.VERSION_CODES.O)
internal object WrapperRenderNodeLayerHelperMethods {
@androidx.annotation.DoNotInline
fun onDescendantInvalidated(ownerView: AndroidComposeView) {
ownerView.parent?.onDescendantInvalidated(ownerView, ownerView)
}
}