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