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.annotation.SuppressLint
20 import android.os.Build
21 import android.view.View
22 import android.view.ViewOutlineProvider
23 import androidx.annotation.RequiresApi
24 import androidx.compose.ui.geometry.MutableRect
25 import androidx.compose.ui.geometry.Offset
26 import androidx.compose.ui.graphics.Canvas
27 import androidx.compose.ui.graphics.CanvasHolder
28 import androidx.compose.ui.graphics.CompositingStrategy
29 import androidx.compose.ui.graphics.Fields
30 import androidx.compose.ui.graphics.Matrix
31 import androidx.compose.ui.graphics.Paint
32 import androidx.compose.ui.graphics.Path
33 import androidx.compose.ui.graphics.RectangleShape
34 import androidx.compose.ui.graphics.RenderEffect
35 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
36 import androidx.compose.ui.graphics.TransformOrigin
37 import androidx.compose.ui.graphics.layer.GraphicsLayer
38 import androidx.compose.ui.graphics.toArgb
39 import androidx.compose.ui.layout.GraphicLayerInfo
40 import androidx.compose.ui.node.OwnedLayer
41 import androidx.compose.ui.unit.IntOffset
42 import androidx.compose.ui.unit.IntSize
43 import java.lang.reflect.Field
44 import java.lang.reflect.Method
45 
46 /** View implementation of OwnedLayer. */
47 @SuppressLint("ViewConstructor")
48 internal class ViewLayer(
49     val ownerView: AndroidComposeView,
50     val container: DrawChildContainer,
51     drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
52     invalidateParentLayer: () -> Unit
53 ) : View(ownerView.context), OwnedLayer, GraphicLayerInfo {
54     private var drawBlock: ((canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit)? = drawBlock
55     private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
56 
57     private val outlineResolver = OutlineResolver()
58     // Value of the layerModifier's clipToBounds property
59     private var clipToBounds = false
60     private var clipBoundsCache: android.graphics.Rect? = null
61     private val manualClipPath: Path?
62         get() =
63             if (!clipToOutline || outlineResolver.outlineClipSupported) {
64                 null
65             } else {
66                 outlineResolver.clipPath
67             }
68 
69     var isInvalidated = false
70         private set(value) {
71             if (value != field) {
72                 field = value
73                 ownerView.notifyLayerIsDirty(this, value)
74             }
75         }
76 
77     private var layerPaint: Paint? = null
78 
79     private var drawnWithZ = false
80     private val canvasHolder = CanvasHolder()
81 
82     private val matrixCache = LayerMatrixCache(getMatrix)
83 
84     override val underlyingMatrix: Matrix
85         get() = matrixCache.calculateMatrix(this)
86 
87     override var frameRate: Float = 0f
88 
89     override var isFrameRateFromParent = false
90 
91     /**
92      * Local copy of the transform origin as GraphicsLayerModifier can be implemented as a model
93      * object. Update this field within [updateLayerProperties] and use it in [resize] or other
94      * methods
95      */
96     private var mTransformOrigin: TransformOrigin = TransformOrigin.Center
97 
98     private var mHasOverlappingRendering = true
99 
100     init {
101         setWillNotDraw(false) // we WILL draw
102         container.addView(this)
103     }
104 
105     override val layerId: Long = generateViewId().toLong()
106 
107     override val ownerViewId: Long
108         get() =
109             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
110                 UniqueDrawingIdApi29.getUniqueDrawingId(ownerView)
111             } else {
112                 -1
113             }
114 
115     @RequiresApi(29)
116     private object UniqueDrawingIdApi29 {
117         @JvmStatic fun getUniqueDrawingId(view: View) = view.uniqueDrawingId
118     }
119 
120     /**
121      * Configure the camera distance on the View in pixels. View already has a get/setCameraDistance
122      * API however, that operates in Dp values.
123      */
124     var cameraDistancePx: Float
125         get() {
126             // View internally converts distance to dp so divide by density here to have
127             // consistent usage of pixels with RenderNode that is backing the View
128             return cameraDistance / resources.displayMetrics.densityDpi
129         }
130         set(value) {
131             // View internally converts distance to dp so multiply by density here to have
132             // consistent usage of pixels with RenderNode that is backing the View
133             cameraDistance = value * resources.displayMetrics.densityDpi
134         }
135 
136     private var mutatedFields: Int = 0
137 
138     override fun updateLayerProperties(scope: ReusableGraphicsLayerScope) {
139         val maybeChangedFields = scope.mutatedFields or mutatedFields
140         if (maybeChangedFields and Fields.TransformOrigin != 0) {
141             this.mTransformOrigin = scope.transformOrigin
142             this.pivotX = mTransformOrigin.pivotFractionX * width
143             this.pivotY = mTransformOrigin.pivotFractionY * height
144         }
145         if (maybeChangedFields and Fields.ScaleX != 0) {
146             this.scaleX = scope.scaleX
147         }
148         if (maybeChangedFields and Fields.ScaleY != 0) {
149             this.scaleY = scope.scaleY
150         }
151         if (maybeChangedFields and Fields.Alpha != 0) {
152             this.alpha = scope.alpha
153         }
154         if (maybeChangedFields and Fields.TranslationX != 0) {
155             this.translationX = scope.translationX
156         }
157         if (maybeChangedFields and Fields.TranslationY != 0) {
158             this.translationY = scope.translationY
159         }
160         if (maybeChangedFields and Fields.ShadowElevation != 0) {
161             this.elevation = scope.shadowElevation
162         }
163         if (maybeChangedFields and Fields.RotationZ != 0) {
164             this.rotation = scope.rotationZ
165         }
166         if (maybeChangedFields and Fields.RotationX != 0) {
167             this.rotationX = scope.rotationX
168         }
169         if (maybeChangedFields and Fields.RotationY != 0) {
170             this.rotationY = scope.rotationY
171         }
172         if (maybeChangedFields and Fields.CameraDistance != 0) {
173             this.cameraDistancePx = scope.cameraDistance
174         }
175         val wasClippingManually = manualClipPath != null
176         val clipToOutline = scope.clip && scope.shape !== RectangleShape
177         if (maybeChangedFields and (Fields.Clip or Fields.Shape) != 0) {
178             this.clipToBounds = scope.clip && scope.shape === RectangleShape
179             resetClipBounds()
180             this.clipToOutline = clipToOutline
181         }
182         val shapeChanged =
183             outlineResolver.update(
184                 scope.outline,
185                 scope.alpha,
186                 clipToOutline,
187                 scope.shadowElevation,
188                 scope.size
189             )
190         if (outlineResolver.cacheIsDirty) {
191             updateOutlineResolver()
192         }
193         val isClippingManually = manualClipPath != null
194         if (wasClippingManually != isClippingManually || (isClippingManually && shapeChanged)) {
195             invalidate() // have to redraw the content
196         }
197         if (!drawnWithZ && elevation > 0) {
198             invalidateParentLayer?.invoke()
199         }
200         if (maybeChangedFields and Fields.MatrixAffectingFields != 0) {
201             matrixCache.invalidate()
202         }
203         if (Build.VERSION.SDK_INT >= 28) {
204             if (maybeChangedFields and Fields.AmbientShadowColor != 0) {
205                 ViewLayerVerificationHelper28.setOutlineAmbientShadowColor(
206                     this,
207                     scope.ambientShadowColor.toArgb()
208                 )
209             }
210             if (maybeChangedFields and Fields.SpotShadowColor != 0) {
211                 ViewLayerVerificationHelper28.setOutlineSpotShadowColor(
212                     this,
213                     scope.spotShadowColor.toArgb()
214                 )
215             }
216         }
217         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
218             if (maybeChangedFields and Fields.RenderEffect != 0) {
219                 ViewLayerVerificationHelper31.setRenderEffect(this, scope.renderEffect)
220             }
221         }
222 
223         val requireLayer =
224             maybeChangedFields and Fields.ColorFilter != 0 ||
225                 maybeChangedFields and Fields.BlendMode != 0
226 
227         if (maybeChangedFields and Fields.CompositingStrategy != 0 || requireLayer) {
228             val strategy =
229                 if (requireLayer) {
230                     CompositingStrategy.Offscreen
231                 } else {
232                     scope.compositingStrategy
233                 }
234 
235             mHasOverlappingRendering =
236                 when (strategy) {
237                     CompositingStrategy.Offscreen -> {
238                         val paint =
239                             if (requireLayer) {
240                                 obtainLayerPaint()
241                                     .apply {
242                                         colorFilter = scope.colorFilter
243                                         blendMode = scope.blendMode
244                                     }
245                                     .asFrameworkPaint()
246                             } else {
247                                 null
248                             }
249                         setLayerType(LAYER_TYPE_HARDWARE, paint)
250                         true
251                     }
252                     CompositingStrategy.ModulateAlpha -> {
253                         setLayerType(LAYER_TYPE_NONE, null)
254                         false
255                     }
256                     else -> { // CompositingStrategy.Auto
257                         setLayerType(LAYER_TYPE_NONE, null)
258                         true
259                     }
260                 }
261         }
262 
263         mutatedFields = scope.mutatedFields
264     }
265 
266     private fun obtainLayerPaint(): Paint = layerPaint ?: Paint().also { layerPaint = it }
267 
268     override fun hasOverlappingRendering(): Boolean {
269         return mHasOverlappingRendering
270     }
271 
272     override fun isInLayer(position: Offset): Boolean {
273         val x = position.x
274         val y = position.y
275         if (clipToBounds) {
276             return 0f <= x && x < width && 0f <= y && y < height
277         }
278 
279         if (clipToOutline) {
280             return outlineResolver.isInOutline(position)
281         }
282 
283         return true
284     }
285 
286     private fun updateOutlineResolver() {
287         this.outlineProvider =
288             if (outlineResolver.androidOutline != null) {
289                 OutlineProvider
290             } else {
291                 null
292             }
293     }
294 
295     private fun resetClipBounds() {
296         this.clipBounds =
297             if (clipToBounds) {
298                 if (clipBoundsCache == null) {
299                     clipBoundsCache = android.graphics.Rect(0, 0, width, height)
300                 } else {
301                     clipBoundsCache!!.set(0, 0, width, height)
302                 }
303                 clipBoundsCache
304             } else {
305                 null
306             }
307     }
308 
309     override fun resize(size: IntSize) {
310         val width = size.width
311         val height = size.height
312         if (width != this.width || height != this.height) {
313             pivotX = mTransformOrigin.pivotFractionX * width
314             pivotY = mTransformOrigin.pivotFractionY * height
315             updateOutlineResolver()
316             layout(left, top, left + width, top + height)
317             resetClipBounds()
318             matrixCache.invalidate()
319         }
320     }
321 
322     override fun move(position: IntOffset) {
323         val left = position.x
324 
325         if (left != this.left) {
326             offsetLeftAndRight(left - this.left)
327             matrixCache.invalidate()
328         }
329         val top = position.y
330         if (top != this.top) {
331             offsetTopAndBottom(top - this.top)
332             matrixCache.invalidate()
333         }
334     }
335 
336     override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
337         drawnWithZ = elevation > 0f
338         if (drawnWithZ) {
339             canvas.enableZ()
340         }
341         container.drawChild(canvas, this, drawingTime)
342         if (drawnWithZ) {
343             canvas.disableZ()
344         }
345     }
346 
347     override fun dispatchDraw(canvas: android.graphics.Canvas) {
348         canvasHolder.drawInto(canvas) {
349             var didClip = false
350             val clipPath = manualClipPath
351             if (clipPath != null || !canvas.isHardwareAccelerated) {
352                 didClip = true
353                 save()
354                 outlineResolver.clipToOutline(this)
355             }
356             drawBlock?.invoke(this, null)
357             if (didClip) {
358                 restore()
359             }
360         }
361         isInvalidated = false
362     }
363 
364     override fun invalidate() {
365         if (!isInvalidated) {
366             isInvalidated = true
367             super.invalidate()
368             ownerView.invalidate()
369         }
370     }
371 
372     override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
373 
374     override fun destroy() {
375         isInvalidated = false
376         ownerView.requestClearInvalidObservations()
377         drawBlock = null
378         invalidateParentLayer = null
379 
380         val recycle = ownerView.recycle(this@ViewLayer)
381 
382         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw || !recycle) {
383             container.removeViewInLayout(this)
384         } else {
385             visibility = GONE
386         }
387     }
388 
389     override fun updateDisplayList() {
390         if (isInvalidated && !shouldUseDispatchDraw) {
391             updateDisplayList(this)
392             isInvalidated = false
393         }
394     }
395 
396     override fun forceLayout() {
397         // Don't do anything. These Views are treated as RenderNodes, so a forced layout
398         // should not do anything. If we keep this, we get more redrawing than is necessary.
399     }
400 
401     override fun mapOffset(point: Offset, inverse: Boolean): Offset {
402         return if (inverse) {
403             matrixCache.mapInverse(this, point)
404         } else {
405             matrixCache.map(this, point)
406         }
407     }
408 
409     override fun mapBounds(rect: MutableRect, inverse: Boolean) {
410         if (inverse) {
411             matrixCache.mapInverse(this, rect)
412         } else {
413             matrixCache.map(this, rect)
414         }
415     }
416 
417     override fun reuseLayer(
418         drawBlock: (canvas: Canvas, parentLayer: GraphicsLayer?) -> Unit,
419         invalidateParentLayer: () -> Unit
420     ) {
421         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || shouldUseDispatchDraw) {
422             container.addView(this)
423         } else {
424             visibility = VISIBLE
425         }
426         matrixCache.reset()
427         clipToBounds = false
428         drawnWithZ = false
429         mTransformOrigin = TransformOrigin.Center
430         this.drawBlock = drawBlock
431         this.invalidateParentLayer = invalidateParentLayer
432         isInvalidated = false
433     }
434 
435     override fun transform(matrix: Matrix) {
436         matrix.timesAssign(matrixCache.calculateMatrix(this))
437     }
438 
439     override fun inverseTransform(matrix: Matrix) {
440         val inverse = matrixCache.calculateInverseMatrix(this)
441         if (inverse != null) {
442             matrix.timesAssign(inverse)
443         }
444     }
445 
446     companion object {
447         private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix ->
448             val newMatrix = view.matrix
449             matrix.set(newMatrix)
450         }
451 
452         val OutlineProvider =
453             object : ViewOutlineProvider() {
454                 override fun getOutline(view: View, outline: android.graphics.Outline) {
455                     view as ViewLayer
456                     outline.set(view.outlineResolver.androidOutline!!)
457                 }
458             }
459         private var updateDisplayListIfDirtyMethod: Method? = null
460         private var recreateDisplayList: Field? = null
461         var hasRetrievedMethod = false
462             private set
463 
464         var shouldUseDispatchDraw = false
465             internal set // internal so that tests can use it.
466 
467         @SuppressLint("BanUncheckedReflection")
468         fun updateDisplayList(view: View) {
469             try {
470                 if (!hasRetrievedMethod) {
471                     hasRetrievedMethod = true
472                     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
473                         updateDisplayListIfDirtyMethod =
474                             View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
475                         recreateDisplayList =
476                             View::class.java.getDeclaredField("mRecreateDisplayList")
477                     } else {
478                         val getDeclaredMethod =
479                             Class::class
480                                 .java
481                                 .getDeclaredMethod(
482                                     "getDeclaredMethod",
483                                     String::class.java,
484                                     arrayOf<Class<*>>()::class.java
485                                 )
486                         updateDisplayListIfDirtyMethod =
487                             getDeclaredMethod.invoke(
488                                 View::class.java,
489                                 "updateDisplayListIfDirty",
490                                 emptyArray<Class<*>>()
491                             ) as Method?
492                         val getDeclaredField =
493                             Class::class
494                                 .java
495                                 .getDeclaredMethod("getDeclaredField", String::class.java)
496                         recreateDisplayList =
497                             getDeclaredField.invoke(View::class.java, "mRecreateDisplayList")
498                                 as Field?
499                     }
500                     updateDisplayListIfDirtyMethod?.isAccessible = true
501                     recreateDisplayList?.isAccessible = true
502                 }
503                 recreateDisplayList?.setBoolean(view, true)
504                 updateDisplayListIfDirtyMethod?.invoke(view)
505             } catch (_: Throwable) {
506                 shouldUseDispatchDraw = true
507             }
508         }
509     }
510 }
511 
512 @RequiresApi(Build.VERSION_CODES.S)
513 private object ViewLayerVerificationHelper31 {
514 
setRenderEffectnull515     fun setRenderEffect(view: View, target: RenderEffect?) {
516         view.setRenderEffect(target?.asAndroidRenderEffect())
517     }
518 }
519 
520 @RequiresApi(Build.VERSION_CODES.P)
521 private object ViewLayerVerificationHelper28 {
522 
setOutlineAmbientShadowColornull523     fun setOutlineAmbientShadowColor(view: View, target: Int) {
524         view.outlineAmbientShadowColor = target
525     }
526 
setOutlineSpotShadowColornull527     fun setOutlineSpotShadowColor(view: View, target: Int) {
528         view.outlineSpotShadowColor = target
529     }
530 }
531