1 /*
2  * Copyright 2021 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.graphics
18 
19 import android.os.Build
20 import androidx.annotation.RequiresApi
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.ui.geometry.Offset
23 
24 /** Convert the [android.graphics.RenderEffect] instance into a Compose-compatible [RenderEffect] */
androidnull25 fun android.graphics.RenderEffect.asComposeRenderEffect(): RenderEffect = AndroidRenderEffect(this)
26 
27 @Immutable
28 actual sealed class RenderEffect {
29 
30     private var internalRenderEffect: android.graphics.RenderEffect? = null
31 
32     /** Obtain a [android.graphics.RenderEffect] from the compose [RenderEffect] */
33     @RequiresApi(Build.VERSION_CODES.S)
34     fun asAndroidRenderEffect(): android.graphics.RenderEffect =
35         internalRenderEffect ?: createRenderEffect().also { internalRenderEffect = it }
36 
37     @RequiresApi(Build.VERSION_CODES.S)
38     protected abstract fun createRenderEffect(): android.graphics.RenderEffect
39 
40     actual open fun isSupported(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
41 }
42 
43 @Immutable
44 internal class AndroidRenderEffect(val androidRenderEffect: android.graphics.RenderEffect) :
45     RenderEffect() {
createRenderEffectnull46     override fun createRenderEffect(): android.graphics.RenderEffect = androidRenderEffect
47 }
48 
49 @Immutable
50 actual class BlurEffect
51 actual constructor(
52     private val renderEffect: RenderEffect?,
53     private val radiusX: Float,
54     private val radiusY: Float,
55     private val edgeTreatment: TileMode
56 ) : RenderEffect() {
57 
58     @RequiresApi(Build.VERSION_CODES.S)
59     override fun createRenderEffect(): android.graphics.RenderEffect =
60         RenderEffectVerificationHelper.createBlurEffect(
61             renderEffect,
62             radiusX,
63             radiusY,
64             edgeTreatment
65         )
66 
67     override fun equals(other: Any?): Boolean {
68         if (this === other) return true
69         if (other !is BlurEffect) return false
70 
71         if (radiusX != other.radiusX) return false
72         if (radiusY != other.radiusY) return false
73         if (edgeTreatment != other.edgeTreatment) return false
74         if (renderEffect != other.renderEffect) return false
75 
76         return true
77     }
78 
79     override fun hashCode(): Int {
80         var result = renderEffect?.hashCode() ?: 0
81         result = 31 * result + radiusX.hashCode()
82         result = 31 * result + radiusY.hashCode()
83         result = 31 * result + edgeTreatment.hashCode()
84         return result
85     }
86 
87     override fun toString(): String {
88         return "BlurEffect(renderEffect=$renderEffect, radiusX=$radiusX, radiusY=$radiusY, " +
89             "edgeTreatment=$edgeTreatment)"
90     }
91 }
92 
93 @Immutable
94 actual class OffsetEffect
95 actual constructor(private val renderEffect: RenderEffect?, private val offset: Offset) :
96     RenderEffect() {
97 
98     @RequiresApi(Build.VERSION_CODES.S)
createRenderEffectnull99     override fun createRenderEffect(): android.graphics.RenderEffect =
100         RenderEffectVerificationHelper.createOffsetEffect(renderEffect, offset)
101 
102     override fun equals(other: Any?): Boolean {
103         if (this === other) return true
104         if (other !is OffsetEffect) return false
105 
106         if (renderEffect != other.renderEffect) return false
107         if (offset != other.offset) return false
108 
109         return true
110     }
111 
hashCodenull112     override fun hashCode(): Int {
113         var result = renderEffect?.hashCode() ?: 0
114         result = 31 * result + offset.hashCode()
115         return result
116     }
117 
toStringnull118     override fun toString(): String {
119         return "OffsetEffect(renderEffect=$renderEffect, offset=$offset)"
120     }
121 }
122 
123 @RequiresApi(Build.VERSION_CODES.S)
124 private object RenderEffectVerificationHelper {
125 
createBlurEffectnull126     fun createBlurEffect(
127         inputRenderEffect: RenderEffect?,
128         radiusX: Float,
129         radiusY: Float,
130         edgeTreatment: TileMode
131     ): android.graphics.RenderEffect =
132         if (radiusX == 0f && radiusY == 0f) {
133             // Workaround for preventing exceptions to be thrown if apps animate blur radii values
134             // through 0f. In which case the visual effect should be a no-op.
135             // The return value for each of the RenderEffect API is an opaque RenderEffect instance
136             // that wraps a native pointer, so return a no-op offset effect instead
137             // See b/241546169
138             android.graphics.RenderEffect.createOffsetEffect(0f, 0f)
139         } else if (inputRenderEffect == null) {
140             android.graphics.RenderEffect.createBlurEffect(
141                 radiusX,
142                 radiusY,
143                 edgeTreatment.toAndroidTileMode()
144             )
145         } else {
146             android.graphics.RenderEffect.createBlurEffect(
147                 radiusX,
148                 radiusY,
149                 inputRenderEffect.asAndroidRenderEffect(),
150                 edgeTreatment.toAndroidTileMode()
151             )
152         }
153 
createOffsetEffectnull154     fun createOffsetEffect(
155         inputRenderEffect: RenderEffect?,
156         offset: Offset
157     ): android.graphics.RenderEffect =
158         if (inputRenderEffect == null) {
159             android.graphics.RenderEffect.createOffsetEffect(offset.x, offset.y)
160         } else {
161             android.graphics.RenderEffect.createOffsetEffect(
162                 offset.x,
163                 offset.y,
164                 inputRenderEffect.asAndroidRenderEffect()
165             )
166         }
167 }
168