前言
最近项目中需要用到涂鸦的功能,在 Github 上搜了一圈也没找到适合的库,索性就自己撸一个出来,正好复习一下自定义 View 的知识。写完之后怎么可以自己藏着呢,当然得写篇博客分享给大家。
在开始本文的内容之前,先展示一波最终的效果
可以看到这个这个自定义 View 的功能还是很丰富的,无论是设置画笔的形状、颜色、粗细,还是进行重置和保存,该有的 API,基本都已经实现了。有需要的读者直接 点击这里 ,希望帮忙点个 star,哈哈哈。
一、定义画笔的行为类
这里所说的「行为」指的就是我们刚才看到的画笔的形状,无论是路径、直线、还是圆形,这些东西说到底都是画笔的行为。
所以我们先定义一个公共的父类,以便进行管理,减少代码量。
abstract class Action {
public int color;
Action() {
color = Color.BLACK;
}
Action(int color) {
this.color = color;
}
public abstract void draw(Canvas canvas);
public abstract void move(float mx, float my);
}
可以看到这个类被定义成抽象类,里面有 draw() 和 move() 两个抽象方法,这两个方法就是留给子类进行继承和拓展的,子类只要实现这两个方法,确定好他们各自的行为,就能让画笔显示出各种各样的效果。
接下来举几个具体的子类来说明一下用法:
// 自由曲线
class MyPath extends Action {
private Path path;
private int size;
MyPath() {
path = new Path();
size = 1;
}
MyPath(float x, float y, int size, int color) {
super(color);
path = new Path();
this.size = size;
path.moveTo(x, y);
path.lineTo(x, y);
}
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setColor(color); // 设置画笔颜色
paint.setStrokeWidth(size); // 设置画笔粗细
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
}
public void move(float mx, float my) {
path.lineTo(mx, my);
}
}
// 直线
class MyLine extends Action {
private float startX;
private float startY;
private float stopX;
private float stopY;
private int size;
MyLine() {
startX = 0;
startY = 0;
stopX = 0;
stopY = 0;
}
MyLine(float x, float y, int size, int color) {
super(color);
startX = x;
startY = y;
stopX = x;
stopY = y;
this.size = size;
}
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(color);
paint.setStrokeWidth(size);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
public void move(float mx, float my) {
stopX = mx;
stopY = my;
}
}
就拿最常见的自由曲线来作为例子讲一下。我们定义 MyPath 这个类,继承自 BaseAction,然后添加了 Path 和 size 两个成员变量。其中的 size 是用来设置画笔的粗细。Path 是用来确定自由曲线的轨迹。
在 MyPath 的 draw() 方法中我们创建了一个 Paint 用于图形的描绘。最后将 path 和 paint 传给 canvas,实现图形的最终绘制。
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setColor(color); // 设置画笔颜色
paint.setStrokeWidth(size); // 设置画笔粗细
paint.setStyle(Paint.Style.STROKE);
canvas.drawPath(path, paint);
}
其他子类都是按照这种思路来实现,具体的实现可以参考下 Github 上的源码 DoodleView。
二、实现自定义的 DoodleView
这个 DoodleView 是直接继承 SurfaceView 的。本来想继承 View 来写,后来仔细想了下最后还是用 SurfaceView 来进行实现。
这里简单说一下 View 和 SurfaceView 的区别。
-
View 在主线程中对页面进行刷新,而 SurfaceView 则是另外开了一个子线程对当前页面进行刷新。
-
View 适合用于主动更新的情况,而 SurfaceView 则适用于被动更新的情况,比如频繁刷新界面。
因为我们这个涂鸦的 View,是频繁进行刷新的,每次触摸屏幕都会进行相应的界面刷新,所以用 SurfaceView 来实现就比较合理了。
这里我直接结合代码来讲一下 DoodleView 的实现思路,因为我是继承自 SurfaceView 来写的,对于 SurfaceView 不是很了解的朋友,可以先看一下这篇文章 Android中的SurfaceView详解
public class DoodleView extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mSurfaceHolder = null;
// 当前所选画笔的形状
private BaseAction curAction = null;
// 默认画笔为黑色
private int currentColor = Color.BLACK;
// 画笔的粗细
private int currentSize = 5;
private Paint mPaint;
private List<BaseAction> mBaseActions;
private Bitmap mBitmap;
private ActionType mActionType = ActionType.Path;
public DoodleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mSurfaceHolder = this.getHolder();
mSurfaceHolder.addCallback(this);
this.setFocusable(true);
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(currentSize);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
mSurfaceHolder.unlockCanvasAndPost(canvas);
mBaseActions = new ArrayList<>();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL) {
return false;
}
float touchX = event.getRawX();
float touchY = event.getRawY();
switch (action) {
case MotionEvent.ACTION_DOWN:
setCurAction(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
for (BaseAction baseAction : mBaseActions) {
baseAction.draw(canvas);
}
curAction.move(touchX, touchY);
curAction.draw(canvas);
mSurfaceHolder.unlockCanvasAndPost(canvas);
break;
case MotionEvent.ACTION_UP:
mBaseActions.add(curAction);
curAction = null;
break;
default:
break;
}
return super.onTouchEvent(event);
}
/**
* 得到当前画笔的类型,并进行实例化
*
* @param x
* @param y
*/
private void setCurAction(float x, float y) {
switch (mActionType) {
case Path:
curAction = new MyPath(x, y, currentSize, currentColor);
break;
case Line:
curAction = new MyLine(x, y, currentSize, currentColor);
break;
default:
break;
}
}
/**
* 设置画笔的颜色
*
* @param color 颜色
*/
public void setColor(String color) {
this.currentColor = Color.parseColor(color);
}
/**
* 设置画笔的粗细
*
* @param size 画笔的粗细
*/
public void setSize(int size) {
this.currentSize = size;
}
/**
* 设置画笔的形状
*
* @param type 画笔的形状
*/
public void setType(ActionType type) {
this.mActionType = type;
}
/**
* 将当前的画布转换成一个 Bitmap
*
* @return Bitmap
*/
public Bitmap getBitmap() {
mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(mBitmap);
doDraw(canvas);
return mBitmap;
}
/**
* 保存涂鸦后的图片
*
* @param doodleView
* @return 图片的保存路径
*/
public String saveBitmap(DoodleView doodleView) {
String path = Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/doodleview/" + System.currentTimeMillis() + ".png";
if (!new File(path).exists()) {
new File(path).getParentFile().mkdir();
}
savePicByPNG(doodleView.getBitmap(), path);
return path;
}
/**
* 将一个 Bitmap 保存在一个指定的路径中
*
* @param bitmap
* @param filePath
*/
public static void savePicByPNG(Bitmap bitmap, String filePath) {
FileOutputStream fileOutputStream;
try {
fileOutputStream = new FileOutputStream(filePath);
if (null != fileOutputStream) {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 开始进行绘画
*
* @param canvas
*/
private void doDraw(Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT);
for (BaseAction action : mBaseActions) {
action.draw(canvas);
}
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
/**
* 回退
*
* @return 是否已经回退成功
*/
public boolean back(){
if(mBaseActions != null && mBaseActions.size() > 0){
mBaseActions.remove(mBaseActions.size() -1);
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
for (BaseAction action : mBaseActions) {
action.draw(canvas);
}
mSurfaceHolder.unlockCanvasAndPost(canvas);
return true;
}
return false;
}
/**
* 重置签名
*/
public void reset(){
if(mBaseActions != null && mBaseActions.size() > 0){
mBaseActions.clear();
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
for (BaseAction action : mBaseActions) {
action.draw(canvas);
}
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
enum ActionType {
Path, Line
}
}
可以看到,我们先定义了一个枚举类,用于区分各种画笔的形状,为了让代码看起来更简洁,我这里只放了 Path 和 Line 两种类型的,如果你还想实现其他类型的形状,直接加进去就行了。
在类的一开始我们定义了一些必要的成员变量,如画笔的颜色、形状、粗细,以及保存画笔行为的 List,以及需要用到的画笔 Paint
准备工作搞定了之后就开始进行核心代码的实现了。
1、构造函数
public DoodleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mSurfaceHolder = this.getHolder();
mSurfaceHolder.addCallback(this);
this.setFocusable(true);
mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(currentSize);
}
可以看到我们在构造函数中先进行了 SurfaceHolder 的一些设置,以及对 Paint 进行了必要的设置。
然后在 surfaceCreated(SurfaceHolder holder) 方法中对 Canas 进行了创建和提交,以及初始化了 List
2、触摸事件的处理
这个方法的实现可以说是这个 DoodleView 的核心了
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL) {
return false;
}
float touchX = event.getRawX();
float touchY = event.getRawY();
switch (action) {
case MotionEvent.ACTION_DOWN:
setCurAction(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
for (BaseAction baseAction : mBaseActions) {
baseAction.draw(canvas);
}
curAction.move(touchX, touchY);
curAction.draw(canvas);
mSurfaceHolder.unlockCanvasAndPost(canvas);
break;
case MotionEvent.ACTION_UP:
mBaseActions.add(curAction);
curAction = null;
break;
default:
break;
}
return super.onTouchEvent(event);
}
我们先拿到触摸的横坐标和纵坐标,然后根据手势来进行相应的处理
-
ACTION_DOWN:当刚开始出触摸屏幕的时候,先设置画笔的形状
-
ACTION_MOVE:手开始移动的时候,调用 move() 和 draw() 对 Canvas 进行绘制,最后将 Canvas 的内容进行提交。
-
ACTION_UP:将手抬起来的时候,将当前画笔的形状添加到 List 中,并将 curAction(当前的画笔形状)设为 null.
3、其他的 API
除了一些核心方法的实现,为了拓展这个 DoodleView 的功能,我还添加了一些实用的 API。
保存涂鸦后的图片
public String saveBitmap(DoodleView doodleView) {
String path = Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/doodleview/" + System.currentTimeMillis() + ".png";
if (!new File(path).exists()) {
new File(path).getParentFile().mkdir();
}
savePicByPNG(doodleView.getBitmap(), path);
return path;
}
public static void savePicByPNG(Bitmap bitmap, String filePath) {
FileOutputStream fileOutputStream;
try {
fileOutputStream = new FileOutputStream(filePath);
if (null != fileOutputStream) {
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
先创建一个用于保存图片的路径,判断路径是否存在,如果不存在的话,就创建一下。否则通过这个路径拿到对应的文件流,并将当前图片转换成 Bitmap 之后放进去。
重置涂鸦的界面
我们进行涂鸦,难免会出现手误,这时候进行重置就显得相当重要了。
public void reset(){
if(mBaseActions != null && mBaseActions.size() > 0){
mBaseActions.clear();
Canvas canvas = mSurfaceHolder.lockCanvas();
canvas.drawColor(Color.WHITE);
for (BaseAction action : mBaseActions) {
action.draw(canvas);
}
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
这里直接获取 Canvas,然后将 List 进行 clear,因为 List 里面没有内容,Canvas 上自然也就没有任何东西,最后将 Canvas 进行提交。
以上便是本文的全部内容,有兴趣的同学可以 点击这里 看一下具体实现,麻烦点个 star,谢谢了。