Fresco 源码分析(四) 后台数据返回到前台的处理 - Drawable体系的介绍(1)

时间:2021-07-06 09:25:24

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);
    }

下篇我们会介绍剩余的内容