blob: 0e2ff9b8fe26d3db8370cc23bad4bb6674756e9a [file]
/*
* Copyright 2024 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.graphics.layer
import android.graphics.Matrix
import android.graphics.Outline
import android.graphics.RenderNode
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.asAndroidColorFilter
import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toAndroidBlendMode
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
/**
* GraphicsLayer implementation for Android Q+ that uses the public RenderNode API
*/
@RequiresApi(Build.VERSION_CODES.Q)
internal class GraphicsLayerV29(
private val canvasHolder: CanvasHolder = CanvasHolder(),
private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
) : GraphicsLayerImpl {
private val renderNode: RenderNode = RenderNode("graphicsLayer")
private var size: Size = Size.Zero
private var layerPaint: android.graphics.Paint? = null
private var matrix: Matrix? = null
private var outlineIsProvided = false
init {
renderNode.clipToBounds = false
renderNode.applyCompositingStrategy(CompositingStrategy.Auto)
}
override var alpha: Float = 1.0f
set(value) {
field = value
renderNode.alpha = value
}
override var blendMode: BlendMode = BlendMode.SrcOver
set(value) {
field = value
obtainLayerPaint().apply { blendMode = value.toAndroidBlendMode() }
updateLayerProperties()
}
override var colorFilter: ColorFilter? = null
set(value) {
field = value
obtainLayerPaint().apply { colorFilter = value?.asAndroidColorFilter() }
updateLayerProperties()
}
override var pivotOffset: Offset = Offset.Unspecified
set(value) {
field = value
if (value.isUnspecified) {
renderNode.resetPivot()
} else {
renderNode.pivotX = value.x
renderNode.pivotY = value.y
}
}
override var scaleX: Float = 1f
set(value) {
field = value
renderNode.scaleX = value
}
override var scaleY: Float = 1f
set(value) {
field = value
renderNode.scaleY = value
}
override var translationX: Float = 0f
set(value) {
field = value
renderNode.translationX = value
}
override var translationY: Float = 0f
set(value) {
field = value
renderNode.translationY = value
}
override var shadowElevation: Float = 0f
set(value) {
field = value
renderNode.elevation = value
}
override var ambientShadowColor: Color = Color.Black
set(value) {
field = value
renderNode.ambientShadowColor = value.toArgb()
}
override var spotShadowColor: Color = Color.Black
set(value) {
field = value
renderNode.spotShadowColor = value.toArgb()
}
override var rotationX: Float = 0f
set(value) {
field = value
renderNode.rotationX = value
}
override var rotationY: Float = 0f
set(value) {
field = value
renderNode.rotationY = value
}
override var rotationZ: Float = 0f
set(value) {
field = value
renderNode.rotationZ = value
}
override var cameraDistance: Float = DefaultCameraDistance
set(value) {
field = value
renderNode.cameraDistance = value
}
override var clip: Boolean = false
set(value) {
field = value
applyClip()
}
private var clipToBounds = false
private var clipToOutline = false
private fun applyClip() {
val newClipToBounds = clip && !outlineIsProvided
val newClipToOutline = clip && outlineIsProvided
if (newClipToBounds != clipToBounds) {
clipToBounds = newClipToBounds
renderNode.setClipToBounds(clipToBounds)
}
if (newClipToOutline != clipToOutline) {
clipToOutline = newClipToOutline
renderNode.setClipToOutline(newClipToOutline)
}
}
override var renderEffect: RenderEffect? = null
set(value) {
field = value
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
RenderNodeVerificationHelper.setRenderEffect(renderNode, value)
}
}
override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
set(value) {
field = value
updateLayerProperties()
}
private fun RenderNode.applyCompositingStrategy(compositingStrategy: CompositingStrategy) {
when (compositingStrategy) {
CompositingStrategy.Offscreen -> {
setUseCompositingLayer(true, layerPaint)
setHasOverlappingRendering(true)
}
CompositingStrategy.ModulateAlpha -> {
setUseCompositingLayer(false, layerPaint)
setHasOverlappingRendering(false)
}
else -> {
setUseCompositingLayer(false, layerPaint)
setHasOverlappingRendering(true)
}
}
}
private fun updateLayerProperties() {
if (requiresCompositingLayer()) {
renderNode.applyCompositingStrategy(CompositingStrategy.Offscreen)
} else {
renderNode.applyCompositingStrategy(compositingStrategy)
}
}
override fun setPosition(x: Int, y: Int, size: IntSize) {
renderNode.setPosition(x, y, x + size.width, y + size.height)
this.size = size.toSize()
}
override fun setOutline(outline: Outline?) {
renderNode.setOutline(outline)
outlineIsProvided = outline != null
applyClip()
}
override var isInvalidated: Boolean = true
override fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
block: DrawScope.() -> Unit
) {
val recordingCanvas = renderNode.beginRecording()
try {
canvasHolder.drawInto(recordingCanvas) {
canvasDrawScope.drawContext.also {
it.density = density
it.layoutDirection = layoutDirection
it.graphicsLayer = layer
it.size = size
it.canvas = this
}
canvasDrawScope.block()
}
} finally {
renderNode.endRecording()
}
isInvalidated = false
}
override fun draw(canvas: Canvas) {
canvas.nativeCanvas.drawRenderNode(renderNode)
}
override fun calculateMatrix(): Matrix {
val m = matrix ?: Matrix().also { matrix = it }
renderNode.getMatrix(m)
return m
}
override val hasDisplayList: Boolean
get() = renderNode.hasDisplayList()
override fun discardDisplayList() {
renderNode.discardDisplayList()
}
override val layerId: Long
get() = renderNode.uniqueId
private fun obtainLayerPaint(): android.graphics.Paint =
layerPaint ?: android.graphics.Paint().also { layerPaint = it }
private fun requiresCompositingLayer(): Boolean =
compositingStrategy == CompositingStrategy.Offscreen ||
requiresLayerPaint() ||
renderEffect != null
private fun requiresLayerPaint(): Boolean =
blendMode != BlendMode.SrcOver || colorFilter != null
}
@RequiresApi(Build.VERSION_CODES.S)
internal object RenderNodeVerificationHelper {
@androidx.annotation.DoNotInline
fun setRenderEffect(renderNode: RenderNode, target: RenderEffect?) {
renderNode.setRenderEffect(target?.asAndroidRenderEffect())
}
}