Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View系列教程04--Draw源碼分析及其實踐

自定義View系列教程04--Draw源碼分析及其實踐

編輯:關於Android編程

通過之前的詳細分析,我們知道:在measure中測量了View的大小,在layout階段確定了View的位置。
完成這兩步之後就進入到了我們相對熟悉的draw階段,在該階段真正地開始對視圖進行繪制。

按照之前的慣例,我們來瞅瞅View中draw( )的源碼

 public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {

            if (!dirtyOpaque) onDraw(canvas);

            dispatchDraw(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            onDrawForeground(canvas);

            return;
        }


        boolean drawTop = false;
        boolean drawBottom = false;
        boolean drawLeft = false;
        boolean drawRight = false;

        float topFadeStrength = 0.0f;
        float bottomFadeStrength = 0.0f;
        float leftFadeStrength = 0.0f;
        float rightFadeStrength = 0.0f;

        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;

            if (drawTop) {
                canvas.saveLayer(left, top, right, top + length, null, flags);
            }

            if (drawBottom) {
                canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
            }

            if (drawLeft) {
                canvas.saveLayer(left, top, left + length, bottom, null, flags);
            }

            if (drawRight) {
                canvas.saveLayer(right - length, top, right, bottom, null, flags);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        if (!dirtyOpaque) onDraw(canvas);

        dispatchDraw(canvas);

        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        if (drawTop) {
            matrix.setScale(1, fadeHeight * topFadeStrength);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, right, top + length, p);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        onDrawForeground(canvas);
    }

以上為draw()的具體實現,在Andorid官方文檔中將該過程概況成了六步:

Draw the background If necessary, save the canvas’ layers to prepare for fading Draw view’s content Draw children If necessary, draw the fading edges and restore layers Draw decorations (scrollbars for instance)

在此就按照該順序對照源碼看看每一步都做了哪些操作。

第一步:
繪制背景,請參見代碼第9-11行。

第二步:
保存當前畫布的堆棧狀態並在該畫布上創建Layer用於繪制View在滑動時的邊框漸變效果,請參見代碼第42-108行
通常情況下我們是不需要處理這一步的,正如上面的描述

If necessary, save the canvas’ layers to prepare for fading

第三步:
繪制View的內容,請參見代碼第18行
這一步是整個draw階段的核心,在此會調用onDraw()方法繪制View的內容。
之前我們在分析layout的時候發現onLayout()方法是一個抽象方法,具體的邏輯由ViewGroup的子類去實現。與之類似,在此onDraw()是一個空方法;因為每個View所要繪制的內容不同,所以需要由具體的子View去實現各自不同的需求。

第四步:
調用dispatchDraw()繪制View的子View,請參見代碼第20行

第五步:
繪制當前視圖在滑動時的邊框漸變效果,請參見代碼第114-157行
通常情況下我們是不需要處理這一步的,正如上面的描述

If necessary, draw the fading edges and restore layers

第六步:
繪制View的滾動條,請參見代碼第29行
其實,不單單是常見的ScrollView和ListView等滑動控件任何一個View(比如:TextView,Button)都是有滾動條的,只是一般情況下我們都沒有將它顯示出來而已。


好了,看完draw()的源碼,我們就要把注意力集中在第三步onDraw()了。

protected void onDraw(Canvas canvas) {

}

此處,該方法只有個輸入參數canvas,我們就先來瞅瞅什麼是canvas。

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect,Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

這段Android官方關於canvas的介紹告訴開發者:
在繪圖時需要明確四個核心的東西(basic components):

用什麼工具畫?
這個小問題很簡單,我們需要用一支畫筆(Paint)來繪圖。
當然,我們可以選擇不同顏色的筆,不同大小的筆。 把圖畫在哪裡呢?
我們把圖畫在了Bitmap上,它保存了所繪圖像的各個像素(pixel)。
也就是說Bitmap承載和呈現了畫的各種圖形。 畫的內容?
根據自己的需求畫圓,畫直線,畫路徑。 怎麼畫?
調用canvas執行繪圖操作。
比如,canvas.drawCircle(),canvas.drawLine(),canvas.drawPath()將我們需要的圖像畫出來。

知道了繪圖過程中必不可少的四樣東西,我們就要看看該怎麼樣構建一個canvas了。
在此依次分析canvas的兩個構造方法Canvas( )和Canvas(Bitmap bitmap)

/**
 * Construct an empty raster canvas. Use setBitmap() to specify a bitmap to
 * draw into.  The initial target density is {@link Bitmap#DENSITY_NONE};
 * this will typically be replaced when a target bitmap is set for the
 * canvas.
 */
public Canvas() {
    if (!isHardwareAccelerated()) {
        mNativeCanvasWrapper = initRaster(null);
        mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
    } else {
        mFinalizer = null;
    }
}

請注意該構造的第一句注釋。官方不推薦通過該無參的構造方法生成一個canvas。如果要這麼做那就需要調用setBitmap( )為其設置一個Bitmap。為什麼Canvas非要一個Bitmap對象呢?原因很簡單:Canvas需要一個Bitmap對象來保存像素,如果畫的東西沒有地方可以保存,又還有什麼意義呢?既然不推薦這麼做,那就接著有參的構造方法。

/**
 * Construct a canvas with the specified bitmap to draw into. The bitmap
 * must be mutable.
 *
 * The initial target density of the canvas is the same as the given
 * bitmap's density.
 *
 * @param bitmap Specifies a mutable bitmap for the canvas to draw into.
 */
public Canvas(Bitmap bitmap) {
    if (!bitmap.isMutable()) {
        throw new IllegalStateException("Immutable bitmap passed to Canvas constructor");
    }
    throwIfCannotDraw(bitmap);
    mNativeCanvasWrapper = initRaster(bitmap);
    mFinalizer = new CanvasFinalizer(mNativeCanvasWrapper);
    mBitmap = bitmap;
    mDensity = bitmap.mDensity;
}

通過該構造方法為Canvas設置了一個Bitmap來保存所繪圖像的像素信息。

好了,知道了怎麼構建一個canvas就來看看怎麼利用它進行繪圖。
下面是一個很簡單的例子:

private void drawOnBitmap(){
    Bitmap bitmap=Bitmap.createBitmap(800, 400, Bitmap.Config.ARGB_8888);
    Canvas canvas=new Canvas(bitmap);
    canvas.drawColor(Color.GREEN);
    Paint paint=new Paint();
    paint.setColor(Color.RED);
    paint.setTextSize(60);
    canvas.drawText("hello , everyone", 150, 200, paint);
    mImageView.setImageBitmap(bitmap);
}

瞅瞅效果:
這裡寫圖片描述

在此處為canvas設置一個Bitmap,然後利用canvas畫了一小段文字,最後使用ImageView顯示了Bitmap。
好了,看到這有人就有疑問了:
我們平常用得最多的View的onDraw()方法,為什麼沒有Bitmap也可以畫出各種圖形呢?
請注意onDraw( )的輸入參數是一個canvas,它與我們自己創建的canvas不同。這個系統傳遞給我們的canvas來自於ViewRootImpl的Surface,在繪圖時系統將會SkBitmap設置到SkCanvas中並返回與之對應Canvas。所以,在onDraw()中也是有一個Bitmap的,只是這個Bitmap是由系統創建的罷了。

好吧,既然已經提到了onDraw( )我們就在它裡面畫一些常見的圖形。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCgoKPHByZSBjbGFzcz0="brush:java;">@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //-------->繪制白色矩形 mPaint.setColor(Color.WHITE); canvas.drawRect(0, 0, 800, 800, mPaint); mPaint.reset(); //-------->繪制直線 mPaint.setColor(Color.RED); mPaint.setStrokeWidth(10); canvas.drawLine(450, 30, 570, 170, mPaint); mPaint.reset(); //-------->繪制帶邊框的矩形 mPaint.setStrokeWidth(10); mPaint.setARGB(150, 90, 255, 0); mPaint.setStyle(Paint.Style.STROKE); RectF rectF1=new RectF(30, 60, 350, 350); canvas.drawRect(rectF1, mPaint); mPaint.reset(); //-------->繪制實心圓 mPaint.setStrokeWidth(14); mPaint.setColor(Color.GREEN); mPaint.setAntiAlias(true); canvas.drawCircle(670, 300, 70, mPaint); mPaint.reset(); //-------->繪制橢圓 mPaint.setColor(Color.YELLOW); RectF rectF2=new RectF(200, 430, 600, 600); canvas.drawOval(rectF2, mPaint); mPaint.reset(); //-------->繪制文字 mPaint.setColor(Color.BLACK); mPaint.setTextSize(60); mPaint.setUnderlineText(true); canvas.drawText("Hello Android", 150, 720, mPaint); mPaint.reset(); }

這裡寫圖片描述
在此只列舉了幾種最常用圖形的繪制,其余的API就不再舉例了,在需要的時候去查看相應文檔就行。


除了調用canvas畫各種圖形,我們有時候還有對canvas做一些操作,比如旋轉,剪裁,平移等等;有時候為了達到理想的效果,我們可能還需要一些特效。在此,對相關內容做一些介紹。

canvas.translate canvas.rotate canvas.clipRect canvas.save和canvas.restore PorterDuffXfermode Bitmap和Matrix Shader PathEffect

嗯哼,它們已經洗干淨,挨個躺這了;我們就依次瞅瞅。

canvas.translate
從字面意思也可以知道它的作用是位移,那麼這個位移到底是怎麼實現的的呢?我們看段代碼:

 protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     canvas.drawColor(Color.GREEN);
     Paint paint=new Paint();
     paint.setTextSize(70);
     paint.setColor(Color.BLUE);
     canvas.drawText("藍色字體為Translate前所畫", 20, 80, paint);
     canvas.translate(100,300);
     paint.setColor(Color.BLACK);
     canvas.drawText("黑色字體為Translate後所畫", 20, 80, paint);
}

這段代碼的主要操作:
1 畫一句話,請參見代碼第7行
2 使用translate在X方向平移了100個單位在Y方向平移了300個單位,請參見代碼第8行
3 再畫一句話,請參見代碼第10行

運行一下,看看它的效果:
這裡寫圖片描述seorxr3SxrXEtaXOu6GjIDxicj4KscjI56Osxr3Sxrrzy/m7rc7E19a1xMq1vMrOu9bDzqqjujEyMCgyMCsxMDApus0zODAoODArMzAwKaGjIDxicj4K1eK+zcrHy7WjrGNhbnZhcy50cmFuc2xhdGXP4LWx09rSxravwcvX+LHqtcTUrbXjo6zSxravwcvX+LHqz7WhoyA8YnI+CtXiw7TLtb/JxNy7ucrHsru5u9axudujrMTHvs3Jz828o7ogPGJyPgo8aW1nIHNyYz0="/uploadfile/Collfiles/20160526/20160526091410330.png" alt="這裡寫圖片描述" title="\">
喏,看到了吧:黑色的是原來的坐標系,藍色的是移動後的坐標系。這就好理解了。

canvas.rotate
與translate類似,可以用rotate實現旋轉。

protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     canvas.drawColor(Color.GREEN);
     Paint paint=new Paint();
     paint.setTextSize(70);
     paint.setColor(Color.BLUE);
     canvas.drawText("綠色字體為Rotate前所繪", 20, 80, paint);
     canvas.rotate(15);
     paint.setColor(Color.BLACK);
     canvas.drawText("黑色字體為Rotate後所繪", 20, 80, paint);
}

這裡寫圖片描述
前面說了canvas.translate相當於把坐標系平移了。與此同理,canvas.rotate相當於把坐標系旋轉了一定角度。

canvas.clipRect
看完了canvas.translate和canv.rotate,接下來看看canvas.clipRect的使用。
canvas.clipRect表示剪裁操作,執行該操作後的繪制將顯示在剪裁區域。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.GREEN);
    Paint paint=new Paint();
    paint.setTextSize(60);
    paint.setColor(Color.BLUE);
    canvas.drawText("綠色部分為Canvas剪裁前的區域", 20, 80, paint);
    Rect rect=new Rect(20,200,900,1000);
    canvas.clipRect(rect);
    canvas.drawColor(Color.YELLOW);
    paint.setColor(Color.BLACK);
    canvas.drawText("黃色部分為Canvas剪裁後的區域", 10, 310, paint);
    }

效果如下圖所示:
這裡寫圖片描述
當我們調用了canvas.clipRect( )後,如果再繼續畫圖那麼所繪的圖只會在所剪裁的范圍內體現。
當然除了按照矩形剪裁以外,還可以有別的剪裁方式,比如:canvas.clipPath( )和canvas.clipRegion( )。

canvas.save和canvas.restore
剛才在說canvas.clipRect( )時,有人可能有這樣的疑問:在調用canvas.clipRect( )後,如果還需要在剪裁范圍外繪圖該怎麼辦?是不是系統有一個canvas.restoreClipRect( )方法呢?去看看官方的API就有點小失望了,我們期待的東西是不存在的;不過可以換種方式來實現這個需求,這就是即將要介紹的canvas.save和canvas.restore。看到這個玩意,可能絕大部分人就想起來了Activity中的onSaveInstanceState和onRestoreInstanceState這兩者用來保存和還原Activity的某些狀態和數據。canvas也可以這樣麼?
canvas.save
先來看這個玩意,它表示畫布的鎖定。如果我們把一個妹子鎖在屋子裡,那麼外界的刮風下雨就影響不到她了;同理,如果對一個canvas執行了save操作就表示將已經所繪的圖形鎖定,之後的繪圖就不會影響到原來畫好的圖形。
既然不會影響到原本已經畫好的圖形,那之後的操作又發生在哪裡呢?
當執行canvas.save( )時會生成一個新的圖層(Layer),並且這個圖層是透明的。此時,所有draw的方法都是在這個圖層上進行,所以不會對之前畫好的圖形造成任何影響。在進行一些繪制操作後再使用canvas.restore()將這個新的圖層與底下原本的畫好的圖像相結合形成一個新的圖像。打個比方:原本在畫板上畫了一個姑娘,我又找了一張和畫板一樣大小的透明的紙(Layer),然後在上面畫了一朵花,最後我把這個紙蓋在了畫板上,呈現給世人的效果就是:一個美麗的姑娘手拿一朵鮮花。
還是繼續看個例子:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.GREEN);
    Paint paint=new Paint();
    paint.setTextSize(60);
    paint.setColor(Color.BLUE);
    canvas.drawText("綠色部分為Canvas剪裁前的區域", 20, 80, paint);
    canvas.save();
    Rect rect=new Rect(20,200,900,1000);
    canvas.clipRect(rect);
    canvas.drawColor(Color.YELLOW);
    paint.setColor(Color.BLACK);
    canvas.drawText("黃色部分為Canvas剪裁後的區域", 10, 310, paint);
    canvas.restore();
    paint.setColor(Color.RED);
    canvas.drawText("XXOO", 20, 170, paint);
}

這個例子由剛才講canvas.clipRect( )稍加修改而來
1 執行canvas.save( )鎖定canvas,請參見代碼第8行
2 在新的Layer上裁剪和繪圖,請參見代碼第9-13行
3 執行canvas.restore( )將Layer合並到原圖,請參見代碼第14行
4 繼續在原圖上繪制,請參見代碼第15-16行

來瞅瞅效果。
這裡寫圖片描述

在使用canvas.save和canvas.restore時需注意一個問題:
save( )和restore( )最好配對使用,若restore( )的調用次數比save( )多可能會造成異常

PorterDuffXfermode
在項目開發中,我們常用到這樣的功能:顯示圓角圖片。
效果如下:
這裡寫圖片描述
看見了吧,圖片的幾個角不是直角而是具有一定弧度的圓角。
這個是咋做的呢?我們來瞅瞅其中一種實現方式

   /**
     * @param bitmap 原圖
     * @param pixels 角度
     * @return 帶圓角的圖
     */
    public Bitmap getRoundCornerBitmap(Bitmap bitmap, float pixels) {
        int width=bitmap.getWidth();
        int height=bitmap.getHeight();
        Bitmap roundCornerBitmap = Bitmap.createBitmap(width,height,Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(roundCornerBitmap);
        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setAntiAlias(true);
        Rect rect = new Rect(0, 0, width, height);
        RectF rectF = new RectF(rect);
        canvas.drawRoundRect(rectF, pixels, pixels, paint);
        PorterDuffXfermode xfermode=new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
        paint.setXfermode(xfermode);
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return roundCornerBitmap;
    }

主要操作如下:
1 生成canvas,請參見代碼第7-10行
注意給canvas設置的Bitmap的大小是和原圖的大小一致的
2 繪制圓角矩形,請參見代碼第11-16行
3 為Paint設置PorterDuffXfermode,請參見代碼第17-18行
4 繪制原圖,請參見代碼第19行
縱觀代碼,發現一個陌生的東西PorterDuffXfermode而且陌生到了我們看到它的名字卻不容易猜測其用途的地步;這在Android的源碼中還是很少有的。
我以前郁悶了很久,不知道它為什麼叫這個名字,直到後來看到《Android多媒體開發高級編程》才略知其原委。
Thomas Porter和Tom Duff於1984年在ACM SIGGRAPH計算機圖形學刊物上發表了《Compositing digital images》。在這篇文章中詳細介紹了一系列不同的規則用於彼此重疊地繪制圖像;這些規則中定義了哪些圖像的哪些部分將出現在輸出結果中。

這就是PorterDuffXfermode的名字由來及其核心作用。

現將PorterDuffXfermode描述的規則做一個介紹:
這裡寫圖片描述

PorterDuff.Mode.CLEAR
繪制不會提交到畫布上 PorterDuff.Mode.SRC
只顯示繪制源圖像 PorterDuff.Mode.DST
只顯示目標圖像,即已在畫布上的初始圖像 PorterDuff.Mode.SRC_OVER
正常繪制顯示,即後繪制的疊加在原來繪制的圖上 PorterDuff.Mode.DST_OVER
上下兩層都顯示但是下層(DST)居上顯示 PorterDuff.Mode.SRC_IN
取兩層繪制的交集且只顯示上層(SRC) PorterDuff.Mode.DST_IN
取兩層繪制的交集且只顯示下層(DST) PorterDuff.Mode.SRC_OUT
取兩層繪制的不相交的部分且只顯示上層(SRC) PorterDuff.Mode.DST_OUT
取兩層繪制的不相交的部分且只顯示下層(DST) PorterDuff.Mode.SRC_ATOP
兩層相交,取下層(DST)的非相交部分和上層(SRC)的相交部分 PorterDuff.Mode.DST_ATOP
兩層相交,取上層(SRC)的非相交部分和下層(DST)的相交部分 PorterDuff.Mode.XOR
挖去兩圖層相交的部分 PorterDuff.Mode.DARKEN
顯示兩圖層全部區域且加深交集部分的顏色 PorterDuff.Mode.LIGHTEN
顯示兩圖層全部區域且點亮交集部分的顏色 PorterDuff.Mode.MULTIPLY
顯示兩圖層相交部分且加深該部分的顏色 PorterDuff.Mode.SCREEN
顯示兩圖層全部區域且將該部分顏色變為透明色

了解了這些規則,再回頭看我們剛才例子中的代碼,就好理解多了。
我們先畫了一個圓角矩形,然後設置了PorterDuff.Mode為SRC_IN,最後繪制了原圖。 所以,它會取圓角矩形和原圖相交的部分但只顯示原圖部分;這樣就形成了圓角的Bitmap。

Bitmap和Matrix
除了剛才提到的給圖片設置圓角之外,在開發中還常有其他涉及到圖片的操作,比如圖片的旋轉,縮放,平移等等,這些操作可以結合Matrix來實現。
在此舉個例子,看看利用matrix實現圖片的平移和縮放。

private void drawBitmapWithMatrix(Canvas canvas){
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mm);
    int width=bitmap.getWidth();
    int height=bitmap.getHeight();
    Matrix matrix = new Matrix();
    canvas.drawBitmap(bitmap, matrix, paint);
    matrix.setTranslate(width/2, height);
    canvas.drawBitmap(bitmap, matrix, paint);
    matrix.postScale(0.5f, 0.5f);
    //matrix.preScale(2f, 2f);
    canvas.drawBitmap(bitmap, matrix, paint);
}

梳理一下這段代碼的主要操作:
1 畫出原圖,請參見代碼第2-8行
2 平移原圖,請參見代碼第9-10行
3 縮放原圖,請參見代碼第11-13行
這裡寫圖片描述

嗯哼,看到效果了吧。
在這裡主要涉及到了Matrix。我們可以通過這個矩陣來實現對圖片的一些操作。有人可能會問:利用Matrix實現了圖片的平移(Translate)是將坐標系進行了平移麼?不是的。Matrix所操作的是原圖的每個像素點,它和坐標系是沒有關系的。比如Scale是對每個像素點都進行了縮放,例如:

matrix.postScale(0.5f, 0.5f);

將原圖的每個像素點的X的坐標都縮放成了原本的0.5
將原圖的每個像素點的Y坐標也都縮放成了原本的0.5

同樣的道理在調用matrix.setTranslate( )時是對於原圖中的每個像素都執行了位移操作。

在使用Matrix時經常用到一系列的set,pre,post方法。它們有什麼區別呢?它們的調用順序又會對實際效果有什麼影響呢?

在此對該問題做一個總結:
在調用set,pre,post時可視為將這些方法插入到一個隊列。

pre表示在隊頭插入一個方法 post表示在隊尾插入一個方法 set表示清空隊列
隊列中只保留該set方法,其余的方法都會清除。

當執行了一次set後pre總是插入到set之前的隊列的最前面;post總是插入到set之後的隊列的最後面。
也可以這麼簡單的理解:
set在隊列的中間位置,per執行隊頭插入,post執行隊尾插入。
當繪制圖像時系統會按照隊列中從頭至尾的順序依次調用這些方法。
請看下面的幾個小示例:

Matrix m = new Matrix();
m.setRotate(45); 
m.setTranslate(80, 80);

只有m.setTranslate(80, 80)有效,因為m.setRotate(45)被清除.

Matrix m = new Matrix();
m.setTranslate(80, 80);
m.postRotate(45);

先執行m.setTranslate(80, 80)後執行m.postRotate(45)

Matrix m = new Matrix();
m.setTranslate(80, 80);
m.preRotate(45);

先執行m.preRotate(45)後執行m.setTranslate(80, 80)

Matrix m = new Matrix();
m.preScale(2f,2f);    
m.preTranslate(50f, 20f);   
m.postScale(0.2f, 0.5f);    
m.postTranslate(20f, 20f);  

執行順序:
m.preTranslate(50f, 20f)–>m.preScale(2f,2f)–>m.postScale(0.2f, 0.5f)–>m.postTranslate(20f, 20f)

Matrix m = new Matrix();
m.postTranslate(20, 20);   
m.preScale(0.2f, 0.5f);
m.setScale(0.8f, 0.8f);   
m.postScale(3f, 3f);
m.preTranslate(0.5f, 0.5f); 

執行順序:
m.preTranslate(0.5f, 0.5f)–>m.setScale(0.8f, 0.8f)–>m.postScale(3f, 3f)

Shader
有時候我們需要實現圖像的漸變效果,這時候Shader就派上用場啦。
先來瞅瞅啥是Shader:

Shader is the based class for objects that return horizontal spans of colors during drawing. A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader.

Android提供的Shader類主要用於渲染圖像以及幾何圖形。
Shader的主要子類如下:

BitmapShader———圖像渲染 LinearGradient——–線性渲染 RadialGradient——–環形渲染 SweepGradient——–掃描渲染 ComposeShader——組合渲染

在開發中調用paint.setShader(Shader shader)就可以實現渲染效果,在此以常用的BitmapShader為示例實現圓形圖片。

protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     Paint paint = new Paint();
     paint.setAntiAlias(true);
     Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.mm);
     int radius = bitmap.getWidth()/2;
     BitmapShader bitmapShader = new BitmapShader(bitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
     paint.setShader(bitmapShader);
     canvas.translate(250,430);
     canvas.drawCircle(radius, radius, radius, paint);
}

效果如下圖所示:
這裡寫圖片描述
嗯哼,看到這樣的代碼心情還是挺舒暢的,十來行就搞定了一個小功能。
好吧,借著這股小舒暢,我們來瞅瞅代碼
1 生成BitmapShader,請參見代碼第7行
2 為Paint設置Shader,請參見代碼第8行
3 畫出圓形圖片,請參見代碼第10行

在這段代碼中,可能稍感陌生的就是BitmapShader構造方法。

BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY)

第一個參數:
bitmap表示在渲染的對象
第二個參數:
tileX 表示在位圖上X方向渲染器平鋪模式(TileMode)
TileMode一共有三種:

REPEAT :重復 MIRROR :鏡像 CLAMP:拉伸

這三種效果類似於給電腦屏幕設置屏保時,若圖片太小可選擇重復,拉伸,鏡像。
若選擇REPEAT(重復 ):橫向或縱向不斷重復顯示bitmap
若選擇MIRROR(鏡像):橫向或縱向不斷翻轉重復
若選擇CLAMP(拉伸) :橫向或縱向拉伸圖片在該方向的最後一個像素。這點和設置電腦屏保有些不同
第三個參數:
tileY表示在位圖上Y方向渲染器平鋪模式(TileMode)。與tileX同理,不再贅述。

PathEffect
我們可以通過canvas.drawPath( )繪制一些簡單的路徑。但是假若需要給路徑設置一些效果或者樣式,這時候就要用到PathEffect了。

PathEffect有如下幾個子類:

CornerPathEffect
用平滑的方式銜接Path的各部分 DashPathEffect
將Path的線段虛線化 PathDashPathEffect
與DashPathEffect效果類似但需要自定義路徑虛線的樣式 DiscretePathEffect
離散路徑效果 ComposePathEffect
兩種樣式的組合。先使用第一種效果然後在此基礎上應用第二種效果 SumPathEffect
兩種樣式的疊加。先將兩種路徑效果疊加起來再作用於Path

在此以CornerPathEffect和DashPathEffect為示例:

protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     canvas.translate(0,300);
     Paint paint = new Paint();
     paint.setAntiAlias(true);
     paint.setStyle(Paint.Style.STROKE);
     paint.setColor(Color.GREEN);
     paint.setStrokeWidth(8);
     Path  path = new Path();
     path.moveTo(15, 60);
     for (int i = 0; i <= 35; i++) {
          path.lineTo(i * 30, (float) (Math.random() * 150));
      }
     canvas.drawPath(path, paint);
     canvas.translate(0, 400);
     paint.setPathEffect(new CornerPathEffect(60));
     canvas.drawPath(path, paint);
     canvas.translate(0, 400);
     paint.setPathEffect(new DashPathEffect(new float[] {15, 8}, 1));
     canvas.drawPath(path, paint);
}

效果如下圖所示:
這裡寫圖片描述
分析一下這段代碼中的主要操作:
1 設置Path為CornerPathEffect效果,請參見代碼第16行
在構建CornerPathEffect時傳入了radius,它表示圓角的度數
2 設置Path為DashPathEffect效果,請參見代碼第19行
在構建DashPathEffect時傳入的參數要稍微復雜些。
DashPathEffect構造方法的第一個參數:
數組float[ ] { }中第一個數表示每條實線的長度,第二個數表示每條虛線的長度。
DashPathEffect構造方法的第二個參數:
phase表示偏移量,動態改變該值會使路徑產生動畫效果


至此關於自定義View的第三個階段draw就寫這麼多吧。其實,在該階段涉及到的東西非常多,我們這提到的僅僅是九牛一毛,也只是一些常用的基礎性的東西。我也期望自己以後有時間和精力和大家一道深入地學習draw部分的相關知識和開發技能。


who is the next one? ——> demo

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved