View的簡介

認識一個新的事物,首先我們從概念上講,我們需要知道,這個事物 是什麼,這個事物有什麼用途?

對應到自定義View 上,首先我們要搞明白 View 的定義以及工作原理。

 1.View是什麼? 

 View是屏幕上的一塊矩形區域,它負責用來顯示一個區域,並且響應這個區域內的事件。可以說,手機屏幕上的任意一部分看的見得地方都是View,它很常見,比如 TextView 、ImageView 、Button以及LinearLayout、RelativeLayout都是繼承子View的。

 對於Activity來說,我們通過setContentView(view)添加的佈局到Activity上,實際上都是添加到了Activity 內部的DecorView上面,這個DecorView,其實就是一個FrameLayout,因此實際上,我們的佈局實際上添加到了FrameLayout裏面。

 2.View 是怎麼工作的?

 View的工作流程分爲兩部分,第一部分 顯示在屏幕上的過程, 第二部分 響應屏幕上的觸摸事件的過程。

  對於顯示在屏幕上的過程:是View 從無到有,經過測量大小(Measure)、確定在屏幕中的位置(Layout)、以及最終繪製在屏幕上(Draw) 這一系列的過程。

  對於響應屏幕上的觸摸事件的過程:則是Touch事件的分發過程(這一部分,在這裏先不涉及)。

  Measure() Layout()方法是final修飾的,無法重寫 ,Draw()雖然不是final的,但是也不建議重寫該方法。

 3.如何自定義View?

  View 爲我們提供了 onMeasure()  onLayout()  onDraw() 這樣的方法,其實所謂的自定義View,也就是對onMeasure() onLayout() onDraw() 三個方法的重寫的過程。

 所謂的自定義View 實際上,就是仿照View的工作流程,去自己實現的過程。根據繼承對象的不同自定義View又分爲繼承View 與ViewGroup兩種。

measure():

  View的onMeasure()方法如下:

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  3.             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  4. }  
  內部調用了 setMeasuredDimension() 方法,這個方法看名字就知道是設置測量的View的寬/高,傳遞的參數就是寬度和高度。

  onMeasure通過父View傳遞過來的大小和模式,以及自身的背景圖片的大小得出自身最終的大小,通過setMeasuredDimension()方法設置給mMeasuredWidth和mMeasuredHeight。

  普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然後根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,文字的大小,文字行高,文字的行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小。

 ViewGroup本身沒有實現onMeasure,但是他的子類都有各自的實現,通常他們都是通過measureChildWithMargins函數或者其他類似於measureChild的函數來遍歷測量子View,被GONE的子View將不參與測量,當所有的子View都測量完畢後,才根據父View傳遞過來的模式和大小來最終決定自身的大小。

  ViewGroup一般都在測量完所有子View後纔會調用setMeasuredDimension()設置自身大小。

  這裏我們就要介紹一下Android中自己定義的測量規格:MeasureSpec。

  因爲測量過程中,系統會把我們代碼或者佈局中設置的View的LayoutParams 轉換成MeasureSpec,然後通過MeasureSpec來測量確定View的寬高。

  MeasureSpec由32位 int值組成,高2位表示的是測量模式(specMode),後面的30位代表的是測量的大小(specSize)。

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. public static class MeasureSpec {  
  2.        private static final int MODE_SHIFT = 30;  
  3.        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  4.   
  5.        /** 測量模式:父View不對子View的大小做任何限制,可以是子佈局需要的任意大小 
  6.         * Measure specification mode: The parent has not imposed any constraint 
  7.         * on the child. It can be whatever size it wants. 
  8.         */  
  9.        public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  10.   
  11.        /** 已經爲子View給出了確切的值 相當於給View的LayoutParams指定了具體數值,或者match_parent. 
  12.         * Measure specification mode: The parent has determined an exact size 
  13.         * for the child. The child is going to be given those bounds regardless 
  14.         * of how big it wants to be. 
  15.         */  
  16.        public static final int EXACTLY     = 1 << MODE_SHIFT;  
  17.   
  18.        /**子View可以跟自己的測量大小一樣大。對應於View的LayoutParams的wrap_content 
  19.         * Measure specification mode: The child can be as large as it wants up 
  20.         * to the specified size. 
  21.         */  
  22.        public static final int AT_MOST     = 2 << MODE_SHIFT;  
  23.   
  24.        /**根據 size和mode 去生成一個MeasureSpec值 
  25.         * Creates a measure specification based on the supplied size and mode. 
  26.         * 
  27.         * The mode must always be one of the following: 
  28.         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li> 
  29.         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li> 
  30.         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li> 
  31.         * @return the measure specification based on size and mode 
  32.         */  
  33.        public static int makeMeasureSpec(int size, int mode) {  
  34.            if (sUseBrokenMakeMeasureSpec) {  
  35.                return size + mode;  
  36.            } else {  
  37.                return (size & ~MODE_MASK) | (mode & MODE_MASK);  
  38.            }  
  39.        }  
  40.   
  41.        /**獲取測量模式 
  42.         * Extracts the mode from the supplied measure specification. 
  43.         * 
  44.         * @param measureSpec the measure specification to extract the mode from 
  45.         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, 
  46.         *         {@link android.view.View.MeasureSpec#AT_MOST} or 
  47.         *         {@link android.view.View.MeasureSpec#EXACTLY} 
  48.         */  
  49.        public static int getMode(int measureSpec) {  
  50.            return (measureSpec & MODE_MASK);  
  51.        }  
  52.   
  53.        /**獲取測量值大小 
  54.         * Extracts the size from the supplied measure specification. 
  55.         * 
  56.         * @param measureSpec the measure specification to extract the size from 
  57.         * @return the size in pixels defined in the supplied measure specification 
  58.         */  
  59.        public static int getSize(int measureSpec) {  
  60.            return (measureSpec & ~MODE_MASK);  
  61.        }  
  62.   
  63.        static int adjust(int measureSpec, int delta) {  
  64.            return makeMeasureSpec(getSize(measureSpec + delta), getMode(measureSpec));  
  65.        }  
  66.   
  67.    }  

  MeasureSpec是由父佈局與View 自身的LayoutParams來決定的


  經過measure 完成後,我們就可以通過getMeasuredWidth/Height 獲取View 的寬高。

  layout()

 Layout() 方法如果是ViewGroup,則循環遍歷所有子View,普通View則空實現,因此如果我們繼承ViewGroup 我們需要遍歷執行所有的child.layout()。

 Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。
子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,影響佈局的因素非常多。

onLayout是ViewGroup用來決定子View擺放位置的,各種佈局的差異都在該方法中得到了體現。
onLayout比layout多一個參數,changed,該參數是在setFrame通過比對上次的位置得出是否發生了變化,通常該參數沒有被使用的意義,因爲父View位置和大小不變,並不能代表子View的位置和大小沒有發生改變。


  draw()

  draw()的過程就是繪製View到屏幕上的過程,draw()的執行遵循如下步驟:

[java] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. /* 
  2.         * Draw traversal performs several drawing steps which must be executed 
  3.         * in the appropriate order: 
  4.         * 
  5.         *      1. Draw the background(繪製背景) 
  6.         *      2. If necessary, save the canvas' layers to prepare for fading(保存畫布的圖層來準備色變) 
  7.         *      3. Draw view's content(繪製內容) 
  8.         *      4. Draw children(繪製children) 
  9.         *      5. If necessary, draw the fading edges and restore layers(畫出褪色的邊緣和恢復層) 
  10.         *      6. Draw decorations (scrollbars for instance)(繪製裝飾 比如scollbar) 
  11.         */  
  一般2和5 是可以跳過的。

  在TextView中在該方法中繪製文字、光標和CompoundDrawable 、ImageView中相對簡單,只是繪製了圖片。
  View 的繪製主要通過dispatchDraw()

先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁後的區域繪製。
遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。

根據滾動值和子View在父View中的座標進行畫布原點座標的移動,根據子在父View中的座標計算出子View的視圖大小,然後對畫布進行剪裁,請看下面的示意圖。
dispatchDraw的邏輯其實比較複雜,但是幸運的是對子View流程都採用該方式,而ViewGroup已經處理好了,我們不必要重載該方法對子View進行繪製事件的派遣分發。


  理解了以上工作流程,就可以試着去自定義View了。

  自定義View一般就分爲 繼承View/VieGroup兩種,在我們自定義View的時候要讓自定義的View的功能儘可能向

系統提供的考慮,比如支持wrap_content,padding 等。


View 的幾個比較重要的方法:


  requestLayout:
  當view確定自身已經不再適合現有的區域時,該view本身調用這個方法要求parent view(父類的視圖)重新調用他的onMeasure onLayout來重新設置自己位置。特別是當view的layoutparameter發生改變,並且它的值還沒能應用到view上時,這時候適合調用這個方法。
  postInvalidate 與 invalidate
  界面刷新 onDraw方法會執行,區別就是Invalidate不能直接在線程中調用,因爲他是違背了單線程模型:Android UI操作並不是線程安全的,並且這些操作必須在UI線程中調用。 鑑於此,如果要使用invalidate的刷新,那我們就得配合handler的使用,使異步非ui線程轉到ui線程中調用,如果要在非ui線程中直接使用就調用postInvalidate方法即可,這樣就省去使用handler的煩惱。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章