Implementing Gradient Text Effects for Android Live Chat Nicknames
The article explains how to give privileged Android live‑chat users gradient nicknames by applying a LinearGradient shader via a custom GradientColorSpan, handling multi‑line text with restart or continuous strategies, and integrating these spans in a LiveGradientTextView that measures, inflates, and renders the gradient consistently across all live‑room scenarios.
We need to give privileged users a gradient nickname color in live‑room comments to highlight personalized identifiers, and the effect must work across all live‑room scenarios.
The design prototype in Figma shows how to create gradient text by selecting a gradient fill for a text layer, adjusting the start and end control points, and optionally adding intermediate color stops. The gradient is rendered as a colored rectangle drawn over the text and blended with the underlying characters.
In Android, gradient text can be achieved by assigning a Shader to a TextPaint . The following snippet creates a linear gradient with two colors and applies it to a TextView :
val gradientColors = intArrayOf(
Color.parseColor("#32C1FE"),
Color.parseColor("#AB1EED")
)
textView.paint.shader = LinearGradient(
0F, 0F, 100.dpFloat, 100.dpFloat,
gradientColors, null, Shader.TileMode.CLAMP
)Four tile modes are supported:
CLAMP – extend edge color (same as Figma).
REPEAT – repeat the gradient.
MIRROR – repeat with mirrored colors.
DECAL – render nothing outside the gradient bounds.
Because a Shader applied to the whole TextView cannot target a specific text segment, a custom Span is required. The GradientColorSpan extends CharacterStyle and sets a linear gradient in its updateDrawState() method.
class GradientColorSpan : CharacterStyle(), UpdateAppearance {
private val gradientColors = intArrayOf(
Color.parseColor("#32C1FE"),
Color.parseColor("#AB1EED")
)
override fun updateDrawState(tp: TextPaint?) {
tp?.shader = LinearGradient(
0F, 0F, 100.dpFloat, 100.dpFloat,
gradientColors, null, Shader.TileMode.REPEAT
)
}
}Applying the span to a portion of text:
content.text = buildSpannedString {
append("普通文本…")
inSpans(GradientColorSpan()) {
append("渐变文本…")
}
append("普通文本…")
}When the target text wraps to multiple lines, two strategies are considered:
Restart gradient – each new line starts a fresh gradient from its first character.
Continue gradient – the gradient spans across lines, requiring calculation of the total width of the selected segment.
The implementation introduces a LiveGradientColorTagSpan that records gradient parameters and a LiveGradientColorSpanDrawable which draws the gradient and shadow on a Canvas using two layered draws.
class LiveGradientColorTagSpan @JvmOverloads constructor(
val gradientColor: IntArray,
val gradientPositions: FloatArray? = null,
val gradientStrategy: GradientBreakStrategy = BREAK_RESTART
) : CharacterStyle(), UpdateAppearance {
override fun updateDrawState(tp: TextPaint?) { /* no‑op */ }
}The custom LiveGradientTextView clears existing gradient spans before each measurement, then after layout it iterates over all LiveGradientColorTagSpan instances, calculates line numbers, slice widths, and injects appropriate LiveGradientColorSpan objects based on the chosen break strategy.
open class LiveGradientTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatTextView(context, attrs, defStyle) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
clearGradientColorSpan()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
(text as? Spannable)?.let { spannable ->
spannable.getSpans(0, spannable.length, LiveGradientColorTagSpan::class.java)
.forEach { inflateGradientColorSpan(spannable, it) }
}
}
// ... inflateGradientColorSpan implementation omitted for brevity ...
}The core of inflateGradientColorSpan() computes the start/end line indices, extracts the text slice for each line, measures its exact width (handling edge cases where a line ends with whitespace), and builds a list of GradientSliceInfo . Depending on the gradientBreakStrategy , it either resets the gradient for each line or adjusts the gradient positions to continue across lines.
Finally, the rendered result shows a smooth gradient nickname that respects line breaks, shadow effects, and custom width constraints, while noting that updating the underlying SpannableStringBuilder may require a layout pass to preserve the gradient spans.
Ximalaya Technology Team
Official account of Ximalaya's technology team, sharing distilled technical experience and insights to grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.