Text with gradient in Android
Asked Answered
K

14

80

How would I extend TextView to allow the drawing of text with a gradient effect?

Kevel answered 21/4, 2010 at 5:43 Comment(0)
K
23

It doesn't appear possible to extend TextView to draw text with a gradient. It is, however, possible to achieve this effect by creating a canvas and drawing on it. First we need to declare our custom UI element. In the initiation we need to create a subclass of Layout. In this case, we will use BoringLayout which only supports text with a single line.

Shader textShader=new LinearGradient(0, 0, 0, 20,
    new int[]{bottom,top},
    new float[]{0, 1}, TileMode.CLAMP);//Assumes bottom and top are colors defined above
textPaint.setTextSize(textSize);
textPaint.setShader(textShader);
BoringLayout.Metrics boringMetrics=BoringLayout.isBoring(text, textPaint);
boringLayout=new BoringLayout(text, textPaint, 0, Layout.Alignment.ALIGN_CENTER,
            0.0f, 0.0f, boringMetrics, false);

We then override onMeasure and onDraw:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    setMeasuredDimension((int) textPaint.measureText(text), (int) textPaint.getFontSpacing());
}

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    boringLayout.draw(canvas);
}

Our implementation of onDraw is at this point quite lazy (it completely ignores the measurement specs!, but so long as you guarantee that the view is given sufficent space, it should work okay.

Alternatively, it would be possible to inherit from a Canvas and override the onPaint method. If this is done, then unfortunately the anchor for text being drawn will always be on the bottom so we have to add -textPaint.getFontMetricsInt().ascent() to our y coordinate.

Kevel answered 27/4, 2010 at 5:30 Comment(0)
W
151
TextView secondTextView = new TextView(this);
Shader textShader=new LinearGradient(0, 0, 0, 20,
            new int[]{Color.GREEN,Color.BLUE},
            new float[]{0, 1}, TileMode.CLAMP);
secondTextView.getPaint().setShader(textShader);
Whydah answered 23/2, 2011 at 7:36 Comment(4)
On my ICS device, this doesn't seem to work -- it is completely transparent. Anyone else seeing the same thing? The view is not hardware accelerated.Ardeen
@Steve Prentice: may be y1 = 20 too short, so you cannot see any gradient, try : new LinearGradient(0,0,0,secondTextView.getPaint().getTextSize(),....) - maybe it will solve your problemGregarious
Fix this: Shader.TileMode.CLAMPAfton
@StevePrentice : I've come across the issue on OS 9.0 as well. Another reason may be that you need to specify the alpha value for the gradient color array. e.g Set one of the colors as 0xFF148CDC instead of 0x148CDC.Yasminyasmine
G
83

I have used the top answer(@Taras) with a gradient of 5 colors, but there is a problem: the textView looks like that I have put a white cover on it. Here is my code and the screenshot.

        textView = (TextView) findViewById(R.id.main_tv);
        textView.setText("Tianjin, China".toUpperCase());

        TextPaint paint = textView.getPaint();
        float width = paint.measureText("Tianjin, China");

        Shader textShader = new LinearGradient(0, 0, width, textView.getTextSize(),
                new int[]{
                        Color.parseColor("#F97C3C"),
                        Color.parseColor("#FDB54E"),
                        Color.parseColor("#64B678"),
                        Color.parseColor("#478AEA"),
                        Color.parseColor("#8446CC"),
                }, null, Shader.TileMode.CLAMP);
        textView.getPaint().setShader(textShader);

enter image description here

After many hours, I found out that I need to call textView.setTextColor() with the first color of the gradient. Then the screenshot:

enter image description here

Hope help someone!

Glinys answered 12/9, 2018 at 7:41 Comment(2)
Why are you making gradient from top left corner to bottom right corner? Shouldn't it be LinearGradient(0, 0, width, 0, ... instead for horizontal gradient?Niedersachsen
@Niedersachsen : We copied this gradient effect from Instagram. 😄Glinys
K
23

It doesn't appear possible to extend TextView to draw text with a gradient. It is, however, possible to achieve this effect by creating a canvas and drawing on it. First we need to declare our custom UI element. In the initiation we need to create a subclass of Layout. In this case, we will use BoringLayout which only supports text with a single line.

Shader textShader=new LinearGradient(0, 0, 0, 20,
    new int[]{bottom,top},
    new float[]{0, 1}, TileMode.CLAMP);//Assumes bottom and top are colors defined above
textPaint.setTextSize(textSize);
textPaint.setShader(textShader);
BoringLayout.Metrics boringMetrics=BoringLayout.isBoring(text, textPaint);
boringLayout=new BoringLayout(text, textPaint, 0, Layout.Alignment.ALIGN_CENTER,
            0.0f, 0.0f, boringMetrics, false);

We then override onMeasure and onDraw:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    setMeasuredDimension((int) textPaint.measureText(text), (int) textPaint.getFontSpacing());
}

@Override
protected void onDraw(Canvas canvas){
    super.onDraw(canvas);
    boringLayout.draw(canvas);
}

Our implementation of onDraw is at this point quite lazy (it completely ignores the measurement specs!, but so long as you guarantee that the view is given sufficent space, it should work okay.

Alternatively, it would be possible to inherit from a Canvas and override the onPaint method. If this is done, then unfortunately the anchor for text being drawn will always be on the bottom so we have to add -textPaint.getFontMetricsInt().ascent() to our y coordinate.

Kevel answered 27/4, 2010 at 5:30 Comment(0)
T
18

Here it is with multiline support as a one liner. This should work for Buttons too.

Shader shader = new LinearGradient(0,0,0,textView.getLineHeight(),
                                  startColor, endColor, Shader.TileMode.REPEAT);
textView.getPaint().setShader(shader);
Twelvetone answered 26/2, 2016 at 22:45 Comment(1)
Works, but for the height you might want to take into account letters that go below the actual line (like y, g, j, etc) and multiply the lineheight by 1.10f, e.g. textView.getLineHeight() * 1.10f.Grilled
H
10

I've rolled up a library that encompasses both of these methods. You can create GradientTextView in XML or just use GradientTextView.setGradient(TextView textView...) to do it on a regular TextView object.

https://github.com/koush/Widgets

Houseboy answered 18/6, 2011 at 0:4 Comment(0)
U
4

Kotlin + coroutines version.

Extension for setting vertical gradient:

private fun TextView.setGradientTextColor(vararg colorRes: Int) {
    val floatArray = ArrayList<Float>(colorRes.size)
    for (i in colorRes.indices) {
        floatArray.add(i, i.toFloat() / (colorRes.size - 1))
    }
    val textShader: Shader = LinearGradient(
        0f,
        0f,
        0f,
        this.height.toFloat(),
        colorRes.map { ContextCompat.getColor(requireContext(), it) }.toIntArray(),
        floatArray.toFloatArray(),
        TileMode.CLAMP
    )
    this.paint.shader = textShader
}

Suspend extension. You need to wait for the view to change its height.

suspend fun View.awaitLayoutChange() = suspendCancellableCoroutine<Unit> { cont ->
val listener = object : View.OnLayoutChangeListener {
    override fun onLayoutChange(
        view: View?,
        left: Int,
        top: Int,
        right: Int,
        bottom: Int,
        oldLeft: Int,
        oldTop: Int,
        oldRight: Int,
        oldBottom: Int
    ) {
        view?.removeOnLayoutChangeListener(this)
        cont.resumeWith(Result.success(Unit))
    }
}

addOnLayoutChangeListener(listener)
cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) }

}

And usage:

lifecycle.coroutineScope.launch {
        binding.tvAmount.text = "Dumb text"
        binding.tvAmount.awaitLayoutChange()
        binding.tvAmount.setGradientTextColor(
            R.color.yellow,
            R.color.green
        )
    }
Unprofitable answered 28/5, 2021 at 13:30 Comment(0)
P
3

A simple but somewhat limited solution would be to use these attributes:

android:fadingEdge="horizontal"
android:scrollHorizontally="true"

I have used it on textfields where I want them to fade out if they get too long.

Parsonage answered 21/4, 2010 at 8:14 Comment(0)
A
3

For Kotlin:

val paint: TextPaint = textView.paint
    val width: Float = paint.measureText(holder.langs.text.toString())

    val textShader: Shader = LinearGradient(0f, 0f, width, holder.langs.textSize, intArrayOf(
            Color.parseColor("#8913FC"),
            Color.parseColor("#00BFFC")), null, Shader.TileMode.CLAMP)
    holder.langs.paint.shader = textShader
Apiculture answered 14/10, 2021 at 6:27 Comment(0)
F
2

Here's my solved way. Implement with text span. screenshot

class LinearGradientForegroundSpan extends CharacterStyle implements UpdateAppearance {
    private int startColor;
    private int endColor;
    private int lineHeight;

    public LinearGradientForegroundSpan(int startColor, int endColor, int lineHeight) {
        this.startColor = startColor;
        this.endColor = endColor;
        this.lineHeight = lineHeight;
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.setShader(new LinearGradient(0, 0, 0, lineHeight,
                startColor, endColor, Shader.TileMode.REPEAT));
    }
}

Styled your gradient text.

    SpannableString gradientText = new SpannableString("Gradient Text");
    gradientText.setSpan(new LinearGradientForegroundSpan(Color.RED, Color.LTGRAY, textView.getLineHeight()),
            0, gradientText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    SpannableStringBuilder sb = new SpannableStringBuilder();
    sb.append(gradientText);
    sb.append(" Normal Text");
    textView.setText(sb);
Filibertofilibuster answered 3/5, 2020 at 14:34 Comment(0)
C
1

Here's a nice way to do it:

/**
 * sets a vertical gradient on the textView's paint, so that on its onDraw method, it will use it.
 *
 * @param viewAlreadyHasSize
 *            set to true only if the textView already has a size
 */
public static void setVerticalGradientOnTextView(final TextView tv, final int positionsAndColorsResId,
        final boolean viewAlreadyHasSize) {
    final String[] positionsAndColors = tv.getContext().getResources().getStringArray(positionsAndColorsResId);
    final int[] colors = new int[positionsAndColors.length];
    float[] positions = new float[positionsAndColors.length];
    for (int i = 0; i < positionsAndColors.length; ++i) {
        final String positionAndColors = positionsAndColors[i];
        final int delimeterPos = positionAndColors.lastIndexOf(':');
        if (delimeterPos == -1 || positions == null) {
            positions = null;
            colors[i] = Color.parseColor(positionAndColors);
        } else {
            positions[i] = Float.parseFloat(positionAndColors.substring(0, delimeterPos));
            String colorStr = positionAndColors.substring(delimeterPos + 1);
            if (colorStr.startsWith("0x"))
                colorStr = '#' + colorStr.substring(2);
            else if (!colorStr.startsWith("#"))
                colorStr = '#' + colorStr;
            colors[i] = Color.parseColor(colorStr);
        }
    }
    setVerticalGradientOnTextView(tv, colors, positions, viewAlreadyHasSize);
}

/**
 * sets a vertical gradient on the textView's paint, so that on its onDraw method, it will use it. <br/>
 *
 * @param colors
 *            the colors to use. at least one should exist.
 * @param tv
 *            the textView to set the gradient on it
 * @param positions
 *            where to put each color (fraction, max is 1). if null, colors are spread evenly .
 * @param viewAlreadyHasSize
 *            set to true only if the textView already has a size
 */
public static void setVerticalGradientOnTextView(final TextView tv, final int[] colors, final float[] positions,
        final boolean viewAlreadyHasSize) {
    final Runnable runnable = new Runnable() {

        @Override
        public void run() {
            final TileMode tile_mode = TileMode.CLAMP;
            final int height = tv.getHeight();
            final LinearGradient lin_grad = new LinearGradient(0, 0, 0, height, colors, positions, tile_mode);
            final Shader shader_gradient = lin_grad;
            tv.getPaint().setShader(shader_gradient);
        }
    };
    if (viewAlreadyHasSize)
        runnable.run();
    else
        runJustBeforeBeingDrawn(tv, runnable);
}

public static void runJustBeforeBeingDrawn(final View view, final Runnable runnable) {
    final OnPreDrawListener preDrawListener = new OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            view.getViewTreeObserver().removeOnPreDrawListener(this);
            runnable.run();
            return true;
        }
    };
    view.getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}

Also, if you wish to use a bitmap of the gradient, instead or a real one, use:

/**
 * sets an image for the textView <br/>
 * NOTE: this function must be called after you have the view have its height figured out <br/>
 */
public static void setBitmapOnTextView(final TextView tv, final Bitmap bitmap) {
    final TileMode tile_mode = TileMode.CLAMP;
    final int height = tv.getHeight();
    final int width = tv.getWidth();
    final Bitmap temp = Bitmap.createScaledBitmap(bitmap, width, height, true);
    final BitmapShader bitmapShader = new BitmapShader(temp, tile_mode, tile_mode);
    tv.getPaint().setShader(bitmapShader);
}

EDIT: Alternative to runJustBeforeBeingDrawn: https://mcmap.net/q/88183/-determining-the-size-of-an-android-view-at-runtime

Consider answered 9/7, 2015 at 8:3 Comment(0)
W
1

The solution that worked for me is to apply a text color before applying any shaders. As the author of the question posted:

After many hours, I found out that I need to call textView.setTextColor() with the first color of the gradient. Then the screenshot:

What works is to have, for instance, a white color setup as text color in the first place. Then we can apply the shader, and it will be applied on top of the white so we will get the desired gradient color.

Walleyed answered 4/5, 2021 at 16:2 Comment(0)
A
0

I've found the way to do this without the TextView class extension.

class MainActivity : AppCompatActivity() {
    private val textGradientOnGlobalLayoutListener = object: ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            textGradient.paint.shader = LinearGradient(0f, 0f,
                    textGradient.width.toFloat(),
                    textGradient.height.toFloat(),
                    color0, color1, Shader.TileMode.CLAMP)
            textGradient.viewTreeObserver.removeOnGlobalLayoutListener(this)
        }
    }
    private val textGradient by lazy {
        findViewById<TextView>(R.id.text_gradient)
    }
    private val color0 by lazy {
        ContextCompat.getColor(applicationContext, R.color.purple_200)
    }
    private val color1 by lazy {
        ContextCompat.getColor(applicationContext, R.color.teal_200)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textGradient.viewTreeObserver.addOnGlobalLayoutListener(textGradientOnGlobalLayoutListener)
    }
}
Acrimony answered 12/12, 2020 at 15:36 Comment(0)
B
0

You can use the below code to draw the Gradient Text View

Sample: https://i.sstatic.net/dzC7d.png

class GradientTextView : AppCompatTextView {
private var gradientShader: LinearGradient? = null
private var paint: Paint? = null

private val gradientColors = intArrayOf(
    context.getColor(R.color.colorGreen), //Start color 
    context.getColor(R.color.colorBlack) // End color
)

constructor(context: Context?) : super(context!!) {
    init()
}

constructor(context: Context?, attrs: AttributeSet?) : super(
    context!!, attrs
) {
    init()
}

constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
    context!!, attrs, defStyleAttr
) {
    init()
}

private fun init() {
    // Set up the paint object
    paint = getPaint()
}

override fun onDraw(canvas: Canvas) {
    val paint = paint
    val width = measuredWidth.toFloat()
    val height = (measuredHeight.toFloat() / 1.4).toFloat()

    if (gradientShader == null) {
        gradientShader = LinearGradient(
            0f, 0f, width, 0f,
            gradientColors,
            null,
            Shader.TileMode.CLAMP
        )
    }

    paint?.shader = gradientShader
    canvas.drawText(text.toString().trim(), 0f, height, paint!!)
}
}

This view can be used the same as the Textview

<GradientTextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/business"
    android:textSize="45sp"/>
Bleak answered 12/6, 2023 at 5:42 Comment(0)
M
-1

Try

import com.sanne.MultiColorTextView;

MultiColorTextView textview= new MultiColorTextView(this);
textview.setText("SOME TEXT");
textview.setTextColor(/*INT ARRAY WITH YOUR COLOURS*/ );

The program sets a gradient colour across the textview and you can also set separate colours for particular text using

multiColorTextView.colorAll("A word");

MutliColorTextView from https://www.github.com/sanneemmanuel/MultiColorTextView

Milkwort answered 12/2, 2023 at 20:2 Comment(1)
Please disclose your affiliation with the linked library in your answer. Not doing so is spam.Dermato

© 2022 - 2024 — McMap. All rights reserved.