Android中Canvas繪圖之MaskFilter圖文詳解(附原始碼下載)

孫群發表於2015-11-17

如果對Canvas繪圖不熟悉,強烈建議您閱讀博文《Android中Canvas繪圖基礎詳解(附原始碼下載)》,該文對Android中的Canvas繪圖基礎進行了詳細的描述。本文著重講解如何使用MaskFilter建立模糊陰影以及浮雕效果。

我們知道Canvas中的各種drawXXX方法決定了繪製的幾何圖形的形狀,而畫筆Paint則決定了以什麼效果繪製這些圖形。Paint中有一個setMaskFilter方法,該方法接收一個MaskFilter型別的引數,MaskFilter有兩個子類,分別是BlurMaskFilter和EmbossMaskFilter,可以分別用來繪製模糊陰影以及浮雕效果。如果Paint的setMaskFilter方法中傳入的是null,那麼就表示不使用任何MaskFilter。

為了演示不同MaskFilter對Canvas繪圖的影響,我做了一個App,介面如下所示:

這裡寫圖片描述

上面用Canvas的各種drawXXX方法繪製了各種圖形,下面有三個RadioButton,預設選擇的是當前繪圖沒有使用MaskFilter,第二項表示使用BlurMaskFilter,第三項表示使用EmbossMaskFilter。選擇相應的RadioButton後,下面會出現對應的調節引數的UI介面,當然,選擇第一項“無MaskFilter”後,是沒有任何調節引數的UI介面的。


繪圖程式碼

無論選擇上面的哪一項,都是用同樣的程式碼來繪製圖形的,唯一不同的地方就是傳遞給畫筆Paint的setMaskFilter的引數MaskFilter不同。繪圖程式碼如下所示:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int offsetX = (int)(10 * density);
        int canvasWidth = canvas.getWidth() - offsetX;
        int canvasHeight = canvas.getHeight();
        int count = 7;
        int deltaY = canvasHeight / (count + 1);
        int smallDeltaY = deltaY / (count + 1);
        canvas.translate(offsetX, 0);

        /*---------------------------繪製文字--------------------------*/
        canvas.translate(0, smallDeltaY);
        canvas.drawText("繪製文字", 0, fontSize, paint);

        /*---------------------------繪製點--------------------------*/
        canvas.translate(0, deltaY + smallDeltaY);
        int pointDeltaX = canvasWidth / 3;
        //設定畫筆線寬
        paint.setStrokeWidth(20 * density);
        //繪製BUTT型別的點
        paint.setStrokeCap(Paint.Cap.BUTT);
        canvas.drawPoint(0, 0, paint);
        //繪製ROUND型別的點
        paint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawPoint(pointDeltaX, 0, paint);
        //繪製SQUARE型別的點
        paint.setStrokeCap(Paint.Cap.SQUARE);
        canvas.drawPoint(pointDeltaX * 2, 0, paint);

        /*---------------------------繪製直線--------------------------*/
        canvas.translate(0, deltaY + smallDeltaY);
        //設定畫筆線寬
        paint.setStrokeWidth(5 * density);
        canvas.drawLine(0, 0, canvasWidth, 0, paint);

        /*---------------------------繪製弧線--------------------------*/
        canvas.translate(0, deltaY + smallDeltaY);
        paint.setStyle(Paint.Style.STROKE);
        RectF arcRecF = new RectF();
        arcRecF.left = 0;
        arcRecF.top = 0;
        arcRecF.right = deltaY * 2;
        arcRecF.bottom = deltaY;
        canvas.drawArc(arcRecF, 225, 135, true, paint);

        /*---------------------------繪製矩形--------------------------*/
        canvas.translate(0, deltaY + smallDeltaY);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(0, 0, canvasWidth / 2, deltaY / 2, paint);

        /*---------------------------繪製橢圓面--------------------------*/
        canvas.translate(0, deltaY + smallDeltaY);
        RectF ovalRecF = new RectF();
        ovalRecF.left = 0;
        ovalRecF.top = 0;
        ovalRecF.right = deltaY * 2;
        ovalRecF.bottom = deltaY;
        canvas.drawOval(ovalRecF, paint);

        /*---------------------------繪製Bitmap--------------------------*/
        if(bitmap != null){
            canvas.translate(0, deltaY + smallDeltaY);
            canvas.drawBitmap(bitmap, 0, 0, paint);
        }
    }

對以上程式碼進行簡單說明:

  1. 首先用Canvas的drawText方法繪製了文字,見上圖中第一行繪製的文字。

  2. 然後用Canvas的drawPoint方法繪製了點,注意,我分別用BUTT、ROUND、SQUARE作為strokeCap繪製了三個點,並且特意將線寬設定的很大,以便我們容易觀察, 見上圖中第二行繪製的三個點圖形。

  3. 然後用Canvas的drawLine方法繪製了一條線段,也讓線段具有一定的線寬,見上圖中第三行繪製的線段圖形。

  4. 然後用Canvas的drawArc方法繪製了一個扇形模樣的橢圓弧,也具有一定的線寬,見上圖中第四行繪製的弧線圖形。

  5. 然後用Canvas的drawRect方法繪製了一個矩形,見上圖中第五行繪製的圖形。

  6. 然後用Canvas的drawOval方法繪製了一個橢圓,見上圖中第六行繪製的圖形。

  7. 最後用Canvas的drawBitmap方法繪製一個Bitmap,見上圖中最後一行繪製的圖形。

繪製不同型別的圖形的目的是為了讓大家更好的觀察不同型別的圖形是如何受不同MaskFilter影響的。上圖就是在給畫筆Paint設定的MaskFilter為null的情況下繪製的效果,即我們正常繪製圖形的效果。


BlurMaskFilter

選中BlurMaskFilter時,會建立模糊陰影效果,選擇的style不同的話,效果不同,模糊半徑的大小也影響效果,其效果如下所示:
這裡寫圖片描述

BlurMaskFilter的建構函式簽名是:

BlurMaskFilter(float radius, BlurMaskFilter.Blur style)

下面對上面兩個引數進行解釋:

  • radius
    第一個引數radius是float型別,表示是Blur Radius,即陰影的模糊半徑,如上圖所示,其值越大表示圖形繪製出來越模糊,其值越小圖形越清晰。

  • style
    第二個引數style是列舉BlurMaskFilter.Blur型別,陰影按細節來分主要分為內陰影和外陰影,內陰影指的是陰影從圖形輪廓向內側擴張,外陰影指的是陰影從圖形輪廓向外側擴張。從陰影角度說,圖形的繪製最多由三部分組成: 外陰影 + 圖形本身內容 + 內陰影,Blur列舉有四種值:

    • NORMAL
      當style為NORMAL時,會同時繪製圖形本身內容+內陰影+外陰影,即正常陰影效果。
      通常情況下,當我們要想使用陰影效果時,一般選擇NORMAL作為style。當陰影模糊半徑為0時,相當於沒有使用陰影效果,隨著陰影模糊半徑變大,圖形更模糊看不清。並且你還可以發現,無論陰影模糊半徑如何變化,用BUTT、SQUARE繪製的點都不受影響,繪製的Bitmap也不受影響。可以仔細觀察上圖中選中NORMAL時圖形的陰影效果。

    • INNER
      當style為INNER時,繪製圖形內容本身+內陰影,不繪製外陰影。
      當陰影模糊半徑為0時,相當於沒有使用陰影效果,隨著陰影模糊半徑變大,可以發現圖形從輪廓線向內顏色變淺。無論陰影模糊半徑如何變化,用BUTT、SQUARE繪製的點仍然都不受影響,繪製的Bitmap也不受影響。可以仔細觀察上圖中選中INNER時圖形的陰影效果。

    • OUTER
      當style為OUTER時,不繪製圖形內容以及內陰影,只繪製外陰影,即圖形輪廓以內完全不繪製,輪廓線以內完全是空白的。
      從介面效果上看就是圖形被掏空了,只是有熒光向外擴散。當陰影模糊半徑為0時,外陰影也幾乎看不到了,此時整個圖形也基本看不到,隨著陰影模糊半徑變大,外陰影逐漸明顯。無論陰影模糊半徑如何變化,用BUTT、SQUARE繪製的點仍然都不受影響,繪製的Bitmap一直基本上不可見。可以仔細觀察上圖中選中OUTER時圖形的陰影效果。

    • SOLID
      當style為SOLID時,只繪製外陰影和圖形內容本身,不繪製內陰影。
      從介面上看,由於不繪製內陰影,所以圖形輪廓線內部不會發虛。繪製的外陰影會導致輪廓線向外發虛。當陰影模糊半徑為0時,相當於沒有陰影效果,隨著陰影模糊半徑的增大,陰影效果明顯。用BUTT、SQUARE繪製的點仍然都不受影響,繪製的Bitmap也不受影響。

小結:

  • 不同的style會影響繪製圖形的不同部分。
  • 無論哪種style,用BUTT和SQUARE作為strokeCap繪製的正方形的點都完全不受BlurMaskFilter的影響,不過用ROUND作為strokeCap畫出的圓形的點會受到影響。
  • 在NORMAL、INNER、SOLID作為style時,繪製的Bitmap基本完全不受影響,在OUTER作為style時,繪製的Bitmap基本不可見。所以BlurMaskFilter對Bitmap的實際用處不大。

EmbossMaskFilter

可以用EmbossMaskFilter建立浮雕效果的MaskFilter,然後通過Paint的setMaskFilter賦值給畫筆Paint。其效果如下圖所示:
這裡寫圖片描述

EmbossMaskFilter的建構函式簽名如下所示:

public EmbossMaskFilter (float[] direction, float ambient, float specular, float blurRadius)

在講解以上引數之前,先簡單介紹一下EmbossMaskFilter的作用機理。所謂的浮雕效果其實就是模擬光照效果,靠近光的一面顯得亮一點,遠離光的一面顯得暗一點,這樣就通過顏色的亮暗營造出浮雕的3D立體效果。如果熟悉OpenGL繪圖,可能就會對Phong式光照模型比較瞭解,一般來說,一個相對完整的光照模型=環境光 + 漫反射 + 鏡面反射。EmbossMaskFilter為了簡化引數並且突出浮雕效果,就把漫反射給去掉了,所以EmbossMaskFilter所使用的光照模型就只有環境光和鏡面反射了。

下面對各個引數進行講解:

  • direction
    direction是一個float型別的陣列,表示光線的方向,是個向量,包含三個值,分別是x分量、y分量、z分量,這三個值的絕對大小並不重要,因為direction最後在真正被Android使用時會被歸一化成一個單位向量,即變成長度為1的向量。direction中的座標是在所畫圖形的右手座標系中定義的,如下圖所示:
    這裡寫圖片描述
    如果對OpenGL開發熟悉的話肯定知道右手座標系。我們以畫橢圓為例,橢圓的中心就是這個三維座標系的中心點。x正半軸從中心點水平向右,y正半軸從中心點垂直向上,由於圖形在Android螢幕內,所以x和y軸都在Android螢幕的平面內。z軸經過橢圓的中心點垂直於螢幕,即同時垂直於x軸和y軸,並且其z正半軸的方向為從螢幕穿過面向我們,即z正半軸的箭頭衝向我們。我們可以通過上面的的UI調節direction中的x、y、z的值,從座標原點(0,0,0)到(x,y,z)的向量即為光照方向。舉個例子,當x為1,y為0,z為0時,光照向量為(1,0,0),表示光是恰好沿著從x負半軸指向x正半軸,此時,由於光照從x負半軸照向x正半軸,所以橢圓上處於x負半軸的地方離光照近,被照亮,即橢圓的左側被照亮,處於x正半軸的地方則顯得較暗,則橢圓的右側則比較暗。大家可以自己調解下引數感受一下,如下圖所示:

    這裡寫圖片描述

  • ambient
    ambient表示環境光因子,float型別,取值是0到1,值越接近於0,環境光越暗,值越接近於1,環境光越亮。

  • specular
    specular表示鏡面反射因子,float型別,取值也是0到1。鏡面反射就是模擬像鏡子一樣的高光反射,值越接近於0,鏡面反射越強,被光照照射到的地方更容易出現很白很亮的狀態,即高光效果。

  • blurRadius
    blurRadius表示模糊半徑,是float型別,其值越大,模糊效果越明顯。


注意

最後需要注意的是,由於在GPU硬體加速模式下,Paint的setMaskFilter不被GPU支援,所以為了能夠正常使用setMaskFilter方法,我們需要將我們要繪製的View使用軟體渲染模式,具體可參見博文《Android中GPU硬體加速控制及其在2D圖形繪製上的侷限》。相關程式碼如下所示:

//為了確保畫筆的setMaskFilter能供起效,我們需要對MyView禁用掉GPU硬體加速
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
    //View從API Level 11才加入setLayerType方法
    //設定myView以軟體渲染模式繪圖
    myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

程式碼為Android Studio工程,已經上傳到CSDN,點此下載

希望本文對大家使用MaskFilter有所幫助!

相關閱讀:
《我的Android博文整理彙總》
《Android中Canvas繪圖基礎詳解(附原始碼下載)》
《Android中GPU硬體加速控制及其在2D圖形繪製上的侷限》

相關文章