1 /*
<lambda>null2  * Copyright 2024 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 androidx.compose.ui.geometry.MutableRect
21 import androidx.compose.ui.geometry.Offset
22 import androidx.compose.ui.geometry.center
23 import androidx.compose.ui.geometry.isUnspecified
24 import androidx.compose.ui.graphics.Canvas
25 import androidx.compose.ui.graphics.CompositingStrategy as OldCompositingStrategy
26 import androidx.compose.ui.graphics.Fields
27 import androidx.compose.ui.graphics.GraphicsContext
28 import androidx.compose.ui.graphics.Matrix
29 import androidx.compose.ui.graphics.Outline
30 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
31 import androidx.compose.ui.graphics.TransformOrigin
32 import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
33 import androidx.compose.ui.graphics.drawscope.DrawScope
34 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
35 import androidx.compose.ui.graphics.isIdentity
36 import androidx.compose.ui.graphics.layer.CompositingStrategy
37 import androidx.compose.ui.graphics.layer.GraphicsLayer
38 import androidx.compose.ui.graphics.layer.drawLayer
39 import androidx.compose.ui.graphics.layer.setOutline
40 import androidx.compose.ui.internal.checkPreconditionNotNull
41 import androidx.compose.ui.internal.requirePrecondition
42 import androidx.compose.ui.layout.GraphicLayerInfo
43 import androidx.compose.ui.node.OwnedLayer
44 import androidx.compose.ui.ui.FrameRateCategory
45 import androidx.compose.ui.unit.Density
46 import androidx.compose.ui.unit.IntOffset
47 import androidx.compose.ui.unit.IntSize
48 import androidx.compose.ui.unit.LayoutDirection
49 import androidx.compose.ui.unit.toSize
50 
51 internal class GraphicsLayerOwnerLayer(
52     private var graphicsLayer: GraphicsLayer,
53     // when we have a context it means the object is created by us and we need to release it
54     private val context: GraphicsContext?,
55     private val ownerView: AndroidComposeView,
56     drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
57     invalidateParentLayer: () -> Unit
58 ) : OwnedLayer, GraphicLayerInfo {
59     private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
60     private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
61 
62     private var size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
63     private var isDestroyed = false
64     private val matrixCache = Matrix()
65     private var inverseMatrixCache: Matrix? = null
66 
67     private var isDirty = false
68         set(value) {
69             if (value != field) {
70                 field = value
71                 ownerView.notifyLayerIsDirty(this, value)
72             }
73         }
74 
75     private var density = Density(1f)
76     private var layoutDirection = LayoutDirection.Ltr
77     private val scope = CanvasDrawScope()
78     private var mutatedFields: Int = 0
79     private var transformOrigin: TransformOrigin = TransformOrigin.Center
80     private var outline: Outline? = null
81     private var isMatrixDirty = false
82     private var isInverseMatrixDirty = false
83     private var isIdentity = true
84     override var frameRate: Float = 0f
85     override var isFrameRateFromParent = false
86 
87     override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
88         val maybeChangedFields = scope.mutatedFields or mutatedFields
89         this.layoutDirection = scope.layoutDirection
90         this.density = scope.graphicsDensity
91         if (maybeChangedFields and Fields.TransformOrigin != 0) {
92             this.transformOrigin = scope.transformOrigin
93         }
94         if (maybeChangedFields and Fields.ScaleX != 0) {
95             graphicsLayer.scaleX = scope.scaleX
96         }
97         if (maybeChangedFields and Fields.ScaleY != 0) {
98             graphicsLayer.scaleY = scope.scaleY
99         }
100         if (maybeChangedFields and Fields.Alpha != 0) {
101             graphicsLayer.alpha = scope.alpha
102         }
103         if (maybeChangedFields and Fields.TranslationX != 0) {
104             graphicsLayer.translationX = scope.translationX
105         }
106         if (maybeChangedFields and Fields.TranslationY != 0) {
107             graphicsLayer.translationY = scope.translationY
108         }
109         if (maybeChangedFields and Fields.ShadowElevation != 0) {
110             graphicsLayer.shadowElevation = scope.shadowElevation
111             // TODO We should somehow move it into the android specific GraphicsLayer
112             //  implementation where the code enabling Z is located. It is not yet clear how we
113             //  can trigger the full layer invalidation from there when such changes happen, but
114             //  seems like we have to figure it out. This issue is tracked in b/333862760
115             if (scope.shadowElevation > 0f && !drawnWithEnabledZ) {
116                 // we need to redraw with enabling Z
117                 invalidateParentLayer?.invoke()
118             }
119         }
120         if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
121             graphicsLayer.ambientShadowColor = scope.ambientShadowColor
122         }
123         if (maybeChangedFields and Fields.SpotShadowColor != 0) {
124             graphicsLayer.spotShadowColor = scope.spotShadowColor
125         }
126         if (maybeChangedFields and Fields.RotationZ != 0) {
127             graphicsLayer.rotationZ = scope.rotationZ
128         }
129         if (maybeChangedFields and Fields.RotationX != 0) {
130             graphicsLayer.rotationX = scope.rotationX
131         }
132         if (maybeChangedFields and Fields.RotationY != 0) {
133             graphicsLayer.rotationY = scope.rotationY
134         }
135         if (maybeChangedFields and Fields.CameraDistance != 0) {
136             graphicsLayer.cameraDistance = scope.cameraDistance
137         }
138         if (maybeChangedFields and Fields.TransformOrigin != 0) {
139             if (transformOrigin == TransformOrigin.Center) {
140                 graphicsLayer.pivotOffset = Offset.Unspecified
141             } else {
142                 graphicsLayer.pivotOffset =
143                     Offset(
144                         transformOrigin.pivotFractionX * size.width,
145                         transformOrigin.pivotFractionY * size.height
146                     )
147             }
148         }
149         if (maybeChangedFields and Fields.Clip != 0) {
150             graphicsLayer.clip = scope.clip
151         }
152         if (maybeChangedFields and Fields.RenderEffect != 0) {
153             graphicsLayer.renderEffect = scope.renderEffect
154         }
155         if (maybeChangedFields and Fields.ColorFilter != 0) {
156             graphicsLayer.colorFilter = scope.colorFilter
157         }
158         if (maybeChangedFields and Fields.BlendMode != 0) {
159             graphicsLayer.blendMode = scope.blendMode
160         }
161         if (maybeChangedFields and Fields.CompositingStrategy != 0) {
162             graphicsLayer.compositingStrategy =
163                 when (scope.compositingStrategy) {
164                     OldCompositingStrategy.Auto -> CompositingStrategy.Auto
165                     OldCompositingStrategy.Offscreen -> CompositingStrategy.Offscreen
166                     OldCompositingStrategy.ModulateAlpha -> CompositingStrategy.ModulateAlpha
167                     else -> throw IllegalStateException("Not supported composition strategy")
168                 }
169         }
170         if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
171             isMatrixDirty = true
172             isInverseMatrixDirty = true
173         }
174 
175         var outlineChanged = false
176 
177         if (outline != scope.outline) {
178             outlineChanged = true
179             outline = scope.outline
180             updateOutline()
181         }
182 
183         mutatedFields = scope.mutatedFields
184         if (maybeChangedFields != 0 || outlineChanged) {
185             triggerRepaint()
186             if (ownerView.isArrEnabled) {
187                 ownerView.requestedFrameRate = frameRate
188             }
189         }
190     }
191 
192     // TODO: GraphicsLayer api should be doing it on its own every time any layer property
193     //  has been updated. This issue is tracked in b/329417380.
194     private fun triggerRepaint() {
195         // onDescendantInvalidated is only supported on O+
196         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
197             WrapperRenderNodeLayerHelperMethods.onDescendantInvalidated(ownerView)
198         } else {
199             ownerView.invalidate()
200         }
201     }
202 
203     private fun updateOutline() {
204         val outline = outline ?: return
205         graphicsLayer.setOutline(outline)
206         if (outline is Outline.Generic && Build.VERSION.SDK_INT < 33) {
207             // before 33 many of the paths are not clipping by rendernode. instead we have to
208             // manually clip on a canvas. it means we have redraw the parent layer when it changes
209             // TODO We should somehow move it into the android specific GraphicsLayer
210             //  implementation where the clipping logic is located.
211             //  This issue is tracked in b/333862760
212             invalidateParentLayer?.invoke()
213         }
214     }
215 
216     override fun isInLayer(position: Offset): Boolean {
217         val x = position.x
218         val y = position.y
219 
220         if (graphicsLayer.clip) {
221             return isInOutline(graphicsLayer.outline, x, y)
222         }
223 
224         return true
225     }
226 
227     override fun move(position: IntOffset) {
228         if (ownerView.isArrEnabled) {
229             ownerView.requestedFrameRate = FrameRateCategory.High.value
230         }
231         graphicsLayer.topLeft = position
232         triggerRepaint()
233     }
234 
235     override fun resize(size: IntSize) {
236         if (size != this.size) {
237             if (ownerView.isArrEnabled) {
238                 ownerView.requestedFrameRate = FrameRateCategory.High.value
239             }
240             this.size = size
241             invalidate()
242         }
243     }
244 
245     private var drawnWithEnabledZ = false
246 
247     override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
248         updateDisplayList()
249         drawnWithEnabledZ = graphicsLayer.shadowElevation > 0
250         scope.drawContext.also {
251             it.canvas = canvas
252             it.graphicsLayer = parentLayer
253         }
254         scope.drawLayer(graphicsLayer)
255     }
256 
257     override fun updateDisplayList() {
258         if (ownerView.isArrEnabled && frameRate != 0f) {
259             ownerView.requestedFrameRate = frameRate
260         }
261         if (isDirty) {
262             if (transformOrigin != TransformOrigin.Center && graphicsLayer.size != size) {
263                 graphicsLayer.pivotOffset =
264                     Offset(
265                         transformOrigin.pivotFractionX * size.width,
266                         transformOrigin.pivotFractionY * size.height
267                     )
268             }
269             graphicsLayer.record(density, layoutDirection, size, recordLambda)
270             isDirty = false
271         }
272     }
273 
274     private val recordLambda: DrawScope.() -> Unit = {
275         drawIntoCanvas { canvas ->
276             this@GraphicsLayerOwnerLayer.drawBlock?.let { it(canvas, drawContext.graphicsLayer) }
277         }
278     }
279 
280     override fun invalidate() {
281         if (!isDirty && !isDestroyed) {
282             ownerView.invalidate()
283             isDirty = true
284         }
285     }
286 
287     override fun destroy() {
288         frameRate = 0f
289         isFrameRateFromParent = false
290         drawBlock = null
291         invalidateParentLayer = null
292         isDestroyed = true
293         isDirty = false
294         if (context != null) {
295             context.releaseGraphicsLayer(graphicsLayer)
296             ownerView.recycle(this)
297         }
298     }
299 
300     override fun mapOffset(point: Offset, inverse: Boolean): Offset {
301         val matrix =
302             if (inverse) {
303                 getInverseMatrix() ?: return Offset.Infinite
304             } else {
305                 getMatrix()
306             }
307         return if (isIdentity) {
308             point
309         } else {
310             matrix.map(point)
311         }
312     }
313 
314     override fun mapBounds(rect: MutableRect, inverse: Boolean) {
315         val matrix = if (inverse) getInverseMatrix() else getMatrix()
316         if (!isIdentity) {
317             if (matrix == null) {
318                 rect.set(0f, 0f, 0f, 0f)
319             } else {
320                 matrix.map(rect)
321             }
322         }
323     }
324 
325     override fun reuseLayer(
326         drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
327         invalidateParentLayer: () -> Unit
328     ) {
329         val context =
330             checkPreconditionNotNull(context) {
331                 "currently reuse is only supported when we manage the layer lifecycle"
332             }
333         requirePrecondition(graphicsLayer.isReleased) {
334             "layer should have been released before reuse"
335         }
336 
337         // recreate a layer
338         graphicsLayer = context.createGraphicsLayer()
339         isDestroyed = false
340 
341         // apply new params
342         this.drawBlock = drawBlock
343         this.invalidateParentLayer = invalidateParentLayer
344 
345         // reset mutable variables to their initial values
346         isMatrixDirty = false
347         isInverseMatrixDirty = false
348         isIdentity = true
349         matrixCache.reset()
350         inverseMatrixCache?.reset()
351         transformOrigin = TransformOrigin.Center
352         drawnWithEnabledZ = false
353         size = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
354         outline = null
355         mutatedFields = 0
356     }
357 
358     override fun transform(matrix: Matrix) {
359         matrix.timesAssign(getMatrix())
360     }
361 
362     override fun inverseTransform(matrix: Matrix) {
363         val inverse = getInverseMatrix()
364         if (inverse != null) {
365             matrix.timesAssign(inverse)
366         }
367     }
368 
369     override val layerId: Long
370         get() = graphicsLayer.layerId
371 
372     override val ownerViewId: Long
373         get() = graphicsLayer.ownerViewId
374 
375     private fun getMatrix(): Matrix {
376         updateMatrix()
377         return matrixCache
378     }
379 
380     override val underlyingMatrix: Matrix
381         get() = getMatrix()
382 
383     private fun getInverseMatrix(): Matrix? {
384         val inverseMatrix = inverseMatrixCache ?: Matrix().also { inverseMatrixCache = it }
385         if (!isInverseMatrixDirty) {
386             if (inverseMatrix[0, 0].isNaN()) {
387                 return null
388             }
389             return inverseMatrix
390         }
391         isInverseMatrixDirty = false
392         val matrix = getMatrix()
393         return if (isIdentity) {
394             matrix
395         } else if (matrix.invertTo(inverseMatrix)) {
396             inverseMatrix
397         } else {
398             inverseMatrix[0, 0] = Float.NaN
399             null
400         }
401     }
402 
403     private fun updateMatrix() {
404         if (isMatrixDirty) {
405             with(graphicsLayer) {
406                 val (x, y) =
407                     if (pivotOffset.isUnspecified) {
408                         this@GraphicsLayerOwnerLayer.size.toSize().center
409                     } else {
410                         pivotOffset
411                     }
412 
413                 matrixCache.resetToPivotedTransform(
414                     x,
415                     y,
416                     translationX,
417                     translationY,
418                     1.0f,
419                     rotationX,
420                     rotationY,
421                     rotationZ,
422                     scaleX,
423                     scaleY,
424                     1.0f
425                 )
426             }
427             isMatrixDirty = false
428             isIdentity = matrixCache.isIdentity()
429         }
430     }
431 }
432