blob: 327d8fefc7c3025ea12d58cf6c042f2dd3ec2b2a [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.PorterDuffXfermode
import android.os.Build
import android.view.DisplayListCanvas
import android.view.RenderNode
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.isUnspecified
import androidx.compose.ui.graphics.BlendMode
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.drawscope.draw
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.graphics.toPorterDuffMode
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import java.util.concurrent.atomic.AtomicBoolean
@RequiresApi(Build.VERSION_CODES.M)
internal class GraphicsLayerV23(
ownerView: View,
private val canvasHolder: CanvasHolder = CanvasHolder(),
private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
) : GraphicsLayerImpl {
private val renderNode = RenderNode.create("Compose", ownerView)
private var size: IntSize = IntSize.Zero
private var layerPaint: android.graphics.Paint? = null
private var matrix: android.graphics.Matrix? = null
private var outlineIsProvided = false
private fun obtainLayerPaint(): android.graphics.Paint =
layerPaint ?: android.graphics.Paint().also { layerPaint = it }
init {
// only need to do this once
if (needToValidateAccess.getAndSet(false)) {
// This is only to force loading the DisplayListCanvas class and causing the
// MRenderNode to fail with a NoClassDefFoundError during construction instead of
// later.
@Suppress("UNUSED_VARIABLE")
val displayListCanvas: DisplayListCanvas? = null
// Ensure that we can access properties of the RenderNode. We want to force an
// exception here if there is a problem accessing any of these so that we can
// fall back to the View implementation.
renderNode.scaleX = renderNode.scaleX
renderNode.scaleY = renderNode.scaleY
renderNode.translationX = renderNode.translationX
renderNode.translationY = renderNode.translationY
renderNode.elevation = renderNode.elevation
renderNode.rotation = renderNode.rotation
renderNode.rotationX = renderNode.rotationX
renderNode.rotationY = renderNode.rotationY
renderNode.cameraDistance = renderNode.cameraDistance
renderNode.pivotX = renderNode.pivotX
renderNode.pivotY = renderNode.pivotY
renderNode.clipToOutline = renderNode.clipToOutline
renderNode.setClipToBounds(false)
renderNode.alpha = renderNode.alpha
renderNode.isValid // only read
renderNode.setLeftTopRightBottom(0, 0, 0, 0)
renderNode.offsetLeftAndRight(0)
renderNode.offsetTopAndBottom(0)
verifyShadowColorProperties(renderNode)
discardDisplayListInternal()
renderNode.setLayerType(View.LAYER_TYPE_NONE)
renderNode.setHasOverlappingRendering(renderNode.hasOverlappingRendering())
}
if (testFailCreateRenderNode) {
throw NoClassDefFoundError()
}
renderNode.setClipToBounds(false)
applyCompositingStrategy(CompositingStrategy.Auto)
}
override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
set(value) {
field = value
updateLayerProperties()
}
private fun applyCompositingStrategy(compositingStrategy: CompositingStrategy) {
renderNode.apply {
when (compositingStrategy) {
CompositingStrategy.Offscreen -> {
setLayerType(View.LAYER_TYPE_HARDWARE)
setLayerPaint(layerPaint)
setHasOverlappingRendering(true)
}
CompositingStrategy.ModulateAlpha -> {
setLayerType(View.LAYER_TYPE_NONE)
setLayerPaint(layerPaint)
setHasOverlappingRendering(false)
}
else -> { // CompositingStrategy.Auto
setLayerType(View.LAYER_TYPE_NONE)
setLayerPaint(layerPaint)
setHasOverlappingRendering(true)
}
}
}
}
override var blendMode: BlendMode = BlendMode.SrcOver
set(value) {
if (field != value) {
field = value
obtainLayerPaint().apply { xfermode = PorterDuffXfermode(value.toPorterDuffMode()) }
updateLayerProperties()
}
}
private fun requiresCompositingLayer(): Boolean =
compositingStrategy == CompositingStrategy.Offscreen ||
blendMode != BlendMode.SrcOver ||
colorFilter != null
private fun updateLayerProperties() {
if (requiresCompositingLayer()) {
applyCompositingStrategy(CompositingStrategy.Offscreen)
} else {
applyCompositingStrategy(compositingStrategy)
}
}
override var colorFilter: ColorFilter? = null
set(value) {
field = value
if (value != null) {
applyCompositingStrategy(CompositingStrategy.Offscreen)
renderNode.setLayerPaint(obtainLayerPaint().apply {
colorFilter = value.asAndroidColorFilter()
})
} else {
updateLayerProperties()
}
}
override var alpha: Float = 1.0f
set(value) {
field = value
renderNode.setAlpha(value)
}
private var shouldManuallySetCenterPivot = false
override var pivotOffset: Offset = Offset.Unspecified
set(value) {
field = value
if (value.isUnspecified) {
shouldManuallySetCenterPivot = true
renderNode.pivotX = size.width / 2f
renderNode.pivotY = size.height / 2f
} else {
shouldManuallySetCenterPivot = false
renderNode.pivotX = value.x
renderNode.pivotY = value.y
}
}
override var scaleX: Float = 1f
set(value) {
field = value
renderNode.setScaleX(value)
}
override var scaleY: Float = 1f
set(value) {
field = value
renderNode.setScaleY(value)
}
override var translationX: Float = 0f
set(value) {
field = value
renderNode.setTranslationX(value)
}
override var translationY: Float = 0f
set(value) {
field = value
renderNode.setTranslationY(value)
}
override var shadowElevation: Float = 0f
set(value) {
field = value
renderNode.setElevation(value)
}
override var ambientShadowColor: Color = Color.Black
set(value) {
field = value
renderNode.setAmbientShadowColor(value.toArgb())
}
override var spotShadowColor: Color = Color.Black
set(value) {
field = value
renderNode.setSpotShadowColor(value.toArgb())
}
override var rotationX: Float = 0f
set(value) {
field = value
renderNode.setRotationX(value)
}
override var rotationY: Float = 0f
set(value) {
field = value
renderNode.setRotationY(value)
}
override var rotationZ: Float = 0f
set(value) {
field = value
renderNode.setRotation(value)
}
override var cameraDistance: Float = DefaultCameraDistance
set(value) {
// Camera distance was negated in older API levels. Maintain the same input parameters
// and negate the given camera distance before it is applied and also negate it when
// it is queried
field = value
renderNode.setCameraDistance(-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)
}
}
// API level 23 does not support RenderEffect so keep the field around for consistency
// however, it will not be applied to the rendered result. Consumers are encouraged
// to use the RenderEffect.isSupported API before consuming a [RenderEffect] instance.
// If RenderEffect is used on an unsupported API level, it should act as a no-op and not
// crash the compose application
override var renderEffect: RenderEffect? = null
override fun setPosition(x: Int, y: Int, size: IntSize) {
renderNode.setLeftTopRightBottom(x, y, x + size.width, y + size.height)
if (this.size != size) {
if (shouldManuallySetCenterPivot) {
renderNode.pivotX = size.width / 2f
renderNode.pivotY = size.height / 2f
}
this.size = size
}
}
override fun setOutline(outline: Outline?) {
renderNode.setOutline(outline)
outlineIsProvided = outline != null
applyClip()
}
override var isInvalidated: Boolean = true
override val hasDisplayList: Boolean
get() = renderNode.isValid
override fun record(
density: Density,
layoutDirection: LayoutDirection,
layer: GraphicsLayer,
block: DrawScope.() -> Unit
) {
val recordingCanvas = renderNode.start(size.width, size.height)
try {
canvasHolder.drawInto(recordingCanvas) {
canvasDrawScope.draw(density, layoutDirection, this, size.toSize(), layer, block)
}
} finally {
renderNode.end(recordingCanvas)
}
isInvalidated = false
}
override fun draw(canvas: androidx.compose.ui.graphics.Canvas) {
(canvas.nativeCanvas as DisplayListCanvas).drawRenderNode(renderNode)
}
override fun calculateMatrix(): Matrix {
val m = matrix ?: android.graphics.Matrix().also { matrix = it }
renderNode.getMatrix(m)
return m
}
override fun discardDisplayList() {
discardDisplayListInternal()
}
override val layerId: Long = 0
private fun verifyShadowColorProperties(renderNode: RenderNode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
RenderNodeVerificationHelper28.setAmbientShadowColor(
renderNode,
RenderNodeVerificationHelper28.getAmbientShadowColor(renderNode)
)
RenderNodeVerificationHelper28.setSpotShadowColor(
renderNode,
RenderNodeVerificationHelper28.getSpotShadowColor(renderNode)
)
}
}
internal fun discardDisplayListInternal() {
// See b/216660268. RenderNode#discardDisplayList was originally called
// destroyDisplayListData on Android M and below. Make sure we gate on the corresponding
// API level and call the original method name on these API levels, otherwise invoke
// the current method name of discardDisplayList
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
RenderNodeVerificationHelper24.discardDisplayList(renderNode)
} else {
RenderNodeVerificationHelper23.destroyDisplayListData(renderNode)
}
}
companion object {
// Used by tests to force failing creating a RenderNode to simulate a device that
// doesn't support RenderNodes before Q.
internal var testFailCreateRenderNode = false
// We need to validate that RenderNodes can be accessed before using the RenderNode
// stub implementation, but we only need to validate it once. This flag indicates that
// validation is still needed.
private val needToValidateAccess = AtomicBoolean(true)
}
}
@RequiresApi(Build.VERSION_CODES.P)
private object RenderNodeVerificationHelper28 {
@androidx.annotation.DoNotInline
fun getAmbientShadowColor(renderNode: RenderNode): Int {
return renderNode.ambientShadowColor
}
@androidx.annotation.DoNotInline
fun setAmbientShadowColor(renderNode: RenderNode, target: Int) {
renderNode.ambientShadowColor = target
}
@androidx.annotation.DoNotInline
fun getSpotShadowColor(renderNode: RenderNode): Int {
return renderNode.spotShadowColor
}
@androidx.annotation.DoNotInline
fun setSpotShadowColor(renderNode: RenderNode, target: Int) {
renderNode.spotShadowColor = target
}
}
@RequiresApi(Build.VERSION_CODES.N)
private object RenderNodeVerificationHelper24 {
@androidx.annotation.DoNotInline
fun discardDisplayList(renderNode: RenderNode) {
renderNode.discardDisplayList()
}
}
@RequiresApi(Build.VERSION_CODES.M)
private object RenderNodeVerificationHelper23 {
@androidx.annotation.DoNotInline
fun destroyDisplayListData(renderNode: RenderNode) {
renderNode.destroyDisplayListData()
}
}