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
分析的邏輯,這裡肯定是一個線程,一般就是應用的主線程。後面重新整理View的時候需要注意必須從主線程重新整理View。addView()
-
裡還調用了requestLayout()
,scheduleTraversals()
裡主要通過scheduleTraversals()
定時了一個Choreographer
任務。mTraversalRunnable
-
是基于VSNC實作的一個控制類,VSNC的主要原理是每隔一個固定的時間(一般為16ms,保證每秒60幀的重新整理率)設定一個高優先級中斷,在中斷的時候處理各種有序任務,這樣所有的任務就可以按照固定的頻率進行處理。VSNC可以用來進行控制界面重新整理、動畫、輸入事件處理,使用VSNC可以使界面顯示更加平滑、流暢。Choreographer
-
就是将一個Choreographer.postCallback()
任務添加到有序任務隊列裡,當下次VSNC中斷到來時執行任務隊列裡的所有任務,在這裡是Runnable
。TraversalRunnable
-
将每次的重新整理任務封裝到TraversalRunnable裡,每次重新整理任務執行的時候調用一次ViewRootImpl
,并在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()
為視窗建立Surface,後面該View樹所有的内容都會繪制在這個Surface上。WindowManagerService
-
從根View開始測量View樹中每個View的大小。performMeasure
-
對View樹進行布局,确認父View裡每個子View的位置。performLayout
-
繪畫View樹裡的所有View。performDraw
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寬度的效果
測量大小
上面介紹的是在布局資源中設定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的内邊距與外邊距區域示意圖:
-
内邊距
内邊距為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>
-
可以設定子View按從水準方向或者垂直方向進行順序布局LinearLayout
-
可以讓子View設定停靠在父View中的任意位置,或者與相對其他子View進行布局RelativeLayout
- 其他系統提供的布局類如
、GridLayout
等也可以在xml檔案裡進行布局配置ListView
通知View布局位置
每個子View被父View布局的時候都會通過
onLayout()
方法收到布局的結果
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
...
}
-
參數表示本次布局相對上一次布局有沒有變化,如果是第一次布局,這個值就是boolean changed
。true
-
幾個參數表示該View在父View中上、下、左、右的位置。int left, int top, int right, int bottom
- 如果該View是一個ViewGroup,需要在自己的
裡調用所有子View的onLayout()
方法。layout()
- 如果View顯示完成後受到事件觸發,需要重新調整布局,調用一次
就可以進行一次View樹的布局操作,新的布局操作會在下一次VSYNC中斷到來時觸發。View.requestLayout()
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可見,正常顯示View.VISIBLE
-
View不可見,但是在進行布局時仍然會考慮,并占據一定區域View.INVISIBLE
-
View不可見,并且進行布局時也不會考慮,不占據認可區域View.GONE