前言
最近專案中需要用到塗鴉的功能,在 Github 上搜了一圈也沒找到適合的庫,索性就自己擼一個出來,正好複習一下自定義 View 的知識。寫完之後怎麼可以自己藏著呢,當然得寫篇部落格分享給大家。原始碼已經上傳到 Github 了,有需要的 點選這裡 ,歡迎 star 和 fork.
在開始本文的內容之前,先展示一波最終的效果
可以看到這個這個自定義 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
準備工作搞定了之後就開始進行核心程式碼的實現了。
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
以上便是本文的全部內容,有興趣的同學可以 點選這裡 看一下具體實現,麻煩點個 star,謝謝了。