[go: nahoru, domu]

blob: 8b27e39169b2cbed3d42f4ffed63a6e6ac4edc7c [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.graphics.Color
import android.graphics.Outline
import android.view.RenderNode
import android.view.DisplayListCanvas
import android.os.Build
import android.view.View
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RenderEffect
/**
* RenderNode on M-O devices, where RenderNode isn't officially supported. This class uses
* a hidden android.view.RenderNode API that we have stubs for in the ui-android-stubs module.
* This implementation has higher performance than the View implementation by both avoiding
* reflection and using the lower overhead RenderNode instead of Views.
*/
@RequiresApi(Build.VERSION_CODES.M)
internal class RenderNodeApi23(val ownerView: AndroidComposeView) : DeviceRenderNode {
private val renderNode = RenderNode.create("Compose", ownerView)
private var internalCompositingStrategy = CompositingStrategy.Auto
init {
if (needToValidateAccess) {
// 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())
needToValidateAccess = false // only need to do this once
}
if (testFailCreateRenderNode) {
throw NoClassDefFoundError()
}
}
override val uniqueId: Long get() = 0
override var left: Int = 0
override var top: Int = 0
override var right: Int = 0
override var bottom: Int = 0
override val width: Int get() = right - left
override val height: Int get() = bottom - top
// 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 var scaleX: Float
get() = renderNode.scaleX
set(value) {
renderNode.scaleX = value
}
override var scaleY: Float
get() = renderNode.scaleY
set(value) {
renderNode.scaleY = value
}
override var translationX: Float
get() = renderNode.translationX
set(value) {
renderNode.translationX = value
}
override var translationY: Float
get() = renderNode.translationY
set(value) {
renderNode.translationY = value
}
override var elevation: Float
get() = renderNode.elevation
set(value) {
renderNode.elevation = value
}
override var ambientShadowColor: Int
get() {
return if (Build.VERSION.SDK_INT >= 28) {
RenderNodeVerificationHelper28.getAmbientShadowColor(renderNode)
} else {
Color.BLACK
}
}
set(value) {
if (Build.VERSION.SDK_INT >= 28) {
RenderNodeVerificationHelper28.setAmbientShadowColor(renderNode, value)
}
}
override var spotShadowColor: Int
get() {
return if (Build.VERSION.SDK_INT >= 28) {
RenderNodeVerificationHelper28.getSpotShadowColor(renderNode)
} else {
Color.BLACK
}
}
set(value) {
if (Build.VERSION.SDK_INT >= 28) {
RenderNodeVerificationHelper28.setSpotShadowColor(renderNode, value)
}
}
override var rotationZ: Float
get() = renderNode.rotation
set(value) {
renderNode.rotation = value
}
override var rotationX: Float
get() = renderNode.rotationX
set(value) {
renderNode.rotationX = value
}
override var rotationY: Float
get() = renderNode.rotationY
set(value) {
renderNode.rotationY = value
}
override var cameraDistance: Float
// 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
get() = -renderNode.cameraDistance
set(value) {
renderNode.cameraDistance = -value
}
override var pivotX: Float
get() = renderNode.pivotX
set(value) {
renderNode.pivotX = value
}
override var pivotY: Float
get() = renderNode.pivotY
set(value) {
renderNode.pivotY = value
}
override var clipToOutline: Boolean
get() = renderNode.clipToOutline
set(value) {
renderNode.clipToOutline = value
}
override var clipToBounds: Boolean = false
set(value) {
field = value
renderNode.setClipToBounds(value)
}
override var alpha: Float
get() = renderNode.alpha
set(value) {
renderNode.alpha = value
}
override var compositingStrategy: CompositingStrategy
get() = internalCompositingStrategy
set(value) {
when (value) {
CompositingStrategy.Offscreen -> {
renderNode.setLayerType(View.LAYER_TYPE_HARDWARE)
renderNode.setHasOverlappingRendering(true)
}
CompositingStrategy.ModulateAlpha -> {
renderNode.setLayerType(View.LAYER_TYPE_NONE)
renderNode.setHasOverlappingRendering(false)
}
else -> { // CompositingStrategy.Auto
renderNode.setLayerType(View.LAYER_TYPE_NONE)
renderNode.setHasOverlappingRendering(true)
}
}
internalCompositingStrategy = value
}
internal fun getLayerType(): Int = when (internalCompositingStrategy) {
CompositingStrategy.Offscreen -> View.LAYER_TYPE_HARDWARE
else -> View.LAYER_TYPE_NONE
}
internal fun hasOverlappingRendering(): Boolean = renderNode.hasOverlappingRendering()
override val hasDisplayList: Boolean
get() = renderNode.isValid
override fun setOutline(outline: Outline?) {
renderNode.setOutline(outline)
}
override fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean {
this.left = left
this.top = top
this.right = right
this.bottom = bottom
return renderNode.setLeftTopRightBottom(left, top, right, bottom)
}
override fun offsetLeftAndRight(offset: Int) {
left += offset
right += offset
renderNode.offsetLeftAndRight(offset)
}
override fun offsetTopAndBottom(offset: Int) {
top += offset
bottom += offset
renderNode.offsetTopAndBottom(offset)
}
override fun record(
canvasHolder: CanvasHolder,
clipPath: Path?,
drawBlock: (Canvas) -> Unit
) {
val canvas = renderNode.start(width, height)
canvasHolder.drawInto(canvas) {
if (clipPath != null) {
save()
clipPath(clipPath)
}
drawBlock(this)
if (clipPath != null) {
restore()
}
}
renderNode.end(canvas)
}
override fun getMatrix(matrix: android.graphics.Matrix) {
return renderNode.getMatrix(matrix)
}
override fun getInverseMatrix(matrix: android.graphics.Matrix) {
return renderNode.getInverseMatrix(matrix)
}
override fun drawInto(canvas: android.graphics.Canvas) {
(canvas as DisplayListCanvas).drawRenderNode(renderNode)
}
override fun setHasOverlappingRendering(hasOverlappingRendering: Boolean): Boolean =
renderNode.setHasOverlappingRendering(hasOverlappingRendering)
override fun dumpRenderNodeData(): DeviceRenderNodeData =
DeviceRenderNodeData(
// Platform RenderNode for API level 23-29 does not provide bounds/dimension properties
uniqueId = 0,
left = 0,
top = 0,
right = 0,
bottom = 0,
width = 0,
height = 0,
scaleX = renderNode.scaleX,
scaleY = renderNode.scaleY,
translationX = renderNode.translationX,
translationY = renderNode.translationY,
elevation = renderNode.elevation,
ambientShadowColor = ambientShadowColor,
spotShadowColor = spotShadowColor,
rotationZ = renderNode.rotation,
rotationX = renderNode.rotationX,
rotationY = renderNode.rotationY,
cameraDistance = renderNode.cameraDistance,
pivotX = renderNode.pivotX,
pivotY = renderNode.pivotY,
clipToOutline = renderNode.clipToOutline,
// No getter on RenderNode for clipToBounds, always return the value we have configured
// on it since this is a write only field
clipToBounds = clipToBounds,
alpha = renderNode.alpha,
renderEffect = renderEffect,
compositingStrategy = internalCompositingStrategy
)
override fun discardDisplayList() {
discardDisplayListInternal()
}
private 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)
}
}
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)
)
}
}
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 var needToValidateAccess = 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()
}
}