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

时间:2021-09-26 08:12:47

在上篇中我们介绍了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.

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

共享状态的特征对于避免浪费内存而言,是不错的,但是当你尝试修改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都是有用相同的透明度.

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

用”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产生了什么变化.

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

既然是这样的,那么我们就用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实例.采用上述的代码后,我们的应用会表现的正常.

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

以上的部分是个人的翻译.

这个只是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的相关使用范例.