1 /*
2  * 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.graphics.Color
20 import android.graphics.Outline
21 import android.os.Build
22 import android.view.DisplayListCanvas
23 import android.view.RenderNode
24 import android.view.View
25 import androidx.annotation.RequiresApi
26 import androidx.compose.ui.graphics.BlendMode
27 import androidx.compose.ui.graphics.Canvas
28 import androidx.compose.ui.graphics.CanvasHolder
29 import androidx.compose.ui.graphics.ColorFilter
30 import androidx.compose.ui.graphics.CompositingStrategy
31 import androidx.compose.ui.graphics.Paint
32 import androidx.compose.ui.graphics.Path
33 import androidx.compose.ui.graphics.RenderEffect
34 
35 /**
36  * RenderNode on M-O devices, where RenderNode isn't officially supported. This class uses a hidden
37  * android.view.RenderNode API that we have stubs for in the ui-android-stubs module. This
38  * implementation has higher performance than the View implementation by both avoiding reflection
39  * and using the lower overhead RenderNode instead of Views.
40  */
41 @RequiresApi(Build.VERSION_CODES.M)
42 internal class RenderNodeApi23(val ownerView: AndroidComposeView) : DeviceRenderNode {
43     private val renderNode = RenderNode.create("Compose", ownerView)
44 
45     private var internalCompositingStrategy = CompositingStrategy.Auto
46 
47     private var layerPaint: Paint? = null
48 
49     init {
50         if (needToValidateAccess) {
51             // This is only to force loading the DisplayListCanvas class and causing the
52             // MRenderNode to fail with a NoClassDefFoundError during construction instead of
53             // later.
54             @Suppress("UNUSED_VARIABLE", "unused") val displayListCanvas: DisplayListCanvas? = null
55 
56             // Ensure that we can access properties of the RenderNode. We want to force an
57             // exception here if there is a problem accessing any of these so that we can
58             // fall back to the View implementation.
59             renderNode.scaleX = renderNode.scaleX
60             renderNode.scaleY = renderNode.scaleY
61             renderNode.translationX = renderNode.translationX
62             renderNode.translationY = renderNode.translationY
63             renderNode.elevation = renderNode.elevation
64             renderNode.rotation = renderNode.rotation
65             renderNode.rotationX = renderNode.rotationX
66             renderNode.rotationY = renderNode.rotationY
67             renderNode.cameraDistance = renderNode.cameraDistance
68             renderNode.pivotX = renderNode.pivotX
69             renderNode.pivotY = renderNode.pivotY
70             renderNode.clipToOutline = renderNode.clipToOutline
71             renderNode.setClipToBounds(false)
72             renderNode.alpha = renderNode.alpha
73             renderNode.isValid // only read
74             renderNode.setLeftTopRightBottom(0, 0, 0, 0)
75             renderNode.offsetLeftAndRight(0)
76             renderNode.offsetTopAndBottom(0)
77             verifyShadowColorProperties(renderNode)
78             discardDisplayListInternal()
79             renderNode.setLayerType(View.LAYER_TYPE_NONE)
80             renderNode.setHasOverlappingRendering(renderNode.hasOverlappingRendering())
81             needToValidateAccess = false // only need to do this once
82         }
83         if (testFailCreateRenderNode) {
84             throw NoClassDefFoundError()
85         }
86     }
87 
88     override val uniqueId: Long
89         get() = 0
90 
91     override var left: Int = 0
92     override var top: Int = 0
93     override var right: Int = 0
94     override var bottom: Int = 0
95     override val width: Int
96         get() = right - left
97 
98     override val height: Int
99         get() = bottom - top
100 
101     // API level 23 does not support RenderEffect so keep the field around for consistency
102     // however, it will not be applied to the rendered result. Consumers are encouraged
103     // to use the RenderEffect.isSupported API before consuming a [RenderEffect] instance.
104     // If RenderEffect is used on an unsupported API level, it should act as a no-op and not
105     // crash the compose application
106     override var renderEffect: RenderEffect? = null
107 
108     override var scaleX: Float
109         get() = renderNode.scaleX
110         set(value) {
111             renderNode.scaleX = value
112         }
113 
114     override var scaleY: Float
115         get() = renderNode.scaleY
116         set(value) {
117             renderNode.scaleY = value
118         }
119 
120     override var translationX: Float
121         get() = renderNode.translationX
122         set(value) {
123             renderNode.translationX = value
124         }
125 
126     override var translationY: Float
127         get() = renderNode.translationY
128         set(value) {
129             renderNode.translationY = value
130         }
131 
132     override var elevation: Float
133         get() = renderNode.elevation
134         set(value) {
135             renderNode.elevation = value
136         }
137 
138     override var ambientShadowColor: Int
139         get() {
140             return if (Build.VERSION.SDK_INT >= 28) {
141                 RenderNodeVerificationHelper28.getAmbientShadowColor(renderNode)
142             } else {
143                 Color.BLACK
144             }
145         }
146         set(value) {
147             if (Build.VERSION.SDK_INT >= 28) {
148                 RenderNodeVerificationHelper28.setAmbientShadowColor(renderNode, value)
149             }
150         }
151 
152     override var spotShadowColor: Int
153         get() {
154             return if (Build.VERSION.SDK_INT >= 28) {
155                 RenderNodeVerificationHelper28.getSpotShadowColor(renderNode)
156             } else {
157                 Color.BLACK
158             }
159         }
160         set(value) {
161             if (Build.VERSION.SDK_INT >= 28) {
162                 RenderNodeVerificationHelper28.setSpotShadowColor(renderNode, value)
163             }
164         }
165 
166     override var rotationZ: Float
167         get() = renderNode.rotation
168         set(value) {
169             renderNode.rotation = value
170         }
171 
172     override var rotationX: Float
173         get() = renderNode.rotationX
174         set(value) {
175             renderNode.rotationX = value
176         }
177 
178     override var rotationY: Float
179         get() = renderNode.rotationY
180         set(value) {
181             renderNode.rotationY = value
182         }
183 
184     override var cameraDistance: Float
185         // Camera distance was negated in older API levels. Maintain the same input parameters
186         // and negate the given camera distance before it is applied and also negate it when
187         // it is queried
188         get() = -renderNode.cameraDistance
189         set(value) {
190             renderNode.cameraDistance = -value
191         }
192 
193     override var pivotX: Float
194         get() = renderNode.pivotX
195         set(value) {
196             renderNode.pivotX = value
197         }
198 
199     override var pivotY: Float
200         get() = renderNode.pivotY
201         set(value) {
202             renderNode.pivotY = value
203         }
204 
205     override var clipToOutline: Boolean
206         get() = renderNode.clipToOutline
207         set(value) {
208             renderNode.clipToOutline = value
209         }
210 
211     override var clipToBounds: Boolean = false
212         set(value) {
213             field = value
214             renderNode.setClipToBounds(value)
215         }
216 
217     override var alpha: Float
218         get() = renderNode.alpha
219         set(value) {
220             renderNode.alpha = value
221         }
222 
223     override var blendMode: BlendMode = BlendMode.SrcOver
224         set(value) {
225             field = value
<lambda>null226             obtainLayerPaint().apply { blendMode = value }
227             updateLayerProperties()
228         }
229 
230     override var colorFilter: ColorFilter? = null
231         set(value) {
232             field = value
<lambda>null233             obtainLayerPaint().apply { colorFilter = value }
234             updateLayerProperties()
235         }
236 
237     override var compositingStrategy: CompositingStrategy
238         get() = internalCompositingStrategy
239         set(value) {
240             internalCompositingStrategy = value
241             updateLayerProperties()
242         }
243 
updateLayerPropertiesnull244     private fun updateLayerProperties() {
245         if (requiresCompositingLayer()) {
246             renderNode.applyCompositingStrategy(CompositingStrategy.Offscreen)
247         } else {
248             renderNode.applyCompositingStrategy(internalCompositingStrategy)
249         }
250     }
251 
requiresCompositingLayernull252     private fun requiresCompositingLayer(): Boolean =
253         compositingStrategy == CompositingStrategy.Offscreen || requiresLayerPaint()
254 
255     private fun requiresLayerPaint(): Boolean =
256         blendMode != BlendMode.SrcOver || colorFilter != null
257 
258     private fun obtainLayerPaint(): Paint = layerPaint ?: Paint().also { layerPaint = it }
259 
RenderNodenull260     private fun RenderNode.applyCompositingStrategy(compositingStrategy: CompositingStrategy) {
261         when (compositingStrategy) {
262             CompositingStrategy.Offscreen -> {
263                 setLayerType(View.LAYER_TYPE_HARDWARE)
264                 setLayerPaint(layerPaint?.asFrameworkPaint())
265                 setHasOverlappingRendering(true)
266             }
267             CompositingStrategy.ModulateAlpha -> {
268                 setLayerType(View.LAYER_TYPE_NONE)
269                 setHasOverlappingRendering(false)
270             }
271             else -> {
272                 setLayerType(View.LAYER_TYPE_NONE)
273                 setHasOverlappingRendering(true)
274             }
275         }
276     }
277 
getLayerTypenull278     internal fun getLayerType(): Int =
279         when (internalCompositingStrategy) {
280             CompositingStrategy.Offscreen -> View.LAYER_TYPE_HARDWARE
281             else -> View.LAYER_TYPE_NONE
282         }
283 
hasOverlappingRenderingnull284     internal fun hasOverlappingRendering(): Boolean = renderNode.hasOverlappingRendering()
285 
286     override val hasDisplayList: Boolean
287         get() = renderNode.isValid
288 
289     override fun setOutline(outline: Outline?) {
290         renderNode.setOutline(outline)
291     }
292 
setPositionnull293     override fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean {
294         this.left = left
295         this.top = top
296         this.right = right
297         this.bottom = bottom
298         return renderNode.setLeftTopRightBottom(left, top, right, bottom)
299     }
300 
offsetLeftAndRightnull301     override fun offsetLeftAndRight(offset: Int) {
302         left += offset
303         right += offset
304         renderNode.offsetLeftAndRight(offset)
305     }
306 
offsetTopAndBottomnull307     override fun offsetTopAndBottom(offset: Int) {
308         top += offset
309         bottom += offset
310         renderNode.offsetTopAndBottom(offset)
311     }
312 
recordnull313     override fun record(canvasHolder: CanvasHolder, clipPath: Path?, drawBlock: (Canvas) -> Unit) {
314         val canvas = renderNode.start(width, height)
315         canvasHolder.drawInto(canvas) {
316             if (clipPath != null) {
317                 save()
318                 clipPath(clipPath)
319             }
320             drawBlock(this)
321             if (clipPath != null) {
322                 restore()
323             }
324         }
325         renderNode.end(canvas)
326     }
327 
getMatrixnull328     override fun getMatrix(matrix: android.graphics.Matrix) {
329         return renderNode.getMatrix(matrix)
330     }
331 
getInverseMatrixnull332     override fun getInverseMatrix(matrix: android.graphics.Matrix) {
333         return renderNode.getInverseMatrix(matrix)
334     }
335 
drawIntonull336     override fun drawInto(canvas: android.graphics.Canvas) {
337         (canvas as DisplayListCanvas).drawRenderNode(renderNode)
338     }
339 
setHasOverlappingRenderingnull340     override fun setHasOverlappingRendering(hasOverlappingRendering: Boolean): Boolean =
341         renderNode.setHasOverlappingRendering(hasOverlappingRendering)
342 
343     override fun dumpRenderNodeData(): DeviceRenderNodeData =
344         DeviceRenderNodeData(
345             // Platform RenderNode for API level 23-29 does not provide bounds/dimension properties
346             uniqueId = 0,
347             left = 0,
348             top = 0,
349             right = 0,
350             bottom = 0,
351             width = 0,
352             height = 0,
353             scaleX = renderNode.scaleX,
354             scaleY = renderNode.scaleY,
355             translationX = renderNode.translationX,
356             translationY = renderNode.translationY,
357             elevation = renderNode.elevation,
358             ambientShadowColor = ambientShadowColor,
359             spotShadowColor = spotShadowColor,
360             rotationZ = renderNode.rotation,
361             rotationX = renderNode.rotationX,
362             rotationY = renderNode.rotationY,
363             cameraDistance = renderNode.cameraDistance,
364             pivotX = renderNode.pivotX,
365             pivotY = renderNode.pivotY,
366             clipToOutline = renderNode.clipToOutline,
367             // No getter on RenderNode for clipToBounds, always return the value we have configured
368             // on it since this is a write only field
369             clipToBounds = clipToBounds,
370             alpha = renderNode.alpha,
371             renderEffect = renderEffect,
372             blendMode = blendMode,
373             colorFilter = colorFilter,
374             compositingStrategy = internalCompositingStrategy
375         )
376 
377     override fun discardDisplayList() {
378         discardDisplayListInternal()
379     }
380 
discardDisplayListInternalnull381     private fun discardDisplayListInternal() {
382         // See b/216660268. RenderNode#discardDisplayList was originally called
383         // destroyDisplayListData on Android M and below. Make sure we gate on the corresponding
384         // API level and call the original method name on these API levels, otherwise invoke
385         // the current method name of discardDisplayList
386         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
387             RenderNodeVerificationHelper24.discardDisplayList(renderNode)
388         } else {
389             RenderNodeVerificationHelper23.destroyDisplayListData(renderNode)
390         }
391     }
392 
verifyShadowColorPropertiesnull393     private fun verifyShadowColorProperties(renderNode: RenderNode) {
394         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
395             RenderNodeVerificationHelper28.setAmbientShadowColor(
396                 renderNode,
397                 RenderNodeVerificationHelper28.getAmbientShadowColor(renderNode)
398             )
399             RenderNodeVerificationHelper28.setSpotShadowColor(
400                 renderNode,
401                 RenderNodeVerificationHelper28.getSpotShadowColor(renderNode)
402             )
403         }
404     }
405 
406     companion object {
407         // Used by tests to force failing creating a RenderNode to simulate a device that
408         // doesn't support RenderNodes before Q.
409         internal var testFailCreateRenderNode = false
410 
411         // We need to validate that RenderNodes can be accessed before using the RenderNode
412         // stub implementation, but we only need to validate it once. This flag indicates that
413         // validation is still needed.
414         private var needToValidateAccess = true
415     }
416 }
417 
418 @RequiresApi(Build.VERSION_CODES.P)
419 private object RenderNodeVerificationHelper28 {
420 
getAmbientShadowColornull421     fun getAmbientShadowColor(renderNode: RenderNode): Int {
422         return renderNode.ambientShadowColor
423     }
424 
setAmbientShadowColornull425     fun setAmbientShadowColor(renderNode: RenderNode, target: Int) {
426         renderNode.ambientShadowColor = target
427     }
428 
getSpotShadowColornull429     fun getSpotShadowColor(renderNode: RenderNode): Int {
430         return renderNode.spotShadowColor
431     }
432 
setSpotShadowColornull433     fun setSpotShadowColor(renderNode: RenderNode, target: Int) {
434         renderNode.spotShadowColor = target
435     }
436 }
437 
438 @RequiresApi(Build.VERSION_CODES.N)
439 private object RenderNodeVerificationHelper24 {
440 
discardDisplayListnull441     fun discardDisplayList(renderNode: RenderNode) {
442         renderNode.discardDisplayList()
443     }
444 }
445 
446 @RequiresApi(Build.VERSION_CODES.M)
447 private object RenderNodeVerificationHelper23 {
448 
destroyDisplayListDatanull449     fun destroyDisplayListData(renderNode: RenderNode) {
450         renderNode.destroyDisplayListData()
451     }
452 }
453