Fresco 源碼分析(四) 后台數據返回到前台的處理 - Drawable體系的介紹(1)


android drawable體系 分析

1. 引

糾結了好久,終究還是要寫這篇博客了,之前個人對於android的drawable類也只是一掃而已,根本也沒有思考過這個是如何的,估計對於一些人也是陌生的,只是知道在android中如何去使用drawable,當然,知道使用是第一步的,但是假如只是停留在這個層次,那么我們對於android的理解還是比較基礎的,個人現在體會最深的一句話是Android is far more than UI layer.
說這些算是一種鋪墊吧,寫說一下個人打算寫這篇博客的思路

2. 本博客結構

本博客打算從一下的幾個方面來說drawable

  1. android drawable日常的用法
  2. drawable體系引發的思考
  3. drawable為何物
  4. drawable類的預覽
  5. view與drawable的關系
  6. 自定義drawable的范例
  7. drawable常見的問題

3. 博客內容

3.1 drawable的日常用法

在學習android中,大家應該都翻閱過google的android的api介紹吧,很多也應該查過google關於drawable的一些用法,就是在drawble下的對應文件夾下建立一些xml文件,或者將我們使用的圖片放置到drawable-*dpi下,然后就可以在xml的布局文件中直接使用R.drawble或者R.color的資源了(PS: R.color是因為在drawble的子文件夾下,我們也可以建立color的文件夾,然后設置color相關的selector以及其他的內容),或者在說代碼中我們采用getResource().getDrawable()的方式來獲取到對應的drawable,然后就可以在代碼中設置給view的background或者ImageView的setImage等等等

或者說有喜歡翻閱android源碼的同學,也會去看看setBackground,setImage或者看看getResource().getDrawable()方法到底做了什么操作,但是我們思考過為什么會出現過drawable以及drawble可以做什么嗎?接下來我們就來說說這個

3.2 為什么可以這樣使用

疑問1. 在drawable的文件夾下,我們可以使用color,state,layer,shape,9png等等,為什么android就可以自動解析呢?

疑問2. 在fresco的使用中,SimpleDraweeView中設置了圖片的url后,加載時能夠看到圖片漸變的效果,然后翻看他們的源碼,看到GenericdraweeView中調用了很多drawble,而且是fresco自定義的drawble,為什么要自定義drawable,而不是使用android原生的drawable呢?

疑問3. 為什么使用android原生的imageView不支持Gif的動畫,結果在android-gif-drawable中自定義了drawable->GifDrawble后結果就支持了呢?

……

像這樣的疑問,我是遇到了多次,才不得不去解開android中的drawble類到底為何物

3.3 drawable為何物

以下內容為個人翻譯自google官方文檔 (http://developer.android.com/reference/android/graphics/drawable/Drawable.html)

drawble是一個抽象的概念,用於描述一個可以繪制的事物,我們最經常使用的就是將drawable作為一種資源,獲取到后,然后顯示到屏幕上.Drawble提供了通用的API來操作多種多樣形式的資源.不像View,Drawable不會接收到與用於交互的一些事件.

另外,為了簡化繪制,Drawble提供了一系列的機制,來讓客戶端與將要繪制到屏幕上的內容的交互.

setBounds(Rect),必須調用,這個需要告訴Drawable哪里需要調用,而且繪制的區域大小是多少.所有的Drawable需要考慮請求的大小,通常簡單的操作是縮放圖像,客戶端會通過getIntrinsicHeight() 和 getIntrinsicWidth() 方法來知道部分drawble最合適的尺寸.

getPadding(Rect),返回一些drawble關於如何約束放置在drawble內部的內容.例如,設計一個給Button約束內容的drawable需要返回正確放置標簽內容的padding值,這個說的有些抽象,看看api的說明吧:返回被drawable用來放置內容的(drawble編輯的內部的)交叉部分的padding值,還是有點迷糊,說白了就是….有點迷糊了…..

setState(int[]) ,允許客戶端告訴drawable,當前需要繪制那個狀態,例如”focused”,”selected”,一些drawble會根據當前的狀態來更改顯示的圖像

setLevel(int),允許客戶端提供一個可以修改當前顯示drawable的單個連貫的控制器,例如電池的級別或者是進度的級別,一些drawble會根據當前的狀態來更改顯示的圖像.(個人舉例:電池: 電量設置的級別為0,25,50,75,100這么幾個級別,那么在0~25的區間會顯示0的狀態,在25~50,會顯示25的這個狀態,50~75會顯示50的狀態依次類推,這個是LevelListDrawable的例子)

drawable可以通過客戶端調用Drawable.Callback的接口來執行動畫.所有的客戶端需要支持這個接口(setCallback(Drawble.Callback)),這樣動畫才會有效.一個簡單的方式來完成這種操作,可以通過系統的工具(其實這里指的就是view)的setBackground和ImageView.

雖然drawable對於應用是不可見的,drawble還是有多重形式的.

Bitmap: 最簡單的drawable,PNG或者是JPEG的圖片

9png : PNG的擴展格式,可以指定內容如何擺放和拉伸

Shape: 用一些簡單的命令而不是bitmap的形式,在一些情況下,可以更好的更改大小

Layers: 一個混合的drawable,可以繪制多個層疊的drawables

States: 一個混合的drawable,根據當前的狀態來選擇對應的drawable

Levels: 一個混合的drawable,根據當前的層級來選擇對應的drawable

Scale: 一個混合的drawable,但是只有一個子drawable,當前整體的大小的更改是根據當前的層級來定的(個人想法: 對於一些應用來講,可能會設計到點擊時變大變小的效果,那么就可以使用這種類型的drawble)

3.4 drawable類的預覽

drawable類包含的內容大概分為如下的幾類

  1. 如何創建一個drawble(從文件,resourceStream,xml中)
  2. 設置當前drawable的一些狀態信息(level,state,bound,alpha,colorFilter,Dither,Tint,不同的drawable子類相應的狀態信息不同,也因此導致了drawable的子類繪制的內容不同,有些設置需要首先了解結合Canvas的一些知識,這里不做介紹,大家可以查閱Tint,Dither,colorFilter等等設置的具體內容)
  3. 獲取當前的狀態信息
  4. drawble如何與客戶端交互(通過設置callback的方式,Drawable.Callback())
  5. drawable的核心: 如何繪制當前的drawable
  6. drawable中一個有意思的問題: mutate()方法

上面的五個方面,我們一個一個的說一下

3.4.1 如何創建一個drawable

在日常我們使用的時候,有兩種方式,一種是從resource的drawable的相關文件夾下初始化,然后在xml的文件中直接使用,另外一種是在程序中通過context.getResources().getDrawable(resId)的方式來獲取,那么在drawble的創建中,肯定是支持這兩種方式的

  1. context.getResources().getDrawable(resId)的方式,即createFromXml()/createFromXmlInner()的方式

Resource.getDrawable的源碼

從以下的代碼中,可以看出,先是獲取TypeValue,然后再去加載這個drawable

    /**
     * Return a drawable object associated with a particular resource ID.
     * Various types of objects will be returned depending on the underlying
     * resource -- for example, a solid color, PNG image, scalable image, etc.
     * The Drawable API hides these implementation details.
     * 
     * @param id The desired resource identifier, as generated by the aapt
     *           tool. This integer encodes the package, type, and resource
     *           entry. The value 0 is an invalid identifier.
     *
     * @throws NotFoundException Throws NotFoundException if the given ID does not exist.
     * 
     * @return Drawable An object that can be used to draw this resource.
     */
    public Drawable getDrawable(int id) throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            return loadDrawable(value, id);
        }
    }

查看loadDrawable的部分,我們關心的部分位於file.endsWith(.xml)的部分,發現是先將xml進行解析,然后將解析的結果進行dr = Drawable.createFromXml(this, rp),中間省略了很多無關緊要的log

 /*package*/ Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {

        ......
        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) {
               ......

                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);
        //                System.out.println("Opened file " + file + ": " + is);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);
                        is.close();
        //                System.out.println("Created stream: " + dr);
                    } 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;
    }
  1. layout中設置background或者src的方式,這種方式就需要查看view在創建的時候,是如何解析這個屬性的,我們以view的background屬性為例

View(Context context, AttributeSet attrs, int defStyle) 部分源碼

猜測也可,經歷的步驟肯定是以下的三個步驟
1. 從布局文件中獲取background
2. 將生成background的drawable
3. 將drawable繪制到屏幕上

    /**
     * Perform inflation from XML and apply a class-specific base style. This
     * constructor of View allows subclasses to use their own base style when
     * they are inflating. For example, a Button class's constructor would call
     * this version of the super class constructor and supply
     * <code>R.attr.buttonStyle</code> for <var>defStyle</var>; this allows
     * the theme's button style to modify all of the base view attributes (in
     * particular its background) as well as the Button class's attributes.
     *
     * @param context The Context the view is running in, through which it can
     *        access the current theme, resources, etc.
     * @param attrs The attributes of the XML tag that is inflating the view.
     * @param defStyle The default style to apply to this view. If 0, no style
     *        will be applied (beyond what is included in the theme). This may
     *        either be an attribute resource, whose value will be retrieved
     *        from the current theme, or an explicit style resource.
     * @see #View(Context, AttributeSet)
     */
    public View(Context context, AttributeSet attrs, int defStyle) {
        this(context);

        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
                defStyle, 0);

        Drawable background = null;

        int leftPadding = -1;
        int topPadding = -1;
        int rightPadding = -1;
        int bottomPadding = -1;

        int padding = -1;

        int viewFlagValues = 0;
        int viewFlagMasks = 0;

        boolean setScrollContainer = false;

        int x = 0;
        int y = 0;

        int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY;

        int overScrollMode = mOverScrollMode;
        final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break;
              ......
            }
        }

        .....

        if (background != null) {
            setBackgroundDrawable(background);
        }

        if (padding >= 0) {
            leftPadding = padding;
            topPadding = padding;
            rightPadding = padding;
            bottomPadding = padding;
        }

        // If the user specified the padding (either with android:padding or
        // android:paddingLeft/Top/Right/Bottom), use this padding, otherwise
        // use the default padding or the padding from the background drawable
        // (stored at this point in mPadding*)
        setPadding(leftPadding >= 0 ? leftPadding : mPaddingLeft,
                topPadding >= 0 ? topPadding : mPaddingTop,
                rightPadding >= 0 ? rightPadding : mPaddingRight,
                bottomPadding >= 0 ? bottomPadding : mPaddingBottom);

                ......

        // Needs to be called after mViewFlags is set
        if (scrollbarStyle != SCROLLBARS_INSIDE_OVERLAY) {
            recomputePadding();
        }
                ......
    }

那么我們關心的第一步和第二部從以下部分截取的源碼部分已經可以看出,是從TypedArray.getDrawable(attr)中獲取到的,那么TypedArray.getDrawable(attr)是如何操作的呢?其實這個只是Drawable的靜態方法的封裝而已~翻閱其源碼,如下

TypedArray.getDrawable(attr)源碼
從這里可以看到,是從Resources.loadDrawable()中獲取到的,這也就是我們第一種方法已經提到的方式嘍

    /**
     * Retrieve the Drawable for the attribute at <var>index</var>.  This
     * gets the resource ID of the selected attribute, and uses
     * {@link Resources#getDrawable Resources.getDrawable} of the owning
     * Resources object to retrieve its Drawable.
     * 
     * @param index Index of attribute to retrieve.
     * 
     * @return Drawable for the attribute, or null if not defined.
     */
    public Drawable getDrawable(int index) {
        final TypedValue value = mValue;
        if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
            if (false) {
                System.out.println("******************************************************************");
                System.out.println("Got drawable resource: type="
                                   + value.type
                                   + " str=" + value.string
                                   + " int=0x" + Integer.toHexString(value.data)
                                   + " cookie=" + value.assetCookie);
                System.out.println("******************************************************************");
            }
            return mResources.loadDrawable(value, value.resourceId);
        }
        return null;
    }
  1. 從文件中直接生成drawable : createFromPath(String pathName),google已經將此進行了封裝,以下是源碼部分:

Drawable.createFromPath(String path)的源碼

從以下的源碼,看到邏輯如下
1. 如果path是null的話,返回null
2. path不是null的話,將path解析為bitmap
3. 從bitmap中生成一個drawable

  /**
     * Create a drawable from file path name.
     */
    public static Drawable createFromPath(String pathName) {
        if (pathName == null) {
            return null;
        }

        Bitmap bm = BitmapFactory.decodeFile(pathName);
        if (bm != null) {
            return drawableFromBitmap(null, bm, null, null, pathName);
        }

        return null;
    }

那么下面,我們便需要看drawableFromBitmap是如何操作的即可

Drawable.drawableFromBitmap(…)的源碼

邏輯如下
1. 如果np不是null的話,返回9png的drawable
2. np是null的話,返回BitmapDrawable

    private static Drawable drawableFromBitmap(Resources res, Bitmap bm, byte[] np,
            Rect pad, String srcName) {

        if (np != null) {
            return new NinePatchDrawable(res, bm, np, pad, srcName);
        }

        return new BitmapDrawable(res, bm);
    }

但是我們的createFromPath的最后調用的drawableFromBitmap的部分,np為null,所以,如果調用Drawable.createFromPath()的方法的話,生成的只是一個單純的BitmapDrawable,所以假如我們的圖片是一個Gif的話,通過這種方式生成,那么也只是一個單純的一幀而已,不是動態的

3.4.2 設置drawable狀態

設置這些狀態信息的時候,按照兩種類型來描述吧,
1. google的官方文檔中drawable的描述中看到的
2. api中的狀態

3.4.2.1 drawable的描述中的方法

setBounds(Rect)
setState(int[])
setLevel(int)
這幾個方法已經在上面描述過,這里不再贅述

3.4.2.2 drawable的描述中的方法

setAlpha : bitmap的透明度設置
setColorFilter : 顏色填充
setDither : 如果設置為true,設備像素點低於八位的話,會優化一些顯示的效果
setFilterBitmap :
setVisiable : Bitmap是否顯示

3.4.3 獲取drawable的狀態信息

獲取的狀態信息與設置的狀態信息是幾乎一致的,不再描述

3.4.4 drawable如何與客戶端交互

drawable與客戶端的交互式通過Callback的方式,這里需要舉例,我們就用view和drawable的例子即可

在android中,以在layout中設置view的background的方式為例,在 “3.4.1 如何創建一個drawable” 中的創建方式2中已經提到了view的setBackground的三個步驟:

  1. 從布局文件中獲取background
  2. 將生成background的drawable
  3. 將drawable繪制到屏幕上
    其中第一步和第二步我們已經在上面分析了,但是view和drawable是如何關聯起來的,這屬於第三步的一部分

View.setBackgroundDrawable(Drawable d)源碼部分分析
邏輯如下:
1. 原來的原來的drawable不是null的話,設置原來的drawable的回調設置為null,並且unscheduleDrawable這個drawable
2. 假如新的drawable不是null的話,關聯drawable,設置回調,設置相關的狀態信息,並且設置是否要請求layout
3. 假如新的drawble是null的話,清空背景
4. 計算透明度,判斷是否要請求,並且設置mBackgroundSizeChanged為true,然后invalidate()

  /**
     * Set the background to a given Drawable, or remove the background. If the
     * background has padding, this View's padding is set to the background's
     * padding. However, when a background is removed, this View's padding isn't
     * touched. If setting the padding is desired, please use
     * {@link #setPadding(int, int, int, int)}.
     *
     * @param d The Drawable to use as the background, or null to remove the
     *        background
     */
    public void setBackgroundDrawable(Drawable d) {
        boolean requestLayout = false;

        mBackgroundResource = 0;

        /*
         * Regardless of whether we're setting a new background or not, we want
         * to clear the previous drawable.
         */
        if (mBGDrawable != null) {
            mBGDrawable.setCallback(null);
            unscheduleDrawable(mBGDrawable);
        }

        if (d != null) {
            Rect padding = sThreadLocal.get();
            if (padding == null) {
                padding = new Rect();
                sThreadLocal.set(padding);
            }
            if (d.getPadding(padding)) {
                setPadding(padding.left, padding.top, padding.right, padding.bottom);
            }

            // Compare the minimum sizes of the old Drawable and the new.  If there isn't an old or
            // if it has a different minimum size, we should layout again
            if (mBGDrawable == null || mBGDrawable.getMinimumHeight() != d.getMinimumHeight() ||
                    mBGDrawable.getMinimumWidth() != d.getMinimumWidth()) {
                requestLayout = true;
            }

            d.setCallback(this);
            if (d.isStateful()) {
                d.setState(getDrawableState());
            }
            d.setVisible(getVisibility() == VISIBLE, false);
            mBGDrawable = d;

            if ((mPrivateFlags & SKIP_DRAW) != 0) {
                mPrivateFlags &= ~SKIP_DRAW;
                mPrivateFlags |= ONLY_DRAWS_BACKGROUND;
                requestLayout = true;
            }
        } else {
            /* Remove the background */
            mBGDrawable = null;

            if ((mPrivateFlags & ONLY_DRAWS_BACKGROUND) != 0) {
                /*
                 * This view ONLY drew the background before and we're removing
                 * the background, so now it won't draw anything
                 * (hence we SKIP_DRAW)
                 */
                mPrivateFlags &= ~ONLY_DRAWS_BACKGROUND;
                mPrivateFlags |= SKIP_DRAW;
            }

            /*
             * When the background is set, we try to apply its padding to this
             * View. When the background is removed, we don't touch this View's
             * padding. This is noted in the Javadocs. Hence, we don't need to
             * requestLayout(), the invalidate() below is sufficient.
             */

            // The old background's minimum size could have affected this
            // View's layout, so let's requestLayout
            requestLayout = true;
        }

        computeOpaqueFlags();

        if (requestLayout) {
            requestLayout();
        }

        mBackgroundSizeChanged = true;
        invalidate();
    }

我們關心的部分在於設置callback,那么看到了,drawable與view的交互是通過callback的方式

看看Drawable的Callback接口

Drawble.Callback 接口

在drawable的一些狀態更改后,會通知外界應該更新狀態即invalidateDrawable()
如果是動畫的話,那么可以scheduleDrawable,實現定期的更新drawable,並且通知外界,動畫停止的話,可以unscheduleDrawable

 /**
     * Implement this interface if you want to create an animated drawable that
     * extends {@link android.graphics.drawable.Drawable Drawable}.
     * Upon retrieving a drawable, use
     * {@link Drawable#setCallback(android.graphics.drawable.Drawable.Callback)}
     * to supply your implementation of the interface to the drawable; it uses
     * this interface to schedule and execute animation changes.
     */
    public static interface Callback {
        /**
         * Called when the drawable needs to be redrawn.  A view at this point
         * should invalidate itself (or at least the part of itself where the
         * drawable appears).
         *
         * @param who The drawable that is requesting the update.
         */
        public void invalidateDrawable(Drawable who);

        /**
         * A Drawable can call this to schedule the next frame of its
         * animation.  An implementation can generally simply call
         * {@link android.os.Handler#postAtTime(Runnable, Object, long)} with
         * the parameters <var>(what, who, when)</var> to perform the
         * scheduling.
         *
         * @param who The drawable being scheduled.
         * @param what The action to execute.
         * @param when The time (in milliseconds) to run.  The timebase is
         *             {@link android.os.SystemClock#uptimeMillis}
         */
        public void scheduleDrawable(Drawable who, Runnable what, long when);

        /**
         * A Drawable can call this to unschedule an action previously
         * scheduled with {@link #scheduleDrawable}.  An implementation can
         * generally simply call
         * {@link android.os.Handler#removeCallbacks(Runnable, Object)} with
         * the parameters <var>(what, who)</var> to unschedule the drawable.
         *
         * @param who The drawable being unscheduled.
         * @param what The action being unscheduled.
         */
        public void unscheduleDrawable(Drawable who, Runnable what);
    }

下篇我們會介紹剩余的內容


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com