Last active
April 17, 2025 06:48
-
-
Save Skaldebane/8e042b76023fbe20a7d70b59a9938f90 to your computer and use it in GitHub Desktop.
AngledSweepGradient - Sweep gradient implementation for Compose with a customizable start angle
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.graphics.SweepGradient | |
import android.os.Build | |
import androidx.annotation.VisibleForTesting | |
import androidx.compose.runtime.Immutable | |
import androidx.compose.runtime.Stable | |
import androidx.compose.ui.geometry.* | |
import androidx.compose.ui.graphics.* | |
import androidx.compose.ui.util.fastForEachIndexed | |
import androidx.core.graphics.transform | |
@Stable | |
fun Brush.Companion.angledSweepGradient( | |
vararg colorStops: Pair<Float, Color>, | |
center: Offset = Offset.Unspecified, | |
startAngle: Float = 0f | |
): Brush = AngledSweepGradient( | |
colors = List(colorStops.size) { i -> colorStops[i].second }, | |
stops = List(colorStops.size) { i -> colorStops[i].first }, | |
center = center, startAngle = startAngle | |
) | |
@Stable | |
fun Brush.Companion.angledSweepGradient( | |
colors: List<Color>, | |
center: Offset = Offset.Unspecified, | |
startAngle: Float = 0f | |
): Brush = AngledSweepGradient( | |
colors = colors, | |
stops = null, | |
center = center, | |
startAngle = startAngle | |
) | |
@Immutable | |
class AngledSweepGradient internal constructor( | |
private val center: Offset, | |
private val colors: List<Color>, | |
private val stops: List<Float>? = null, | |
private val startAngle: Float, | |
) : ShaderBrush() { | |
override fun createShader(size: Size): Shader = | |
AngledSweepGradientShader( | |
if (center.isUnspecified) { | |
size.center | |
} else { | |
Offset( | |
if (center.x == Float.POSITIVE_INFINITY) size.width else center.x, | |
if (center.y == Float.POSITIVE_INFINITY) size.height else center.y | |
) | |
}, | |
colors, | |
stops, | |
startAngle | |
) | |
override fun equals(other: Any?): Boolean { | |
if (this === other) return true | |
if (other !is AngledSweepGradient) return false | |
if (center != other.center) return false | |
if (colors != other.colors) return false | |
if (stops != other.stops) return false | |
if (startAngle != other.startAngle) return false | |
return true | |
} | |
override fun hashCode(): Int { | |
var result = center.hashCode() | |
result = 31 * result + colors.hashCode() | |
result = 31 * result + (stops?.hashCode() ?: 0) | |
result = 31 * result + startAngle.hashCode() | |
return result | |
} | |
override fun toString(): String { | |
val centerValue = if (center.isSpecified) "center=$center, " else "" | |
return "AngledSweepGradient(" + | |
centerValue + | |
"colors=$colors, stops=$stops, startAngle=$startAngle)" | |
} | |
} | |
internal fun AngledSweepGradientShader( | |
center: Offset, | |
colors: List<Color>, | |
colorStops: List<Float>?, | |
startAngle: Float, | |
): Shader { | |
validateColorStops(colors, colorStops) | |
val numTransparentColors = countTransparentColors(colors) | |
val shader = SweepGradient( | |
center.x, | |
center.y, | |
makeTransparentColors(colors, numTransparentColors), | |
makeTransparentStops(colorStops, colors, numTransparentColors) | |
) | |
shader.transform { setRotate(startAngle, center.x, center.y) } | |
return shader | |
} | |
/*private fun List<Color>.toIntArray(): IntArray = | |
IntArray(size) { i -> this[i].toArgb() }*/ | |
private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) { | |
if (colorStops == null) { | |
if (colors.size < 2) { | |
throw IllegalArgumentException( | |
"colors must have length of at least 2 if colorStops " + | |
"is omitted." | |
) | |
} | |
} else if (colors.size != colorStops.size) { | |
throw IllegalArgumentException( | |
"colors and colorStops arguments must have" + | |
" equal length." | |
) | |
} | |
} | |
@VisibleForTesting | |
internal fun countTransparentColors(colors: List<Color>): Int { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
return 0 | |
} | |
var numTransparentColors = 0 | |
// Don't count the first and last value because we don't add stops for those | |
for (i in 1 until colors.lastIndex) { | |
if (colors[i].alpha == 0f) { | |
numTransparentColors++ | |
} | |
} | |
return numTransparentColors | |
} | |
@VisibleForTesting | |
internal fun makeTransparentColors( | |
colors: List<Color>, | |
numTransparentColors: Int | |
): IntArray { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
// No change for Android O+, map the colors directly to their argb equivalent | |
return IntArray(colors.size) { i -> colors[i].toArgb() } | |
} | |
val values = IntArray(colors.size + numTransparentColors) | |
var valuesIndex = 0 | |
val lastIndex = colors.lastIndex | |
colors.fastForEachIndexed { index, color -> | |
if (color.alpha == 0f) { | |
if (index == 0) { | |
values[valuesIndex++] = colors[1].copy(alpha = 0f).toArgb() | |
} else if (index == lastIndex) { | |
values[valuesIndex++] = colors[index - 1].copy(alpha = 0f).toArgb() | |
} else { | |
val previousColor = colors[index - 1] | |
values[valuesIndex++] = previousColor.copy(alpha = 0f).toArgb() | |
val nextColor = colors[index + 1] | |
values[valuesIndex++] = nextColor.copy(alpha = 0f).toArgb() | |
} | |
} else { | |
values[valuesIndex++] = color.toArgb() | |
} | |
} | |
return values | |
} | |
internal fun makeTransparentStops( | |
stops: List<Float>?, | |
colors: List<Color>, | |
numTransparentColors: Int | |
): FloatArray? { | |
if (numTransparentColors == 0) { | |
return stops?.toFloatArray() | |
} | |
val newStops = FloatArray(colors.size + numTransparentColors) | |
newStops[0] = stops?.get(0) ?: 0f | |
var newStopsIndex = 1 | |
for (i in 1 until colors.lastIndex) { | |
val color = colors[i] | |
val stop = stops?.get(i) ?: i.toFloat() / colors.lastIndex | |
newStops[newStopsIndex++] = stop | |
if (color.alpha == 0f) { | |
newStops[newStopsIndex++] = stop | |
} | |
} | |
newStops[newStopsIndex] = stops?.get(colors.lastIndex) ?: 1f | |
return newStops | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.runtime.Immutable | |
import androidx.compose.runtime.Stable | |
import androidx.compose.ui.geometry.* | |
import androidx.compose.ui.graphics.* | |
import org.jetbrains.skia.FilterTileMode | |
import org.jetbrains.skia.GradientStyle | |
@Stable | |
fun Brush.Companion.angledSweepGradient( | |
vararg colorStops: Pair<Float, Color>, | |
center: Offset = Offset.Unspecified, | |
startAngle: Float = 0f | |
): Brush = AngledSweepGradient( | |
colors = List(colorStops.size) { i -> colorStops[i].second }, | |
stops = List(colorStops.size) { i -> colorStops[i].first }, | |
center = center, startAngle = startAngle | |
) | |
@Stable | |
fun Brush.Companion.angledSweepGradient( | |
colors: List<Color>, | |
center: Offset = Offset.Unspecified, | |
startAngle: Float = 0f | |
): Brush = AngledSweepGradient( | |
colors = colors, | |
stops = null, | |
center = center, | |
startAngle = startAngle | |
) | |
@Immutable | |
class AngledSweepGradient internal constructor( | |
private val center: Offset, | |
private val colors: List<Color>, | |
private val stops: List<Float>? = null, | |
private val startAngle: Float, | |
) : ShaderBrush() { | |
override fun createShader(size: Size): Shader = | |
AngledSweepGradientShader( | |
if (center.isUnspecified) { | |
size.center | |
} else { | |
Offset( | |
if (center.x == Float.POSITIVE_INFINITY) size.width else center.x, | |
if (center.y == Float.POSITIVE_INFINITY) size.height else center.y | |
) | |
}, | |
colors, | |
stops, | |
startAngle | |
) | |
override fun equals(other: Any?): Boolean { | |
if (this === other) return true | |
if (other !is AngledSweepGradient) return false | |
if (center != other.center) return false | |
if (colors != other.colors) return false | |
if (stops != other.stops) return false | |
if (startAngle != other.startAngle) return false | |
return true | |
} | |
override fun hashCode(): Int { | |
var result = center.hashCode() | |
result = 31 * result + colors.hashCode() | |
result = 31 * result + (stops?.hashCode() ?: 0) | |
result = 31 * result + startAngle.hashCode() | |
return result | |
} | |
override fun toString(): String { | |
val centerValue = if (center.isSpecified) "center=$center, " else "" | |
return "AngledSweepGradient(" + | |
centerValue + | |
"colors=$colors, stops=$stops, startAngle=$startAngle)" | |
} | |
} | |
internal fun AngledSweepGradientShader( | |
center: Offset, | |
colors: List<Color>, | |
colorStops: List<Float>?, | |
startAngle: Float, | |
): Shader { | |
validateColorStops(colors, colorStops) | |
return Shader.makeSweepGradient( | |
x = center.x, | |
y = center.y, | |
startAngle = 0f + startAngle, | |
endAngle = 360f + startAngle, | |
colors = colors.toIntArray(), | |
positions = colorStops?.toFloatArray(), | |
style = GradientStyle.DEFAULT.withTileMode(FilterTileMode.REPEAT) | |
) | |
} | |
private fun List<Color>.toIntArray(): IntArray = | |
IntArray(size) { i -> this[i].toArgb() } | |
private fun validateColorStops(colors: List<Color>, colorStops: List<Float>?) { | |
if (colorStops == null) { | |
if (colors.size < 2) { | |
throw IllegalArgumentException( | |
"colors must have length of at least 2 if colorStops " + | |
"is omitted." | |
) | |
} | |
} else if (colors.size != colorStops.size) { | |
throw IllegalArgumentException( | |
"colors and colorStops arguments must have" + | |
" equal length." | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment