[go: nahoru, domu]

blob: 6e820deb6229e74c9fda059e4e508ec97e4ac90b [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.annotation.SuppressLint
import android.os.Build
import android.view.View
import android.view.ViewOutlineProvider
import androidx.annotation.RequiresApi
import androidx.compose.ui.geometry.MutableRect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.CanvasHolder
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Fields
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.RenderEffect
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.GraphicLayerInfo
import androidx.compose.ui.node.OwnedLayer
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import java.lang.reflect.Field
import java.lang.reflect.Method
/**
* View implementation of OwnedLayer.
*/
internal class ViewLayer(
val ownerView: AndroidComposeView,
val container: DrawChildContainer,
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit
) : View(ownerView.context), OwnedLayer, GraphicLayerInfo {
private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
private val outlineResolver = OutlineResolver()
// Value of the layerModifier's clipToBounds property
private var clipToBounds = false
private var clipBoundsCache: android.graphics.Rect? = null
private val manualClipPath: Path? get() =
if (!clipToOutline || outlineResolver.outlineClipSupported) {
null
} else {
outlineResolver.clipPath
}
var isInvalidated = false
private set(value) {
if (value != field) {
field = value
ownerView.notifyLayerIsDirty(this, value)
}
}
private var drawnWithZ = false
private val canvasHolder = CanvasHolder()
private val matrixCache = LayerMatrixCache(getMatrix)
/**
* 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 mTransformOrigin: TransformOrigin = TransformOrigin.Center
private var mHasOverlappingRendering = true
init {
setWillNotDraw(false) // we WILL draw
container.addView(this)
}
override val layerId: Long = generateViewId().toLong()
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
}
/**
* Configure the camera distance on the View in pixels. View already has a get/setCameraDistance
* API however, that operates in Dp values.
*/
var cameraDistancePx: Float
get() {
// View internally converts distance to dp so divide by density here to have
// consistent usage of pixels with RenderNode that is backing the View
return cameraDistance / resources.displayMetrics.densityDpi
}
set(value) {
// View internally converts distance to dp so multiply by density here to have
// consistent usage of pixels with RenderNode that is backing the View
cameraDistance = value * resources.displayMetrics.densityDpi
}
private var mutatedFields: Int = 0
override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
val maybeChangedFields = scope.mutatedFields or mutatedFields
if (maybeChangedFields and Fields.TransformOrigin != 0) {
this.mTransformOrigin = scope.transformOrigin
this.pivotX = mTransformOrigin.pivotFractionX * width
this.pivotY = mTransformOrigin.pivotFractionY * height
}
if (maybeChangedFields and Fields.ScaleX != 0) {
this.scaleX = scope.scaleX
}
if (maybeChangedFields and Fields.ScaleY != 0) {
this.scaleY = scope.scaleY
}
if (maybeChangedFields and Fields.Alpha != 0) {
this.alpha = scope.alpha
}
if (maybeChangedFields and Fields.TranslationX != 0) {
this.translationX = scope.translationX
}
if (maybeChangedFields and Fields.TranslationY != 0) {
this.translationY = scope.translationY
}
if (maybeChangedFields and Fields.ShadowElevation != 0) {
this.elevation = scope.shadowElevation
}
if (maybeChangedFields and Fields.RotationZ != 0) {
this.rotation = scope.rotationZ
}
if (maybeChangedFields and Fields.RotationX != 0) {
this.rotationX = scope.rotationX
}
if (maybeChangedFields and Fields.RotationY != 0) {
this.rotationY = scope.rotationY
}
if (maybeChangedFields and Fields.CameraDistance != 0) {
this.cameraDistancePx = scope.cameraDistance
}
val wasClippingManually = manualClipPath != null
val clipToOutline = scope.clip && scope.shape !== RectangleShape
if (maybeChangedFields and (Fields.Clip or Fields.Shape) != 0) {
this.clipToBounds = scope.clip && scope.shape === RectangleShape
resetClipBounds()
this.clipToOutline = clipToOutline
}
val shapeChanged = outlineResolver.update(
scope.outline,
scope.alpha,
clipToOutline,
scope.shadowElevation,
scope.size
)
if (outlineResolver.cacheIsDirty) {
updateOutlineResolver()
}
val isClippingManually = manualClipPath != null
if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) {
invalidate() // have to redraw the content
}
if (!drawnWithZ && elevation > 0) {
invalidateParentLayer?.invoke()
}
if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
matrixCache.invalidate()
}
if (Build.VERSION.SDK_INT >= 28) {
if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
ViewLayerVerificationHelper28.setOutlineAmbientShadowColor(
this,
scope.ambientShadowColor.toArgb()
)
}
if (maybeChangedFields and Fields.SpotShadowColor != 0) {
ViewLayerVerificationHelper28.setOutlineSpotShadowColor(
this,
scope.spotShadowColor.toArgb()
)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (maybeChangedFields and Fields.RenderEffect != 0) {
ViewLayerVerificationHelper31.setRenderEffect(this, scope.renderEffect)
}
}
if (maybeChangedFields and Fields.CompositingStrategy != 0) {
mHasOverlappingRendering = when (scope.compositingStrategy) {
CompositingStrategy.Offscreen -> {
setLayerType(LAYER_TYPE_HARDWARE, null)
true
}
CompositingStrategy.ModulateAlpha -> {
setLayerType(LAYER_TYPE_NONE, null)
false
}
else -> { // CompositingStrategy.Auto
setLayerType(LAYER_TYPE_NONE, null)
true
}
}
}
mutatedFields = scope.mutatedFields
}
override fun hasOverlappingRendering(): Boolean {
return mHasOverlappingRendering
}
override fun isInLayer(position: Offset): Boolean {
val x = position.x
val y = position.y
if (clipToBounds) {
return 0f <= x && x < width && 0f <= y && y < height
}
if (clipToOutline) {
return outlineResolver.isInOutline(position)
}
return true
}
private fun updateOutlineResolver() {
this.outlineProvider = if (outlineResolver.androidOutline != null) {
OutlineProvider
} else {
null
}
}
private fun resetClipBounds() {
this.clipBounds = if (clipToBounds) {
if (clipBoundsCache == null) {
clipBoundsCache = android.graphics.Rect(0, 0, width, height)
} else {
clipBoundsCache!!.set(0, 0, width, height)
}
clipBoundsCache
} else {
null
}
}
override fun resize(size: IntSize) {
val width = size.width
val height = size.height
if (width != this.width || height != this.height) {
pivotX = mTransformOrigin.pivotFractionX * width
pivotY = mTransformOrigin.pivotFractionY * height
updateOutlineResolver()
layout(left, top, left + width, top + height)
resetClipBounds()
matrixCache.invalidate()
}
}
override fun move(position: IntOffset) {
val left = position.x
if (left != this.left) {
offsetLeftAndRight(left - this.left)
matrixCache.invalidate()
}
val top = position.y
if (top != this.top) {
offsetTopAndBottom(top - this.top)
matrixCache.invalidate()
}
}
override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
drawnWithZ = elevation > 0f
if (drawnWithZ) {
canvas.enableZ()
}
container.drawChild(canvas, this, drawingTime)
if (drawnWithZ) {
canvas.disableZ()
}
}
override fun dispatchDraw(canvas: android.graphics.Canvas) {
canvasHolder.drawInto(canvas) {
var didClip = false
val clipPath = manualClipPath
if (clipPath != null || !canvas.isHardwareAccelerated) {
didClip = true
save()
outlineResolver.clipToOutline(this)
}
drawBlock?.invoke(this, null)
if (didClip) {
restore()
}
}
isInvalidated = false
}
override fun invalidate() {
if (!isInvalidated) {
isInvalidated = true
super.invalidate()
ownerView.invalidate()
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
}
override fun destroy() {
isInvalidated = false
ownerView.requestClearInvalidObservations()
drawBlock = null
invalidateParentLayer = null
// L throws during RenderThread when reusing the Views. The stack trace
// wasn't easy to decode, so this work-around keeps up to 10 Views active
// only for L. On other versions, it uses the WeakHashMap to retain as many
// as are convenient.
val recycle = ownerView.recycle(this@ViewLayer)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw || !recycle) {
container.removeViewInLayout(this)
} else {
visibility = GONE
}
}
override fun updateDisplayList() {
if (isInvalidated && !shouldUseDispatchDraw) {
updateDisplayList(this)
isInvalidated = false
}
}
override fun forceLayout() {
// Don't do anything. These Views are treated as RenderNodes, so a forced layout
// should not do anything. If we keep this, we get more redrawing than is necessary.
}
override fun mapOffset(point: Offset, inverse: Boolean): Offset {
return if (inverse) {
matrixCache.calculateInverseMatrix(this)?.map(point) ?: Offset.Infinite
} else {
matrixCache.calculateMatrix(this).map(point)
}
}
override fun mapBounds(rect: MutableRect, inverse: Boolean) {
if (inverse) {
val matrix = matrixCache.calculateInverseMatrix(this)
if (matrix != null) {
matrix.map(rect)
} else {
rect.set(0f, 0f, 0f, 0f)
}
} else {
matrixCache.calculateMatrix(this).map(rect)
}
}
override fun reuseLayer(
drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
invalidateParentLayer: () -> Unit
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw) {
container.addView(this)
} else {
visibility = VISIBLE
}
clipToBounds = false
drawnWithZ = false
mTransformOrigin = TransformOrigin.Center
this.drawBlock = drawBlock
this.invalidateParentLayer = invalidateParentLayer
}
override fun transform(matrix: Matrix) {
matrix.timesAssign(matrixCache.calculateMatrix(this))
}
override fun inverseTransform(matrix: Matrix) {
val inverse = matrixCache.calculateInverseMatrix(this)
if (inverse != null) {
matrix.timesAssign(inverse)
}
}
companion object {
private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix ->
val newMatrix = view.matrix
matrix.set(newMatrix)
}
val OutlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: android.graphics.Outline) {
view as ViewLayer
outline.set(view.outlineResolver.androidOutline!!)
}
}
private var updateDisplayListIfDirtyMethod: Method? = null
private var recreateDisplayList: Field? = null
var hasRetrievedMethod = false
private set
var shouldUseDispatchDraw = false
internal set // internal so that tests can use it.
@SuppressLint("BanUncheckedReflection")
fun updateDisplayList(view: View) {
try {
if (!hasRetrievedMethod) {
hasRetrievedMethod = true
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
updateDisplayListIfDirtyMethod =
View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
recreateDisplayList =
View::class.java.getDeclaredField("mRecreateDisplayList")
} else {
val getDeclaredMethod = Class::class.java.getDeclaredMethod(
"getDeclaredMethod",
String::class.java,
arrayOf<Class<*>>()::class.java
)
updateDisplayListIfDirtyMethod = getDeclaredMethod.invoke(
View::class.java,
"updateDisplayListIfDirty", emptyArray<Class<*>>()
) as Method?
val getDeclaredField = Class::class.java.getDeclaredMethod(
"getDeclaredField",
String::class.java
)
recreateDisplayList = getDeclaredField.invoke(
View::class.java,
"mRecreateDisplayList"
) as Field?
}
updateDisplayListIfDirtyMethod?.isAccessible = true
recreateDisplayList?.isAccessible = true
}
recreateDisplayList?.setBoolean(view, true)
updateDisplayListIfDirtyMethod?.invoke(view)
} catch (_: Throwable) {
shouldUseDispatchDraw = true
}
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
private object ViewLayerVerificationHelper31 {
@androidx.annotation.DoNotInline
fun setRenderEffect(view: View, target: RenderEffect?) {
view.setRenderEffect(target?.asAndroidRenderEffect())
}
}
@RequiresApi(Build.VERSION_CODES.P)
private object ViewLayerVerificationHelper28 {
@androidx.annotation.DoNotInline
fun setOutlineAmbientShadowColor(view: View, target: Int) {
view.outlineAmbientShadowColor = target
}
@androidx.annotation.DoNotInline
fun setOutlineSpotShadowColor(view: View, target: Int) {
view.outlineSpotShadowColor = target
}
}