Android——View的工作原理

View的工作原理

1.1 ViewRoot和DecorView簡介

1.1.1 ViewRoot相關

  • View的三大流程:1.View的測量流程;2.佈局流程;3.繪製流程。

  • ViewRoot對應ViewRootImpl類,是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot完成的。

  • 在ActivityThread中,當Activity對象創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯:

    root = new ViewRootImpl(view.getContext(),display);
    root.setView(view,wparams,panelParentView);
    
  • View的繪製流程是從ViewRoot的performTraversals方法開始的,經過measurelayoutdraw三個流程纔將View繪製完成。measure用來測量View的寬和高;layout用來確認View在父容器中的放置位置;draw用來將View繪製在屏幕上。

    performTraversals工作流程

  • performTraversals會依次調用 performMeasure、performLayout、performDraw三個方法,這三個方法分別完成頂級View的measure、layout、draw三大流程。其中在performMeasure中會調用measure方法,measure方法又會調用onMeasure方法,在onMeasure方法中會對所有子元素進行measure過程,這樣measure流程就從父容器流轉到子元素中了。子元素重複父容器過程,反覆直到整個View樹的遍歷。performLayout和performDraw的傳遞流程和performMeasure類似,不同的是:performDraw的傳遞過程是在draw方法中通過dispatchDraw實現的,本質上無差異。

  • measure過程決定了View的寬/高。measure完成後,可以通過getMeasuredWidthgetMeasuredHeight獲取View測量後的寬/高。正常情況下measure過程的View的寬/高就是實際的View的寬/高。

  • layout過程決定了View的四個頂點的座標和實際的View的寬/高。layout完成後,可以通過getTopgetBottomgetLeftgetRight獲取View的四個頂點的位置,且可以通過getWidthgetHeight獲取View的最終寬/高。

  • draw過程決定了View的顯示。draw完成後,View的內容呈現在屏幕上。

1.1.2 DecorView相關

  • DecorView作爲頂級View,一般情況下內部包含一個豎直方向的LinearLayout,LinearLayout中有上下兩部分,標題欄和內容欄(具體與Android版本主題有關)。

  • 在Activity中通過setContentView設置的佈局文件就是被加到內容欄中,而內容欄的id是content,所有Activity指定佈局的方法叫做setContentView而非setView。

  • 獲取content的方法ViewGroup content = findViewById(R.android.id.content)

  • 獲取View的方法content.getChildAt(0)

  • DecorView實際上是一個FrameLayout,View層的事件都是經過DecorView後傳遞給View的。

    頂級View結構DecorView

1.2 理解MeasureSpec

  • MeasureSpec在很大程度上決定一個View的尺寸規格。
  • 一個View的實際尺寸規格,還會受父容器影響。即父容器會影響View的MeasureSpec的創建過程。
  • 系統會將View的LayoutParams根據父容器施加的規則轉換成對應的MeasureSpec,再根據這個MeasureSpec來測量出View的寬/高。

1.2.1 MeasureSpec

  • MeasureSpec代表一個32位的int值高2位代表SpecMode低30位代表SpecSize。SpecMode指測量模式;SpecSize指某種測量模式下的規格大小。

  • MeasureSpec內部的一些常量定義:

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
        ...
        
        /**
        * Measure specification mode: The parent has not imposed any constraint
        * on the child. It can be whatever size it wants.
        */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        
        /**
        * Measure specification mode: The parent has determined an exact size
        * for the child. The child is going to be given those bounds regardless
        * of how big it wants to be.
        */
        public static final int EXACTLY     = 1 << MODE_SHIFT;
    
        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
    
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,@MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }
    
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }
    
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
        
        ...
    

    MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,同時提供了打包解包的方法。一組SpecMode和SpecSize可以打包成一個MeasureSpec,一個MeasureSpec也可以拆成原始的SpecMode和SpecSize。(MeasureSpec這裏指MeasureSpec代表的int,而非對象本身)

  • SpecMode三種類型

    • 1.UNSPECIFIED:父容器不對View有任何限制,要多大給多大,該情況一般用於系統內部,表示一種測量狀態。
    • 2.EXACTLY:父容器已經檢測出View所需要的精確大小,這時View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。
    • 3.AT_MOST:父容器指定一個可用大小即SpecSize,View的大小不能大於這個值,具體的值還要看View的具體實現。它對應於LayoutParams中的wrap_content。

1.2.2 MeasureSpec和LayoutParams的對應關係

  • 系統內部是通過MeasureSpec來進行View的測量。正常情況下我們使用View指定的MeasureSpec,但我們也可以給View設置LayoutParams。

  • 在View測量的時候,系統會將LayoutParams在父容器的約束下轉換成MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬/高。

  • 注意:MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而決定View的寬/高。另外,對於頂級View(即DecorView)和普通View而言,MeasureSpec的轉換過程是不同的:

    • DecorView:其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定。
    • 普通View:其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同確定。
  • MeasureSpec一旦確定,onMeasure中就可以確定View的測量寬/高。

  • ViewRootImpl中的measureHierarchy方法中的一段代碼,展示了DecorView的MeasureSpec的創建過程:

    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    

    其中desiredWindowHeightchildWidthMeasureSpec是屏幕尺寸。

  • 再看getRootMeasureSpec方法的實現:

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    上述代碼可見DecorView的MeasureSpec的產生過程。其遵守如下規則(根據LayoutParams中的寬/高的參數來劃分):

    • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口大小。
    • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小。
    • 固定大小(比如100dp):精確模式,大小爲LayoutParams中指定的大小。
  • 對於普通佈局中的View來說,View的measure過程由ViewGroup傳遞而來。ViewGroup的measureChildWithMargins方法:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    上述方法會對子元素進行measure,在調用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec,可見子元素的MeasureSpec的創建與父容器的MeasureSpec和子元素自身的LayoutParams有關,此外還和View的margin及paddig有關。

    getChildMeasureSpec的具體代碼:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
    
        int size = Math.max(0, specSize - padding);
    
        int resultSize = 0;
        int resultMode = 0;
    
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
    
        // Parent asked to see how big we want to be
        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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
    

    該方法是根據父容器的MeasureSpec和View自身的LayoutParams確定子元素的MeasureSpec,參數中的padding是指父容器中已佔用的空間大小,因此子元素可以的大小爲父容器的尺寸減去padding:

    int specSize = MeasureSpec.getSize(spec);
    int size = Math.max(0, specSize - padding);
    
  • 普通View的MeasureSpec的創建規則

    EXACTLY AT_MOST UNSPECIFIED
    dp/px EXACTLY(childSize) EXACTLY(childSize) EXACTLY(childSize)
    match_parent EXACTLY(parentSize) AT_MOST(parentSize) UNSPECIFIED(0)
    wrap_parent AT_MOST(parentSize) AT_MOST(parentSize) UNSPECIFIED(0)

    :1.橫排爲childparentSpecmode,縱排爲LayoutParams;2.parentSize指父容器中目前可使用的大小。

  • 只要提供了父容器的MeasureSpec和子元素的LayoutParams,就可以快速確定出子元素的MeasureSpec。有了MeasureSpec就可以進一步確定出子元素測量後的大小。

1.3 View的工作流程

1.3.1 measure過程

  • measure過程要分兩種情況:

    • 原始的View:通過measure方法完成測量過程。
    • ViewGroup:完成自己的測量過程外,遍歷去調用所有子元素的measure方法。
  • View的measure過程

    • View的measure過程由其measure方法完成。measure方法是fianl類型,在其方法內會調用View的onMeasure方法。onMeasure的方法實現:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
    }
    

    setMeasuredDimension方法會設置View寬/高的測量值。再看getDefaultSize方法的實現:

    public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
    }
    

    可見getDefaultSize返回的大小是measureSpec中的specSize,這個specSize就是View測量後的大小。對於UNSPECIFIED情況,View的大小爲getDefaultSize第一個參數size,即寬/高爲getSuggestedMinimumWidth和getSuggestedMinimumHeight方法的返回值:

    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    
    protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
    

    從getSuggestedMinimumWidth代碼可見,如果View沒有設置背景,View的寬度爲mMinWidth,mMinWidth對應android:minWidth屬性值,如果該屬性值不指定,mMinWidth默認0;如果View設置了背景,View的寬度爲max(mMinWidth, mBackground.getMinimumWidth())。再看DrawablegetMinimumWidth方法

    public int getMinimumWidth() {
     final int intrinsicWidth = getIntrinsicWidth();
     return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    

    getMinimumWidth返回的就是Drawable的原始寬度(前提是這個Drawable有原始寬度),否則就返回0。ShapeDrawable無原始寬/高,而BitmapDrawable有原始寬/高(圖片的尺寸)。

    • 從getDefaultSize方法的實現看,在非UNSPECIFIED情況下,View的寬/高由SpecSize決定。

    • 直接繼承View的自定義控件需要重寫onMeasure方法,且要設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。這是因爲View在佈局中使用wrap_content,那它的SpecMode是AT_MOST模式,它的寬/高等於SpecSize,根據之前的普通View的MeasureSpec的創建規則可知,這種情況下View的寬/高就是父容器當前剩餘的空間大小,與match_parent表現一致。解決方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    	int heightSpecsize = MeasureSpec.getsize(heightMeasureSpec);
    	if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(mWidth, mHeight);
        }else if(widthSpecMode == MeasureSpec. AT_MOST){
            setMeasuredDimension(mWidth, heightSpecSize);
    	}else if(heightSpecMode == MeasureSpec.AT MOST) {
    		setMeasuredDimension(widthSpecsize, mHeight);
    	}
    }
    

    只需要給View指定一個默認的內部寬/高(mWidth和mHeight),並且在wrap_content時設置此寬/高即可。

    • 對於非wrap_content情況,我們沿用系統的測量值即可。
  • ViewGroup的measure過程

    • ViewGroup除了完成自己的measure過程以外,還會遍歷調用子元素的measure方法,各個子元素再遞歸去執行這個過程。

    • 與View不同的是,ViewGroup是抽象類,沒有重寫View的onMeasure方法,而是提供一個叫measureChildren的方法:

      protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec){
      	final int size = mChildrenCount;
          final View[] children = mChildren;
          for (int i = 0; i < size; ++i) {
              final View child = children[i];
              if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                  measureChild(child, widthMeasureSpec, heightMeasureSpec);
              }
          }
      }
      

      再看measureChild方法:

      protected void measureChild(View child, int parentWidthMeasureSpec,
                  int parentHeightMeasureSpec) {
          final LayoutParams lp = child.getLayoutParams();
          
          final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                  mPaddingLeft + mPaddingRight, lp.width);
          final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                  mPaddingTop + mPaddingBottom, lp.height);
      
          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      }
      

      measureChild方法做的就是抽出子元素的LayoutParams,然後通過getChildMeasureSpec創建子元素的MeasureSpec,接着將MeasureSpec傳遞給View的measure方法進行測量。

    • ViewGroup是抽象類,其測量過程的onMeasure交由各子類去實現,比如LinearLayout、RelativeLayout等。之所以ViewGroup不像View一樣統一onMeasure方法,是因爲ViewGroup的子類的佈局特性差異過大。

  • 注意:在極端情況下,系統可能需要多次measure才能確認最終的測量寬/高,在onMeasure中獲取測量寬/高可能不準確。因此,在onLayout方法中取獲取View的測量寬/高或者最終寬/高。


  • 如果我們想在Activity啓動時獲取某個View的寬/高,實際上在onCreat、onStart、onResume中均無法正確獲取(未完成測量時,獲取爲0)。這是因爲View的measure過程和Activity的生命週期方法不是同步執行的。解決方法有四個:

    • 1.onWindowFocusChanged:View已經初始化完成,寬/高已經準備完成。注意該方法會被多次調用,當Activity的窗口得到焦點和失去焦點時均會被調用。頻繁的進行onResume和onPause,也會被調用。

      @Override
      public void onWindowFocusChanged(boolean hasFocus) {
          super.onWindowFocusChanged(hasFocus);
          if(hasFocus){
              int width = view.getMeasuredWidth();
              int height = view.getMeasuredHeight();
          }
      }
      
    • 2.view.post(runnable):通過post將一個runnable投遞到消息隊列尾部,然後等待Looper調用此runnable的時候,View已經初始化好了。

      protected void onStart() {
          super.onStart();
          view.post(new Runnable() {
              @Override
              public void run() {
                  int width = view.getMeasuredWidth();
                  int height = view.getMeasuredHeight();
              }
          });
      }
      
    • 3.ViewTreeObserver:ViewTreeObserver的衆多回調可以完成這個功能,比如OnGlobalLayoutListener接口,當View樹的狀態放生改變或者View樹內部的View的可見性發生改變時,OnGlobalLayoutListener方法會被調用。

          protected void onStart() {
              super.onStart();
              ViewTreeObserver observer = view.getViewTreeObserver();
              observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                  @Override
                  public void onGlobalLayout() {
                      view.getViewTreeObserver().removeOnDrawListener(this);
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
              });
          }
      
    • 4.view.measure(int widthMeasureSpec,int heightMeasureSpec):通過手動對View進行measure來得到View的寬高。比較複雜。

  • 手動對View進行measure來得到View的寬高,要根據View的LayoutParams區分:

    • match_parent:無法測量。構造此種MeasureSpec需要知道parentSize,無法獲取。

    • 具體數值(dp/px):寬高都是100px情況舉例:

      int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
      int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
      view.measure(widthMeasureSpec,heightMeasureSpec)
      
    • wrap_content

      int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
      int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
      view.measure(widthMeasureSpec,heightMeasureSpec)
      

      注意:(1<<30)-1,View尺寸使用30位二進制表示,最大是30個1,2^30-1,即(1<<30)-1

1.3.2 layout過程

  • 當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並調用其layout方法,在layout方法中,onLayout方法又會被調用。layout方法確定View本身的位置,onLayout確定所有子元素的位置:

    public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
    
        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
    
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
    
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
    
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
    
        final boolean wasLayoutValid = isLayoutValid();
    
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    
        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                // We have a robust focus, so parents should no longer be wanting focus.
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
                // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
                // layout. In this case, there's no guarantee that parent layouts will be evaluated
                // and thus the safest action is to clear focus here.
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    // Give up and clear focus once we've reached the top-most parent which wants
                    // focus.
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }
    
        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }
    

    首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值。View 的四個頂點確定後,View在父容器中的位置也就確定了;接着會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,onLayout 的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法。

  • View的getMeasureWidth(getMeasureHeight)和getWidth(getHeight)的區別:

    public final int getWidth() {
        return mRight - mLeft;
    }
    
    public final int getHeight() {
        return mBottom - mTop;
    }
    

    在View的默認實現中, View的測量寬/高最終寬/高是相等的,只不過測量寬/高形成於View的measure過程,而最終寬/高形成於View的layout過程。

  • 特殊情況1:(重寫layout)

    public void layout(int l,int t,int ,int b){
    	super.layout(l,t,r + 100,b + 100);
    }
    

    上述代碼會導致在View的最終寬/高總是比測量寬/高大100px,但沒有實際意義。

  • 特殊情況2:(多次measure)

    過程中可能View的最終寬/高和測量寬/高不一致,但是最終會一致。

1.3.3 draw過程

  • draw的作用是將View繪製到屏幕上。

  • 過程:

    • 1.繪製背景:background.draw(canvas)
    • 2.繪製自己:onDraw
    • 3.繪製children :dispatchDraw
    • 4.繪製裝飾:onDrawScrollBars
  • View繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw 會遍歷調用所有子元素的draw方法,重複直至繪製完成。

    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
    
        // Step 1, draw the background, if needed
        int saveCount;
    
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
    
        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            drawAutofilledHighlight(canvas);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
    
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
    
            if (debugDraw()) {
                debugDrawFocus(canvas);
            }
    
            // we're done...
            return;
        }
    ...
    
  • View 有一個特殊的方法setWillNotDraw

    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
    

    如果一個View不需要繪製任何內容,那麼設置這個標記位爲true以後,系統會進行相應的優化。默認View 沒有啓用這個優化標記位,但是ViewGroup會默認啓用這個優化標記位。注意:VicwGroup需要通過onDraw來繪製內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標記位。

1.4 自定義View

1.4.1 自定義View的分類

  • 1.繼承View重寫onDraw方法

    主要用於實現一些不規則的效果。預期效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形。採用這種方式需要自己支持wrap_content,padding也需要自己處理。

  • 2.繼承ViewGroup派生特殊的Layout

    主要用於實現自定義佈局。需要合適地處理ViewGroup的測量、佈局這兩個過程;同時處理子元素的測量和佈局過程。

  • 3.繼承特定的View(比如TextView)

    一般用於擴展某種已有的Viewdev功能。不需要自己支持wrap_content和padding 等。

  • 4.繼承特定的ViewGroup

    不需要自己處理ViewGroup的測量和佈局這兩個過程。與2的區別:方法2更接近View的底層。

1.4.2 自定義View須知

  • 讓View支持wrap_content

    直接繼承View或者ViewGroup的控件,如果不在onMeasure中對wrap_content做特殊處理,那麼當外界佈局中使用wrap_content時就無法達到預期效果。

  • 讓View支持padding

    直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性是無法起作用的。直接繼承ViewGroup的控件要在onMeasure和onLayout中考慮padding和子元素的margin對其造成的影響,不然將導致padding和子元素的margin失效。

  • 不要在View中使用Handler

    View內部本身就提供了post系列的方法,可以替代handler的作用。

  • View中如果有線程或者動畫,需要及時停止

    onDetachedFromWindow是停止線程或者動畫的好時機。當包含此View的Activity退出或者當前View被remove時,View 的onDetachedFromWindow方法會被調用;當包含View的Activity啓動時,View 的onAttachedToWindow方法會被調用。View變得不可見時我們也需要停止線程和動畫,如果不及時處理這種問題,有可能會造成內存泄漏。

  • View帶有滑動嵌套情形時,需要處理好滑動衝突

    如果有滑動衝突,要合適地處理滑動衝突,否則將會嚴重影響View的效果。

1.4.3 添加自定義屬性

  • 1.在values目錄下面創建自定義屬性的XML,比如atrs.xml。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="CircleView">
            <attr name="circle_color" format="color"/>
        </declare-styleable>
    </resources>
    

    除了color,還可以指定reference(資源id)、dimension(尺寸)、string、interger、boolean等。

  • 2.在View的構造函數中解析自定義屬性的值並處理。

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //預加載自定義屬性集合CircleView
        TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
        //解析CircleView屬性集合中的circle_color屬性,並設置默認顏色值
        mColor=typedArray.getColor(R.styleable.CircleView_circle_color,Color.RED);
        //實現資源
        typedArray.recycle();
        init();
    }
    
  • 3.在佈局文件中使用自定義屬性。

    <com.virtual.testview.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        app:circle_color="#F00A0A"
        android:background="#000000"/>
    

    注意:使用自定義屬性,必須在佈局文件中添加schemas聲明: xmlns:app="http://schemas.android.com/apk/res-auto"。app是自定義前綴,可以更換其他名字

1.5 參考資料

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