Skip to content

Commit d11f824

Browse files
committed
WIP shader
Change-Id: I8f491d0a330f38e13baf08c61528a7348939b912
1 parent abcefcd commit d11f824

File tree

11 files changed

+725
-276
lines changed

11 files changed

+725
-276
lines changed

compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidShader.android.kt

Lines changed: 23 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -14,221 +14,31 @@
1414
* limitations under the License.
1515
*/
1616

17+
@file:Suppress("DEPRECATION")
18+
1719
package androidx.compose.ui.graphics
1820

19-
import android.graphics.BitmapShader
20-
import android.graphics.ComposeShader
21-
import android.graphics.LinearGradient
22-
import android.graphics.RadialGradient
23-
import android.graphics.SweepGradient
24-
import android.os.Build
25-
import androidx.annotation.VisibleForTesting
26-
import androidx.compose.ui.geometry.Offset
27-
import androidx.compose.ui.util.fastForEachIndexed
21+
import androidx.compose.ui.graphics.shader.asAndroidShader
22+
import androidx.compose.ui.graphics.shader.asComposeShader
2823

24+
@Deprecated(
25+
message = "Direct typealias to platform type replaced by " +
26+
"androidx.compose.ui.graphics.shader.Shader interface. " +
27+
"To get a reference to the platform type, use asAndroidShader() extension.",
28+
replaceWith = ReplaceWith("androidx.compose.ui.graphics.shader.Shader"),
29+
)
2930
actual typealias Shader = android.graphics.Shader
3031

31-
internal actual fun ActualLinearGradientShader(
32-
from: Offset,
33-
to: Offset,
34-
colors: List<Color>,
35-
colorStops: List<Float>?,
36-
tileMode: TileMode,
37-
): Shader {
38-
validateColorStops(colors, colorStops)
39-
val numTransparentColors = countTransparentColors(colors)
40-
return LinearGradient(
41-
from.x,
42-
from.y,
43-
to.x,
44-
to.y,
45-
makeTransparentColors(colors, numTransparentColors),
46-
makeTransparentStops(colorStops, colors, numTransparentColors),
47-
tileMode.toAndroidTileMode(),
48-
)
49-
}
50-
51-
internal actual fun ActualRadialGradientShader(
52-
center: Offset,
53-
radius: Float,
54-
colors: List<Color>,
55-
colorStops: List<Float>?,
56-
tileMode: TileMode,
57-
): Shader {
58-
validateColorStops(colors, colorStops)
59-
val numTransparentColors = countTransparentColors(colors)
60-
return RadialGradient(
61-
center.x,
62-
center.y,
63-
radius,
64-
makeTransparentColors(colors, numTransparentColors),
65-
makeTransparentStops(colorStops, colors, numTransparentColors),
66-
tileMode.toAndroidTileMode(),
67-
)
68-
}
69-
70-
internal actual fun ActualSweepGradientShader(
71-
center: Offset,
72-
colors: List<Color>,
73-
colorStops: List<Float>?,
74-
): Shader {
75-
validateColorStops(colors, colorStops)
76-
val numTransparentColors = countTransparentColors(colors)
77-
return SweepGradient(
78-
center.x,
79-
center.y,
80-
makeTransparentColors(colors, numTransparentColors),
81-
makeTransparentStops(colorStops, colors, numTransparentColors),
82-
)
83-
}
84-
85-
internal actual fun ActualImageShader(
86-
image: ImageBitmap,
87-
tileModeX: TileMode,
88-
tileModeY: TileMode,
89-
): Shader {
90-
return BitmapShader(
91-
image.asAndroidBitmap(),
92-
tileModeX.toAndroidTileMode(),
93-
tileModeY.toAndroidTileMode(),
94-
)
95-
}
96-
97-
/**
98-
* Returns the number of transparent (alpha = 0) values that aren't at the beginning or end of the
99-
* gradient so that the color stops can be added. On O and newer devices, this always returns 0
100-
* because no stops need to be added.
101-
*/
102-
@VisibleForTesting
103-
internal fun countTransparentColors(colors: List<Color>): Int {
104-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
105-
return 0
106-
}
107-
var numTransparentColors = 0
108-
// Don't count the first and last value because we don't add stops for those
109-
for (i in 1 until colors.lastIndex) {
110-
if (colors[i].alpha == 0f) {
111-
numTransparentColors++
112-
}
113-
}
114-
return numTransparentColors
115-
}
116-
117-
/**
118-
* There was a change in behavior between Android N and O with how transparent colors are
119-
* interpolated with skia gradients. More specifically Android O treats all fully transparent colors
120-
* the same regardless of the rgb channels, however, Android N and older releases interpolated
121-
* between the color channels as well. Because Color.Transparent is transparent black, this would
122-
* introduce some muddy colors as part of gradients with transparency for Android N and below. In
123-
* order to make gradient rendering consistent and match the behavior of Android O+, detect whenever
124-
* Color.Transparent is used and a stop matching the color of the previous value, but alpha = 0 is
125-
* added and another stop at the same point with the same color as the following value, but with
126-
* alpha = 0 is used.
127-
*/
128-
@VisibleForTesting
129-
internal fun makeTransparentColors(colors: List<Color>, numTransparentColors: Int): IntArray {
130-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
131-
// No change for Android O+, map the colors directly to their argb equivalent
132-
return IntArray(colors.size) { i -> colors[i].toArgb() }
133-
}
134-
val values = IntArray(colors.size + numTransparentColors)
135-
var valuesIndex = 0
136-
val lastIndex = colors.lastIndex
137-
colors.fastForEachIndexed { index, color ->
138-
if (color.alpha == 0f) {
139-
if (index == 0) {
140-
values[valuesIndex++] = colors[1].copy(alpha = 0f).toArgb()
141-
} else if (index == lastIndex) {
142-
values[valuesIndex++] = colors[index - 1].copy(alpha = 0f).toArgb()
143-
} else {
144-
val previousColor = colors[index - 1]
145-
values[valuesIndex++] = previousColor.copy(alpha = 0f).toArgb()
146-
147-
val nextColor = colors[index + 1]
148-
values[valuesIndex++] = nextColor.copy(alpha = 0f).toArgb()
149-
}
150-
} else {
151-
values[valuesIndex++] = color.toArgb()
152-
}
153-
}
154-
return values
155-
}
156-
157-
/**
158-
* See [makeTransparentColors].
159-
*
160-
* On N and earlier devices that have transparent values, we must duplicate the color stops for
161-
* fully transparent values so that the color value before and after can be interpolated.
162-
*/
163-
@VisibleForTesting
164-
internal fun makeTransparentStops(
165-
stops: List<Float>?,
166-
colors: List<Color>,
167-
numTransparentColors: Int,
168-
): FloatArray? {
169-
if (numTransparentColors == 0) {
170-
return stops?.toFloatArray()
171-
}
172-
val newStops = FloatArray(colors.size + numTransparentColors)
173-
newStops[0] = stops?.get(0) ?: 0f
174-
var newStopsIndex = 1
175-
for (i in 1 until colors.lastIndex) {
176-
val color = colors[i]
177-
val stop = stops?.get(i) ?: i.toFloat() / colors.lastIndex
178-
newStops[newStopsIndex++] = stop
179-
if (color.alpha == 0f) {
180-
newStops[newStopsIndex++] = stop
181-
}
182-
}
183-
newStops[newStopsIndex] = stops?.get(colors.lastIndex) ?: 1f
184-
return newStops
185-
}
186-
187-
private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) {
188-
if (colorStops == null) {
189-
if (colors.size < 2) {
190-
throw IllegalArgumentException(
191-
"colors must have length of at least 2 if colorStops " + "is omitted."
192-
)
193-
}
194-
} else if (colors.size != colorStops.size) {
195-
throw IllegalArgumentException(
196-
"colors and colorStops arguments must have" + " equal length."
197-
)
198-
}
199-
}
200-
201-
internal actual class TransformShader {
202-
private var aMatrix: android.graphics.Matrix? = null
203-
204-
private fun obtainMatrix(): android.graphics.Matrix =
205-
aMatrix ?: android.graphics.Matrix().also { aMatrix = it }
206-
207-
actual fun transform(matrix: Matrix?) {
208-
val tmp: android.graphics.Matrix?
209-
if (matrix == null) {
210-
tmp = null
211-
aMatrix = null
212-
} else {
213-
tmp = obtainMatrix().apply { setFrom(matrix) }
214-
}
215-
// TODO(b/419811019): Handle the chase where the shader already had a matrix set.
216-
shader?.setLocalMatrix(tmp)
217-
}
218-
219-
actual var shader: Shader? = null
220-
set(value) {
221-
if (aMatrix != null) {
222-
// TODO(b/419811019): Handle the chase where the shader already had a matrix set.
223-
value?.setLocalMatrix(aMatrix)
224-
}
225-
field = value
226-
}
227-
}
228-
229-
internal actual fun ActualCompositeShader(dst: Shader, src: Shader, blendMode: BlendMode): Shader =
230-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
231-
ComposeShader(dst, src, blendMode.toAndroidBlendMode())
232-
} else {
233-
ComposeShader(dst, src, blendMode.toPorterDuffMode())
234-
}
32+
@Deprecated(
33+
message = "Only for providing compatibility with deprecated functions. " +
34+
"Use platform-specific extension to get platform reference",
35+
)
36+
internal actual fun androidx.compose.ui.graphics.shader.Shader.asOldShader(): android.graphics.Shader =
37+
asAndroidShader()
38+
39+
@Deprecated(
40+
message = "Only for providing compatibility with deprecated functions. " +
41+
"Use platform-specific extension to get platform reference",
42+
)
43+
internal actual fun Shader.asNewShader(): androidx.compose.ui.graphics.shader.Shader =
44+
asComposeShader()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package androidx.compose.ui.graphics.shader
2+
3+
actual sealed interface Shader
4+
5+
@JvmInline
6+
private value class AndroidShaderImpl(
7+
val internalShader: android.graphics.Shader
8+
): Shader
9+
10+
fun android.graphics.Shader.asComposeShader(): Shader =
11+
AndroidShaderImpl(this)
12+
13+
fun Shader.asAndroidShader(): android.graphics.Shader =
14+
(this as AndroidShaderImpl).internalShader

0 commit comments

Comments
 (0)