07.5 精通自定义 View 之 绘图进阶——Shader 之 LinearGradient

返回自定义 View 目录

通过 LinearGradient 可以实现线性渐变效果。

7.5.1 概述

1. 构造函数

第一个构造函数:

1
2
public LinearGradient(float x0, float y0, float x1, float y1,
int color0, int color1, TileMode tile)

  • (x0,y0) :起始渐变点坐标;(x1,y1) :结束渐变点坐标。
  • color0:起始颜色;color1:终止颜色。颜色值必须使用 0xAARRGGBB 形式的 16 进制表示,表示透明度的 AA 一定不能少。
  • TileMode tile:与 BitmapShader 一样,用于指定控件区域大于指定的渐变区域时,空白区域的颜色填充方法。

第二个构造函数:

1
2
public LinearGradient(float x0, float y0, float x1, float y1,
int colors[], float positions[], TileMode tile)

  • colors[]:用于指定渐变的颜色值数组。同样,颜色值必须使用 0xAARRGGBB 形式的 16 进制表示,表示透明度的 AA 一定不能少。
  • positions[]:与渐变的颜色相对应,取值是 0-1 的 float 类型,表示在每一个颜色在整条渐变线中的百分比位置

2. 双色渐变使用示例

自定义一个从左到右的双色渐变控件,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestView extends View {
private Paint mPaint;
private int mWidth, mHeight;
private LinearGradient mLinearGradient;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
mLinearGradient = new LinearGradient(0, mHeight / 2f, mWidth, mHeight / 2f,
0xFFFF0000, 0xFF00FF00, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
}
}

3. 多色渐变使用示例

在上面示例的基础上,使用多色渐变来构造 LinearGradient 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestView extends View {
private Paint mPaint;
private int mWidth, mHeight;
private LinearGradient mLinearGradient;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
int[] colors = {0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFF00FFFF};
float[] pos = {0f, 0.2f, 0.4f, 0.6f, 1.0f};
mLinearGradient = new LinearGradient(0, mHeight / 2f, mWidth, mHeight / 2f,
colors, pos, Shader.TileMode.CLAMP);
mPaint.setShader(mLinearGradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
}
}

效果如下图所示。

从这里可以看出,渐变的开始点同样是控件左边中点,渐变的结束点也同样是控件右边中点;这里我们指定了五种渐变颜色,而且指定了每个颜色的位置,前四种颜色是按 20% 均匀分布的,最后两种颜色相距 40%;最后通过 canvas.drawRect() 函数把整个控件区域画出来。

注意:colors 和 pos 的个数一定要相等,也就是说必须指定每一个颜色值的位置。如果元素个数不相等,则会直接报错,如下图所示。

4. TileMode 填充模式

从构造函数中可以看出,LiearGradient 只有一个 TileMode 参数,这说明 X 轴与 Y 轴共用这一个 TileMode 填充参数,而不能像 BitmapShader 那样分别指定 X 轴与 Y 轴的填充参数。

示例:分别指定不同的填充模式

从上到下依次为:CLAMP、MIRROR、REPEAT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestView extends View {
private Paint mPaint;
private int mWidth, mHeight;
private LinearGradient mGradientCLAMP, mGradientMIRROR, mGradientREPEAT;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = 200;
int[] colors = {0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFF00FFFF};
float[] pos = {0f, 0.2f, 0.4f, 0.6f, 1.0f};
mGradientCLAMP = new LinearGradient(0, 0, mWidth / 2f, mHeight / 2f,
colors, pos, Shader.TileMode.CLAMP);
mGradientMIRROR = new LinearGradient(0, mHeight, mWidth / 2f, mHeight * 3f / 2,
colors, pos, Shader.TileMode.MIRROR);
mGradientREPEAT = new LinearGradient(0, mHeight * 2f, mWidth / 2f, mHeight*5 / 2f,
colors, pos, Shader.TileMode.REPEAT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setShader(mGradientCLAMP);
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
mPaint.setShader(mGradientMIRROR);
canvas.drawRect(0, mHeight, mWidth, mHeight * 2, mPaint);
mPaint.setShader(mGradientREPEAT);
canvas.drawRect(0, mHeight * 2, mWidth, mHeight * 3, mPaint);
}
}

5. Shader 填充与显示区域

所有 Shader 都是一样的:Shader 的布局和显示是分离的;Shader 总是从控件的左上角开始布局的;如果单张图片无法覆盖整个控件,则会使用 TileMode 重复模式来填充空白区域;而 canvas.draw 系列函数则只表示哪部分区域被显示出来。

下面利用 drawText() 函数实现一个渐变文字效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestView extends View {
private Paint mPaint;
private String mText;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mPaint.setTextSize(80);
mText = "欢迎关注先先生的Blog";
float width = mPaint.measureText(mText);
int[] colors = {0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFFFFFF00, 0xFF00FFFF};
float[] pos = {0f, 0.2f, 0.4f, 0.6f, 1.0f};
LinearGradient gradient = new LinearGradient(0, 0, width / 2f, 0,
colors, pos, Shader.TileMode.MIRROR);
mPaint.setShader(gradient);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mText, 0, 100, mPaint);
}
}

7.5.2 示例:闪光文字效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class TestView extends AppCompatTextView {
private Paint mPaint;
private LinearGradient mLinearGradient;
private int mDx;
private Matrix mMatrix;
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = getPaint();
mMatrix = new Matrix();
int length = (int) mPaint.measureText(getText().toString());
createAnim(length);
createLinearGradient(length);
}
private void createAnim(int length) {
ValueAnimator animator = ValueAnimator.ofInt(0, 2 * length);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mDx = (Integer) animation.getAnimatedValue();
postInvalidate();
}
});
animator.setRepeatMode(ValueAnimator.RESTART);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setDuration(2000);
animator.start();
}
private void createLinearGradient(int length) {
mLinearGradient = new LinearGradient(-length, 0, 0, 0,
new int[]{getCurrentTextColor(), 0xFF00FF00, getCurrentTextColor()},
new float[]{0, 0.5f, 1},
Shader.TileMode.CLAMP);
}
@Override
protected void onDraw(Canvas canvas) {
mMatrix.reset();
mMatrix.setTranslate(mDx, 0);
mLinearGradient.setLocalMatrix(mMatrix);
mPaint.setShader(mLinearGradient);
super.onDraw(canvas);
}
}

在 res/layout/act_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.xxt.xtest.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:textSize="24sp"
android:text="欢迎关注先先生的Blog"/>
</LinearLayout>

控件派生自 TextView 的子类,所以可以使用 TextView 的自带方法 getCurrentTextColor() 来获取文字颜色、画笔、文字长度等;利用 Shader.setLocalMatrix(Matrix localM) 设置逐渐平移的矩阵,最后用 ValueAnimator 来控制矩阵平移的位移 mDx,移动距离是从 0 到两倍的 text 距离。