Android 线性颜色渐变

207 阅读5分钟

最近遇到一个需要用到线性颜色渐变的需求,而且后期还可能改为颜色“闪动”的效果。
预期效果如下:

textgradient.png

于是就去研究了一下线性颜色渐变,这里做下总结。

实现线性颜色渐变,有四种方式:

1.自定义View继承自TextView,获取View 的Paint对象,并给Paint对象设置渐变。

2.用canvas#drawText方法,在onDraw方法中设置渐变并绘制。

3.用StaticLayout实现多行文本颜色渐变。

4.用DynamicLayout实现多行文本颜色渐变。

下面详细说明这四种方式:

1. 直接获取Paint对象,并给Paint设置LinearGradient

    public class LinearGradientTextView extends TextView {
    private LinearGradient mLinearGradient;
    private Paint mPaint;
    private int mViewWidth = 0;

    public LinearGradientTextView1(Context context) {
        this(context, null);
    }

    public LinearGradientTextView1(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LinearGradientTextView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();

            mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                    new int[]{0xFFFA3DB3, 0xFF3D53FB}, null,
                    Shader.TileMode.REPEAT);

            mPaint = getPaint();
            mPaint.setShader(mLinearGradient);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        setGravity(Gravity.LEFT);
        super.onDraw(canvas);
    }
}    

运行效果如下图:

getPaintGradient.png

代码很简单,就是在onSizeChanged获取mPaint, 并给mPaint设置线性渐变,然后在onDraw 方法里绘制出来。如果你只是想在TextView中显示渐变颜色的文本,这种方式是最简单的。

2. Canvas#drawText实现颜色渐变

这种方式更多用于自定义绘图或者进行图片处理时绘制文字。当然也可以用于TextView 绘制渐变文本。下面给出的例子是在ImageView中绘制颜色渐变的文本:

    public class GradientImageView extends ImageView {
	private LinearGradient mLinearGradient;
	private Paint mPaint;
	private int mViewWidth = 0;
	private String mSrcString;

	public GradientImageView(Context context) {
    	this(context, null);
	}

	public GradientImageView(Context context, @Nullable AttributeSet attrs) {
    	this(context, attrs, 0);
	}

	public GradientImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    	super(context, attrs, defStyleAttr);
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    	super.onSizeChanged(w, h, oldw, oldh);
    	if (mViewWidth == 0) {
        	mViewWidth = getMeasuredWidth();
        	mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                new int[]{0xFFFA3DB3, 0xFF3D53FB}, null,
                Shader.TileMode.REPEAT);

        	mPaint = new Paint();
        	mPaint.setTextSize(ScreenUtil.dpToPx(getResources(), 16));
        	mPaint.setShader(mLinearGradient);

        	mSrcString = "there are several linearGradient lines:+" + "\n" +
						"This is the first line of gradient text" + "\n" +
						"this is the second line of gradient text";
    	}
	}

	@Override
	protected void onDraw(Canvas canvas) {
    	super.onDraw(canvas);
    	canvas.drawText(mSrcString, 10, 300, mPaint);
	}
}

运行结果如下图:

drawTextGradient.png

需要注意的是,这种方式实现的渐变文本,是不能换行的。也就是说,不管文本有多长,都只能一行显 示(例子中就是把三行的字符串显示为一行)

3. 用StaticLayout实现多行文本颜色渐变

StaticLayout是一个用来处理文本换行的控件,可以用它来实现多行文本颜色渐变。
StaticLayout常用的构造函数是这个:

    public StaticLayout (CharSequence source, 
                TextPaint paint, 
                int width, 
                Layout.Alignment align, 
                float spacingmult, 
                float spacingadd, 
                boolean includepad)  

还有两个不常用的:

public StaticLayout(CharSequence source, 
    int bufstart, int bufend, 
    TextPaint paint, int outerwidth, 
    Layout.Alignment align, float spacingmult, 
    float spacingadd, boolean includepad) 	

public StaticLayout(CharSequence source, i
    nt bufstart, int bufend, 
    TextPaint paint, int outerwidth, 
    Layout.Alignment align, float spacingmult, 
    float spacingadd, boolean includepad, 
    TextUtils.TruncateAt ellipsize, int ellipsizedWidth)

在android api 28 之后,上面几个构造函数都将被废弃,将使用StaticLayout.Builder替代。StaticLayout.Builder的用法也很简单,就是先调用 StaticLayout#obtain方法构造StaticLayout.Builder对象,再调用Builder对象的一系列seter方法,最后调用build()方法。

详细请看官方文档: StaticLayout.Builder用法

下面说明一下各个参数的含义:
source: 要显示的文本。
bufstart:要处理文本的开始字符位置。
bufend:要处理文本的结束字符位置。
paint:画笔对象。
width:文本宽度,超过这个区域会自动换行。
outerwidth:换行宽度,超过这个宽度会自动换行。
align:对齐方式。
spacingmult:行间距,通常设置为1.0f,设置了这个值后,行间距将变为默认间距乘以这个数值,如1.5 表示1.5倍行间距。
spacingadd:行间距增加值,最终的行间距 = 默认间距 * spacingmult + spacingadd。
includepad:设置是否包括超出字体上升和下降的额外空间,一般设置为true,设置了true之后,文本会 垂直居中显示,可避免在某些多语言下,文案被裁剪。
ellipsize: 当文本超出区域或者行数超出限制时,省略的显示位置。
ellipsizedWidth:显示省略的那一行可显示文本的宽度,设置0则那一行显示为 ...

下面进入正题,看下怎么用 StaticLayout显示多行颜色渐变,主要代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (mViewWidth == 0) {
        mViewWidth = getMeasuredWidth();

        mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
                new int[]{0xFFD70F00, 0xFFD53A02, 0xFFDB9501, 0xFF3C9B03, 0xFF04C0AF, 					0xFF020098, 0xFF4C0177}, null,
                Shader.TileMode.REPEAT);

        mSrcString = getResources().getString(R.string.home_lineargradient_text);
        mTextPaint = new TextPaint();
        mTextPaint.setTextSize(ScreenUtil.dpToPx(getResources(), 20));
        mTextPaint.setShader(mLinearGradient);

        mStaticLayout = new StaticLayout(mSrcString, 0, 120, mTextPaint, mViewWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true,
                TextUtils.TruncateAt.END,
                ScreenUtil.dpToPx(getResources(), 10));
    }
}

@Override
protected void onDraw(Canvas canvas) {
    setGravity(Gravity.LEFT);
    mStaticLayout.draw(canvas);
}

在创建StaticLayout的时候,把mLinearGradient设置进去,再在onDraw方法中调用 StaticLayout#draw方法,就能实现多行的颜色渐变。
这里需要注意的是,StaticLayout只能处理那些不能被再次编辑的文本,也就是说它处理的文本是固定的,不能变化的,如果要处理可以变化的文本,请使用DynamicLayout。

运行效果如下图所示:

staticlayoutgradient.png

4. 用DynamicLayout实现线性颜色渐变

DynamicLayout也是一个处理文本换行的控件,它和StaticLayout的用法几乎一模一样。
区别就是,DynamicLayout可以处理可编辑的文本,也就是说它处理的文本可以改变。
关于它的构造方法及参数的说明请参见StaticLyaout,此处不再赘述。

在DynamicLayout的构造方法的源码中,有这么一段代码(只截取关键代码)

    public DynamicLayout(CharSequence base, CharSequence display,
                     TextPaint paint,
                     int width, Alignment align, TextDirectionHeuristic textDir,
                     float spacingmult, float spacingadd,
                     boolean includepad, int breakStrategy, int hyphenationFrequency,
                     int justificationMode, TextUtils.TruncateAt ellipsize,
                     int ellipsizedWidth) {
                       .......
       if (base instanceof Spannable) {
            if (mWatcher == null)
                mWatcher = new ChangeWatcher(this);

            // Strip out any watchers for other DynamicLayouts.
            Spannable sp = (Spannable) base;
            ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
            for (int i = 0; i < spans.length; i++)
                sp.removeSpan(spans[i]);

            sp.setSpan(mWatcher, 0, base.length(),
                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
       }               
}

当传入构造方法的base是实现了 Spannable接口的实例时,DynamicLayout会自动给这个base设置ChangeWatcher监听器。ChangeWatcher实际上就是一个TextWatcher接口,用于监听base序列的改变。 当base改变时,ChangeWatcher#onSpanChanged方法会回调,然后去刷新DynamicLayout的布局。这也就是DynamicLayout能处理可变化文本的原因。

说回正题,用DynamicLayout实现多行渐变文本也很简单,关键代码如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (mViewWidth == 0) {
        mViewWidth = getMeasuredWidth();

        mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
             new int[]{0xFFD70F00, 0xFFD53A02, 0xFFDB9501, 0xFF3C9B03, 0xFF04C0AF, 					0xFF020098, 0xFF4C0177}, null,
                Shader.TileMode.REPEAT);

        mSrcString = getResources().getString(R.string.home_lineargradient_text);
        mSpannableStringBuilder = new SpannableStringBuilder(mSrcString+"\n");
        mTextPaint = new TextPaint();
        mTextPaint.setTextSize(ScreenUtil.dpToPx(getResources(), 16));
        mTextPaint.setShader(mLinearGradient);

        mDynamicLayout = new DynamicLayout(mSpannableStringBuilder, mTextPaint, 				mViewWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mSpannableStringBuilder.append("crazy English liyang !" + "\n");
                postInvalidate();
            }
        });
    }
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.translate(10, 300);
    mDynamicLayout.draw(canvas);
}

代码中给这个View设置了点击监听,当点击这个View时,它的文本会增加一行,其他的代码和StaticLayout的相似,不做介绍了。运行结果如下:

DynamicLayoutGradient.png

当点击图片后,会出现多一行文本,如图:

DynamicLayoutGradientClick.png

知识拓展

上面总结了四种实现线性颜色渐变的方式,但是都只能实现“静止”的颜色渐变,没法实现“动态”的渐变。为了以后 能 反手给设计师一巴掌 (不,是满足设计师的要求),下面说明如何实现“动态的渐变”。
思路也简单,就是让“静止的渐变”每隔一定时间,就位移一定距离,然后刷新重绘,这样就有动态的效果了。

下面看看关键代码:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    if (mViewWidth == 0) {
        mViewWidth = getMeasuredWidth();

        mLinearMatrix = new Matrix();
        mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0,
        	new int[]{0xFFD70F00, 0xFFD53A02, 0xFFDB9501, 0xFF3C9B03, 0xFF04C0AF, 					0xFF020098, 0xFF4C0177}, null,
            Shader.TileMode.REPEAT);

        mSrcString = getResources().getString(R.string.home_lineargradient_text);
        mTextPaint = new TextPaint();
        mTextPaint.setTextSize(ScreenUtil.dpToPx(getResources(), 20));
        mTextPaint.setShader(mLinearGradient);

        mStaticLayout = new StaticLayout(mSrcString, mTextPaint, mViewWidth, 					Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true);
    }
}

@Override
protected void onDraw(Canvas canvas) {
    if (mLinearMatrix != null) {
        mTranslate += mViewWidth / 5;
        if (mTranslate > 2 * mViewWidth) {
            mTranslate = -mViewWidth;
        }
        mLinearMatrix.setTranslate(mTranslate, 0);
        mLinearGradient.setLocalMatrix(mLinearMatrix);

        canvas.translate(0, 10);
        mStaticLayout.draw(canvas);
        postInvalidateDelayed(50);
    }
}

这里通过Matrix对象,把位移每隔50毫秒设置给LinearGradient对象,然后再刷新重绘。这里要注意,要给mTranslate设置一个范围(代码中是设置 2*mViewWidth),不然mTranslate的值一直增大,可能造成数值溢出。

运行效果如下:

dongtaigif.gif

以上就是我所总结的全部内容,有不同意见的朋友,欢迎交流指教。