Android中View(视图)绘制不同状态背景图片原理深入分析以及StateListDrawable使用详解

今天继续给大家分享下View的相关知识,重点有一下两点:


           1、View的几种不同状态属性
           2、如何根据不同状态去切换我们的背景图片。


开篇介绍:android背景选择器selector用法汇总


        对Android开发有经验的同学,对 <selector>节点的使用一定很熟悉,该节点的作用就是定义一组状态资源图片,使其能够
  在不同的状态下更换某个View的背景图片。例如,如下的hello_selection.xml文件定义:
  1. <?xml version="1.0" encoding="utf-8" ?>   
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3.   <!-- 触摸时并且当前窗口处于交互状态 -->  
  4.   <item android:state_pressed="true" android:state_window_focused="true" android:drawable= "@drawable/pic1" />
  5.   <!--  触摸时并且没有获得焦点状态 -->  
  6.   <item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/pic2" />  
  7.   <!--选中时的图片背景-->  
  8.   <item android:state_selected="true" android:drawable="@drawable/pic3" />   
  9.   <!--获得焦点时的图片背景-->  
  10.   <item android:state_focused="true" android:drawable="@drawable/pic4" />  
  11.   <!-- 窗口没有处于交互时的背景图片 -->  
  12.   <item android:drawable="@drawable/pic5" />
  13. </selector>
复制代码
更多关于 <selector>节点的使用请参考: Android selector背景选择器
其实,前面说的xml文件,最终会被Android框架解析成StateListDrawable类对象。


知识点一:StateListDrawable类介绍

  类功能说明:该类定义了不同状态值下与之对应的图片资源,即我们可以利用该类保存多种状态值,多种图片资源。常用方法为:
       public void addState (int[] stateSet, Drawable drawable)
       功能: 给特定的状态集合设置drawable图片资源使用方式:参考前面的 hello_selection.xml文件,我们利用代码去构建一个相同的StateListDrawable类对象,如下:
  1. //初始化一个空对象
  2. StateListDrawable stalistDrawable = new StateListDrawable();
  3. //获取对应的属性值 Android框架自带的属性 attr
  4. int pressed = android.R.attr.state_pressed;
  5. int window_focused = android.R.attr.state_window_focused;
  6. int focused = android.R.attr.state_focused;
  7. int selected = android.R.attr.state_selected;

  8. stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1));
  9. stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2);
  10. stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3);
  11. stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4);
  12. //没有任何状态时显示的图片,我们给它设置我空集合
  13. stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5);
复制代码
上面的“-”负号表示对应的属性值为false
        当我们为某个View使用其作为背景色时,会根据状态进行背景图的转换。


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



知识点二:View的五种状态值

       一般来说,Android框架为View定义了四种不同的状态,这些状态值的改变会引发View相关操作,例如:更换背景图片、是否
   触 发点击事件等;视
      视图几种不同状态含义见下图:


其中selected和focused的区别有如下几点:
      1,我们通过查看setSelected()方法,来获取相关信息。
        SDK中对setSelected()方法----对于与selected状态有如下说明:
             public void setSelected (boolean selected)
             Since: APILevel 1
             Changes the selection state of this view. Aview can be selected or not. Note that selection is not the same a s
        focus. Views are typically selected in the context of an AdapterView like ListView or GridView ;the selected view is
        the view that is highlighted.
            Parameters selected   true if the view must be selected, false otherwise

           由以上可知: selected不同于focus状态,通常在AdapterView类群下例如ListView或者GridView会使某个View处于
      selected状态,并且获得该状态的View处于高亮状态。

    2、一个窗口只能有一个视图获得焦点(focus),而一个窗口可以有多个视图处于”selected”状态中。

      总结:focused状态一般是由按键操作引起的;
                pressed状态是由触摸消息引起的;
                selected则完全是由应用程序主动调用setSelected()进行控制。

      例如:当我们触摸某个控件时,会导致pressed状态改变;获得焦点时,会导致focus状态变化。于是,我们可以通过这种
   更新后状态值去更新我们对应的Drawable对象了。



问题:如何根据状态值的改变去绘制/显示对应的背景图?


       当View任何状态值发生改变时,都会调用refreshDrawableList()方法去更新对应的背景Drawable对象。
       其整体调用流程如下: View.java类中
  1. //路径:\frameworks\base\core\java\android\view\View.java
  2.     /* Call this to force a view to update its drawable state. This will cause
  3.      * drawableStateChanged to be called on this view. Views that are interested
  4.      * in the new state should call getDrawableState.
  5.      */
  6.     //主要功能是根据当前的状态值去更换对应的背景Drawable对象
  7.     public void refreshDrawableState() {
  8.         mPrivateFlags |= DRAWABLE_STATE_DIRTY;
  9.         //所有功能在这个函数里去完成
  10.         drawableStateChanged();
  11.         ...
  12.     }
  13.     /* This function is called whenever the state of the view changes in such
  14.      * a way that it impacts the state of drawables being shown.
  15.      */
  16.     // 获得当前的状态属性--- 整型集合 ; 调用Drawable类的setState方法去获取资源。
  17.     protected void drawableStateChanged() {
  18.             //该视图对应的Drawable对象,通常对应于StateListDrawable类对象
  19.         Drawable d = mBGDrawable;   
  20.         if (d != null && d.isStateful()) {  //通常都是成立的
  21.                 //getDrawableState()方法主要功能:会根据当前View的状态属性值,将其转换为一个整型集合
  22.                 //setState()方法主要功能:根据当前的获取到的状态,更新对应状态下的Drawable对象。
  23.             d.setState(getDrawableState());
  24.         }
  25.     }
  26.     /*Return an array of resource IDs of the drawable states representing the
  27.      * current state of the view.
  28.      */
  29.     public final int[] getDrawableState() {
  30.         if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
  31.             return mDrawableState;
  32.         } else {
  33.                 //根据当前View的状态属性值,将其转换为一个整型集合,并返回
  34.             mDrawableState = onCreateDrawableState(0);
  35.             mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
  36.             return mDrawableState;
  37.         }
  38.     }
复制代码
通过这段代码我们可以明白View内部是如何获取更细后的状态值以及动态获取对应的背景Drawable对象----setState()方法
去完成的。 这儿我简单的分析 下Drawable类里的setState()方法的功能,把流程给走一下:
   
         Step 1 、 setState()函数原型 ,
             函数位于:frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
  1.     //如果状态态值发生了改变,就回调onStateChange()方法。
  2.     public boolean setState(final int[] stateSet) {
  3.         if (!Arrays.equals(mStateSet, stateSet)) {
  4.             mStateSet = stateSet;
  5.             return onStateChange(stateSet);
  6.         }
  7.         return false;
  8.     }
复制代码
该函数的主要功能: 判断状态值是否发生了变化,如果发生了变化,就调用onStateChange()方法进一步处理。
   
       Step 2 、onStateChange()函数原型:
            该函数位于 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
  1.     //状态值发生了改变,我们需要找出第一个吻合的当前状态的Drawable对象
  2.     protected boolean onStateChange(int[] stateSet) {
  3.             //要找出第一个吻合的当前状态的Drawable对象所在的索引位置, 具体匹配算法请自己深入源码看看
  4.         int idx = mStateListState.indexOfStateSet(stateSet);
  5.         ...
  6.         //获取对应索引位置的Drawable对象
  7.         if (selectDrawable(idx)) {
  8.             return true;
  9.         }
  10.         ...
  11.     }
复制代码
该函数的主要功能: 根据新的状态值,从StateListDrawable实例对象中,找到第一个完全吻合该新状态值的索引下标处 ;
   继而,调用selectDrawable()方法去获取 索引下标的当前Drawable对象。
          具体查找算法在 mStateListState.indexOfStateSet(stateSet) 里实现了。基本思路是:查找第一个能完全吻合该新状态值
   的索引下标,如果找到了,则立即返回。 具体实现过程,只好看看源码咯。
  
       Step 3 、selectDrawable()函数原型:
            该函数位于 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 类中
  1.     public boolean selectDrawable(int idx)
  2.     {
  3.         if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
  4.                 //获取对应索引位置的Drawable对象
  5.             Drawable d = mDrawableContainerState.mDrawables[idx];
  6.             ...
  7.             mCurrDrawable = d; //mCurrDrawable即使当前Drawable对象
  8.             mCurIndex = idx;
  9.             ...
  10.         } else {
  11.            ...
  12.         }
  13.         //请求该View刷新自己,这个方法我们稍后讲解。
  14.         invalidateSelf();
  15.         return true;
  16.     }
复制代码
该函数的主要功能是选择当前索引下标处的Drawable对象,并保存在mCurrDrawable中。




知识点三: 关于Drawable.Callback接口
   
    该接口定义了如下三个函数:
  1.     //该函数位于 frameworks\base\graphics\java\android\graphics\drawable\Drawable.java 类中
  2.     public static interface Callback {
  3.             //如果Drawable对象的状态发生了变化,会请求View重新绘制,
  4.             //因此我们对应于该View的背景Drawable对象能够”绘制出来”.
  5.         public void invalidateDrawable(Drawable who);
  6.         //该函数目前还不懂
  7.         public void scheduleDrawable(Drawable who, Runnable what, long when);
  8.          //该函数目前还不懂
  9.         public void unscheduleDrawable(Drawable who, Runnable what);
  10.     }
复制代码
其中比较重要的函数为:


      public voidinvalidateDrawable(Drawable who)
        函数功能:如果Drawable对象的状态发生了变化,会请求View重新绘制,因此我们对应于该View的背景Drawable对象
   能够重新”绘制“出来。


    Android框架View类继承了该接口,同时实现了这三个函数的默认处理方式,其中invalidateDrawable()方法如下:
  1. public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource
  2. {
  3.         ...
  4.         //Invalidates the specified Drawable.
  5.     //默认实现,重新绘制该视图本身
  6.     public void invalidateDrawable(Drawable drawable) {
  7.         if (verifyDrawable(drawable)) { //是否是同一个Drawable对象,通常为真
  8.             final Rect dirty = drawable.getBounds();
  9.             final int scrollX = mScrollX;
  10.             final int scrollY = mScrollY;
  11.             //重新请求绘制该View,即重新调用该View的draw()方法  ...
  12.             invalidate(dirty.left + scrollX, dirty.top + scrollY,
  13.                     dirty.right + scrollX, dirty.bottom + scrollY);
  14.         }
  15.     }
  16.         ...
  17. }
复制代码
因此,我们的Drawable类对象必须将View设置为回调对象,否则,即使改变了状态,也不会显示对应的背景图。 如下:
            Drawable d  ;                // 图片资源                        
            d.setCallback(View v) ;  // 视图v的背景资源为 d 对象



知识点四:View绘制背景图片过程


      在 Android中View绘制流程以及invalidate()等相关方法分析中,我们知道了一个视图的背景绘制过程时在
  View 类里的draw()方法里完成的,我们这儿在回顾下draw()的流程,同时重点讲解下绘制背景的操作。
  1. //方法所在路径:frameworks\base\core\java\android\view\View.java
  2. //draw()绘制过程
  3. private void draw(Canvas canvas){  
  4. //该方法会做如下事情  
  5.   //1 、绘制该View的背景  
  6.     //其中背景图片绘制过程如下:
  7.         //是否透明, 视图通常是透明的 , 为true
  8.          if (!dirtyOpaque) {
  9.            //开始绘制视图的背景
  10.        final Drawable background = mBGDrawable;
  11.        if (background != null) {
  12.            final int scrollX = mScrollX;  //获取偏移值
  13.            final int scrollY = mScrollY;
  14.            //视图的布局坐标是否发生了改变, 即是否重新layout了。
  15.            if (mBackgroundSizeChanged) {
  16.                    //如果是,我们的Drawable对象需要重新设置大小了,即填充该View。
  17.                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
  18.                mBackgroundSizeChanged = false;
  19.            }
  20.            //View没有发生偏移
  21.            if ((scrollX | scrollY) == 0) {
  22.                background.draw(canvas); //OK, 该方法会绘制当前StateListDrawable的当前背景Drawable
  23.            } else {
  24.                    //View发生偏移,由于背景图片值显示在布局坐标中,即背景图片不会发生偏移,只有视图内容onDraw()会发生偏移
  25.                    //我们调整canvas对象的绘制区域,绘制完成后对canvas对象属性调整回来
  26.                canvas.translate(scrollX, scrollY);
  27.                background.draw(canvas); //OK, 该方法会绘制当前StateListDrawable的当前背景Drawable
  28.                canvas.translate(-scrollX, -scrollY);
  29.            }
  30.        }
  31.    }
  32.         ...
  33. //2、为绘制渐变框做一些准备操作  
  34. //3、调用onDraw()方法绘制视图本身  
  35. //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。  
  36. //5、绘制渐变框   
  37. }  
复制代码
That's all ! 我们用到的知识点也就这么多吧。 如果大家有丝丝不明白的话,可以去看下源代码,具体去分析下这些流程到底   是怎么走下来的。       我们从宏观的角度分析了View绘制不同状态背景的原理,View框架就是这么做的。为了易于理解性,   下面我们通过一个小Demo来演示前面种种流程。     Demo 说明:
          我们参照View框架中绘制不同背景图的实现原理,自定义一个View类,通过给它设定StateListDrawable对象,使其能够在    不同状态时能动态"绘制"背景图片。 基本流程方法和View.java类实现过程一模一样。     截图如下:


一、主文件MainActivity.java如下:
  1. /**
  2. *
  3. * @author http://http://blog.csdn.net/qinjuning
  4. */
  5. public class MainActivity extends Activity
  6. {

  7.     @Override
  8.     public void onCreate(Bundle savedInstanceState)
  9.     {
  10.         super.onCreate(savedInstanceState);   

  11.         LinearLayout ll  =  new LinearLayout(MainActivity.this);
  12.         CustomView customView = new CustomView(MainActivity.this);
  13.         //简单设置为 width 200px - height 100px吧
  14.         ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(200 , 100);
  15.         customView.setLayoutParams(lp);
  16.         //需要将该View设置为可点击/触摸状态,否则触摸该View没有效果。
  17.         customView.setClickable(true);
  18.         
  19.         ll.addView(customView);
  20.         setContentView(ll);
  21.     }
  22. }
复制代码
功能很简单,为Activity设置了视图 。


二、 自定义View如下 , CustomView.java :
  1. /**
  2. * @author http://http://blog.csdn.net/qinjuning
  3. */
  4. //自定义View
  5. public class CustomView extends View   /*extends Button*/
  6. {
  7.     private static String TAG = "TackTextView";
  8.    
  9.     private Context mContext = null;
  10.     private Drawable mBackground = null;
  11.     private boolean mBGSizeChanged = true;;   //视图View布局(layout)大小是否发生变化
  12.    
  13.     public CustomView(Context context)
  14.     {
  15.         super(context);
  16.         mContext = context;      
  17.         initStateListDrawable(); // 初始化图片资源
  18.     }

  19.     // 初始化图片资源
  20.     private void initStateListDrawable()
  21.     {
  22.         //有两种方式获取我们的StateListDrawable对象:
  23.         // 获取方式一、手动构建一个StateListDrawable对象
  24.         StateListDrawable statelistDrawable = new StateListDrawable();
  25.         
  26.         int pressed = android.R.attr.state_pressed;
  27.         int windowfocused = android.R.attr.state_window_focused;
  28.         int enabled = android.R.attr.state_enabled;
  29.         int stateFoucesd = android.R.attr.state_focused;
  30.         //匹配状态时,是一种优先包含的关系。
  31.         // "-"号表示该状态值为false .即不匹配
  32.         statelistDrawable.addState(new int[] { pressed, windowfocused },
  33.                         mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed));
  34.         statelistDrawable.addState(new int[]{ -pressed, windowfocused },
  35.                         mContext.getResources().getDrawable(R.drawable.btn_power_on_nor));   
  36.                
  37.         mBackground = statelistDrawable;
  38.         
  39.         //必须设置回调,当改变状态时,会回掉该View进行invalidate()刷新操作.
  40.         mBackground.setCallback(this);      
  41.         //取消默认的背景图片,因为我们设置了自己的背景图片了,否则可能造成背景图片重叠。
  42.         this.setBackgroundDrawable(null);
  43.         
  44.         // 获取方式二、、使用XML获取StateListDrawable对象
  45.         // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);
  46.     }
  47.    
  48.     protected void drawableStateChanged()
  49.     {
  50.         Log.i(TAG, "drawableStateChanged");
  51.         Drawable d = mBackground;
  52.         if (d != null && d.isStateful())
  53.         {
  54.             d.setState(getDrawableState());
  55.             Log.i(TAG, "drawableStateChanged  and is 111");
  56.         }

  57.        Log.i(TAG, "drawableStateChanged  and is 222");
  58.        super.drawableStateChanged();
  59.     }
  60.     //验证图片是否相等 , 在invalidateDrawable()会调用此方法,我们需要重写该方法。
  61.     protected boolean verifyDrawable(Drawable who)
  62.     {
  63.         return who == mBackground || super.verifyDrawable(who);
  64.     }
  65.     //draw()过程,绘制背景图片...
  66.     public void draw(Canvas canvas)
  67.     {
  68.         Log.i(TAG, " draw -----");
  69.         if (mBackground != null)
  70.         {
  71.             if(mBGSizeChanged)
  72.             {
  73.                 //设置边界范围
  74.                 mBackground.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
  75.                 mBGSizeChanged = false ;
  76.             }
  77.             if ((getScrollX() | getScrollY()) == 0)  //是否偏移
  78.             {
  79.                 mBackground.draw(canvas); //绘制当前状态对应的图片
  80.             }
  81.             else
  82.             {
  83.                 canvas.translate(getScrollX(), getScrollY());
  84.                 mBackground.draw(canvas); //绘制当前状态对应的图片
  85.                 canvas.translate(-getScrollX(), -getScrollY());
  86.             }
  87.         }
  88.         super.draw(canvas);
  89.     }
  90.     public void onDraw(Canvas canvas) {   
  91.         ...
  92.     }
  93. }
复制代码
将该View设置的背景图片转换为节点 xml,形式如下:
  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  2.   <item android:state_pressed="true"
  3.         android:state_window_focused="true"
  4.         android:drawable="@drawable/btn_power_on_pressed"></item>
  5.   <item android:state_pressed="false"
  6.         android:state_window_focused="true"  
  7.         android:drawable="@drawable/btn_power_on_nor"></item>   
  8.       
  9. </selector>
复制代码
基本上所有功能都在这儿显示出来了, 和我们前面说的一模一样吧。
          当然了,如果你想偷懒,大可用系统定义好的一套工具 , 即直接使用setBackgroundXXX()或者在设置对应的属性,但是,
     万变不离其宗,掌握了绘制原理,可以潇洒走江湖了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值