Android 帶你擼一個好玩的塗鴉 View

developerHaoz發表於2019-02-19

前言

最近專案中需要用到塗鴉的功能,在 Github 上搜了一圈也沒找到適合的庫,索性就自己擼一個出來,正好複習一下自定義 View 的知識。寫完之後怎麼可以自己藏著呢,當然得寫篇部落格分享給大家。原始碼已經上傳到 Github 了,有需要的 點選這裡 ,歡迎 star 和 fork.

在開始本文的內容之前,先展示一波最終的效果

DoodleView
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<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,謝謝了。


猜你喜歡

相關文章