Android中View(視圖)繪製不一樣狀態背景圖片原理深刻分析以及StateListDrawable使用詳解

今天繼續給你們分享下View的相關知識,重點有一下兩點:java


           一、View的幾種不一樣狀態屬性android

           二、如何根據不一樣狀態去切換咱們的背景圖片。算法

 

 

開篇介紹:android背景選擇器selector用法彙總canvas


        對Android開發有經驗的同窗,對 <selector>節點的使用必定很熟悉,該節點的做用就是定義一組狀態資源圖片,使其可以框架

  在不一樣的狀態下更換某個View的背景圖片。例如,以下的hello_selection.xml文件定義:ide

<?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類對象。函數

 

 

知識點一:StateListDrawable類介紹工具


    類功能說明:該類定義了不一樣狀態值下與之對應的圖片資源,即咱們能夠利用該類保存多種狀態值,多種圖片資源。佈局

    經常使用方法爲:this

       public void addState (int[] stateSet, Drawable drawable)

       功能: 給特定的狀態集合設置drawable圖片資源

       使用方式:參考前面的hello_selection.xml文件咱們利用代碼去構建一個相同的StateListDrawable類對象,以下:

//初始化一個空對象
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,顯然狀態改變後,咱們的圖片會跟着改變。

 


知識點二: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 as

        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處於高亮狀態。

 

    二、一個窗口只能有一個視圖得到焦點(focus),而一個窗口能夠有多個視圖處於」selected」狀態中。

 

      總結:focused狀態通常是由按鍵操做引發的;

                pressed狀態是由觸摸消息引發的;

                selected則徹底是由應用程序主動調用setSelected()進行控制。

 

      例如:當咱們觸摸某個控件時,會致使pressed狀態改變;得到焦點時,會致使focus狀態變化。因而,咱們能夠經過這種

   更新後狀態值去更新咱們對應的Drawable對象了。

 


問題:如何根據狀態值的改變去繪製/顯示對應的背景圖?


       當View任何狀態值發生改變時,都會調用refreshDrawableList()方法去更新對應的背景Drawable對象。

       其總體調用流程以下: View.java類中

//路徑:\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.
     */ 
    //主要功能是根據當前的狀態值去更換對應的背景Drawable對象
    public void refreshDrawableState() {
        mPrivateFlags |= DRAWABLE_STATE_DIRTY;
        //全部功能在這個函數裏去完成
        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.
     */
    // 得到當前的狀態屬性--- 整型集合 ; 調用Drawable類的setState方法去獲取資源。
    protected void drawableStateChanged() {
    	//該視圖對應的Drawable對象,一般對應於StateListDrawable類對象
        Drawable d = mBGDrawable;   
        if (d != null && d.isStateful()) {  //一般都是成立的
        	//getDrawableState()方法主要功能:會根據當前View的狀態屬性值,將其轉換爲一個整型集合
        	//setState()方法主要功能:根據當前的獲取到的狀態,更新對應狀態下的Drawable對象。
            d.setState(getDrawableState());
        }
    }
    /*Return an array of resource IDs of the drawable states representing the
     * current state of the view.
     */
    public final int[] getDrawableState() {
        if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
            return mDrawableState;
        } else {
        	//根據當前View的狀態屬性值,將其轉換爲一個整型集合,並返回
            mDrawableState = onCreateDrawableState(0);
            mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
            return mDrawableState;
        }
    }


       經過這段代碼咱們能夠明白View內部是如何獲取更細後的狀態值以及動態獲取對應的背景Drawable對象----setState()方法

去完成的。這兒我簡單的分析下Drawable類裏的setState()方法的功能,把流程給走一下:

    

         Step 1 、 setState()函數原型 ,

             函數位於:frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中


//若是狀態態值發生了改變,就回調onStateChange()方法。
    public boolean setState(final int[] stateSet) {
        if (!Arrays.equals(mStateSet, stateSet)) {
            mStateSet = stateSet;
            return onStateChange(stateSet);
        }
        return false;
    }

           該函數的主要功能: 判斷狀態值是否發生了變化,若是發生了變化,就調用onStateChange()方法進一步處理。

    

       Step 2 、onStateChange()函數原型:

            該函數位於 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中


//狀態值發生了改變,咱們須要找出第一個吻合的當前狀態的Drawable對象
    protected boolean onStateChange(int[] stateSet) {
    	//要找出第一個吻合的當前狀態的Drawable對象所在的索引位置, 具體匹配算法請本身深刻源碼看看
        int idx = mStateListState.indexOfStateSet(stateSet);
        ...
        //獲取對應索引位置的Drawable對象
        if (selectDrawable(idx)) {
            return true;
        }
        ...
    }


          該函數的主要功能: 根據新的狀態值,從StateListDrawable實例對象中,找到第一個徹底吻合該新狀態值的索引下標處 ;

   繼而,調用selectDrawable()方法去獲取索引下標的當前Drawable對象。

         具體查找算法在 mStateListState.indexOfStateSet(stateSet) 裏實現了。基本思路是:查找第一個能徹底吻合該新狀態值

   的索引下標,若是找到了,則當即返回。 具體實現過程,只好看看源碼咯。

  

       Step 3 、selectDrawable()函數原型:

            該函數位於 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中

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中。



知識點三: 關於Drawable.Callback接口

   

    該接口定義了以下三個函數:     

//該函數位於 frameworks\base\graphics\java\android\graphics\drawable\Drawable.java 類中
    public static interface Callback {
    	//若是Drawable對象的狀態發生了變化,會請求View從新繪製,
    	//所以咱們對應於該View的背景Drawable對象可以」繪製出來」.
        public void invalidateDrawable(Drawable who);
        //該函數目前還不懂
        public void scheduleDrawable(Drawable who, Runnable what, long when);
         //該函數目前還不懂
        public void unscheduleDrawable(Drawable who, Runnable what);
    }


其中比較重要的函數爲:


      public voidinvalidateDrawable(Drawable who)

        函數功能:若是Drawable對象的狀態發生了變化,會請求View從新繪製,所以咱們對應於該View的背景Drawable對象

   可以從新」繪製「出來。


    Android框架View類繼承了該接口,同時實現了這三個函數的默認處理方式,其中invalidateDrawable()方法以下:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource 
{
	...
	//Invalidates the specified Drawable.
    //默認實現,從新繪製該視圖自己
    public void invalidateDrawable(Drawable drawable) {
        if (verifyDrawable(drawable)) { //是不是同一個Drawable對象,一般爲真
            final Rect dirty = drawable.getBounds();
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            //從新請求繪製該View,即從新調用該View的draw()方法  ...
            invalidate(dirty.left + scrollX, dirty.top + scrollY,
                    dirty.right + scrollX, dirty.bottom + scrollY);
        }
    }
	...
}


   所以,咱們的Drawable類對象必須將View設置爲回調對象,不然,即便改變了狀態,也不會顯示對應的背景圖。 以下:

            Drawable d  ;                // 圖片資源                        

            d.setCallback(View v) ;  // 視圖v的背景資源爲 d 對象


 

知識點四:View繪製背景圖片過程


 咱們知道了一個視圖的背景繪製過程時在View類裏的draw()方法裏完成的,咱們這兒在回顧下draw()的流程,同時重點講解下繪製背景的操做。


//方法所在路徑:frameworks\base\core\java\android\view\View.java
//draw()繪製過程
private void draw(Canvas canvas){  
//該方法會作以下事情  
  //1 、繪製該View的背景  
    //其中背景圖片繪製過程以下:
	//是否透明, 視圖一般是透明的 , 爲true
	 if (!dirtyOpaque) {
	   //開始繪製視圖的背景
       final Drawable background = mBGDrawable;
       if (background != null) {
           final int scrollX = mScrollX;  //獲取偏移值
           final int scrollY = mScrollY;
           //視圖的佈局座標是否發生了改變, 便是否從新layout了。
           if (mBackgroundSizeChanged) {
          	 //若是是,咱們的Drawable對象須要從新設置大小了,即填充該View。
               background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
               mBackgroundSizeChanged = false;
           }
           //View沒有發生偏移
           if ((scrollX | scrollY) == 0) {
               background.draw(canvas); //OK, 該方法會繪製當前StateListDrawable的當前背景Drawable
           } else {
          	 //View發生偏移,因爲背景圖片值顯示在佈局座標中,即背景圖片不會發生偏移,只有視圖內容onDraw()會發生偏移
          	 //咱們調整canvas對象的繪製區域,繪製完成後對canvas對象屬性調整回來
               canvas.translate(scrollX, scrollY);
               background.draw(canvas); //OK, 該方法會繪製當前StateListDrawable的當前背景Drawable
               canvas.translate(-scrollX, -scrollY);
           }
       }
   }
	...
 //二、爲繪製漸變框作一些準備操做  
 //三、調用onDraw()方法繪製視圖自己  
 //四、調用dispatchDraw()方法繪製每一個子視圖,dispatchDraw()已經在Android框架中實現了,在ViewGroup方法中。  
 //五、繪製漸變框    
}


      That's all ! 咱們用到的知識點也就這麼多吧。 若是你們有絲絲不明白的話,能夠去看下源代碼,具體去分析下這些流程到底

  是怎麼走下來的。

      咱們從宏觀的角度分析了View繪製不一樣狀態背景的原理,View框架就是這麼作的。爲了易於理解性,

  下面咱們經過一個小Demo來演示前面種種流程。

   

 Demo 說明:


          咱們參照View框架中繪製不一樣背景圖的實現原理,自定義一個View類,經過給它設定StateListDrawable對象,使其可以在

   不一樣狀態時能動態"繪製"背景圖片。 基本流程方法和View.java類實現過程如出一轍。

    截圖以下:


                      


                 初始背景圖                                                            觸摸後顯示的背景圖(pressed)


  1、主文件MainActivity.java以下:

/**
 * 
 * @author http://http://blog.csdn.net/qinjuning
 */
public class MainActivity extends Activity
{

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);    

        LinearLayout ll  =  new LinearLayout(MainActivity.this);
        CustomView customView = new CustomView(MainActivity.this); 
        //簡單設置爲 width 200px - height 100px吧 
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(200 , 100);
        customView.setLayoutParams(lp);
        //須要將該View設置爲可點擊/觸摸狀態,不然觸摸該View沒有效果。
        customView.setClickable(true);
        
        ll.addView(customView);
        setContentView(ll); 
    }
}


   功能很簡單,爲Activity設置了視圖 。


2、 自定義View以下 , CustomView.java :

/** 
 * @author http://http://blog.csdn.net/qinjuning
 */
//自定義View
public class CustomView extends View   /*extends Button*/
{
    private static String TAG = "TackTextView";
    
    private Context mContext = null;
    private Drawable mBackground = null;
    private boolean mBGSizeChanged = true;;   //視圖View佈局(layout)大小是否發生變化
    
    public CustomView(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);
        
        // 獲取方式2、、使用XML獲取StateListDrawable對象
        // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);
    }
    
    protected void drawableStateChanged()
    {
        Log.i(TAG, "drawableStateChanged");
        Drawable d = mBackground;
        if (d != null && d.isStateful())
        {
            d.setState(getDrawableState());
            Log.i(TAG, "drawableStateChanged  and is 111");
        }

       Log.i(TAG, "drawableStateChanged  and is 222");
       super.drawableStateChanged();
    }
    //驗證圖片是否相等 , 在invalidateDrawable()會調用此方法,咱們須要重寫該方法。
    protected boolean verifyDrawable(Drawable who)
    {
        return who == mBackground || super.verifyDrawable(who);
    }
    //draw()過程,繪製背景圖片...
    public void draw(Canvas canvas)
    {
        Log.i(TAG, " draw -----");
        if (mBackground != null)
        {
            if(mBGSizeChanged)
            {
                //設置邊界範圍
                mBackground.setBounds(0, 0, getRight() - getLeft(), getBottom() - getTop());
                mBGSizeChanged = 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) {    
        ...
    }
}



   將該View設置的背景圖片轉換爲節點xml,形式以下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_pressed="true" 
        android:state_window_focused="true" 
        android:drawable="@drawable/btn_power_on_pressed"></item>
  <item android:state_pressed="false" 
        android:state_window_focused="true"  
        android:drawable="@drawable/btn_power_on_nor"></item>    
      
</selector>


          基本上全部功能都在這兒顯示出來了, 和咱們前面說的如出一轍吧。

          固然了,若是你想偷懶,大可用系統定義好的一套工具 , 即直接使用setBackgroundXXX()或者在設置對應的屬性,可是,

     萬變不離其宗,掌握了繪製原理,能夠瀟灑走江湖了。

相關文章
相關標籤/搜索