从源码上剖析Android View绘制Drawable的原理

来源:互联网 发布:网络中断 123.207 编辑:程序博客网 时间:2024/04/30 01:15

一、引言

对于Drawable,相比每个Android 开发者都无比熟悉,在开发过程中我们经常setBackground设置背景,那么对于Drawable你了解多少呢?对于View是怎样把Drawable绘制出来又了解多少呢?对View根据不同状态绘制不同的背景又了解多少呢?也就是我们经常使用的selector,今天我们从源码上来深度剖析这些原理,从本质上卸下Drawable的神秘面纱。

二、背景介绍

在源码路径:frameworks/base/graphics/java/android/graphics/drawable/Drawable.java
进入Drawable.java里面去看源码,你会看到这样一段描述文字:

A Drawable is a general abstraction for "something that can be drawn."  Most 1. often you will deal with Drawable as the type of resource retrieved for 2. drawing things to the screen; the Drawable class provides a generic API for 3. dealing with an underlying visual resource that may take a variety of forms. 4. Unlike a {@link android.view.View}, a Drawable does not have any facility to 5. receive events or otherwise interact with the user.

也就是说Drawable是Android开发中的通用可绘制对象,View类默认针对Drawable进行一些必要的绘制,如背景。

三、Drawable的核心分析

1、先来介绍背景选择器selector
对Android开发有经验的同学,对 节点的使用一定很熟悉,该节点的作用就是定义一组状态资源图片,使其能够在不同的状态下更换某个View的背景图片。比如demo_selector.xml

<?xml version="1.0" encoding="utf-8" ?>   <selector xmlns:android="http://schemas.android.com/apk/res/android">   <!-- 触摸时并且当前窗口处于交互状态 -->    <item android:state_pressed="true" android:state_window_focused="true" android:drawable= "@drawable/pic1" />  <!--  触摸时并且没有获得焦点状态 -->    <item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/pic2" />    <!--选中时的图片背景-->    <item android:state_selected="true" android:drawable="@drawable/pic3" />     <!--获得焦点时的图片背景-->    <item android:state_focused="true" android:drawable="@drawable/pic4" />    <!-- 窗口没有处于交互时的背景图片 -->    <item android:drawable="@drawable/pic5" /> </selector>

其实这个xml文件会被Android框架解析成StateListDrawable类对象。通常我们在XML文件中设置View的background如下:

android:background="@drawable/drawable_bg"/*设置其他Drawable背景*/android:background="#ff124a54"/*设置颜色背景*/

对于background我们应该明确一下几点知识:android:background属性值在实际操作中都会实例化为Drawable,常用的属性值与Drawable对象关系如下:

  • “#ffffffff”=>ColorDrawable
  • @drawable/shape_bg”=>GradientDrawable(假设shape_bg.xml文件为顶层为shape标签)
  • “@drawable/bmp_bg”=>BitmapDrawable(假设bmp_bg为.png/.jpg/.bmp文件)
  • “@drawable/9patch_bg”=>NinePatchDrawable(假设9patch_bg为.9.png文件)

2、接下来分析Drawable的ConstantState

在用代码生成Drawable的时候,相信你会经常看看ConstantState的身影,那么ConstantState到底是什么鬼呢?下面我们来揭开它的神秘面纱。
如果把Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容,Drawable的源码如下:

 /**     * This abstract class is used by {@link Drawable}s to store shared constant state and data     * between Drawables. {@link BitmapDrawable}s created from the same resource will for instance     * share a unique bitmap stored in their ConstantState.     *     * <p>     * {@link #newDrawable(Resources)} can be used as a factory to create new Drawable instances     * from this ConstantState.     * </p>     *     * Use {@link Drawable#getConstantState()} to retrieve the ConstantState of a Drawable. Calling     * {@link Drawable#mutate()} on a Drawable should typically create a new ConstantState for that     * Drawable.     */    public static abstract class ConstantState {        /**         * Create a new drawable without supplying resources the caller         * is running in.  Note that using this means the density-dependent         * drawables (like bitmaps) will not be able to update their target         * density correctly. One should use {@link #newDrawable(Resources)}         * instead to provide a resource.         */        public abstract Drawable newDrawable();        /**         * Create a new Drawable instance from its constant state.  This         * must be implemented for drawables that change based on the target         * density of their caller (that is depending on whether it is         * in compatibility mode).         */        public Drawable newDrawable(Resources res) {            return newDrawable();        }        /**         * Create a new Drawable instance from its constant state. This must be         * implemented for drawables that can have a theme applied.         */        public Drawable newDrawable(Resources res, Theme theme) {            return newDrawable(null);        }        /**         * Return a bit mask of configuration changes that will impact         * this drawable (and thus require completely reloading it).         */        public abstract int getChangingConfigurations();        /**         * @return Total pixel count         * @hide         */        public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {            return 0;        }        /** @hide */        protected final boolean isAtlasable(Bitmap bitmap) {            return bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888;        }        /**         * Return whether this constant state can have a theme applied.         */        public boolean canApplyTheme() {            return false;        }    }    /**     * Return a {@link ConstantState} instance that holds the shared state of this Drawable.     *     * @return The ConstantState associated to that Drawable.     * @see ConstantState     * @see Drawable#mutate()     */    public ConstantState getConstantState() {        return null;    }

可以看出,ConstantState其实是Drawable中的静态抽象类,并且getConstantState()方法默认返回null,这我们可以推测ConstantState类和getConstantState()方法会被具体的Drawable实现类继承和重写。这样不难想象,由于不同Drawbale如BitmapDrawable、GradientDrawable这些自身存储的绘制内容数据原则上是不一样的,这就意味着Drawable为了易于扩展,ConstantState对象应该会存储属性可变的数据。
参考StateListDrawable的源码,确实是有DrawableContainer继承ConstantState的身影。

    static class StateListState extends DrawableContainerState {        int[] mThemeAttrs;        int[][] mStateSets;        StateListState(StateListState orig, StateListDrawable owner, Resources res) {            super(orig, owner, res);            if (orig != null) {                // Perform a shallow copy and rely on mutate() to deep-copy.                mThemeAttrs = orig.mThemeAttrs;                mStateSets = orig.mStateSets;            } else {                mThemeAttrs = null;                mStateSets = new int[getCapacity()][];            }        }        void mutate() {            mThemeAttrs = mThemeAttrs != null ? mThemeAttrs.clone() : null;            final int[][] stateSets = new int[mStateSets.length][];            for (int i = mStateSets.length - 1; i >= 0; i--) {                stateSets[i] = mStateSets[i] != null ? mStateSets[i].clone() : null;            }            mStateSets = stateSets;        }        int addStateSet(int[] stateSet, Drawable drawable) {            final int pos = addChild(drawable);            mStateSets[pos] = stateSet;            return pos;        }        int indexOfStateSet(int[] stateSet) {            final int[][] stateSets = mStateSets;            final int N = getChildCount();            for (int i = 0; i < N; i++) {                if (StateSet.stateSetMatches(stateSets[i], stateSet)) {                    return i;                }            }            return -1;        }        @Override        public Drawable newDrawable() {            return new StateListDrawable(this, null);        }        @Override        public Drawable newDrawable(Resources res) {            return new StateListDrawable(this, res);        }        @Override        public boolean canApplyTheme() {            return mThemeAttrs != null || super.canApplyTheme();        }        @Override        public void growArray(int oldSize, int newSize) {            super.growArray(oldSize, newSize);            final int[][] newStateSets = new int[newSize][];            System.arraycopy(mStateSets, 0, newStateSets, 0, oldSize);            mStateSets = newStateSets;        }    }

那么Drawable跟ConstantState之间有什么关系呢?
大家可以看看国外这篇文章http://www.curious-creature.com/2009/05/02/drawable-mutations/
其实两者之间的关系如下图所示:
这里写图片描述
由同一个Resource实例化生成的多个Drawable公用一个ConstanteState,这是出于对内存节省策略的考虑。那么当我们需要修改多个Drawable中的其中一个的属性时,就会出现修改后的Drawable影响到了其他Drawable的情况,针对这种情况,我们的想法自然是复制一份ConstantState从而避免相互间的干扰。

从根源上复制Drawable的方法可以使用:/*假设dr为原有Drawable*/Drawable newDr = dr.getConstantState().newDrawable();/*或者*/Drawable newDr = dr.getConstantState().newDrawable().mutate();

例如,我在Switch.java中有这样子的定义:

 if (mTrackDrawable != null) {            mTrackDrawable.setCallback(this);            if (mTrackDrawable.getConstantState() != null) {                mTrackOnDrawable = mTrackDrawable.getConstantState().newDrawable().mutate();                mTrackOnDrawable.setState(new int[]{android.R.attr.state_checked});            }            if (mTrackDrawable.getConstantState() != null) {                mTrackOffDrawable = mTrackDrawable.getConstantState().newDrawable().mutate();                mTrackOffDrawable.setState(new int[]{-android.R.attr.state_checked});            }        }

3、StateListDrawable类简介
类功能说明:该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。
常用方法为:

   public void addState (int[] stateSet, Drawable drawable)       功能: 给特定的状态集合设置drawable图片资源

还记得我在前面介绍Selector的时候有说过XML最终会转换成StateListDrawable类,所以我们还是以demo_selector.xml来分析。

//初始化一个空对象StateListDrawable stalistDrawable = new StateListDrawable();//获取对应的属性值 Android框架自带的属性 attrint pressed = android.R.attr.state_pressed;int window_focused = android.R.attr.state_window_focused;int focused = android.R.attr.state_focused;int selected = android.R.attr.state_selected;stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1));stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2);stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3);stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4);//没有任何状态时显示的图片,我们给它设置我空集合stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5);

上面的“-”负号表示对应的属性值为false
当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。

     public boolean isStateful ()     功能: 表明该状态改变了,对应的drawable图片是否会改变。     注:在StateListDrawable类中,该方法返回为true,显然状态改变后,我们的图片会跟着改变

说到状态那么不得不说在Android中View的各种状态, 一般来说,Android框架为View定义了四种不同的状态,这些状态值的改变会引发View相关操作,例如:更换背景图片、是否触发点击事件等;我们来看下图:(注明:图片表格是借鉴网上资源)
这里写图片描述
注意:selected不同于focus状态,通常在AdapterView类群下例如ListView或者GridView会使某个View处于
selected状态,并且获得该状态的View处于高亮状态。而一个窗口只能有一个视图获得焦点(focus),而一个窗口可以有多个视图处于”selected”状态中。
总结:focused状态一般是由按键操作引起的;
pressed状态是由触摸消息引起的;
selected则完全是由应用程序主动调用setSelected()进行控制。

4、View如何根据状态值的改变去绘制/显示对应的背景图?
当View任何状态值发生改变时,都会调用refreshDrawableList()方法去更新对应的背景Drawable对象。
源码路径:frameworks\base\core\java\android\view\View.java

    /**     * Call this to force a view to update its drawable state. This will cause     * drawableStateChanged to be called on this view. Views that are interested     * in the new state should call getDrawableState.     *     * @see #drawableStateChanged     * @see #getDrawableState     */    public void refreshDrawableState() {        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;        //改变drawable状态        drawableStateChanged();        ViewParent parent = mParent;        if (parent != null) {        //如果该View之上仍有父View,则一直递归通知            parent.childDrawableStateChanged(this);        }    }

该方法主要功能是根据当前的状态值去更换对应的背景Drawable对象。重点看看drawableStateChanged()

/**     * This function is called whenever the state of the view changes in such     * a way that it impacts the state of drawables being shown.     * <p>     * If the View has a StateListAnimator, it will also be called to run necessary state     * change animations.     * <p>     * Be sure to call through to the superclass when overriding this function.     *     * @see Drawable#setState(int[])     */    @CallSuper    protected void drawableStateChanged() {    //获取当前Drawable状态(pressed),刷新mBackground的状态        final int[] state = getDrawableState();        final Drawable bg = mBackground;        if (bg != null && bg.isStateful()) {            bg.setState(state);        }        final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;        if (fg != null && fg.isStateful()) {            fg.setState(state);        }        if (mScrollCache != null) {            final Drawable scrollBar = mScrollCache.scrollBar;            if (scrollBar != null && scrollBar.isStateful()) {                scrollBar.setState(state);            }        }        if (mStateListAnimator != null) {            mStateListAnimator.setState(state);        }    }

该方法获得当前的状态属性— 整型集合 ; 调用Drawable类的setState方法去获取资源,最后进去看看setState()方法的具体实现:

 public boolean setState(final int[] stateSet) {        if (!Arrays.equals(mStateSet, stateSet)) {            mStateSet = stateSet;            return onStateChange(stateSet);        }        return false;    }

判断状态值是否发生了变化,如果发生了变化,就调用onStateChange()方法进一步处理。

 @Override    protected boolean onStateChange(int[] stateSet) {        final boolean changed = super.onStateChange(stateSet);        int idx = mStateListState.indexOfStateSet(stateSet);        if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "                + Arrays.toString(stateSet) + " found " + idx);        if (idx < 0) {            idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);        }        return selectDrawable(idx) || changed;    }

根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处
继而,调用selectDrawable()方法去获取索引下标的当前Drawable对象 具体查找算法在 mStateListState.indexOfStateSet(stateSet) 里实现了。基本思路是:查找第一个能完全吻合该新状态的索引下标,如果找到了,则立即返回。

 private int indexOfStateSet(int[] stateSet) {            int[][] stateSets = this.mStateSets;            int N = this.getChildCount();            for(int i = 0; i < N; ++i) {                if(StateSet.stateSetMatches(stateSets[i], stateSet)) {                    return i;                }            }            return -1;        }

再来看看selectDrawable的具体实现

    public boolean selectDrawable(int idx)    {        if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {            //获取对应索引位置的Drawable对象            Drawable d = mDrawableContainerState.mDrawables[idx];            ...            mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象            mCurIndex = idx;            ...        } else {           ...        }        //请求该View刷新自己,这个方法我们稍后讲解。        invalidateSelf();        return true;    }

该函数的主要功能是选择当前索引下标处的Drawable对象,并保存在mCurrDrawable中。

5、View如何绘制Drawable
经过上面一系列的Drawable状态切换和匹配Drawale,最终我们有了需要重新绘制的pressed状态下的Drawable,此时请求绘制自身并最终会调用View的draw()方法。

public void draw(Canvas canvas) {        ...        // draw方法会先绘制背景        // 不透明才绘制背景        if (!dirtyOpaque) {              final Drawable background = mBackground;            if (background != null) {                final int scrollX = mScrollX;                final int scrollY = mScrollY;                if (mBackgroundSizeChanged) {                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);                    mBackgroundSizeChanged = false;                }                if ((scrollX | scrollY) == 0) {                    background.draw(canvas);//开始绘制的动作                } else {                    canvas.translate(scrollX, scrollY);                    background.draw(canvas);//开始绘制的动作                    canvas.translate(-scrollX, -scrollY);                }            }        }

由于Drawable的draw()方法由其实现类完成,绘制动作各有不同,但最终还是调用canvas.draw…的方法去绘制。

6、关于Drawable.Callback接口

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

对于CallBack接口,主要看看invalidateDrawable(Drawable who)方法,这个方法的功能是如果Drawable对象的状态发生了变化,会请求View重新绘制,因此我们对应于该View的背景Drawable对象能够重新”绘制“出来。
7、自定义View绘制背景Drawable

public class MyView extends View   {    private Context mContext = null;    private Drawable mBackground = null;    private boolean mSizeChanged = true;  //视图View布局(layout)大小是否发生变化    public MyView(Context context)    {        super(context);        mContext = context;               initStateListDrawable(); // 初始化图片资源    }    // 初始化图片资源    private void initStateListDrawable()    {        //有两种方式获取我们的StateListDrawable对象:        // 1、代码构建一个StateListDrawable对象        StateListDrawable statelistDrawable = new StateListDrawable();        int pressed = android.R.attr.state_pressed;        int windowfocused = android.R.attr.state_window_focused;        int enabled = android.R.attr.state_enabled;        int stateFoucesd = android.R.attr.state_focused;        //匹配状态时,是一种优先包含的关系。        // "-"号表示该状态值为false .即不匹配        statelistDrawable.addState(new int[] { pressed, windowfocused },         mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed));        statelistDrawable.addState(new int[]{ -pressed, windowfocused },                 mContext.getResources().getDrawable(R.drawable.btn_power_on_nor));                 mBackground = statelistDrawable;        //必须设置回调,当改变状态时,会回掉该View进行invalidate()刷新操作.        mBackground.setCallback(this);               //取消默认的背景图片,因为我们设置了自己的背景图片了,否则可能造成背景图片重叠。        this.setBackgroundDrawable(null);        // 获取方式二、、使用XML获取StateListDrawable对象        // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);    }    protected void drawableStateChanged()    {        Drawable d = mBackground;        if (d != null && d.isStateful())        {            d.setState(getDrawableState());        }       super.drawableStateChanged();    }    //验证图片是否相等 , 在invalidateDrawable()会调用此方法,我们需要重写该方法。    protected boolean verifyDrawable(Drawable who)    {        return who == mBackground || super.verifyDrawable(who);    }    //draw()过程,绘制背景图片...    public void draw(Canvas canvas)    {        if (mBackground != null)        {            if(mSizeChanged)            {                //设置边界范围                mBackground.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());                mSizeChanged = false ;            }            if ((getScrollX() | getScrollY()) == 0)  //是否偏移            {                mBackground.draw(canvas); //绘制当前状态对应的图片            }            else            {                canvas.translate(getScrollX(), getScrollY());                mBackground.draw(canvas); //绘制当前状态对应的图片                canvas.translate(-getScrollX(), -getScrollY());            }        }        super.draw(canvas);    }    public void onDraw(Canvas canvas) {            ...    }}
0 0
原创粉丝点击