這兩天閑來(lái)無(wú)事做了一個(gè)簡(jiǎn)易的畫(huà)板程序孕荠,和大家分享一下预厌。
效果圖:
這是一個(gè)灰常簡(jiǎn)單的畫(huà)板阿迈,不過(guò)麻雀雖小,五臟俱全:
- 支持撤銷(xiāo)(undo)轧叽;
- 支持反撤銷(xiāo)(redo)苗沧;
- 支持橡皮擦(eraser);
- 支持清除功能(clear)炭晒;
- 支持保存為圖像(save)待逞。
github地址點(diǎn)這里,歡迎fork,star
關(guān)鍵代碼
非常簡(jiǎn)短,只有200來(lái)行
/**
* 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:
//這里終點(diǎn)設(shè)為兩點(diǎn)的中心點(diǎn)的目的在于使繪制的曲線(xiàn)更平滑网严,如果終點(diǎn)直接設(shè)置為x,y识樱,效果和lineto是一樣的,實(shí)際是折線(xiàn)效果
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;
}
}
原理分析
總的來(lái)講,思路其實(shí)很簡(jiǎn)單:
- 接收到move事件后,在屏幕上畫(huà)出相應(yīng)的軌跡怜庸;
- 撤銷(xiāo)功能:在畫(huà)軌跡時(shí)当犯,記錄每一步的軌跡和畫(huà)筆屬性,每次撤銷(xiāo)時(shí)把最后一步刪除休雌,然后重繪灶壶;
- 反撤銷(xiāo)功能:撤銷(xiāo)時(shí)把撤銷(xiāo)的軌跡和畫(huà)筆屬性保存在另一個(gè)列表里,反撤銷(xiāo)時(shí)從這個(gè)列表里取出來(lái)放到記錄繪制信息的列表里杈曲,然后重繪驰凛;
- 橡皮擦功能:這里主要應(yīng)用到android的圖象混合(Xfermode)知識(shí),后面會(huì)對(duì)其進(jìn)行講解担扑;
- 清除功能:這個(gè)非常簡(jiǎn)單恰响,清除屏上的像素記錄即可;
- 繪制:考慮到性能問(wèn)題涌献,這里使用了雙緩沖繪圖技術(shù)胚宦。
android的圖象混合(Xfermode)
圖象混合本質(zhì)上用一句話(huà)解釋就是:
按照某種算法將畫(huà)布上你想要繪制的區(qū)域的每個(gè)像素的ARGB和你將要在這個(gè)區(qū)域繪制的ARGB進(jìn)行組合變換。
舉個(gè)例子燕垃,
我現(xiàn)在有個(gè)自定義View, 背景畫(huà)成綠色的枢劝,
我再在上面繪制一個(gè)藍(lán)色的圓,在不設(shè)置Xfermode(不進(jìn)行圖像混合)的情況下效果是這樣的:
代碼:
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.GREEN);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, 300, mPaint);
}
現(xiàn)在我改一下代碼卜壕,給Paint設(shè)置一個(gè)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);
}
效果是這樣的:
為什么設(shè)置Xfermode后圓會(huì)變成黑色呢?代碼里并沒(méi)有設(shè)置Paint的顏色為黑色呀轴捎!
原因是鹤盒,設(shè)置Xfermode為new PorterDuffXfermode(PorterDuff.Mode.CLEAR)后,按照這個(gè)混合模式的算法侦副,canvas上圓繪制的區(qū)域的所有像素點(diǎn)ARGB全部被置0了侦锯,
因此實(shí)際上圓繪制區(qū)域是透明的,顯示為黑色是因?yàn)閍ctivity的window背景色是黑色的秦驯。
Xfermode有三個(gè)子類(lèi)尺碰,AvoidXfermode,PixelXorXfermode译隘,PorterDuffXfermode亲桥,前兩個(gè)在API 16上已經(jīng)過(guò)時(shí)了,現(xiàn)在最常用的是PorterDuffXfermode细燎,目前支持18種圖像混合算法,分別產(chǎn)生不同的混合效果皂甘,做橡皮擦功能用到了CLEAR算法玻驻,其他算法的效果大家有興趣可以參考google官方的介紹,api demo里也有相關(guān)的例子。
需要注意的是璧瞬,Xfermode的某些算法不支持硬件加速户辫,例如PorterDuffXfermode的DARKEN,LIGHTEN以及OVERLAY是不支持硬件加速的嗤锉。具體參見(jiàn)android developer文檔(需要梯子fk):hardware-accel.html#unsupported
android雙緩沖繪圖技術(shù)##
在理解android的雙緩沖繪圖概念之前渔欢,我們先想一想,何謂緩沖瘟忱?
所謂緩沖奥额,簡(jiǎn)單地說(shuō)就是將多個(gè)將要執(zhí)行的獨(dú)立的任務(wù)集結(jié)起來(lái),一起提交访诱。
打個(gè)比方垫挨,現(xiàn)實(shí)生活中,你現(xiàn)在要將很多磚從A處搬到B處触菜,原始的方法是徒手一次搬幾塊九榔,這就是沒(méi)有使用“緩存”的方法。你也可以用一輛拖車(chē)涡相,先把磚搬到拖車(chē)上哲泊,再把拖車(chē)?yán)紹處,這就是使用了“緩存”的方法催蝗。
每個(gè)canvas都有對(duì)應(yīng)的一個(gè)bitmap切威,繪圖的過(guò)程實(shí)際上就是往這個(gè)bitmap上寫(xiě)入ARGB信息,然后把這些信息交給GPU進(jìn)行顯示生逸。這里面其實(shí)已經(jīng)包含了一次緩沖的過(guò)程牢屋。
所以,講到這里槽袄,雙緩沖的概念我想你已經(jīng)明白了烙无。沒(méi)錯(cuò),繪圖時(shí)的雙緩沖其實(shí)就是再增加一個(gè)canvas遍尺,把想要繪制的內(nèi)容先繪制到這個(gè)增加的canvas對(duì)應(yīng)的bitmap上截酷,寫(xiě)完后再把這個(gè)bitmap的ARGB信息一次提交給上下文的canvas去繪制。雙緩沖技術(shù)在繪制數(shù)據(jù)量較大時(shí)在性能上有明顯的提升乾戏,畫(huà)板程序之所以用到了雙緩存迂苛,也是基于提高繪制效率的考慮。
關(guān)于雙緩沖技術(shù)更詳細(xì)的分析鼓择,可以參考我的另一篇博文:
android雙緩沖繪圖技術(shù)分析
轉(zhuǎn)載請(qǐng)說(shuō)明出處:http://www.reibang.com/p/548d2799fd6e