View相關知識點

目錄介紹

  • 1.View測量佈局繪製總體流程
    • 1.1 MeasureSpec是什麼?
    • 1.2 LayoutParams是什麼?
    • 1.3 View的測量流程(Measure)
    • 1.4 在getChildMeasureSpec方法中都作了什麼?
    • 1.5 Layout佈局過程
    • 1.6 Draw過程
  • 2.getWidth,getMeasureWidth的區別
  • 3.requestLayout()、invalidate()與postInvalidate()有什麼區別?
  • 4.自定義View總體思想和類型
  • 5.何時能夠獲取到View的寬高,爲何?
  • 6.獲取控件寬高的幾種方法
  • 7.子線程中真的不能更新UI嗎?
  • 8.經常使用佈局測量流程
    • 8.1 LinearLayout
    • 8.2 FrameLayout
    • 8.3 RelativeLayout

此文爲我我的總結學習的View相關知識點和常考知識點,文章中說的每個點都須要你我的去理解而不是去背,若是這些你都搞懂那麼恭喜你與View相關的知識點應該是難不住你了。

1. View測量佈局繪製總體流程

首先明確兩個概念:html

1.1 MeasureSpec是什麼?

MeasureSpec是一個大小跟模式的組合值,MeasureSpec中的值是一個整型(32位)將size和mode打包成一個Int型,其中高兩位是mode,後面30位存的是size,爲了減小對象的分配開支因此使用了int類型去進行存儲。要注意的是通常的int值是十進制的數,而MeasureSpec 是二進制存儲的。必定要注意的是MeasureSpec是父View對子View的指望寬高要求,能夠認爲是父View傳遞給子View的。java

SpecMode有三類,每一類都表示特殊的含義,以下所示:android

  1. UNSPECIFIED: 父容器不對View有任何限制,要多大給多大,這種狀況通常用於系統內部,表示一種測量的狀態。 (如ListView或ScrollView)
  2. EXACTLY :一個明確的大小值,如多少多少dp或matchparent
  3. AT_MOST :對應於LayoutParams中的wrap_content。

1.2 LayoutParams是什麼?

其實其中保存的就是咱們XML文件對View的賦值。canvas

<View    
	android:layout_width="100dp"    
	android:layout_height="100dp"   />
複製代碼

好比上面這種狀況layoutParams.width和layoutParams.height就是100dp數組

具體分爲三種bash

  1. LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小;
  2. LayoutParams.WRAP_CONTENT:最大模式,大小不定,可是不能超過窗口的大小;
  3. 具體的大小值(好比100dp):精確模式,大小爲LayoutParams中指定的大小。

1.3 View的測量流程(Measure):

首先由一段代碼來講明 代碼所示:ide

protected void onMeasure(int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        //獲取子View的LayoutParams
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //根據子View自身的LayoutParams和父View的MeasureSpec和可用空間獲取子View自身的MeasureSpec
        //獲取寬度MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        //獲取高度MeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //根據父View對子View的指望MeasureSpec結合自身的規則進行最終的測量得出自身的指望寬高
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        widthUsed+=child.getMeasuredWidth();
        heightUsed+=child.getMeasuredHeight();
    }
    //給父View設置上最終的指望寬高
    setMeasuredDimension(widthUsed, heightUsed);
}
複製代碼

以ViewGroup爲例

1.首先會遍歷全部子View。(for循環)函數

2.根據子View自身的LayoutParams和父View自身的MeasureSpec以及父View的可用空間獲取子View自身的MeasureSpec,這個MeasureSpec是父View對子View的指望寬高。(對應getChildMeasureSpec方法,最終在getChildMeasureSpec方法中使用MeasureSpec.makeMeasureSpec(size, mode) 來求得結果)佈局

(有這一步的緣由是由於咱們在XML中定義的View寬高好比說是match_parent或wrap_content這種格式,那麼咱們其實並不知道他具體應該被賦值多大,google就要幫咱們計算你match_parent的時候是多大,wrap_content的是多大,這個計算過程,就是計算出來的父View的MeasureSpec不斷往子View傳遞,結合子View的LayoutParams 一塊兒再算出子View的MeasureSpec,而後繼續傳給子View,不斷計算每一個View的MeasureSpec,子View有了MeasureSpec才能測量本身和本身的子View。)post

3.子View根據父View對其的指望寬高和自身的規則算出其最終的指望寬高。(child.measure(childWidthMeasureSpec, childHeightMeasureSpec)) (這裏的自身規則指的是其在OnMeasure中的邏輯,好比TextView會根據其中字符串的長度高度肯定最終的大小值)。

MeasureSpec中的值既然是父View對子View的指望值,那麼最外層的View是如何設置的?

在最外層的DecorView中,有這樣一段代碼:

private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
   int measureSpec;
   switch (rootDimension) {
   case ViewGroup.LayoutParams.MATCH_PARENT:
   measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
   break;
   ......
  }
return measureSpec;
}
複製代碼

能夠看到咱們最外層的View也就是DecorView中根據getRootMeasureSpec這個方法獲取的MeasureSpec的Mode是EXACTLY,size是屏幕的寬高。 也就是說咱們最外層的DecorView中默認的寬高就是屏幕的寬高,EXACTLY表明固定大小。

1.4 在getChildMeasureSpec方法中都作了什麼?

在這個方法中子View根據自身的LayoutParams和父View自身的MeasureSpec及可用空間獲取子View自身的MeasureSpec。

能夠看到當咱們定義子View爲match_parent或wrap_content的時候,最終生成的MeasureSpec的Size爲父View的大小,而在View的默認實現中當調用measure開始測量後走到onMearsure設置最終指望寬高的時候默認實現爲直接使用MeasureSpec中的Size值。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }

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;
}
複製代碼

也就是說當咱們自定義View的時候若是咱們須要使本身的View支持wrap_content,那麼就必須重寫OnMeasure方法並對wrap_content作一個特殊的測量,不然在wrap_content的狀況下咱們自定義View的大小就會和父View的大小相同。

1.5 Layout佈局過程

Layout的做用是ViewGroup用來肯定子元素的位置,當ViewGroup的位置被肯定後,它在onLayout中會遍歷全部的子元素並調用其layout方法,在layout方法中的onLayout方法又會被調用。 layout方法中會調用setFrame方法保存其在ViewGroup中的位置,自定義ViewGroup的時候必須重寫OnLayout方法,在其中進行子View位置的設置。

  • 在View中onLayout默認是一個空實現
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
}  
複製代碼
  • 在ViewGroup中是抽象方法,因此重寫ViewGroup的時候必須去實現OnLayout方法。
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(l, t, r,b);
        } 
複製代碼

具體的計算過程能夠看下最簡單FrameLayout 的onLayout 函數的源碼,每一個不一樣的ViewGroup 的實現都不同。 MeasuredWidth和MeasuredHeight這兩個參數爲layout過程提供了一個很重要的依據(若是不知道View的大小,你怎麼固定四個點的位置呢),可是這兩個參數也不是必須的,layout過程當中的4個參數l, t, r, b徹底能夠由咱們任意指定,而View的最終的佈局位置和大小(mRight - mLeft=實際寬或者mBottom-mTop=實際高)徹底由這4個參數決定,但一般狀況下用的就是第一步在measure過程當中計算出來的指望寬高。

從measure和layout方法中能夠看出的另外一點是measure只是進行一些初始化參數的工做,真正的測量邏輯是在OnMeasure中進行的。而layout方法直接對你的View進行了位置和大小的肯定,真正的邏輯不是在OnLayout中進行的。

1.6 Draw過程

View的繪製主要分爲四部分:

  1. 繪製背景background.draw(canvas)。
  2. 繪製本身(onDraw)。
  3. 繪製children(dispatchDraw)。
  4. 繪製裝飾(onDrawScrollBars)。

OnDraw

onDraw(canvas) 方法是view用來draw 本身的,具體如何繪製,顏色線條什麼樣式就須要子View本身去實現,View.java 的onDraw(canvas) 是空實現,ViewGroup 也沒有實現,每一個View的內容是各不相同的,因此須要由子類去實現具體邏輯。

dispatchDraw

dispatchDraw(canvas) 方法是用來繪製子View的,View.java 的dispatchDraw()方法是一個空方法,由於View沒有子View,不須要實現dispatchDraw ()方法,ViewGroup就不同了,它實現了dispatchDraw ()方法並在其中遍歷子View而後調用子View的draw()方法。

當咱們自定義ViewGroup的時候默認是不會執行OnDraw方法的(ViewGroup默認調用了setWillNotDraw(true),由於系統默認認爲咱們不會在ViewGroup中繪製內容),咱們若是須要進行繪製能夠在dispatchDraw中去進行或者調用setWillNotDraw(false)方法。

從setWillNotDraw這個方法的註釋中能夠看出,若是一個View不須要繪製任何內容,那麼設置這個標記位爲true之後,系統會進行相應的優化。默認狀況下,View沒有啓用這個優化標記位,可是ViewGroup會默認啓用這個優化標記位。這個標記位對實際開發的意義是:當咱們的自定義控件繼承於ViewGroup而且自己不具有繪製功能時,就能夠開啓這個標記位從而便於系統進行後續的優化。固然,當明確知道一個ViewGroup須要經過onDraw來繪製內容時,咱們須要顯式地關閉WILL_NOT_DRAW這個標記位。

/**
* If this view doesn't do any drawing on its own,set this flag to * allow further optimizations. By default,this flag is not set on * View,but could be set on some View subclasses such as ViewGroup. * * Typically,if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ public void setWillNotDraw(boolean willNotDraw) { setFlags(willNotDraw ? WILL_NOT_DRAW : 0,DRAW_MASK); } 複製代碼

2.getWidth,getMeasureWidth的區別

首先要明確一點,測量獲得的寬高並不必定是View的最終寬高,當measure執行完畢後(準確的是咱們在onMeasure中調用setMeasuredDimension(width,height)方法後)咱們就能夠獲得View的一個指望寬高,一般狀況下指望寬高是和最終的寬高相同的,可是也有特殊狀況(好比在layout方法最終賦值View寬高的時候手動的修改值而不用測量獲得的值)。

  • getMeasureWidth()方法在measure()過程結束後就能夠獲取到了,另外,getMeasureWidth()方法中的值是經過setMeasuredDimension()方法來進行設置的。
  • getWidth()方法要在layout()過程結束後才能獲取到,當在layout方法中調用setFrame()後就能夠獲取此值了,這個值是View的真實寬高。
    • getWidth()方法中的值則是經過視圖右邊的座標減去左邊的座標計算出來的。
public final int getWidth() {
    return mRight - mLeft;
}
複製代碼

3.requestLayout()、invalidate()與postInvalidate()有什麼區別?

invalidate和postInvalidate都是調用onDraw()方法,而後去達到重繪view的目的。 invalidate()用於主線程,postInvalidate()用於子線程, postInvalidate的原理其實就是經過主線程的handler完成線程的調度最終在主線程中調用invalidate方法。 requestLayout()會調用measure和layout方法,當View的大小位置須要改變的時候調用。若是view的大小發生了變化那麼requestlayout也會調用draw()方法。

4.自定義View總體思想和類型

自定義View

1.繼承自系統View(ImageView,TextView等)

通常重寫OnMearsure方法,由於系統View再其自身的OnMearsure,OnDraw中都處理好了內容,咱們通常不須要進行修改,複寫的時候一般直接super父類方法而後實現本身的邏輯便可。 好比實現一個正方形的ImageView

2.繼承View

若是你的View是定義了明確寬高的話,那麼一般不須要咱們重寫OnMeasure的,若是寬高定義爲了wrap_content的話咱們須要早OnMeasure中針對wrap_content這種模式進行一個修改並設置最終寬高,由於默認狀況下View的wrap_content和match_parent大小是相同的(在getChildMeasureSpec方法計算得出)。 若是咱們的一些用到的屬性是跟View的大小變化相關的話,那麼咱們能夠經過OnSizeChanged去進行監聽(OnSizeChanged在layout方法中的setFrame執行時會被調用,也就是說當咱們調用requestLayout時能夠經過OnSizeChanged去獲取新的控件寬高等值)。 咱們能夠在OnDraw中進行內容的繪製,onDraw不要進行過多的耗時操做,如頻繁的建立對象。

3.繼承自ViewGroup

須要重寫OnMeasure而且對子View進行遍歷測量,而後自身去調用setMeasureDimens設置自身寬高。 onLayout必須重寫並遍歷子View調用其layout方法進行佈局和大小的肯定。(若是不調用會沒有子View顯示) onDraw默認不執行,若是須要進行繪製能夠調用setWillNotDraw(false)取消onDraw的禁用或者在dispatchDraw中進行繪製。 TagLayout(流式佈局)佈局思路: 須要定義一個已使用寬度(widthUsed)和高度(heightUsed),在OnMeasure執行完對全部子View測量後,OnLayout方法中根據自身定義的規則若是widthUsed+view.getMeasureWidth>viewGroup.getMeasureWidth的話須要進行換行,widthUsed清零且heightUsed+=view.getMeasureHeight,子View調用layout時傳入的四個點座標就是(widthUsed,heightUsed,widthUsed+view.getMeasureWidth,heightUsed+view.getMeasureHeight),以此類推完成全部子View的佈局;

4.繼承自系統ViewGroup

這種狀況不須要咱們重寫OnMearsure和OnLayout,由於系統已經幫咱們寫好了,一般這種狀況下是咱們將本身定義的佈局添加到ViewGroup中,對整個的View進行一個封裝複用。

5.何時能夠獲取到View的寬高,爲何?

在OnResume執行完後能夠獲取寬高,由於View的測繪流程是由ViewRootImpl的performTraversals開始的。當Activity建立時執行到handleResumeActivity方法中先會執行OnResume方法而後WindowManager會調用addView將DecorView添加進去,以後ViewRootImpl纔會被建立出來從而調用performTraversals開始View的測繪流程。

final void handleResumeActivity( ... ... ) {
     // 最終會執行到 onResume(),不是重點
     r = performResumeActivity(token, clearHide, reason);

     if (r != null) {
         final Activity a = r.activity;

         if (r.window == null && !a.mFinished && willBeVisible) {
             r.window = r.activity.getWindow();
             View decor = r.window.getDecorView();
             ViewManager wm = a.getWindowManager();
             // 5. 執行到 WindowManagerImpl 的 addView()
             // 而後會跳轉到 WindowManagerGlobal 的 addView()
             if (a.mVisibleFromClient) {
                 if (!a.mWindowAdded) {
                     a.mWindowAdded = true;
                     wm.addView(decor, l);
                 }
             }
         }
     }
}

public void addView( ... ... ) {
     ViewRootImpl root;
     synchronized (mLock) {
         // 初始化一個 ViewRootImpl 的實例
         root = new ViewRootImpl(view.getContext(), display);
         try {
             // 調用 setView,爲 root 佈局 setView
             // 其中 view 爲傳下來的 DecorView 對象
             // 也就是說,實際上根佈局並非咱們認爲的 DecorView,而是 ViewRootImpl
             root.setView(view, wparams, panelParentView);
         }
     }
}

// 6. 將 DecorView 加載到 WindowManager, View 的繪製流程今後刻纔開始public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    // 請求對 View 進行測量和繪製
    // 與 setContentView() 不一樣,此處的方法是 ViewRootImpl 的方法
    requestLayout();
}

@Overridepublic void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        // 7. 此方法內部有一個 post 了一個 Runnable 對象
        // 在其中又調用一個 doTraversal() 方法;
        // 再以後又會調用到 performTraversals() 方法,而後 View 的測繪流程就今後處開始了
        scheduleTraversals();
    }
}

private void performTraversals() {
    ... ...
    // Ask host how big it wants to be
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ... ...
    performLayout(lp, mWidth, mHeight);
    ... ...
    performDraw();
    ... ...
}
複製代碼

6.獲取控件寬高的幾種方法

1.onWindowFocusChanged 這個方法會被調用屢次,在View初始化完畢後會調用,當Activity的窗口獲得焦點和失去焦點都會被調用一次(Activity繼續執行和暫停執行時)。

2.ViewTreeObserver 當View樹的狀態發生改變或者View樹內部的View可見性發現改變時,onGlobalLayout方法將被回調。

3.View.post(new Runnble) 內部分兩種狀況: 第一種View已經完成測繪(這種直接調用主線程handler.post(new Runnable)發送一個Message並回調給Runnble處理) 第二種View沒有完成測繪,這種會先將Runnble任務經過數組保存下來,當View開始測繪時(ViewRootImpl.performTraversals())會將包存下來的Runnble任務經過主線程handler進行發送消息,因爲消息在messagequeue中是串行處理的,因此view.post的Runnble任務會在view的測繪完成後在開始執行其自身的消息,這時View已經完成測繪,天然就能夠獲取到寬高了。 更詳細的可參考: www.cnblogs.com/dasusu/p/80…

7.子線程中真的不能更新UI嗎?

衆所周知安卓不容許在非UI線程中去更新UI,每當咱們對View狀態作出改變的時候(如調用requestLayout()或invalidate()等方式時)都會去檢查當前線程是不是主線程,而**檢查線程的判斷是在ViewRootImpl的checkThread()方法中去執行的。**也就是說在ViewRootImpl沒有建立出來的時候(OnResume執行完後ViewRootImpl才建立出來的)checkThread()這一步檢測是不會執行的,在這種狀況下咱們在子線程中是能夠更新UI的。

ViewRootImpl.java
void checkThread() {
           if (mThread != Thread.currentThread()) {
              throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
             }
}
複製代碼

8.經常使用佈局測量流程

8.1 LinearLayout設置權重測量流程

詳細分析可參考https://toutiao.io/posts/08f9tz/preview

垂直佈局分析

設置了權重的View會被測量兩次,沒有隻會測量一次。(特殊狀況:若是子View的lp.weight>0且lp.height==0且LinearLayout設置了明確寬高的(mode==MeasureSpec.EXACTLY)狀況下子View也只會測量一次。)

1.LinearLayout中的第一個循環會遍歷全部的子View計算其高度並將高度進行累加。

  • 若是子View的lp.weight>0且lp.height==0且LinearLayout設置了明確寬高的(mode==MeasureSpec.EXACTLY)狀況下子View只會測量一次。

第一次測量完成後會根據LinearLayout總高度-累加高度算出剩餘高度,剩餘高度有多是負值,最後根據剩餘高度和總權重算出每一份權重的佔比。 2.第二個循環會對全部設置了權重weight的子View進行測量,並根據子View設置的權重值分配子View最終的高度。

結論:簡而言之就是第一次循環算出全部子View的高度和,而後用Linearlayout自身高度-已用高度算出剩餘高度並根據剩餘高度/總權重算出每一份權重的大小,第二次循環給設置了權重的View根據權重設置的值分配大小。

8.2 FrameLayout測量過程

FrameLayout只會測量一次,計算出全部子View的寬高以後,若是FrameLayout自身MeasureSpec.MODE=EXACTLY,那麼它最終寬高就是設置的值,若是是MeasureSpec.MODE=AT_MOST(wrap_content)的話那麼最終寬高會選取全部子View中的最大寬和最大高做爲最終寬高。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; i++) {
    final View child = getChildAt(i);
    if (mMeasureAllChildren || child.getVisibility() != GONE) {
        //子View測量自身寬高,由於Framelayout內部View可重疊放置因此當前可用寬高都傳的0    
        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        //記錄最大寬高
        maxWidth = Math.max(maxWidth,
                child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
        maxHeight = Math.max(maxHeight,
                child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        childState = combineMeasuredStates(childState, child.getMeasuredState());
        if (measureMatchParentChildren) {
            if (lp.width == LayoutParams.MATCH_PARENT ||
                    lp.height == LayoutParams.MATCH_PARENT) {
                mMatchParentChildren.add(child);
            }
        }
    }
}
   //修正最大寬高
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } //設置最終FrameLayou寬高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); } 複製代碼

8.3 RelativeLayout測量過程

在OnMeasure中會測量兩次子View,第一次水平方向根據水平方向規則(toLeft,toBottom等)測量獲取子View左右值(mLeft,mRight),高度可認爲設置爲最大值。第二次測量根據豎直方向的規則(Above,Bottom等)測量獲取子View上下值(mTop,mBottom)。

爲何須要測量兩次?

由於RelativeLayout子View以前既能夠是水平依賴也能夠是豎直依賴,因此水平豎直方向都須要去進行一次測量。 這裏須要注意的一點是在規則的處理上alignParentLeft的優先級是高於toLeft的。 詳情可見:www.jianshu.com/p/87bc61b8a…

相關文章
相關標籤/搜索