高级UI之画板Canvas

Canvas可以用来绘制直线、点、几何图形、曲线、Bitmap、圆弧等等,做出很多很棒的效果,例如QQ的消息气泡就是使用Canvas画的

Canvas中常用的方法

  • 初始化参数

    1
    2
    3
    4
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    paint.setStyle(Paint.Style.FILL);
    paint.setStrokeWidth(8);
  • 绘制直线

    1
    canvas.drawLine(0, 0, 100, 100, paint);
  • 绘制一组直线

    1
    2
    float[] lines = {0, 0, 100, 100, 200, 200, 300, 300};
    canvas.drawLines(lines,paint);
  • 绘制点

    1
    canvas.drawPoint(100, 100, paint);
  • 绘制矩形

    1
    2
    3
    Rect rect = new Rect(0, 0, 200, 100);
    canvas.drawRect(rect, paint);
    //canvas.drawRect(0, 0, 200, 100, paint);
  • 绘制圆角矩形

    1
    2
    RectF rectF = new RectF(100, 100, 300, 200);
    canvas.drawRoundRect(rectF, 20, 20, paint);
  • 绘制圆形

    1
    2
    canvas.drawCircle(300, 300, 200, paint);
    canvas.drawOval(100, 100, 300, 200, paint);
  • 绘制弧度

    1
    2
    RectF rectF = new RectF(100, 100, 300, 200);
    canvas.drawArc(rectF, 0, 90, true, paint);

使用Path参与绘制

  • 绘制直线

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //使用Path
    Path path = new Path();
    //落笔位置
    path.moveTo(100, 100);
    //移动
    path.lineTo(200, 100);
    path.lineTo(200, 200);
    //闭合线
    path.close();
    //按路径绘制
    canvas.drawPath(path, paint);
  • 绘制其他线条,使用path.addXxx()

    1
    2
    3
    4
    float[] radii = {10, 10, 20, 30, 40, 40, 60, 50};
    RectF rectF = new RectF(100, 100, 600, 500);
    path.addRoundRect(rectF, radii, Path.Direction.CCW);
    canvas.drawPath(path, paint);

使用Region区域绘制

1
2
3
4
5
6
7
//创建一块矩形区域
Region region = new Region(100, 100, 500, 400);
RegionIterator iterator = new RegionIterator(region);
Rect rect = new Rect();
while (iterator.next(rect)) {
canvas.drawRect(rect, paint);
}

以上只是画出一个矩形,另外并没有什么现象,这是因为只有一个Region
两个Region实现取交集,使用并且不断分割交集部分

1
2
3
4
5
6
7
8
9
10
11
Path path = new Path();
RectF rectF = new RectF(100, 100, 600, 800);
path.addOval(rectF, Path.Direction.CCW);
Region region1 = new Region();
Region region2 = new Region(100, 100, 500, 400);
region1.setPath(path, region2);
RegionIterator iterator = new RegionIterator(region1);
Rect rect = new Rect();
while (iterator.next(rect)) {
canvas.drawRect(rect, paint);
}

以上执行结果如下

Region的其他操作
并集:region.union(r);
交集:region.op(r, Op.INTERSECT);

Canvas的细节问题

当canvas执行drawXXX的时候就会新建一个新的画布图层
虽然新建一个画布图层,但是还是会沿用之前设置的平移变换,不可逆的(save和restore来解决)
之所以这样设计,是考虑到了绘制复杂图形的时候,可能会变换画布位置,那么就会造成之前绘制的图像发生错位,导致前功尽弃

Canvas变换

平移(Translate)

1
2
3
4
5
6
Rect rect = new Rect(100, 100, 800, 1000);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.translate(100, 100);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

缩放(Scale)

1
2
3
4
5
6
Rect rect = new Rect(100, 100, 600, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.scale(1.2F, 1.5F);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

旋转(Rotate)

1
2
3
4
5
6
7
Rect rect = new Rect(400, 100, 700, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.rotate(25);//默认围绕原点
//canvas.rotate(25, 400, 100);//指定旋转点
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

斜拉画布(Skew)

1
2
3
4
5
6
Rect rect = new Rect(100, 100, 400, 800);
paint.setColor(Color.RED);
canvas.drawRect(rect, paint);
canvas.skew(0.5F, 0);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);

裁剪画布(clip)

1
2
3
4
5
6
RectF rectF = new RectF(100, 100, 500, 800);
paint.setColor(Color.RED);
canvas.drawRect(rectF, paint);
paint.setColor(Color.GREEN);
canvas.clipRect(new Rect(200, 200, 400, 400));
canvas.drawColor(Color.BLUE);

变换操作的影响

当对画板进行操作以后,会对后续的画布操作造成影响,那么要实现不对后续操作就需要在操作前保存画布,需要时候恢复画布

1
2
3
canvas.save(); //保存当前画布
···//画布操作
canvas.restore();//恢复保存的画布

canvas实际上是保存到画布栈里面去了,每一次保存,就使得一个当前画布入栈,每一次恢复,就有一个canvas出栈
因此,一般来说保存和恢复是成对出现的

自定义Drawable动画

Drawable就是一个可画的对象,其可能是一张位图(BitmapDrawable),也可能是一个图形(ShapeDrawable),还有可能是一个图层(LayerDrawable),我们根据画图的需求,创建相应的可画对象,就可以将这个可画对象当作一块“画布(Canvas)”,在其上面操作可画对象,并最终将这种可画对象显示在画布上,有点类似于”内存画布”
自定义Drawable

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class RevealDrawableView extends Drawable {

private int orientation;
private Drawable selected;
private Drawable unselected;
private int widthLeft;
private int heightLeft;
private int widthRight;
private int heightRight;
public static final int HORIZONTAL = 1;
public static final int VERTICAL = 2;

public RevealDrawableView(Drawable unselected, Drawable selected, int orientation) {
this.unselected = unselected;
this.selected = selected;
this.orientation = orientation;
}

@Override
public void draw(@NonNull Canvas canvas) {

//得到当前level,转化成百分比
int level = getLevel();
if (level == 10000 || level == 0) { //滑出画入状态
unselected.draw(canvas);
} else if (level == 5000) { //在正中间状态
selected.draw(canvas);
} else {
Rect bounds = getBounds();//得到当前Drawable自身的矩形区域
float ratio = (level / 5000F) - 1F;//得到比例-1~+1
int width = bounds.width();
int height = bounds.height();
widthLeft = widthRight = width;
heightLeft = heightRight = height;
//得到左右宽高
if (orientation == HORIZONTAL) {
widthLeft = (int) (width * Math.abs(ratio));
widthRight = width - widthLeft;
}
if (orientation == VERTICAL) {
heightLeft = (int) (height * Math.abs(ratio));
heightRight = height - heightLeft;
}
int gravity = ratio < 0 ? Gravity.LEFT : Gravity.RIGHT;
//得到当前左边区域
Rect rectLeft = new Rect();
//抠图位置,宽度,高度,目标,输出位置
Gravity.apply(gravity, widthLeft, heightLeft, bounds, rectLeft);
canvas.save();//保存画布
canvas.clipRect(rectLeft);//剪切画布
unselected.draw(canvas);//画未选中图片
canvas.restore();//恢复画布

//得到右边矩形区域
gravity = ratio < 0 ? Gravity.RIGHT : Gravity.LEFT;
Rect rectRight = new Rect();
//抠图位置,宽度,高度,目标,输出位置
Gravity.apply(gravity, widthRight, heightRight, bounds, rectRight);
canvas.save();
canvas.clipRect(rectRight);
selected.draw(canvas);//画选中图片
canvas.restore();
}
}

@Override
protected void onBoundsChange(Rect bounds) {
//设置矩形
unselected.setBounds(bounds);
selected.setBounds(bounds);
}

@Override
public int getIntrinsicWidth() {
//得到Drawable的实际宽度
return Math.max(unselected.getIntrinsicWidth(), selected.getIntrinsicWidth());
}

@Override
public int getIntrinsicHeight() {
//得到Drawable的实际高度
return Math.max(unselected.getIntrinsicHeight(), selected.getIntrinsicHeight());
}

@Override
protected boolean onLevelChange(int level) {
//每次level改变时刷新
invalidateSelf();
return true;
}

@Override
public void setAlpha(int alpha) {

}

@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {

}

@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}

自定义控件

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class GallaryHScrollView extends HorizontalScrollView implements OnTouchListener {

private LinearLayout container;
private int centerX;
private int width;

public GallaryHScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public GallaryHScrollView(Context context) {
super(context);
init();
}

private void init() {
//ScrollView水平滑动,存在大量ImageView
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
container = new LinearLayout(getContext());
container.setLayoutParams(params);
setOnTouchListener(this);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//得到某一张图片的宽度
View view = container.getChildAt(0);
width = view.getWidth();
//得到中间x坐标
centerX = getWidth() / 2;
//中心坐标改为中心图片的左边界
centerX = centerX - width / 2;
//给LinearLayout和hzv之间设置边框距离
container.setPadding(centerX, 0, centerX, 0);
}

@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
//渐变图片
reveal();
}
return false;
}

private void reveal() {
// 渐变效果
//得到滑出去的距离
int scrollX = getScrollX();
//找到两张渐变的图片的下标--左,右
int index_left = scrollX / width;
int index_right = index_left + 1;
//设置图片的level
for (int i = 0; i < container.getChildCount(); i++) {
if (i == index_left || i == index_right) {
//变化
float ratio = 5000f / width;//比例
ImageView iv_left = (ImageView) container.getChildAt(index_left);
//scrollX%icon_width:代表滑出去的距离
iv_left.setImageLevel((int) (5000 - scrollX % width * ratio));
//右边
if (index_right < container.getChildCount()) {
ImageView iv_right = (ImageView) container.getChildAt(index_right);
//scrollX%icon_width:代表滑出去的距离
//滑出去了icon_width/2 icon_width/2%icon_width
iv_right.setImageLevel((int) (10000 - scrollX % width * ratio));
}
} else {
//灰色
ImageView iv = (ImageView) container.getChildAt(i);
iv.setImageLevel(0);
}
}
}

//添加图片的方法
public void addImageViews(Drawable[] revealDrawables) {
for (int i = 0; i < revealDrawables.length; i++) {
ImageView img = new ImageView(getContext());
img.setImageDrawable(revealDrawables[i]);
container.addView(img);
if (i == 0) {
img.setImageLevel(5000);
}
}
addView(container);
}
}

测试工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SourceUtils {
private static int[] mImgIds = new int[]{
R.drawable.avft, R.drawable.box_stack, R.drawable.bubble_frame,
R.drawable.bubbles, R.drawable.bullseye, R.drawable.circle_filled,
R.drawable.circle_outline, R.drawable.avft, R.drawable.box_stack,
R.drawable.bubble_frame, R.drawable.bubbles, R.drawable.bullseye,
R.drawable.circle_filled, R.drawable.circle_outline
};
private static int[] mImgIds_active = new int[]{
R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active,
R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active,
R.drawable.circle_outline_active, R.drawable.avft_active, R.drawable.box_stack_active,
R.drawable.bubble_frame_active, R.drawable.bubbles_active, R.drawable.bullseye_active,
R.drawable.circle_filled_active, R.drawable.circle_outline_active
};

public static int[] getImgIds() {
return mImgIds;
}

public static int[] getImgIdsActive() {
return mImgIds_active;
}
}

布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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">

<com.cj5785.testcanvas.GallaryHScrollView
android:id="@+id/ghs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="@android:color/darker_gray"
android:scrollbars="none" />

</LinearLayout>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RevealDrawableActivity extends AppCompatActivity {

private Drawable[] revealDrawables;
private GallaryHScrollView gallaryHScrollView;
protected int level = 10000;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_reveal_drawable);
revealDrawables = new Drawable[SourceUtils.getImgIds().length];
for (int i = 0; i < SourceUtils.getImgIds().length; i++) {
RevealDrawableView rd = new RevealDrawableView(
getResources().getDrawable(SourceUtils.getImgIds()[i]),
getResources().getDrawable(SourceUtils.getImgIdsActive()[i]),
RevealDrawableView.HORIZONTAL);
revealDrawables[i] = rd;
}
gallaryHScrollView = (GallaryHScrollView) findViewById(R.id.ghs);
gallaryHScrollView.addImageViews(revealDrawables);
}
}

效果

自定义控件Search动画

自定义View控件

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 SearchView extends View {
private Paint paint;
private BaseController controller;

public SearchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
paint = new Paint();
paint.setStrokeWidth(8);
}

public void setController(BaseController controller) {
this.controller = controller;
controller.setSearchView(this);
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
controller.draw(canvas, paint);
}

public void startAnimation() {
if (controller != null) {
controller.startAnim();
}
}

public void resetAnimation() {
if (controller != null) {
controller.resetAnim();
}
}
}

通过控制器来控制绘画

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
public abstract class BaseController {

public static final int STATE_ANIM_RESET = 0;
public static final int STATE_ANIM_START = 1;
public int state = STATE_ANIM_RESET;

public float progress = -1;
private SearchView searchView;

public abstract void draw(Canvas canvas, Paint paint);

public void startAnim() {

}

public void resetAnim() {

}

public int getWidth() {
return searchView.getWidth();
}

public int getHeight() {
return searchView.getHeight();
}

public ValueAnimator startValueAnimation() {
final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(5000);
animator.setInterpolator(new AnticipateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
progress = (float) animator.getAnimatedValue();
searchView.invalidate();
}
});
animator.start();
progress = 0;
return animator;
}

public void setSearchView(SearchView searchView) {
this.searchView = searchView;
}
}

实现控制器方法

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class MyController extends BaseController {

private int color = Color.GREEN;
private int cx, cy, cr;
private RectF rectF;
private int j = 15;

public MyController() {
rectF = new RectF();
}

@Override
public void draw(Canvas canvas, Paint paint) {
canvas.drawColor(color);
switch (state) {
case STATE_ANIM_START:
drawStartAnimView(canvas, paint);
break;
case STATE_ANIM_RESET:
drawResetAnimView(canvas, paint);
break;
}
}

private void drawStartAnimView(Canvas canvas, Paint paint) {
canvas.save();
if (progress <= 0.5f) {
//绘制圆和把手
canvas.drawArc(rectF, 45, 360 * (progress * 2 - 1),
false, paint);
canvas.drawLine(rectF.right - j, rectF.bottom - j,
rectF.right + cr - j, rectF.bottom + cr - j, paint);
} else {
//绘制把手
canvas.drawLine(
rectF.right - j + cr * (progress * 2 - 1),
rectF.bottom - j + cr * (progress * 2 - 1),
rectF.right - j + cr, rectF.bottom + cr - j, paint);
}
//绘制下面的横线
canvas.drawLine(
(rectF.right - j + cr) * (1 - progress * 0.8f), rectF.bottom + cr - j,
rectF.right - j + cr, rectF.bottom + cr - j, paint);
canvas.restore();
rectF.left = cx - cr + progress * 250;
rectF.right = cx + cr + progress * 250;
rectF.top = cy - cr;
rectF.bottom = cy + cr;
}

private void drawResetAnimView(Canvas canvas, Paint paint) {
cr = getWidth() / 20;
cx = getWidth() / 2;
cy = getHeight() / 2;
rectF.left = cx - cr;
rectF.right = cx + cr;
rectF.top = cy - cr;
rectF.bottom = cy + cr;
canvas.save();
paint.reset();
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
canvas.rotate(45, cx, cy);
canvas.drawLine(cx + cr, cy, cx + cr * 2, cy, paint);
canvas.drawArc(rectF, 0, 360, false, paint);
canvas.restore();
}

@Override
public void startAnim() {
super.startAnim();
state = STATE_ANIM_START;
startValueAnimation();
}

@Override
public void resetAnim() {
super.resetAnim();
state = STATE_ANIM_RESET;
startValueAnimation();
}
}

布局

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.cj5785.testcanvas.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="start"
android:onClick="start"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="reset"
android:onClick="reset"/>

</RelativeLayout>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SearchActivity extends AppCompatActivity {

private SearchView serachView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
serachView = findViewById(R.id.search_view);
serachView.setController(new MyController());
}

public void start(View view) {
serachView.startAnimation();
}

public void reset(View view) {
serachView.resetAnimation();
}
}

测试结果

Donate comment here