Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義控件三部曲之繪圖篇(十七)——為Bitmap添加陰影並封裝控件

自定義控件三部曲之繪圖篇(十七)——為Bitmap添加陰影並封裝控件

編輯:關於Android編程

上篇給大家講解了如何控件添加陰影效果,但是在為Bitmap圖片添加陰影時,卻沒辦法添加具有指定顏色的陰影,這篇我們就來使用自定義的控件及自定義屬性來初步封裝下控件。前方高能預警——本篇內容涉及內容較多,難度較大,需要多加思考。

一、使用BlurMaskFilter為圖片構造定色陰影效果

上面我們講了通過setShadowLayer為圖片添加陰影效果,但是圖片的的陰影是用原圖形的副本加上邊緣發光效果組成的。我們怎麼能給圖片添加一個灰色的陰影呢?
我們來分析一下setShadowLayer的陰影形成過程(假定陰影畫筆是灰色),對於文字和圖形,它首先產生一個跟原型一樣的灰色副本。然後對這個灰色副本應用BlurMaskFilter,使其內外發光;這樣就形成了所謂的陰影!當然最後再偏移一段距離。
所以,我們要給圖片添加灰色陰影效果,所以我們能不能仿一下這個過程:先繪制一個跟圖片一樣大小的灰色圖形,然後給這個灰色圖形應用BlurMaskFilter使其內外發光,然後偏移原圖形繪制出來,不就可以了麼
所以,這裡涉及到三個點:

繪制出一個一樣大小的灰色圖形 對灰色圖形應用BlurMaskFilter使其內外發光 偏移原圖形一段距離繪制出來

下面我們就通過例子來一步步看是怎麼實現出來的吧

1、繪制出一個一樣大小的灰色圖像

首先,我們來看怎麼能繪出一個指定Bitmap所對應的灰色圖像。我們知道canvas.drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint)中的paint的畫筆顏色對畫出來的bitmap是沒有任何影響的,因為原來Bitmap長什麼樣,無論你畫筆是什麼顏色,畫出來的圖片還是跟原圖片長的一樣。所以如果我們需要畫一張對應的灰色圖像,我們需要新建一個一樣大小的空白圖,但是新圖片的透明度要與原圖片保持一致。所以如何從原圖片中抽出Alpha值成為了關鍵。即我們只需要創建一個與原圖片一樣大小且Alpha相同的圖片即可。
其實Bitmap中已經存在抽取出只具有Alpha值圖片的函數:

public Bitmap extractAlpha();

extraAlpha()函數的功能是:新建一張空白圖片,圖片具有與原圖片一樣的Alpha值,這個新建的Bitmap做為結果返回。這個空白圖片中每個像素都具有與原圖片一樣的Alpha值,而且具體的顏色時,只有在使用canvas.drawBitmap繪制時,由傳入的paint的顏色指定。
所以總結來講:

extractAlpha()新建一張僅具有Alpha值的空白圖像 這張圖像的顏色,是由canvas.drawBitmap時的畫筆指定的。

(1)、extractAlpha()使用示例
下面,我們就用個例子先來看下extractAlpha()函數的用法
我們拿一張圖片來做試驗,下面這張PNG圖片中,一只小貓和一只小狗,其余地方都是透明色。
這裡寫圖片描述
下面我們分別利用extractAlpha()畫出它對應的紅色和綠色的陰影圖
這裡寫圖片描述
對應的代碼為:

public class ExtractAlphaView extends View {
    private Paint mPaint;
    private Bitmap mBitmap,mAlphaBmp;
    public ExtractAlphaView(Context context) {
        super(context);
        init();
    }

    public ExtractAlphaView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ExtractAlphaView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
        mAlphaBmp = mBitmap.extractAlpha();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = 200;
        int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
         mPaint.setColor(Color.RED);
        canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);

        mPaint.setColor(Color.GREEN);
        canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
    }
}

首先看init函數:

private void init(){
    setLayerType(LAYER_TYPE_SOFTWARE,null);
    mPaint = new Paint();
    mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
    mAlphaBmp = mBitmap.extractAlpha();
}

首先是禁用硬件加速,這基本上是我們做自定義控件的標配!為了防止功能不好用,記得每次都加上這個函數!然後是利用extratAlpha()來生成僅具有透明度的空白圖像。
最後看OnDraw函數:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = 200;
    int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
    mPaint.setColor(Color.RED);
    canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);

    mPaint.setColor(Color.GREEN);
    canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
}

這裡分別將畫筆的顏色設置為紅色和綠色,然後兩次把mAlphaBmp畫出來。上面我們已經提到,在畫僅具有透明度的空白圖像時,圖像的顏色是由畫筆顏色指定的。所以從效果圖中也可以看出畫出來的圖像分別紅色的綠色的。
這就是Bitmpa.extraAlpha()的用法!

2、對灰色圖形應用BlurMaskFilter使其內外發光

在第一步完成了之後,我們進行第二步,將陰影添加內外發光效果。就形成了陰影的模樣。
代碼很簡單,只需要使用Paint.setMaskFilter函數添加發光效果即可,代碼如下:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = 200;
    int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();
    mPaint.setColor(Color.RED);
    mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
    canvas.drawBitmap(mAlphaBmp,null,new Rect(0,0,width,height),mPaint);

    mPaint.setColor(Color.GREEN);
    canvas.drawBitmap(mAlphaBmp,null,new Rect(0,height,width,2*height),mPaint);
}

明顯可以看出這裡只添加了一行代碼:mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));就是添加內外發光效果,難度不大,不再細講。

3、偏移原圖形一段距離繪制出來

這段比較簡單了,只需要先把陰影畫出來,然後再把原圖像蓋上去,但需要注意的是,陰影需要相對原圖像偏移一段距離。完整代碼如下:

public class ExtractAlphaView extends View {
    private Paint mPaint;
    private Bitmap mBitmap,mAlphaBmp;
    public ExtractAlphaView(Context context) {
        super(context);
        init();
    }

    public ExtractAlphaView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ExtractAlphaView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        mPaint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.blog12);
        mAlphaBmp = mBitmap.extractAlpha();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = 200;
        int height = width * mAlphaBmp.getHeight()/mAlphaBmp.getWidth();

        //繪制陰影
        mPaint.setColor(Color.RED);
        mPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.NORMAL));
        canvas.drawBitmap(mAlphaBmp,null,new Rect(10,10,width,height),mPaint);
        mPaint.setColor(Color.GREEN);
        canvas.drawBitmap(mAlphaBmp,null,new Rect(10,height+10,width,2*height),mPaint);

        //繪制原圖像
        mPaint.setMaskFilter(null);
        canvas.drawBitmap(mBitmap,null,new Rect(0,0,width,height),mPaint);
        canvas.drawBitmap(mBitmap,null,new Rect(0,height,width,2*height),mPaint);
    }
}

關鍵部分在onDraw函數中,先畫出來陰影,然後再畫出來原圖像,需要注意的是在畫原圖像時,需要利用mPaint.setMaskFilter(null);將發光效果去掉。只有陰影需要發光效果,原圖像是不需要發光效果的。另一點注意的是,陰影要偏移一點位置,這裡是偏移了10個像素。
效果圖如下:
這裡寫圖片描述

二、封裝控件

上面我們初步實現了圖片的陰影效果,但這只是本篇內容的一小部分,最最重要的,如何將它封裝成一個控件,具有如下功能:

讓用戶定義圖片內容 讓用戶定義偏移距離 讓用戶定義陰影顏色和陰影模糊程度 可以使用wrap_content屬性自適應大小

1、自定義控件屬性

有關自定義控件屬性,大家首先需要看下這篇文章《PullScrollView詳解(一)——自定義控件屬性》,在這篇文章中講解了自定義控件屬性的方法與提取方法。下面將會直接用到自定義屬性的內容,下面涉及到的時候就自認為大家已經學會了自定義控件屬性的方法了。
在這裡,我們需要自定義四個屬性,分別對應: 自定義圖片內容、自定義偏移距離、自定義陰影顏色、自定義陰影模糊程度 這四個需求,所以我們先利用declare-styleable標簽來定義這些屬性
attr.xml



    
        
        
        
        
        
    

這裡定義了五個xml屬性,src來引用圖片資源,仿照setShadowLayer另外定義shadowDx、shadowDy、shadowColor、shadowRadius來定義陰影的邊距、顏色和模糊半徑。
然後在布局中使用:(main.xml)



    

在布局中使用很簡單,直接定義控件所使用的圖片資源、陰影相關參數就可以了,難度不大就不再講了,下面我們來看如何在代碼中中提取用戶傳入的這些屬性。
BitmapShadowView中提取屬性值並繪陰影
先列出完整代碼,然後再細講:

public class BitmapShadowView extends View {
    private Paint mPaint;
    private Bitmap mBmp,mShadowBmp;
    private int mDx = 10,mDy = 10;
    private float mRadius = 0;
    private int mShadowColor;

    public BitmapShadowView(Context context, AttributeSet attrs) throws Exception{
        super(context, attrs);

        init(context,attrs);
    }

    public BitmapShadowView(Context context, AttributeSet attrs, int defStyle) throws Exception{
        super(context, attrs, defStyle);
        init(context,attrs);
    }

    private void init(Context context,AttributeSet attrs) throws Exception {
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        /**
         * 提取屬性定義
         */
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
        int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
        if (BitmapID == -1){
            throw new Exception("BitmapShadowView 需要定義Src屬性,而且必須是圖像");
        }
        mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
        mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
        mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
        mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
        mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);

        typedArray.recycle();
        /**
         * 其它定義
         */
        mPaint = new Paint();
        mShadowBmp = mBmp.extractAlpha();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth()-mDx;
        int height = width * mBmp.getHeight()/mBmp.getWidth();

        //繪制陰影
        mPaint.setColor(mShadowColor);
        mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
        canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);

        //繪制原圖像
        mPaint.setMaskFilter(null);
        canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
    }
}

在這段代碼中分兩部分,首先根據屬性來初始化各變量,然後再利用這些變量畫出bitmap與陰影。
首先看初始化部分:

private void init(Context context,AttributeSet attrs) throws Exception {
    setLayerType(LAYER_TYPE_SOFTWARE,null);
    /**
     * 提取屬性定義
     */
    TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
    int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
    if (BitmapID == -1){
        throw new Exception("BitmapShadowView 需要定義Src屬性,而且必須是圖像");
    }
    mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
    mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
    mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
    mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
    mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);

    typedArray.recycle();
    /**
     * 其它定義
     */
    mPaint = new Paint();
    mShadowBmp = mBmp.extractAlpha();
}

初始化的時候,首先是利用TypedArray來初始化各項參數,由於我們是做圖片的陰影,所以圖片資源必須賦值,所以我們在提取圖片資源時,對其添加容錯:

int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
if (BitmapID == -1){
    throw new Exception("BitmapShadowView 需要定義Src屬性,而且必須是圖像");
}

當提取失敗時,拋出異常,終止程序,這樣用戶在寫代碼時就可以及時發現問題,而不必等上線以後才發現沒有bitmap;
有關其它屬性值的提取,這裡就不再細講了。
在提取完屬性以後,就是定義畫筆paint和根據源圖像利用extractAlpha()來生成陰影圖像;
在初始化以後就是利用這些屬性來進行繪圖了:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = getWidth()-mDx;
    int height = width * mBmp.getHeight()/mBmp.getWidth();

    //繪制陰影
    mPaint.setColor(mShadowColor);
    mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
    canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);

    //繪制原圖像
    mPaint.setMaskFilter(null);
    canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
}

首先,圖片寬度與控件寬度操持一致(但需要把陰影的位置空出來),所以寬度為:int width = getWidth()-mDx
根據圖片的寬高比換算出圖片的高度:int height = width * mBmp.getHeight()/mBmp.getWidth()
我們依控件左上角(0,0)顯示原圖像,陰影在(mDx,mDy)處顯示;
到這裡自定義屬性的定義與提取就結束了,最終效果圖為:
Alt text
從效果圖中可以明顯看出,明顯給原圖片添加了紅色的陰影效果。
目前,我們初步實現了可以讓用戶自定義控件屬性的功能,但我們在使用這個控件時,必須強制設置指定的寬高或者fill_parent來強制平屏,這樣明顯是不可取的,我們需要它能夠讓用戶使用wrap_conetent時,自己計算寬高;

2、wrap_content自適應寬高

在自適應寬高時,需要了解onMeasure()、onLayout()與onDraw()的知識,以前在寫FlowLayout時,已經單獨寫過一篇:《FlowLayout詳解(一)——onMeasure()與onLayout()》,這裡就不再細講onMeasure()的原理了,如果不理解onMeasure用法的同學需要提前把這篇文章看完再回來;在第三篇中我還會重新講解一遍onMeasure()、onLayout()與onDraw(),這裡涉及內容不多,看完上一篇然後再理解以下內容應該不會有問題
在看完上面的文章,大家就應該知道,對於View控件的自適應寬高,只需要在上面的代碼中重寫onMeasure()方法就可以了:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);


   int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
   int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
   int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
   int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

   int width = mBmp.getWidth();
   int height = mBmp.getHeight();
   setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
}

在onMeasure方法中,當用戶指定屬性是wrap_content時,就使用圖片的寬高做為控件的寬高。
此時整個自定義控件的完整代碼為:

public class BitmapShadowView extends View {
    private Paint mPaint;
    private Bitmap mBmp,mShadowBmp;
    private int mDx = 10,mDy = 10;
    private float mRadius = 0;
    private int mShadowColor;

    public BitmapShadowView(Context context, AttributeSet attrs) throws Exception{
        super(context, attrs);

        init(context,attrs);
    }

    public BitmapShadowView(Context context, AttributeSet attrs, int defStyle) throws Exception{
        super(context, attrs, defStyle);
        init(context,attrs);
    }


    private void init(Context context,AttributeSet attrs) throws Exception {
        setLayerType(LAYER_TYPE_SOFTWARE,null);
        /**
         * 提取屬性定義
         */
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.BitmapShadowView);
        int BitmapID = typedArray.getResourceId(R.styleable.BitmapShadowView_src,-1);
        if (BitmapID == -1){
            throw new Exception("BitmapShadowView 需要定義Src屬性,而且必須是圖像");
        }
        mBmp = BitmapFactory.decodeResource(getResources(),BitmapID);
        mDx = typedArray.getInt(R.styleable.BitmapShadowView_shadowDx,0);
        mDy = typedArray.getInt(R.styleable.BitmapShadowView_shadowDy,0);
        mRadius = typedArray.getFloat(R.styleable.BitmapShadowView_shadowRadius,0);
        mShadowColor = typedArray.getColor(R.styleable.BitmapShadowView_shadowColor,Color.BLACK);

        typedArray.recycle();
        /**
         * 其它定義
         */
        mPaint = new Paint();
        mShadowBmp = mBmp.extractAlpha();

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        int width = mBmp.getWidth();
        int height = mBmp.getHeight();
        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth: width, (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight: height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth()-mDx;
        int height = width * mBmp.getHeight()/mBmp.getWidth();

        //繪制陰影
        mPaint.setColor(mShadowColor);
        mPaint.setMaskFilter(new BlurMaskFilter(mRadius, BlurMaskFilter.Blur.NORMAL));
        canvas.drawBitmap(mShadowBmp,null,new Rect(mDx,mDy,width,height),mPaint);

        //繪制原圖像
        mPaint.setMaskFilter(null);
        canvas.drawBitmap(mBmp,null,new Rect(0,0,width,height),mPaint);
    }
}

所以當我們對這個自定義的控件使用如下布局(使用wrap_content):



    

    

效果圖如下:
這裡寫圖片描述
所以,這時候如果我們需要產生灰色陰影,只需要把xml中的app:shadowColor的值改一下即可:(為了方便看陰影,我把Activiy背景改成了白色)




    

    

效果圖如下:

這裡寫圖片描述

到這裡,整個控件的封裝就結束了,但細心的同學可以發現,BitmapShadowView的構造函數默認有三個,而我這裡只寫了兩個具有AttributeSet attrs參數的,而下面這個構造函數卻是沒有實現的:

public BitmapShadowView(Context context){
    super(context);
}

因為當從XML中生成控件時,都會調用具有AttributeSet attrs參數的方法,而從代碼中生成控件則會調用上面僅具有context函數的方法,所以如果需要從代碼中生成需要添加上這個方法,並且需要在代碼中提供接口供外部設置各種屬性才好,我這裡就略去了這部分內容了,大家可以自己來填充這個控件,使其更完整。

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