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

一、引言

对于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框架自带的属性 attr
int 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) {    
        ...
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值