这两天闲来无事做了一个简易的画板程序,和大家分享一下。
效果图:
这是一个灰常简单的画板,不过麻雀虽小,五脏俱全:
- 支持撤销(undo);
- 支持反撤销(redo);
- 支持橡皮擦(eraser);
- 支持清除功能(clear);
- 支持保存为图像(save)。
github地址点这里,欢迎fork,star
关键代码
非常简短,只有200来行
/**
* Created by wensefu on 17-3-21.
*/
public class PaletteView extends View {
private Paint mPaint;
private Path mPath;
private float mLastX;
private float mLastY;
private Bitmap mBufferBitmap;
private Canvas mBufferCanvas;
private static final int MAX_CACHE_STEP = 20;
private List<DrawingInfo> mDrawingList;
private List<DrawingInfo> mRemovedList;
private Xfermode mClearMode;
private float mDrawSize;
private float mEraserSize;
private boolean mCanEraser;
private Callback mCallback;
public enum Mode {
DRAW,
ERASER
}
private Mode mMode = Mode.DRAW;
public PaletteView(Context context) {
this(context,null);
}
public PaletteView(Context context, AttributeSet attrs) {
super(context, attrs);
setDrawingCacheEnabled(true);
init();
}
public interface Callback {
void onUndoRedoStatusChanged();
}
public void setCallback(Callback callback){
mCallback = callback;
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setFilterBitmap(true);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mDrawSize = 20;
mEraserSize = 40;
mPaint.setStrokeWidth(mDrawSize);
mPaint.setColor(0XFF000000);
mClearMode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
}
private void initBuffer(){
mBufferBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
mBufferCanvas = new Canvas(mBufferBitmap);
}
private abstract static class DrawingInfo {
Paint paint;
abstract void draw(Canvas canvas);
}
private static class PathDrawingInfo extends DrawingInfo{
Path path;
@Override
void draw(Canvas canvas) {
canvas.drawPath(path, paint);
}
}
public Mode getMode() {
return mMode;
}
public void setMode(Mode mode) {
if (mode != mMode) {
mMode = mode;
if (mMode == Mode.DRAW) {
mPaint.setXfermode(null);
mPaint.setStrokeWidth(mDrawSize);
} else {
mPaint.setXfermode(mClearMode);
mPaint.setStrokeWidth(mEraserSize);
}
}
}
public void setEraserSize(float size) {
mEraserSize = size;
}
public void setPenRawSize(float size) {
mEraserSize = size;
}
public void setPenColor(int color) {
mPaint.setColor(color);
}
public void setPenAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
private void reDraw(){
if (mDrawingList != null) {
mBufferBitmap.eraseColor(Color.TRANSPARENT);
for (DrawingInfo drawingInfo : mDrawingList) {
drawingInfo.draw(mBufferCanvas);
}
invalidate();
}
}
public boolean canRedo() {
return mRemovedList != null && mRemovedList.size() > 0;
}
public boolean canUndo(){
return mDrawingList != null && mDrawingList.size() > 0;
}
public void redo() {
int size = mRemovedList == null ? 0 : mRemovedList.size();
if (size > 0) {
DrawingInfo info = mRemovedList.remove(size - 1);
mDrawingList.add(info);
mCanEraser = true;
reDraw();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public void undo() {
int size = mDrawingList == null ? 0 : mDrawingList.size();
if (size > 0) {
DrawingInfo info = mDrawingList.remove(size - 1);
if (mRemovedList == null) {
mRemovedList = new ArrayList<>(MAX_CACHE_STEP);
}
if (size == 1) {
mCanEraser = false;
}
mRemovedList.add(info);
reDraw();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public void clear() {
if (mBufferBitmap != null) {
if (mDrawingList != null) {
mDrawingList.clear();
}
if (mRemovedList != null) {
mRemovedList.clear();
}
mCanEraser = false;
mBufferBitmap.eraseColor(Color.TRANSPARENT);
invalidate();
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
}
public Bitmap buildBitmap() {
Bitmap bm = getDrawingCache();
Bitmap result = Bitmap.createBitmap(bm);
destroyDrawingCache();
return result;
}
private void saveDrawingPath(){
if (mDrawingList == null) {
mDrawingList = new ArrayList<>(MAX_CACHE_STEP);
} else if (mDrawingList.size() == MAX_CACHE_STEP) {
mDrawingList.remove(0);
}
Path cachePath = new Path(mPath);
Paint cachePaint = new Paint(mPaint);
PathDrawingInfo info = new PathDrawingInfo();
info.path = cachePath;
info.paint = cachePaint;
mDrawingList.add(info);
mCanEraser = true;
if (mCallback != null) {
mCallback.onUndoRedoStatusChanged();
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mBufferBitmap != null) {
canvas.drawBitmap(mBufferBitmap, 0, 0, null);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getAction() & MotionEvent.ACTION_MASK;
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
if (mPath == null) {
mPath = new Path();
}
mPath.moveTo(x,y);
break;
case MotionEvent.ACTION_MOVE:
//这里终点设为两点的中心点的目的在于使绘制的曲线更平滑,如果终点直接设置为x,y,效果和lineto是一样的,实际是折线效果
mPath.quadTo(mLastX, mLastY, (x + mLastX) / 2, (y + mLastY) / 2);
if (mBufferBitmap == null) {
initBuffer();
}
if (mMode == Mode.ERASER && !mCanEraser) {
break;
}
mBufferCanvas.drawPath(mPath,mPaint);
invalidate();
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
if (mMode == Mode.DRAW || mCanEraser) {
saveDrawingPath();
}
mPath.reset();
break;
}
return true;
}
}
原理分析
总的来讲,思路其实很简单:
- 接收到move事件后,在屏幕上画出相应的轨迹;
- 撤销功能:在画轨迹时,记录每一步的轨迹和画笔属性,每次撤销时把最后一步删除,然后重绘;
- 反撤销功能:撤销时把撤销的轨迹和画笔属性保存在另一个列表里,反撤销时从这个列表里取出来放到记录绘制信息的列表里,然后重绘;
- 橡皮擦功能:这里主要应用到android的图象混合(Xfermode)知识,后面会对其进行讲解;
- 清除功能:这个非常简单,清除屏上的像素记录即可;
- 绘制:考虑到性能问题,这里使用了双缓冲绘图技术。
android的图象混合(Xfermode)
图象混合本质上用一句话解释就是:
按照某种算法将画布上你想要绘制的区域的每个像素的ARGB和你将要在这个区域绘制的ARGB进行组合变换。
举个例子,
我现在有个自定义View, 背景画成绿色的,
我再在上面绘制一个蓝色的圆,在不设置Xfermode(不进行图像混合)的情况下效果是这样的:
代码:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GREEN);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300, mPaint);
}
现在我改一下代码,给Paint设置一个Xfermode,
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.BLUE);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GREEN);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300, mPaint);
}
效果是这样的:
为什么设置Xfermode后圆会变成黑色呢?代码里并没有设置Paint的颜色为黑色呀!
原因是,设置Xfermode为new PorterDuffXfermode(PorterDuff.Mode.CLEAR)后,按照这个混合模式的算法,canvas上圆绘制的区域的所有像素点ARGB全部被置0了,
因此实际上圆绘制区域是透明的,显示为黑色是因为activity的window背景色是黑色的。
Xfermode有三个子类,AvoidXfermode,PixelXorXfermode,PorterDuffXfermode,前两个在API 16上已经过时了,现在最常用的是PorterDuffXfermode,目前支持18种图像混合算法,分别产生不同的混合效果,做橡皮擦功能用到了CLEAR算法,其他算法的效果大家有兴趣可以参考google官方的介绍,api demo里也有相关的例子。
需要注意的是,Xfermode的某些算法不支持硬件加速,例如PorterDuffXfermode的DARKEN,LIGHTEN以及OVERLAY是不支持硬件加速的。具体参见android developer文档(需要梯子fk):hardware-accel.html#unsupported
android双缓冲绘图技术##
在理解android的双缓冲绘图概念之前,我们先想一想,何谓缓冲?
所谓缓冲,简单地说就是将多个将要执行的独立的任务集结起来,一起提交。
打个比方,现实生活中,你现在要将很多砖从A处搬到B处,原始的方法是徒手一次搬几块,这就是没有使用“缓存”的方法。你也可以用一辆拖车,先把砖搬到拖车上,再把拖车拉到B处,这就是使用了“缓存”的方法。
每个canvas都有对应的一个bitmap,绘图的过程实际上就是往这个bitmap上写入ARGB信息,然后把这些信息交给GPU进行显示。这里面其实已经包含了一次缓冲的过程。
所以,讲到这里,双缓冲的概念我想你已经明白了。没错,绘图时的双缓冲其实就是再增加一个canvas,把想要绘制的内容先绘制到这个增加的canvas对应的bitmap上,写完后再把这个bitmap的ARGB信息一次提交给上下文的canvas去绘制。双缓冲技术在绘制数据量较大时在性能上有明显的提升,画板程序之所以用到了双缓存,也是基于提高绘制效率的考虑。
关于双缓冲技术更详细的分析,可以参考我的另一篇博文:
网友评论