天天看點

Android View 系統 3 - View的繪畫顯示View的顯示View樹的第一次重新整理View 的測量View 的繪畫設定View的可見性

View的顯示

每一個View的顯示都要經曆三個過程:測量(Measure)、布局(Layout)、繪制(Draw)。這三個過程的執行時機就是由前面提到的

ViewRootImpl

來控制的,同時每個繼承自

View

的子類都可以繼承下面三個方法來重寫這三個流程,實作自己的顯示内容:

class MyView extends View {
    ...
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...}

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {...}

    @Override
    protected void onDraw(Canvas canvas) {...}
}
           

View樹的第一次重新整理

前面提到

WindowManagerGlobal.addView()

的實作裡為每個View樹建立了一個

ViewRootImpl

,并且最後調用了

ViewRootImpl.setView()

将根View以及視窗配置參數傳遞給了

ViewRootImpl

,而

ViewRootImpl.setView()

的實作裡就觸發了View樹的第一次重新整理:

frameworks/base/core/java/android/view/ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            requestLayout();
    ...
            res = mWindowSession.addToDisplay(...;
    ...
}

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

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

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        ...
        performTraversals();
        ...
    }
}
           
  • setView()

    裡會調用

    requestLayout()

    請求一次視窗布局,同時還調用了

    mWindowSession.addToDisplay

    請求

    WindowManagerService

    建立底層管理的視窗

    WindowState

  • requestLayout()

    裡會先通過

    checkThread()

    确認執行重新整理的線程與建立

    ViewRootImpl

    的線程一緻。從

    addView()

    分析的邏輯,這裡肯定是一個線程,一般就是應用的主線程。後面重新整理View的時候需要注意必須從主線程重新整理View。
  • requestLayout()

    裡還調用了

    scheduleTraversals()

    scheduleTraversals()

    裡主要通過

    Choreographer

    定時了一個

    mTraversalRunnable

    任務。
  • Choreographer

    是基于VSNC實作的一個控制類,VSNC的主要原理是每隔一個固定的時間(一般為16ms,保證每秒60幀的重新整理率)設定一個高優先級中斷,在中斷的時候處理各種有序任務,這樣所有的任務就可以按照固定的頻率進行處理。VSNC可以用來進行控制界面重新整理、動畫、輸入事件處理,使用VSNC可以使界面顯示更加平滑、流暢。
  • Choreographer.postCallback()

    就是将一個

    Runnable

    任務添加到有序任務隊列裡,當下次VSNC中斷到來時執行任務隊列裡的所有任務,在這裡是

    TraversalRunnable

  • ViewRootImpl

    将每次的重新整理任務封裝到TraversalRunnable裡,每次重新整理任務執行的時候調用一次

    doTraversal()

    ,并在

    doTraversal()

    裡調用

    performTraversals()

    執行真正的組織重新整理操作。
frameworks/base/core/java/android/view/ViewRootImpl.java

private void performTraversals() {
    ...
    if (mFirst || ...) {
        ...
        //第一次重新整理請求視窗布局
        relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
        ...
    }
    ...
    //執行測量操作
    performMeasure(...);
    ...
    //執行布局操作
    performLayout(...);
    ...
    //執行繪畫操作
    performDraw();    
}

private int relayoutWindow(...) throws RemoteException {
    ...
    //通過WindowSession将請求傳遞給WindowManagerService
    int relayoutResult = mWindowSession.relayout(...);
}
           
  • relayoutWindow

    如果是第一次請求重新整理,會先通過

    relayoutWindow()

    請求

    WindowManagerService

    為視窗建立Surface,後面該View樹所有的内容都會繪制在這個Surface上。
  • performMeasure

    從根View開始測量View樹中每個View的大小。
  • performLayout

    對View樹進行布局,确認父View裡每個子View的位置。
  • performDraw

    繪畫View樹裡的所有View。

View 的測量

View 的測量過程就是計算View的顯示大小的過程,

ViewRootImpl.performMeasure()

就是從根View開始,對View樹中的每個View進行測量。

設定大小

在布局檔案中每個View可以通過

layout_width

layout_height

兩個屬性指定View的大小:

<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="這是match_parent寬度" />

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="這是wrap_content寬度" />

<Button
    android:layout_width="90dp"
    android:layout_height="wrap_content"
    android:text="這是90dp寬度" />
           

layout_width

layout_height

兩個屬性的值可以為3種:

  • match_parent/fill_parent: 大小為父View允許的最大值(fill_parent 為 Android2.3 之前使用)
  • wrap_content: 大小為該View實際需要的大小
  • 固定大小: 大小固定為某個具體值,可以使用的機關有

    dp/dip

    px

    pt

    in

    mm

    機關參考

如下為使用上面三種類型指定View寬度的效果

Android View 系統 3 - View的繪畫顯示View的顯示View樹的第一次重新整理View 的測量View 的繪畫設定View的可見性

測量大小

上面介紹的是在布局資源中設定View 的大小,但是View隻有在經過測量過程才能夠确定最終的顯示大小。父View在測量一個子View的大小時,會調用子View的

onMeasure

方法,子View可以重寫這個方法實作自己的測量計算:

class MyView extends View {

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 擷取測量模式
        int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
        int width = View.MeasureSpec.getSize(widthMeasureSpec);
        int height = View.MeasureSpec.getSize(heightMeasureSpec);
        switch (widthMode) {
            case MeasureSpec.UNSPECIFIED: //父View未指定大小,子View可以設定任意大小
                width = ;
                break;
            case MeasureSpec.EXACTLY: //父View已經設定了子View的具體大小,子View無法再更改
                break;
            case MeasureSpec.AT_MOST: //父View指定了子View大小的上限,子View可以在該上限内任意設定
                width = width / ;
                break;
        }
        ...
        //設定最終計算完的大小
        setMeasuredDimension(width, height);
    }
}
           

onMeasure

方法的兩個參數

widthMeasureSpec

heightMeasureSpec

是父View為子View計算過的寬高,這兩個參數的值是經過

View.MeasureSpec

類封裝過的,我們可以通過

View.MeasureSpec.getMode

獲得父View指定的測量模式,通過

View.MeasureSpec.getSize

獲得父View計算的測量大小。測量模式有如下三種:

  • MeasureSpec.UNSPECIFIED 父View未指定大小,子View可以設定任意大小
  • MeasureSpec.EXACTLY 父View已經設定了子View的具體大小,子View無法再更改
  • MeasureSpec.AT_MOST 父View指定了子View大小的上限,子View可以在該上限内任意設定

最後要記得調用

View.setMeasuredDimension

設定最終計算完的View大小。

View測量的大小可以通過

View.getMeasuredWidth()

View.getMeasuredHeight()

獲得:

int measuredWidth = view.getMeasuredWidth();
int measuredHeight = view.getMeasuredHeight();
           

内邊距與外邊距

上面介紹的是View測量後的大小,但這并不是一個View會占據的最終大小,還需要考慮上View的内邊距。如下為View的内邊距與外邊距區域示意圖:

Android View 系統 3 - View的繪畫顯示View的顯示View樹的第一次重新整理View 的測量View 的繪畫設定View的可見性
  • 内邊距

    内邊距為View顯示主體内容(如:

    TextView

    的文本内容、

    ImageView

    的圖檔内容、

    ViewGroup

    等View容器的子View等)時在上、下、左、右四個邊上縮進的距離。

    View在測量時隻會根據自己所要顯示的主體内容所需要的大小進行測量,得出的大小一般稱為測量尺寸。測量尺寸通過

    View.getMeasuredWidth()

    View.getMeasuredHeight()

    獲得。

    父View在子View測量完後還需要加上子View設定的内邊距,得到該子View的繪制尺寸。測量尺寸可以通過

    View.getWidth()

    View.getHeight()

    獲得。
  • 外邊距

    外邊距為View在ViewGroup中布局時與其他View的最小間隔距離,在View自身測量、繪制時不會考慮,隻有在父View中進行布局時才會考慮。

在布局資源檔案裡可以對View設定内邊距和外邊距:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/red"
    android:padding="10dp"
    android:layout_margin="10dp"/>
           

注意: 通過

android:background

屬性給View設定背景時,該背景會覆寫包含内邊距在内的繪制區域,不會覆寫外邊距的區域。

View 的布局

View的布局就是父View确定每個子View的顯示位置的過程,布局過程是從

ViewRootImpl.performLayout()

開始的,從根View開始請求View樹中的每個ViewGroup進行布局操作。

設定布局位置

Android系統提供的

LinearLayout

RelativeLayout

等布局類,可以xml配置檔案裡就可以進行布局配置:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Text1"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Text2"/>
</LinearLayout>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="Text3"/>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:text="Text4"/>
</RelativeLayout>
           
  • LinearLayout

    可以設定子View按從水準方向或者垂直方向進行順序布局
  • RelativeLayout

    可以讓子View設定停靠在父View中的任意位置,或者與相對其他子View進行布局
  • 其他系統提供的布局類如

    GridLayout

    ListView

    等也可以在xml檔案裡進行布局配置

通知View布局位置

每個子View被父View布局的時候都會通過

onLayout()

方法收到布局的結果

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    ...
}
           
  • boolean changed

    參數表示本次布局相對上一次布局有沒有變化,如果是第一次布局,這個值就是

    true

  • int left, int top, int right, int bottom

    幾個參數表示該View在父View中上、下、左、右的位置。
  • 如果該View是一個ViewGroup,需要在自己的

    onLayout()

    裡調用所有子View的

    layout()

    方法。
  • 如果View顯示完成後受到事件觸發,需要重新調整布局,調用一次

    View.requestLayout()

    就可以進行一次View樹的布局操作,新的布局操作會在下一次VSYNC中斷到來時觸發。

View 的繪畫

View 的繪畫同樣也是從

ViewRootImpl.performDraw()

開始的,從根View開始繪制View樹中的每個子View。每個View都需要繼承

View.onDraw()

方法來實作自己的繪畫操作。Canvas提供了繪畫線條、文字、圖檔等的接口。

class MyView extends View {

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawLine(x, y);
        canvas.drawText(str);
        canvas.drawBitmapMesh(mBitmap);
    }
}
           

當View狀态發生變化需要重新繪畫時,可以調用

View.invalidate()

方法觸發一次繪畫操作,下次VSYNC到來時這個View的

onDraw()

方法就會調用。

設定View的可見性

通過

View.setVisibility(int visibility)

可以設定View的可見性,可以傳入的參數如下:

  • View.VISIBLE

    View可見,正常顯示
  • View.INVISIBLE

    View不可見,但是在進行布局時仍然會考慮,并占據一定區域
  • View.GONE

    View不可見,并且進行布局時也不會考慮,不占據認可區域