Canvas / Gradient / Shader / Text

Gradient Text

Recently there was a comment on the article on Text Shadows asking how to fill text with a gradient. In this article we’ll look at a simple technique for doing precisely that.

Creating text filled with a gradient is actually really easy, although it’s not immediately obvious how to do it in the same was as simply changing the text colour. The trick is to use a Shader, or more specifically a LinearGradient (which extends Shader), to do all of the work for us.

The constructor for LinearGradient that we’ll use takes seven arguments, although there is also a more complex form which allows for multiple colour steps to be defined. The first four of these represent the start x & y and end x & y coordinates of the line along which the gradient will be drawn (this effectively controls the direction of the gradient). The next two arguments represent the start and end colours of the gradient, and the final argument dictates how areas outside of the rectangle defined in the first four arguments will be drawn.

So how do we go about applying this? In its simplest form, we can get a reference to a standard TextView object, and set the Shader on it’s Paint object:

Shader myShader = new LinearGradient( 
    0, 0, 0, 100, 
    Color.WHITE, Color.BLACK, 
    Shader.TileMode.CLAMP );
textview.getPaint().setShader( myShader );

The TileMode that we’re using means that the colour of areas outside of the rectangle (0, 0) – (0, 100) will match the colour of the closest edge of that rectangle – i.e. extending the colour of the edges of the rectangle.

While this will certainly work, it does suffer from the small drawback that the gradient will need to be static dimensions which are completely detached from the size of the text within the TextView. So what if we want the dimensions of the gradient to actually match our text? We simply need to extend TextView.

Now the obvious thought is to extend TextView and simply override onDraw() to create a new LinearGradient which matches the dimensions of the textView control each time the control is drawn. However this is rather inefficient because it means instantiating a Shader object each time onDraw is called. Object instantiation is a rather expensive business and should be avoided at all costs in your onDraw method. If you perform object instantiation in your onDraw you’ll find that your frame rates suffer badly as a result.

So if we don’t do it in onDraw, then where should we do it? The answer is quite obvious when we think about how often we actually need to create a new LinearGradient: only when the size of the TextView changes. Therefore, the most efficient place to do this is actually in onLayout:

public class GradientTextView extends TextView
{
    public GradientTextView( Context context )
    {
        super( context, null, -1 );
    }
    public GradientTextView( Context context, 
        AttributeSet attrs )
    {
        super( context, attrs, -1 );
    }
    public GradientTextView( Context context, 
        AttributeSet attrs, int defStyle )
    {
        super( context, attrs, defStyle );
    }

    @Override
    protected void onLayout( boolean changed, 
        int left, int top, int right, int bottom )
    {
        super.onLayout( changed, left, top, right, bottom );
        if(changed)
        {
            getPaint().setShader( new LinearGradient( 
                0, 0, 0, getHeight(), 
                Color.WHITE, Color.BLACK, 
                Shader.TileMode.CLAMP ) );
        }
    }
}

So, whenever the layout changes we create and set a new LinearGradient based on the height of the TextView.

Running this gives us the following:

gradient_text

This same technique can be used to apply different kinds of gradients (LinearGradient, RadialGradient, SweepGradient), filling text with a bitmap (BitmapShader), or even combining shaders (ComposeShader). The more adventurous can even create their own custom shaders!

While we’ve only covered a really simple example here, we have covered a very useful technique which is pretty easy to implement, and can give us some impressive results.

The source code for this article is available here.

© 2013 – 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

5 Comments

  1. Does this method works in Android Ice Cream Sandwich?
    I used a similar concept using shader and linear gradient to display a gradient. But its not being displayed in Ice Cream Sandwich.

  2. If someone uses Android Studio and latest SDK (I use 2.1 alpha and SDK 23), there might be warning that “you should avoid object allocations during draw/layout operations”. To avoid this simply do following:

    Create method:
    private LinearGradient getLinearGradient(int height) {
    return new LinearGradient(0, 0, 0, height, colorStart, colorEnd, Shader.TileMode.CLAMP);
    }

    and change onLayout like this:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (changed)
    getPaint().setShader(getLinearGradient(getHeight()));
    }

    It’ll do the same as from this post, but will not generate warning, which can be stressful, especially when someone still wants to have gradients for Texts. 🙂

    The problem is that, when constructor finishes it’s job (I wanted to init LinearGradient there), it doesn’t know it’s height yet. Height is determined later in view’s lifecycle. Sure, there are another ways to get rid of those annoying Studio warnings, but this is the quickest. I’ve tested it on more than 10000 items in a RecyclerView and it still runs like a charm.

    Thanx for this, tho.

Leave a Reply to shivam gupta Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.