android 动作轨迹回退,Android 带你撸一个好玩的 DoodleView(涂鸦)

在开始本文的内容之前,先展示一波最终的效果

1502443

DoodleView

可以看到这个这个自定义 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 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,谢谢了。

作者:developerHaoz

链接:http://www.jianshu.com/p/f05f71446771

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值