Fresco 源碼分析(四) 後臺數據返回到前臺的處理 - Drawable體系的介紹(2)

在上篇中我們介紹了drawble類的一些內容(鏈接地址: http://blog.csdn.net/ieyudeyinji/article/details/48879647),下面我們會介紹drawable的核心部分

3.4.6 mutate()方法

muatate方法是drawble比較有意思的一個方法,這個方法也是需要我們格外注意的一個方法,以下內容爲個人翻譯自google developer的文章(原文鏈接地址:http://android-developers.blogspot.com/2009/05/drawable-mutations.html):

Android的drawables在編寫程序時是相當有用的.Drwable是一個插件化的繪製容器,一般是跟view進行關聯的.比如說,BitmapDrawable是用來顯示圖片的,ShapeDrawable是用來繪製形狀和元素的.我們甚至都可以來結合使用它們來創建複雜的顯示效果.

Drawables允許我們在不繼承它們的情況下,方便的定製控件的顯示效果.實際上,因爲drawables使用這麼方便,在Android大部分原生的app中和控件中使用了drawables;在Android的Framework層大概用到了700處drawables.由於Drwables在系統中廣泛的使用,Andorid在從資源中加載時,做了優化.例如,在每次我們創建Button的時候,就從Framework層的資源庫(ndroid.R.drawable.btn_default)中加載了一個新的drawable.這意味着,在應用中所有的按鈕使用的是不同的drawable作爲他們的背景.但是,這些drawables卻是擁有相同的狀態,稱作”constant state”.state中包含的內容是根據使用的drawable類型而定的,但是通常情況下,是包含我們在資源中定義的所有的屬性.以Button爲例,恆定的狀態”constant state”包含一個bitmap image.通過這種方式.在所有的應用中所有的按鈕共享一個bitmap,這樣節省了很多的內存.

下面的圖片展示了在我們指定相同的圖片資源作爲不同的兩個view的背景時,創建了那些類.正如我們所見,創建了兩個drawable,但是他們共享着相同的狀態,因此使用了相同的bitmap.

共享狀態的特徵對於避免浪費內存而言,是不錯的,但是當你嘗試修改drawable的狀態時,會帶來一些問題的.假如: 一個應用有一個書籍的列表,每本書的名字後面有一顆星,當用戶標記爲喜歡的時候,是完全不透明的,在用戶沒有標記爲喜歡的時候,是透明的.爲了實現這個效果,我們很可能在adapter的getView()的方法中書寫如下的代碼

Book book = ...;
TextView listItem = ...;

listItem.setText(book.getTitle());

Drawable star = context.getResources().getDrawable(R.drawable.star);
if (book.isFavorite()) {
  star.setAlpha(255); // opaque
} else {
  star.setAlpha(70); // translucent
}

不幸的是,這部分代碼會產生一個奇怪的結果,所有的drawables都是有用相同的透明度.

用”constant state” 可以解釋這樣的結果.儘管,我們爲每一項得到的是一個新的drawable,但是constant state 仍然是相同的,對於BitmapDrawable而言,透明度是”constant state”的一部分.因此,更改一個drawable的透明度,就會更改所有其他的drawable的透明度.更糟糕的是,想要在Android 1.0 和Android1.1版本上解決這個問題,是沒那麼容易的.

Android 1.5提供了一個合適的方法來解決這個方法,那就是使用mutate()方法.當你調用drawable的這個方法時,更改當前drawable的”constant state”,是不會影響其他的drawables.注意: 就算是mutate()了一個drawable,bitmaps仍然是共享的.下圖顯示了當我們調用mutate()之後,drawable產生了什麼變化.

既然是這樣的,那麼我們就用mutate()來更改之前的程序

Drawable star = context.getResources().getDrawable(R.drawable.star);
if (book.isFavorite()) {
  star.mutate().setAlpha(255); // opaque
} else {
  star. mutate().setAlpha(70); // translucent
}

爲了使用方便,mutate()方法返回了drawable自身,這樣允許我們使用鏈式調用.但是這不會產生一個新的drawable實例.採用上述的代碼後,我們的應用會表現的正常.

以上的部分是個人的翻譯.

這個只是google在developer中的介紹,那麼在程序中是如何體現出來的呢?這個我們就來看看android中是如何加載drawable的xml文件即可.
這裏我們還要看之前看過的一段程序

Resources.loadDrawable(TypedValue value, int id)部分源碼分析

再次分析這段邏輯部分
1. 從緩存中獲取drawable,如果不是null,直接返回
2. 如果是null,從PreloadedDrawables中尋找這個key
3. 如果找到了,那麼根據ConstantState創建一個新的這樣的drawable
4. 如果沒有找到,執行5-6以下的邏輯
5. 如果是顏色,生成ColorDrawable
6. 如果不是顏色,根據xml或者assets的類型做對應的解析
7. 如果這時drawable不是null的話,設置drawable的狀態,並且緩存drawable,如果是preloading,那麼緩存到sPreloadedDrawables中,否則,緩存到sDrawableCache中(在android的系統啓動中preloading是true的,緩存的是系統級別的drawable:sPreloadedDrawables,否則,正常應用啓動時,preloading是false的,緩存的就是應用級別的drawable:sDrawableCache,至於詳細的分析邏輯請參見私房菜的博客:android 系統資源的加載和獲取 : http://blog.csdn.net/shift_wwx/article/details/39502463)

 /*package*/ Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {
                //start log part
                ......
                //end log part

        final long key = (((long) value.assetCookie) << 32) | value.data;
        Drawable dr = getCachedDrawable(key);

        if (dr != null) {
            return dr;
        }

        Drawable.ConstantState cs = sPreloadedDrawables.get(key);
        if (cs != null) {
            dr = cs.newDrawable(this);
        } else {
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
                    value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                dr = new ColorDrawable(value.data);
            }

            if (dr == null) {
                if (value.string == null) {
                    throw new NotFoundException(
                            "Resource is not a Drawable (color or path): " + value);
                }

                String file = value.string.toString();

                                ......

                if (file.endsWith(".xml")) {
                    try {
                        XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXml(this, rp);
                        rp.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }

                } else {
                    try {
                        InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);
                        is.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }
                }
            }
        }

        if (dr != null) {
            dr.setChangingConfigurations(value.changingConfigurations);
            cs = dr.getConstantState();
            if (cs != null) {
                if (mPreloading) {
                    sPreloadedDrawables.put(key, cs);
                } else {
                    synchronized (mTmpValue) {
                        //Log.i(TAG, "Saving cached drawable @ #" +
                        //        Integer.toHexString(key.intValue())
                        //        + " in " + this + ": " + cs);
                        mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs));
                    }
                }
            }
        }

        return dr;
    }

3.4.5 如何繪製當前的drawable

以上介紹了那麼多,其實都只是知識的鋪墊而已,drawable最核心的還是要繪製一些東西.翻看Drawable類的draw(Canvas canvas)發現這個方法是抽象的,這是理所當然的事情嘍,因爲Drawable根本不知道應該繪製什麼,至於需要繪製出什麼效果,其實是應該交由具體的子類來實現的.Drawable的子類很多,我們就挑選兩個來分析一下.
挑選的呢,就選擇BitmapDrawable和StateListDrawable吧,因爲這兩個使用的頻率是很高的.

3.4.5.1 BitmapDrawable的繪製

我們直接來分析BitmapDrawable的繪製方法,中間遇到一些技術點,再看相關的技術點

BitmapDrawable.draw(Canvas canvas)分析
1. 獲取當前的bitmap
2. 如果bitmap爲null,不做邏輯的操作
3. bitmap不爲null時,做如下的操作
4. 獲取當前的BitmapState
5. 如果需要重新構造Shader,做6-8如下的操作
6. 獲取到x軸底紋和y軸底紋,如果都是null的話,設置mPaint的Shader爲null
7. 否則,重新生成一個BitmapShader,並且給mPaint
8. 設置是否要重新構造Shader爲false,並且將邊界拷貝到mDstRect中
9. 獲取到mPaint的Shader,如果shader是null,做10-11如下的操作,否則,做12-13如下的操作
10. 如果要應用Gravity,那麼計算Gravity,並且賦值給mDstRect
11. 在canvas中用mPaint繪製mDstRect大小的bitmap
12. 如果要應用Gravity,那麼設置mDstRect大小爲邊界的大小,並且設置應用Gravity爲false
13. 用畫筆在canvas中繪製目標區域即可

@Override
    public void draw(Canvas canvas) {
        Bitmap bitmap = mBitmap;
        if (bitmap != null) {
            final BitmapState state = mBitmapState;
            if (mRebuildShader) {
                Shader.TileMode tmx = state.mTileModeX;
                Shader.TileMode tmy = state.mTileModeY;

                if (tmx == null && tmy == null) {
                    state.mPaint.setShader(null);
                } else {
                    Shader s = new BitmapShader(bitmap,
                            tmx == null ? Shader.TileMode.CLAMP : tmx,
                            tmy == null ? Shader.TileMode.CLAMP : tmy);
                    state.mPaint.setShader(s);
                }
                mRebuildShader = false;
                copyBounds(mDstRect);
            }

            Shader shader = state.mPaint.getShader();
            if (shader == null) {
                if (mApplyGravity) {
                    Gravity.apply(state.mGravity, mBitmapWidth, mBitmapHeight,
                            getBounds(), mDstRect);
                    mApplyGravity = false;
                }
                canvas.drawBitmap(bitmap, null, mDstRect, state.mPaint);
            } else {
                if (mApplyGravity) {
                    mDstRect.set(getBounds());
                    mApplyGravity = false;
                }
                canvas.drawRect(mDstRect, state.mPaint);
            }
        }
    }

3.4.5.2 StateListDrawable的繪製

在查看Drawable類的體系結構時,發現StateListDrawable並非直接繼承自Drawable,而是繼承自DrawableContainer,然後發現DrawableContainer的子類有幾個

DrawableContainer
–| AnimationDrawable 幀動畫
–| LevelListDrawable 層級的
–| StateListDrawable 狀態相關

分析子類之前,還是要先了解一下父類做的操作的

3.4.5.2.1 DrawableContainer的分析

DrawableContainer正如其名字,是一個Drawable的容器,繼承自這個類,可以實現多個drawable的切換,但是在外界調用者看來,是沒有什麼區別的.
既然是一個容器,那麼肯定有增刪改查的一些操作,但是由於Drawable是用於填充的後臺操作,也就不需要刪除的操作,所以,在這裏,有的是Drawable的增加,修改和查詢的操作

增加 : 容器中增加一個drawable,用於填充drawable時的操作
修改: 修改當前展示的drawable.對於客戶端而言,通知外界的更改方式,然後內部實現更改展示的drawable即可.
查詢:獲得當前的所有和單個的drawable.對於客戶端而言,不需要知道所有的drawable,只需要獲取當前展示的drawable即可(這也是爲什麼基類Drawable中會出現一個方法:getCurrent()).

那麼從以上的三個方面而言,我們最關心的就是修改的實現,這便是位於DrawableContainer的selectDrawable(int idx)方法

DrawableContainer.selectDrawable(int idx)分析

  1. 如果下標不變,返回false即可
  2. 如果下標更改,下標不符合要求後,設置當前的Drawable爲不顯示,然後設置當前的drawable爲null,並且有效的下標爲-1
  3. 如果下標符合要求,那麼設置當前的drawable爲不顯示,更改當前顯示的drawable爲傳遞而來下標的drawable,並且更改這個drawable的狀態
  4. 通知回調函數,當前的狀態已經更改
  5. 返回true

    public boolean selectDrawable(int idx)
    {
        if (idx == mCurIndex) {
            return false;
        }
        if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
            Drawable d = mDrawableContainerState.mDrawables[idx];
            if (mCurrDrawable != null) {
                mCurrDrawable.setVisible(false, false);
            }
            mCurrDrawable = d;
            mCurIndex = idx;
            if (d != null) {
                d.setVisible(isVisible(), true);
                d.setAlpha(mAlpha);
                d.setDither(mDrawableContainerState.mDither);
                d.setColorFilter(mColorFilter);
                d.setState(getState());
                d.setLevel(getLevel());
                d.setBounds(getBounds());
            }
        } else {
            if (mCurrDrawable != null) {
                mCurrDrawable.setVisible(false, false);
            }
            mCurrDrawable = null;
            mCurIndex = -1;
        }
        invalidateSelf();
        return true;
    }
    

說歸說,但是在DrawableContainer類中沒有發現調用這個方法的地方,這也是設計的優雅之處,我只是向外提供了這樣的一個方法,告訴你如何通知容器發生了變化,但是何時調用,這個是需要具體的類來實現的.
下面我們就以StateListDrawable爲例,來分析這個過程.

3.4.5.2.1 StateListDrawable的分析

這個在分析之前,我們可以想象一下,StateListDrawable,就是一羣狀態的結合,最常用的方式,就是在drawable的xml中,我們寫selector的xml.也就是說狀態在更改後,會通知外界更改.那麼響應state的變化,在Drawable的類中,看到使用的是如下的方法.;

Drawable.setState(final int[] stateSet)源碼分析

  1. 如果當前的狀態未更改,返回的是false
  2. 如果當前的狀態更改了,返回的是onStateChange

    /**
     * Specify a set of states for the drawable. These are use-case specific,
     * so see the relevant documentation. As an example, the background for
     * widgets like Button understand the following states:
     * [{@link android.R.attr#state_focused},
     *  {@link android.R.attr#state_pressed}].
     *
     * <p>If the new state you are supplying causes the appearance of the
     * Drawable to change, then it is responsible for calling
     * {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
     * true will be returned from this function.
     *
     * <p>Note: The Drawable holds a reference on to <var>stateSet</var>
     * until a new state array is given to it, so you must not modify this
     * array during that time.</p>
     *
     * @param stateSet The new set of states to be displayed.
     *
     * @return Returns true if this change in state has caused the appearance
     * of the Drawable to change (hence requiring an invalidate), otherwise
     * returns false.
     */
    public boolean setState(final int[] stateSet) {
        if (!Arrays.equals(mStateSet, stateSet)) {
            mStateSet = stateSet;
            return onStateChange(stateSet);
        }
        return false;
    }
    

那麼我們需要關心的就是onStateChange的方法嘍,在StateListDrawable中複寫了這個方法,我們來看看

StateListDrawable.onStateChange(int[] stateSet)源碼分析

  1. 通知state計算下標
  2. 如果下標爲0,轉化爲通用的下標
  3. 調用DrawableContainer.selectDrawable(int idx),如果是true,返回即可
  4. 如果是false,返回基類的onStateChange

    @Override
    protected boolean onStateChange(int[] stateSet) {
        int idx = mStateListState.indexOfStateSet(stateSet);
        if (idx < 0) {
            idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
        }
        if (selectDrawable(idx)) {
            return true;
        }
        return super.onStateChange(stateSet);
    }
    

其實,分析到這裏,我們還沒有提到是如何繪製的,由於DrawableContainer是個容器,其實直接調用當前的drawable的繪製方法即可,只是一個傳遞的作用.

擴展: 看到這裏,我們便可以想象到,LevelListDrawable實現的核心方法就是onLevelChange的時候,判斷是否調用DrawableContainer.selectDrawable(int idx)的方法即可,實時確實如此.

3.5 view與drawable的關係

view與drawable的關係,其實就類似於調用者和被調用者一樣.view中的一些繪製信息,如background和ImageView的src的圖片繪製,會交給Drawable來實現,然後呢,view的一些狀態的更改,如可見,不可見,選中,未選中,透明度等等狀態信息的更改會通知Drawable,然後不同的drawable會相應不同的變化,並且判斷是否要通知callback來做對應的更改即可.view實現這些callback的方法,然後view校驗drawable的信息,並且判斷是否要重繪當前的view.

介紹了這麼多比較抽象的內容,下篇博客我們介紹drawable的相關使用範例.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章