Android中View(視圖)繪製不同狀態背景圖片

 轉載出處:http://blog.csdn.net/qinjuning


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


           1、View的幾種不同狀態屬性

           2、如何根據不同狀態去切換我們的背景圖片。

 

 

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


        對Android開發有經驗的同學,對 <selector>節點的使用一定很熟悉,該節點的作用就是定義一組狀態資源圖片,使其能夠

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

[java] view plaincopyprint?

  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類對象,如下:

[java] view plaincopyprint?

  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.   

  9. stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1));  

  10. stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2);  

  11. stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3);  

  12. stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4);  

  13. //沒有任何狀態時顯示的圖片,我們給它設置我空集合  

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

 

    2、一個窗口只能有一個視圖獲得焦點(focus),而一個窗口可以有多個視圖處於”selected”狀態中。

 

      總結:focused狀態一般是由按鍵操作引起的;

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

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

 

      例如:當我們觸摸某個控件時,會導致pressed狀態改變;獲得焦點時,會導致focus狀態變化。於是,我們可以通過這種

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

 


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


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

       其整體調用流程如下: View.java類中

[java] view plaincopyprint?

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


[java] view plaincopyprint?

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


[java] view plaincopyprint?

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

[java] view plaincopyprint?

  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接口

   

    該接口定義瞭如下三個函數:     

[java] view plaincopyprint?

  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()方法如下:

[java] view plaincopyprint?

  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()的流程,同時重點講解下繪製背景的操作。


[java] view plaincopyprint?

  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(00,  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類實現過程一模一樣。

    截圖如下:


                      


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


  一、主文件MainActivity.java如下:

[java] view plaincopyprint?

  1. /** 

  2.  *  

  3.  * @author http://http://blog.csdn.net/qinjuning 

  4.  */  

  5. public class MainActivity extends Activity  

  6. {  

  7.   

  8.     @Override  

  9.     public void onCreate(Bundle savedInstanceState)  

  10.     {  

  11.         super.onCreate(savedInstanceState);      

  12.   

  13.         LinearLayout ll  =  new LinearLayout(MainActivity.this);  

  14.         CustomView customView = new CustomView(MainActivity.this);   

  15.         //簡單設置爲 width 200px - height 100px吧   

  16.         ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(200 , 100);  

  17.         customView.setLayoutParams(lp);  

  18.         //需要將該View設置爲可點擊/觸摸狀態,否則觸摸該View沒有效果。  

  19.         customView.setClickable(true);  

  20.           

  21.         ll.addView(customView);  

  22.         setContentView(ll);   

  23.     }  

  24. }  



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


二、 自定義View如下 , CustomView.java :


[java] view plaincopyprint?

  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.     // 初始化圖片資源  

  21.     private void initStateListDrawable()  

  22.     {  

  23.         //有兩種方式獲取我們的StateListDrawable對象:  

  24.         // 獲取方式一、手動構建一個StateListDrawable對象  

  25.         StateListDrawable statelistDrawable = new StateListDrawable();  

  26.           

  27.         int pressed = android.R.attr.state_pressed;  

  28.         int windowfocused = android.R.attr.state_window_focused;  

  29.         int enabled = android.R.attr.state_enabled;  

  30.         int stateFoucesd = android.R.attr.state_focused;  

  31.         //匹配狀態時,是一種優先包含的關係。  

  32.         // "-"號表示該狀態值爲false .即不匹配  

  33.         statelistDrawable.addState(new int[] { pressed, windowfocused },   

  34.                 mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed));  

  35.         statelistDrawable.addState(new int[]{ -pressed, windowfocused },   

  36.                 mContext.getResources().getDrawable(R.drawable.btn_power_on_nor));      

  37.                  

  38.         mBackground = statelistDrawable;  

  39.           

  40.         //必須設置回調,當改變狀態時,會回掉該View進行invalidate()刷新操作.  

  41.         mBackground.setCallback(this);         

  42.         //取消默認的背景圖片,因爲我們設置了自己的背景圖片了,否則可能造成背景圖片重疊。  

  43.         this.setBackgroundDrawable(null);  

  44.           

  45.         // 獲取方式二、、使用XML獲取StateListDrawable對象  

  46.         // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);  

  47.     }  

  48.       

  49.     protected void drawableStateChanged()  

  50.     {  

  51.         Log.i(TAG, "drawableStateChanged");  

  52.         Drawable d = mBackground;  

  53.         if (d != null && d.isStateful())  

  54.         {  

  55.             d.setState(getDrawableState());  

  56.             Log.i(TAG, "drawableStateChanged  and is 111");  

  57.         }  

  58.   

  59.        Log.i(TAG, "drawableStateChanged  and is 222");  

  60.        super.drawableStateChanged();  

  61.     }  

  62.     //驗證圖片是否相等 , 在invalidateDrawable()會調用此方法,我們需要重寫該方法。  

  63.     protected boolean verifyDrawable(Drawable who)  

  64.     {  

  65.         return who == mBackground || super.verifyDrawable(who);  

  66.     }  

  67.     //draw()過程,繪製背景圖片...  

  68.     public void draw(Canvas canvas)  

  69.     {  

  70.         Log.i(TAG, " draw -----");  

  71.         if (mBackground != null)  

  72.         {  

  73.             if(mBGSizeChanged)  

  74.             {  

  75.                 //設置邊界範圍  

  76.                 mBackground.setBounds(00, getRight() - getLeft(), getBottom() - getTop());  

  77.                 mBGSizeChanged = false ;  

  78.             }  

  79.             if ((getScrollX() | getScrollY()) == 0)  //是否偏移  

  80.             {  

  81.                 mBackground.draw(canvas); //繪製當前狀態對應的圖片  

  82.             }  

  83.             else  

  84.             {  

  85.                 canvas.translate(getScrollX(), getScrollY());  

  86.                 mBackground.draw(canvas); //繪製當前狀態對應的圖片  

  87.                 canvas.translate(-getScrollX(), -getScrollY());  

  88.             }  

  89.         }  

  90.         super.draw(canvas);  

  91.     }  

  92.     public void onDraw(Canvas canvas) {      

  93.         ...  

  94.     }  

  95. }  




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

[java] view plaincopyprint?

  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()或者在設置對應的屬性,但是,

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

     


        示例Demo下載地址: http://download.csdn.net/detail/qinjuning/4237298


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章