1. 程式人生 > >自定義view流程(結合原始碼分析)

自定義view流程(結合原始碼分析)

一、View的繪製流程

主要是:測量(measure)、佈局(layout)、繪製(draw)三大流程。

  1. 對於一個普通View(不是容器)
    主要是關心測量和繪製兩個過程,測量可以確定自身的寬、高、大小,繪製可以顯示出view的具體內容(呈現在螢幕上的)。
  2. 對於ViewGroup(容器控制元件)主要是關心測量和佈局兩個過程,測量不僅僅要測量自身還要測量所有的子view,佈局主要是指定所有子view在自身上的位置。
  3. 具體實現是重寫onMeasure、onLayout、onDraw方法,在這些方法中進行編碼處理。

二、View的測量

  1. View的測量主要是由自身的MeasureSpec決定的,而自身的MeasureSpec又由父容器的MeasureSpec和自身的LayoutParams決定的。

  2. MeasureSpec包含SpecMode和SpecSize兩部分,SpecMode是測量規則,SpecSize是在一定規則下的測量大小。

  3. SpecMode分為三種模式
    a、UNAPECIFIED:父容器對view沒有任何限制,要多大給多大,該模式多為系統自己使用,自定義一般不考慮該模式。
    b、EXACTLY:父容器已經知道view所需要的精確大小(SpecSize)。
    c、AT_MOST:父容器給了一個最大值(SpecSize)。

    當父容器的SpecMode為EXACTLY時:
           如果view的LayoutParams為match_parent和具體的值,父容器會為其指定為EXACTLY模式。
           如果view的LayoutParams為wrap_content,父容器會為其指定為AT_MOST模式。
    當父容器的SpecMode為AT_MOST時:


           如果view的LayoutParams為具體的數值,父容器會為其指定為EXACTLY模式。
           如果view的LayoutParams為match_parent,父容器會為其指定為AT_MOST模式。
           如果view的LayoutParams為wrap_content,父容器會為其指定為AT_MOST模式。

    父view在測量子view之前會先呼叫getChildMeasureSpec方法確定子view的MeasureSpec,下面看getChildMeasureSpec的原始碼:

	  /**
	   * 確定子view的MeasureSpec的具體方法
	   */
	   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
	
	       //父view自己的模式和大小
	       int specMode = MeasureSpec.getMode(spec);
	       int specSize = MeasureSpec.getSize(spec);
	
	       //父view的大小減去padding值,就是現在子view可用的空間大小
	       int size = Math.max(0, specSize - padding);
	       
	       //對於viewGroup來說,resultSize和resultMode除了用於確定自身大小外,還要傳給下級子view
	       //這個變數存的是最終子view測量大小
	       int resultSize = 0;
	       //這個變數存的是最終子view的測量模式,如果這個子view是viewGroup的話,那麼這個測量模式是要給子view的下級子view使用的,就這樣一層一層的遞迴
	       int resultMode = 0;
	       
	       //根據specMode確定子view的MeasureSpec
	       switch (specMode) {
		       case MeasureSpec.EXACTLY://父容器的模式是EXACTLY,說明父容器的大小是精確值(父容器的大小已經確定了)
		           if (childDimension >= 0) {
		               //子view的LayoutParams的值是具體的值,比如100dp,那麼子view的大小就用這個100dp,子view的模式也是精確值模式
		               resultSize = childDimension;
		               resultMode = MeasureSpec.EXACTLY;
		           } else if (childDimension == LayoutParams.MATCH_PARENT) {
		               //子view的LayoutParams的值是MATCH_PARENT,父view是精確值,所以子view的大小就是父view的可用大小(也是精確值),子view的模式也是精確值模式
		               resultSize = size;
		               resultMode = MeasureSpec.EXACTLY;
		           } else if (childDimension == LayoutParams.WRAP_CONTENT) {
		               //子view的LayoutParams的值是WRAP_CONTENT,父view是精確值,但是子view自己的大小是不確定的(最大為父view的可用size),所以子view的模式是最大模                     式
		               resultSize = size;
		               resultMode = MeasureSpec.AT_MOST;
		           }
		           break;
	      
		       case MeasureSpec.AT_MOST: //父容器的模式是AT_MOST,說明父容器的大小是不確定的(父容器的大小是一個最大值,這個最大值是父容器的上層父容器給的)
		           if (childDimension >= 0) {
		               //子view的LayoutParams的值是具體的值,比如100dp,那麼子view的大小就用這個100dp,子view的模式也是精確值模式
		               resultSize = childDimension;
		               resultMode = MeasureSpec.EXACTLY;
		           } else if (childDimension == LayoutParams.MATCH_PARENT) {      
		               //子view的LayoutParams的值是MATCH_PARENT,那麼子view的大小就是父view的可用大小,而父view的模式是AT_MOST,說明父view的大小是不確定的,所以子view的大小也是不確定的,子view的模式是AT_MOST模式
		               resultSize = size;
		               resultMode = MeasureSpec.AT_MOST;
		           } else if (childDimension == LayoutParams.WRAP_CONTENT) {
		               //子view的LayoutParams的值是WRAP_CONTENT,父view的大小不確定,子view自身的大小也不確定,所以子view的模式是AT_MOST模式
		               resultSize = size;
		               resultMode = MeasureSpec.AT_MOST;
		           }
		           break;
	
		       case MeasureSpec.UNSPECIFIED: //這種模式一般是系統自己用的,自定義控制元件一般不考慮這種情況
		           if (childDimension >= 0) {
		               // Child wants a specific size... let him have it
		               resultSize = childDimension;
		               resultMode = MeasureSpec.EXACTLY;
		           } else if (childDimension == LayoutParams.MATCH_PARENT) {
		               // Child wants to be our size... find out how big it should
		               // be
		               resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
		               resultMode = MeasureSpec.UNSPECIFIED;
		           } else if (childDimension == LayoutParams.WRAP_CONTENT) {
		               // Child wants to determine its own size.... find out how
		               // big it should be
		               resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
		               resultMode = MeasureSpec.UNSPECIFIED;
		           }
		           break;
	       }
	
	       //最終根據resultSize和resultMode生成子view的MeasureSpec
	       return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
	   }           

綜上所述:如果View的SpecMode為EXACTLY時,其大小是確定的,不需要做特殊處理。如果View的SpecMode為AT_MOST時,其大小是不確定的,所以在測量時需要視情況設定一個預設值,否則wrap_content是無效的、不顯示內容的。

  1. 示例程式碼實現如下:
  	 @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
         super.onMeasure(widthMeasureSpec,heightMeasureSpec);
           //獲取父容器為其指定的測量模式和測量尺寸
           int widthSpecMode =MeasureSpec.getMode(widthMeasureSpec);
           int widthSpecSize =MeasureSpec.getSize(widthMeasureSpec);
           int hightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
           int hightSpecSize =MeasureSpec.getSize(heightMeasureSpec);

           //寬或者高的SpecMode是AT_MOST時就設定一個預設值,如果不是就用SpecSize
           if (widthSpecMode ==MeasureSpec.AT_MOST && hightSpecMode == MeasureSpec.AT_MOST) {
               setMeasuredDimension(500,500);
           } else if (widthSpecMode ==MeasureSpec.AT_MOST) {
               setMeasuredDimension(500,hightSpecSize);
           } else {
               setMeasuredDimension(widthSpecSize,500);
           }
      }

三、View的繪製

  1. View的繪製是通過重寫onDraw方法實現的,可以在onDraw裡使用canvas、paint繪製圖形來實現自定義效果。
  2. 需要注意的是在繪製的時候需要考慮padding的影響,如果不做處理padding會無效,因為padding是跟view本身有關的。不用關心margin,因為margin是跟父容器相關的,跟view自身無關。
  3. 為了可以方便的在xml中改變效果,還需要對外提供自定義屬性。

四、ViewGroup的測量和佈局

  1. ViewGroup的onMeasure方法中,既要測量自身的大小,又要測量子View的大小,測量子View的大小可以使用measureChildren測量所有子View,也可以自己寫for迴圈遍歷測量子View,呼叫的方法是measureChild和measureChildWithMargins(這兩個方法是測量單個子View的)。下面對這些方法的原始碼進行分析:

measureChildren的原始碼:

      //測量所有子view
      protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
               final int size = mChildrenCount;
                  final View[] children = mChildren;
                 //迴圈測量子view
                 for (int i = 0; i < size; ++i) {
                            final View child = children[i];
                           if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                           //執行測量子view的方法
                           measureChild(child, widthMeasureSpec, heightMeasureSpec);
                 }
            }
        }

measureChild的原始碼:

   //具體測量一個子view的方法
   protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {

       //獲取到子view的引數LayoutParams,後面會用
       final LayoutParams lp = child.getLayoutParams();

       //根據LayoutParams裡面設定的寬的match_parent或者wrap_content(即lp.width),在結合父view的MeasureSpec來確定子view的寬的MeasureSpec
       final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
               mPaddingLeft + mPaddingRight, lp.width);

       //根據LayoutParams裡面設定的高的match_parent或者wrap_content(即lp.height),在結合父view的MeasureSpec來確定子view的高的MeasureSpec
       final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
               mPaddingTop + mPaddingBottom, lp.height);

       //上面的幾行程式碼是確定子view的MeasureSpec的,這行程式碼是真正進行子view測量的,將上面確定下來的子view的MeasureSpec傳給子view。
       child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
   }

  1. onMeasure示例程式碼實現如下:
       /**
        * 模擬水平方向可滑動的LinearLayout的測量過程,這裡不考慮padding和margin的影響
        *
        * @param widthMeasureSpec
        * @param heightMeasureSpec
        */
       @Override
       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
           super.onMeasure(widthMeasureSpec, heightMeasureSpec);

           //獲取父容器為其指定的測量模式和測量尺寸
           int widthMode = MeasureSpec.getMode(widthMeasureSpec);
           int widthSize = MeasureSpec.getSize(widthMeasureSpec);
           int hightMode = MeasureSpec.getMode(heightMeasureSpec);
           int hightSize = MeasureSpec.getSize(heightMeasureSpec);

           //測量所有子view的寬和高
           measureChildren(widthMeasureSpec, heightMeasureSpec);

           //測量自身的寬和高
           int measureWidth = 0;
           int measureHight = 0;
           int childCount = getChildCount();
           if (childCount != 0) {
                //計算出由子view決定的寬度
                for (int i = 0; i < childCount;i++) {
                    measureWidth +=getChildAt(i).getMeasuredWidth();
                }

                //計算出由子view決定的高度(選取子view中高度最大值為其測量高度)
                for (int j = 0; j < childCount;j++) {
                    if(getChildAt(j).getMeasuredHeight() > measureHight) {
                        measureHight =getChildAt(j).getMeasuredHeight();
                    }
                }
           }

           if (widthMode == MeasureSpec.AT_MOST && hightMode ==MeasureSpec.AT_MOST) {
                setMeasuredDimension(measureWidth,measureHight);
           } else if (widthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(measureWidth,hightSize);
           } else if (hightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(widthSize,measureHight);
           }
       }
  1. 容器控制元件的佈局,主要是指定每一個子view在自身上的位置,重寫onLayout方法,程式碼實現如下:
	   /**
        * 對子view進行佈局,這裡不考慮padding和margin的影響
        *
        * @param changed
        * @param left
        * @param top
        * @param right
        * @param bottom
        */
       @Override
       protected void onLayout(boolean changed, int left, int top, int right,int bottom) {

           //每個子view的左起點
           int childLeft = 0;
           //子view的個數
           int childCount = getChildCount();

           //為每個子view指定位置
           for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                childView.layout(childLeft, 0,childLeft + childView.getMeasuredWidth(), childView.getMeasuredHeight());
                childLeft +=childView.getMeasuredWidth();
           }
       }

五、在現有控制元件的基礎上進行自定義

上面所說的自定義控制元件,都是直接繼承View或者ViewGroup的,實際開發中有很多需求是不需要重頭自己定義一個控制元件的,可以繼承一個現有控制元件,去重寫其特定的某一個方法來擴充套件功能。具體用哪種方式去實現,要具體情況具體分析了,如何選取一種最適合的自定義方式,是值得思考的,也是一個難點。