android實(shí)現(xiàn)畫(huà)板功能

這兩天閑來(lái)無(wú)事做了一個(gè)簡(jiǎn)易的畫(huà)板程序孕荠,和大家分享一下预厌。
效果圖:

這是一個(gè)灰常簡(jiǎn)單的畫(huà)板阿迈,不過(guò)麻雀雖小,五臟俱全:

  1. 支持撤銷(xiāo)(undo)轧叽;
  2. 支持反撤銷(xiāo)(redo)苗沧;
  3. 支持橡皮擦(eraser);
  4. 支持清除功能(clear)炭晒;
  5. 支持保存為圖像(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末三幻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子呐能,更是在濱河造成了極大的恐慌念搬,老刑警劉巖抑堡,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異朗徊,居然都是意外死亡首妖,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)爷恳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)有缆,“玉大人,你說(shuō)我怎么就攤上這事温亲∨锉冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵铸豁,是天一觀(guān)的道長(zhǎng)灌曙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)节芥,這世上最難降的妖魔是什么在刺? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮头镊,結(jié)果婚禮上蚣驼,老公的妹妹穿的比我還像新娘。我一直安慰自己相艇,他們只是感情好颖杏,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著坛芽,像睡著了一般留储。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咙轩,一...
    開(kāi)封第一講書(shū)人閱讀 52,255評(píng)論 1 308
  • 那天获讳,我揣著相機(jī)與錄音,去河邊找鬼活喊。 笑死丐膝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钾菊。 我是一名探鬼主播帅矗,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼煞烫!你這毒婦竟也來(lái)了浑此?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滞详,失蹤者是張志新(化名)和其女友劉穎凛俱,沒(méi)想到半個(gè)月后喘落,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡最冰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了稀火。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暖哨。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凰狞,靈堂內(nèi)的尸體忽然破棺而出篇裁,到底是詐尸還是另有隱情,我是刑警寧澤赡若,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布达布,位于F島的核電站,受9級(jí)特大地震影響逾冬,放射性物質(zhì)發(fā)生泄漏黍聂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一身腻、第九天 我趴在偏房一處隱蔽的房頂上張望产还。 院中可真熱鬧,春花似錦嘀趟、人聲如沸脐区。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)牛隅。三九已至,卻和暖如春酌泰,著一層夾襖步出監(jiān)牢的瞬間媒佣,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工宫莱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丈攒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓授霸,卻偏偏與公主長(zhǎng)得像巡验,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碘耳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 在前面一篇文章中辛辨,我們分析了Android應(yīng)用程序窗口的繪圖表面的創(chuàng)建過(guò)程捕捂。Android應(yīng)用程序窗口的繪圖表面在...
    一個(gè)不掉頭發(fā)的開(kāi)發(fā)閱讀 4,592評(píng)論 0 14
  • 在上篇說(shuō)道BitmapShader的使用關(guān)于Shader.TileMode這個(gè)參數(shù)在說(shuō)明一下Shader.Tile...
    大大大寒閱讀 1,533評(píng)論 3 1
  • 對(duì)時(shí)光瑟枫,越來(lái)越感到恐慌。 又一個(gè)月即將結(jié)束指攒,依然一事無(wú)成慷妙,沒(méi)有開(kāi)單,沒(méi)有業(yè)績(jī)允悦,開(kāi)始有危機(jī)感了膝擂。因?yàn)榫鸵獩](méi)錢(qián)花。 有...
    流浪癡人閱讀 347評(píng)論 0 0
  • 堅(jiān)持原創(chuàng)分享第111天 申荷永總結(jié)以下“沙盤(pán)游戲”中的動(dòng)作要點(diǎn)隙弛。 一架馋、得之于心 如何開(kāi)始“沙盤(pán)游戲”...
    宛如初夏閱讀 524評(píng)論 0 0
  • 趕集去,到灞橋趕集去全闷,不論你是騎著毛驢兒揮著小皮鞭從橋上走過(guò)叉寂,還是折下橋邊的柳枝編成小草帽悠哉悠哉的閑逛,你都會(huì)感...
    原朔閱讀 1,727評(píng)論 0 6