[go: nahoru, domu]

blob: b1c77180093ea783df12e63e94df1c14b4ed5ca0 [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.runtime.snapshots.Snapshot
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.Fields
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.layer.GraphicsLayer
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: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit
) : OwnedLayer, GraphicLayerInfo {
private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> 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 = Snapshot.withoutReadObservation {
// we don't really care about observation here as density is applied manually
// not observing the density changes saves performance on recording reads
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)
// in compose the default is to not clip.
clipToBounds = false
}
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
}
private var mutatedFields: Int = 0
override fun updateLayerProperties(
scope: ReusableGraphicsLayerScope,
layoutDirection: LayoutDirection,
density: Density,
) {
val maybeChangedFields = scope.mutatedFields or mutatedFields
if (maybeChangedFields and Fields.TransformOrigin != 0) {
this.transformOrigin = scope.transformOrigin
}
val wasClippingManually = renderNode.clipToOutline && !outlineResolver.outlineClipSupported
if (maybeChangedFields and Fields.ScaleX != 0) {
renderNode.scaleX = scope.scaleX
}
if (maybeChangedFields and Fields.ScaleY != 0) {
renderNode.scaleY = scope.scaleY
}
if (maybeChangedFields and Fields.Alpha != 0) {
renderNode.alpha = scope.alpha
}
if (maybeChangedFields and Fields.TranslationX != 0) {
renderNode.translationX = scope.translationX
}
if (maybeChangedFields and Fields.TranslationY != 0) {
renderNode.translationY = scope.translationY
}
if (maybeChangedFields and Fields.ShadowElevation != 0) {
renderNode.elevation = scope.shadowElevation
}
if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
renderNode.ambientShadowColor = scope.ambientShadowColor.toArgb()
}
if (maybeChangedFields and Fields.SpotShadowColor != 0) {
renderNode.spotShadowColor = scope.spotShadowColor.toArgb()
}
if (maybeChangedFields and Fields.RotationZ != 0) {
renderNode.rotationZ = scope.rotationZ
}
if (maybeChangedFields and Fields.RotationX != 0) {
renderNode.rotationX = scope.rotationX
}
if (maybeChangedFields and Fields.RotationY != 0) {
renderNode.rotationY = scope.rotationY
}
if (maybeChangedFields and Fields.CameraDistance != 0) {
renderNode.cameraDistance = scope.cameraDistance
}
if (maybeChangedFields and Fields.TransformOrigin != 0) {
renderNode.pivotX = transformOrigin.pivotFractionX * renderNode.width
renderNode.pivotY = transformOrigin.pivotFractionY * renderNode.height
}
val clipToOutline = scope.clip && scope.shape !== RectangleShape
if (maybeChangedFields and (Fields.Clip or Fields.Shape) != 0) {
renderNode.clipToOutline = clipToOutline
renderNode.clipToBounds = scope.clip && scope.shape === RectangleShape
}
if (maybeChangedFields and Fields.RenderEffect != 0) {
renderNode.renderEffect = scope.renderEffect
}
if (maybeChangedFields and Fields.CompositingStrategy != 0) {
renderNode.compositingStrategy = scope.compositingStrategy
}
val shapeChanged = outlineResolver.update(
scope.shape,
scope.alpha,
clipToOutline,
scope.shadowElevation,
layoutDirection,
density
)
if (outlineResolver.cacheIsDirty) {
renderNode.setOutline(outlineResolver.outline)
}
val isClippingManually = clipToOutline && !outlineResolver.outlineClipSupported
if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) {
invalidate()
} else {
triggerRepaint()
}
if (!drawnWithZ && renderNode.elevation > 0f) {
invalidateParentLayer?.invoke()
}
if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
matrixCache.invalidate()
}
mutatedFields = scope.mutatedFields
}
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) {
if (oldLeft != newLeft) {
renderNode.offsetLeftAndRight(newLeft - oldLeft)
}
if (oldTop != newTop) {
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, parentLayer: GraphicsLayer?) {
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, null)
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) {
val clipPath = if (renderNode.clipToOutline && !outlineResolver.outlineClipSupported) {
outlineResolver.clipPath
} else {
null
}
drawBlock?.let { drawBlock ->
renderNode.record(canvasHolder, clipPath) {
drawBlock(it, null)
}
}
isDirty = false
}
}
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: Canvas, parentLayer: GraphicsLayer?) -> 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)
}
}