1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.platform
18 
19 import android.os.Build
20 import android.view.View
21 import androidx.annotation.RequiresApi
22 import androidx.compose.ui.geometry.MutableRect
23 import androidx.compose.ui.geometry.Offset
24 import androidx.compose.ui.graphics.Canvas
25 import androidx.compose.ui.graphics.CanvasHolder
26 import androidx.compose.ui.graphics.Fields
27 import androidx.compose.ui.graphics.Matrix
28 import androidx.compose.ui.graphics.Paint
29 import androidx.compose.ui.graphics.RectangleShape
30 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
31 import androidx.compose.ui.graphics.TransformOrigin
32 import androidx.compose.ui.graphics.layer.GraphicsLayer
33 import androidx.compose.ui.graphics.nativeCanvas
34 import androidx.compose.ui.graphics.toArgb
35 import androidx.compose.ui.layout.GraphicLayerInfo
36 import androidx.compose.ui.node.OwnedLayer
37 import androidx.compose.ui.unit.IntOffset
38 import androidx.compose.ui.unit.IntSize
39 
40 /** RenderNode implementation of OwnedLayer. */
41 @RequiresApi(Build.VERSION_CODES.M)
42 internal class RenderNodeLayer(
43     val ownerView: AndroidComposeView,
44     drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
45     invalidateParentLayer: () -> Unit
46 ) : OwnedLayer, GraphicLayerInfo {
47     private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
48     private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
49 
50     /** True when the RenderNodeLayer has been invalidated and not yet drawn. */
51     private var isDirty = false
52         set(value) {
53             if (value != field) {
54                 field = value
55                 ownerView.notifyLayerIsDirty(this, value)
56             }
57         }
58 
59     private val outlineResolver = OutlineResolver()
60     private var isDestroyed = false
61     private var drawnWithZ = false
62 
63     /**
64      * Optional paint used when the RenderNode is rendered on a software backed canvas and is
65      * somewhat transparent (i.e. alpha less than 1.0f)
66      */
67     private var softwareLayerPaint: Paint? = null
68 
69     private val matrixCache = LayerMatrixCache(getMatrix)
70 
71     private val canvasHolder = CanvasHolder()
72 
73     /**
74      * Local copy of the transform origin as GraphicsLayerModifier can be implemented as a model
75      * object. Update this field within [updateLayerProperties] and use it in [resize] or other
76      * methods
77      */
78     private var transformOrigin: TransformOrigin = TransformOrigin.Center
79 
80     private val renderNode =
81         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
82                 RenderNodeApi29(ownerView)
83             } else {
84                 RenderNodeApi23(ownerView)
85             }
86             .apply {
87                 setHasOverlappingRendering(true)
88                 // in compose the default is to not clip.
89                 clipToBounds = false
90             }
91 
92     override val layerId: Long
93         get() = renderNode.uniqueId
94 
95     override val ownerViewId: Long
96         get() =
97             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
98                 UniqueDrawingIdApi29.getUniqueDrawingId(ownerView)
99             } else {
100                 -1
101             }
102 
103     @RequiresApi(29)
104     private object UniqueDrawingIdApi29 {
105         @JvmStatic fun getUniqueDrawingId(view: View) = view.uniqueDrawingId
106     }
107 
108     private var mutatedFields: Int = 0
109 
110     override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
111         val maybeChangedFields = scope.mutatedFields or mutatedFields
112         if (maybeChangedFields and Fields.TransformOrigin != 0) {
113             this.transformOrigin = scope.transformOrigin
114         }
115         val wasClippingManually = renderNode.clipToOutline && !outlineResolver.outlineClipSupported
116         if (maybeChangedFields and Fields.ScaleX != 0) {
117             renderNode.scaleX = scope.scaleX
118         }
119         if (maybeChangedFields and Fields.ScaleY != 0) {
120             renderNode.scaleY = scope.scaleY
121         }
122         if (maybeChangedFields and Fields.Alpha != 0) {
123             renderNode.alpha = scope.alpha
124         }
125         if (maybeChangedFields and Fields.TranslationX != 0) {
126             renderNode.translationX = scope.translationX
127         }
128         if (maybeChangedFields and Fields.TranslationY != 0) {
129             renderNode.translationY = scope.translationY
130         }
131         if (maybeChangedFields and Fields.ShadowElevation != 0) {
132             renderNode.elevation = scope.shadowElevation
133         }
134         if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
135             renderNode.ambientShadowColor = scope.ambientShadowColor.toArgb()
136         }
137         if (maybeChangedFields and Fields.SpotShadowColor != 0) {
138             renderNode.spotShadowColor = scope.spotShadowColor.toArgb()
139         }
140         if (maybeChangedFields and Fields.RotationZ != 0) {
141             renderNode.rotationZ = scope.rotationZ
142         }
143         if (maybeChangedFields and Fields.RotationX != 0) {
144             renderNode.rotationX = scope.rotationX
145         }
146         if (maybeChangedFields and Fields.RotationY != 0) {
147             renderNode.rotationY = scope.rotationY
148         }
149         if (maybeChangedFields and Fields.CameraDistance != 0) {
150             renderNode.cameraDistance = scope.cameraDistance
151         }
152         if (maybeChangedFields and Fields.TransformOrigin != 0) {
153             renderNode.pivotX = transformOrigin.pivotFractionX * renderNode.width
154             renderNode.pivotY = transformOrigin.pivotFractionY * renderNode.height
155         }
156         val clipToOutline = scope.clip && scope.shape !== RectangleShape
157         if (maybeChangedFields and (Fields.Clip or Fields.Shape) != 0) {
158             renderNode.clipToOutline = clipToOutline
159             renderNode.clipToBounds = scope.clip && scope.shape === RectangleShape
160         }
161         if (maybeChangedFields and Fields.RenderEffect != 0) {
162             renderNode.renderEffect = scope.renderEffect
163         }
164         if (maybeChangedFields and Fields.ColorFilter != 0) {
165             renderNode.colorFilter = scope.colorFilter
166         }
167         if (maybeChangedFields and Fields.BlendMode != 0) {
168             renderNode.blendMode = scope.blendMode
169         }
170         if (maybeChangedFields and Fields.CompositingStrategy != 0) {
171             renderNode.compositingStrategy = scope.compositingStrategy
172         }
173         val shapeChanged =
174             outlineResolver.update(
175                 scope.outline,
176                 scope.alpha,
177                 clipToOutline,
178                 scope.shadowElevation,
179                 scope.size,
180             )
181         if (outlineResolver.cacheIsDirty) {
182             renderNode.setOutline(outlineResolver.androidOutline)
183         }
184         val isClippingManually = clipToOutline && !outlineResolver.outlineClipSupported
185         if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) {
186             invalidate()
187         } else {
188             triggerRepaint()
189         }
190         if (!drawnWithZ && renderNode.elevation > 0f) {
191             invalidateParentLayer?.invoke()
192         }
193 
194         if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
195             matrixCache.invalidate()
196         }
197 
198         mutatedFields = scope.mutatedFields
199     }
200 
201     override fun isInLayer(position: Offset): Boolean {
202         val x = position.x
203         val y = position.y
204         if (renderNode.clipToBounds) {
205             return 0f <= x && x < renderNode.width && 0f <= y && y < renderNode.height
206         }
207 
208         if (renderNode.clipToOutline) {
209             return outlineResolver.isInOutline(position)
210         }
211 
212         return true
213     }
214 
215     override fun resize(size: IntSize) {
216         val width = size.width
217         val height = size.height
218         renderNode.pivotX = transformOrigin.pivotFractionX * width
219         renderNode.pivotY = transformOrigin.pivotFractionY * height
220         if (
221             renderNode.setPosition(
222                 renderNode.left,
223                 renderNode.top,
224                 renderNode.left + width,
225                 renderNode.top + height
226             )
227         ) {
228             renderNode.setOutline(outlineResolver.androidOutline)
229             invalidate()
230             matrixCache.invalidate()
231         }
232     }
233 
234     override fun move(position: IntOffset) {
235         val oldLeft = renderNode.left
236         val oldTop = renderNode.top
237         val newLeft = position.x
238         val newTop = position.y
239         if (oldLeft != newLeft || oldTop != newTop) {
240             if (oldLeft != newLeft) {
241                 renderNode.offsetLeftAndRight(newLeft - oldLeft)
242             }
243             if (oldTop != newTop) {
244                 renderNode.offsetTopAndBottom(newTop - oldTop)
245             }
246             triggerRepaint()
247             matrixCache.invalidate()
248         }
249     }
250 
251     override fun invalidate() {
252         if (!isDirty && !isDestroyed) {
253             ownerView.invalidate()
254             isDirty = true
255         }
256     }
257 
258     /**
259      * This only triggers the system so that it knows that some kind of painting must happen without
260      * actually causing the layer to be invalidated and have to re-record its drawing.
261      */
262     private fun triggerRepaint() {
263         // onDescendantInvalidated is only supported on O+
264         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
265             WrapperRenderNodeLayerHelperMethods.onDescendantInvalidated(ownerView)
266         } else {
267             ownerView.invalidate()
268         }
269     }
270 
271     override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
272         val androidCanvas = canvas.nativeCanvas
273         if (androidCanvas.isHardwareAccelerated) {
274             updateDisplayList()
275             drawnWithZ = renderNode.elevation > 0f
276             if (drawnWithZ) {
277                 canvas.enableZ()
278             }
279             renderNode.drawInto(androidCanvas)
280             if (drawnWithZ) {
281                 canvas.disableZ()
282             }
283         } else {
284             val left = renderNode.left.toFloat()
285             val top = renderNode.top.toFloat()
286             val right = renderNode.right.toFloat()
287             val bottom = renderNode.bottom.toFloat()
288             // If there is alpha applied, we must render into an offscreen buffer to
289             // properly blend the contents of this layer against the background content
290             if (renderNode.alpha < 1.0f) {
291                 val paint =
292                     (softwareLayerPaint ?: Paint().also { softwareLayerPaint = it }).apply {
293                         alpha = renderNode.alpha
294                     }
295                 androidCanvas.saveLayer(left, top, right, bottom, paint.asFrameworkPaint())
296             } else {
297                 canvas.save()
298             }
299             // If we are software rendered we must translate the canvas based on the offset provided
300             // in the move call which operates directly on the RenderNode
301             canvas.translate(left, top)
302             canvas.concat(matrixCache.calculateMatrix(renderNode))
303             clipRenderNode(canvas)
304             drawBlock?.invoke(canvas, null)
305             canvas.restore()
306             isDirty = false
307         }
308     }
309 
310     /**
311      * Manually clips the content of the RenderNodeLayer in the provided canvas. This is used only
312      * in software rendered use cases
313      */
314     private fun clipRenderNode(canvas: Canvas) {
315         if (renderNode.clipToOutline || renderNode.clipToBounds) {
316             outlineResolver.clipToOutline(canvas)
317         }
318     }
319 
320     override fun updateDisplayList() {
321         if (isDirty || !renderNode.hasDisplayList) {
322             val clipPath =
323                 if (renderNode.clipToOutline && !outlineResolver.outlineClipSupported) {
324                     outlineResolver.clipPath
325                 } else {
326                     null
327                 }
328             drawBlock?.let { drawBlock ->
329                 renderNode.record(canvasHolder, clipPath) { drawBlock(it, null) }
330             }
331             isDirty = false
332         }
333     }
334 
335     override fun destroy() {
336         if (renderNode.hasDisplayList) {
337             renderNode.discardDisplayList()
338         }
339         drawBlock = null
340         invalidateParentLayer = null
341         isDestroyed = true
342         isDirty = false
343         ownerView.requestClearInvalidObservations()
344         ownerView.recycle(this)
345     }
346 
347     override val underlyingMatrix: Matrix
348         get() = matrixCache.calculateMatrix(renderNode)
349 
350     override var frameRate: Float = 0f
351 
352     override var isFrameRateFromParent = false
353 
354     override fun mapOffset(point: Offset, inverse: Boolean): Offset {
355         return if (inverse) {
356             matrixCache.mapInverse(renderNode, point)
357         } else {
358             matrixCache.map(renderNode, point)
359         }
360     }
361 
362     override fun mapBounds(rect: MutableRect, inverse: Boolean) {
363         if (inverse) {
364             matrixCache.mapInverse(renderNode, rect)
365         } else {
366             matrixCache.map(renderNode, rect)
367         }
368     }
369 
370     override fun reuseLayer(
371         drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
372         invalidateParentLayer: () -> Unit
373     ) {
374         matrixCache.reset()
375         isDirty = false
376         isDestroyed = false
377         drawnWithZ = false
378         transformOrigin = TransformOrigin.Center
379         this.drawBlock = drawBlock
380         this.invalidateParentLayer = invalidateParentLayer
381     }
382 
383     override fun transform(matrix: Matrix) {
384         matrix.timesAssign(matrixCache.calculateMatrix(renderNode))
385     }
386 
387     override fun inverseTransform(matrix: Matrix) {
388         val inverse = matrixCache.calculateInverseMatrix(renderNode)
389         if (inverse != null) {
390             matrix.timesAssign(inverse)
391         }
392     }
393 
394     companion object {
395         private val getMatrix: (DeviceRenderNode, android.graphics.Matrix) -> Unit = { rn, matrix ->
396             rn.getMatrix(matrix)
397         }
398     }
399 }
400 
401 /**
402  * This class is here to ensure that the classes that use this API will get verified and can be AOT
403  * compiled. It is expected that this class will soft-fail verification, but the classes which use
404  * this method will pass.
405  */
406 @RequiresApi(Build.VERSION_CODES.O)
407 internal object WrapperRenderNodeLayerHelperMethods {
onDescendantInvalidatednull408     fun onDescendantInvalidated(ownerView: AndroidComposeView) {
409         ownerView.parent?.onDescendantInvalidated(ownerView, ownerView)
410     }
411 }
412