每一个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) {...}
}
前面提到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的显示大小的过程,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种:
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 = 160;
break;
case MeasureSpec.EXACTLY: //父View已经设置了子View的具体大小,子View无法再更改
break;
case MeasureSpec.AT_MOST: //父View指定了子View大小的上限,子View可以在该上限内任意设置
width = width / 2;
break;
}
...
//设置最终计算完的大小
setMeasuredDimension(width, height);
}
}
onMeasure
方法的两个参数widthMeasureSpec
、heightMeasureSpec
是父View为子View计算过的宽高,这两个参数的值是经过View.MeasureSpec
类封装过的,我们可以通过View.MeasureSpec.getMode
获得父View指定的测量模式,通过View.MeasureSpec.getSize
获得父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的显示位置的过程,布局过程是从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布局的时候都会通过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中上、下、左、右的位置。onLayout()
里调用所有子View的layout()
方法。View.requestLayout()
就可以进行一次View树的布局操作,新的布局操作会在下一次VSYNC中断到来时触发。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.setVisibility(int visibility)
可以设置View的可见性,可以传入的参数如下:
View.VISIBLE
View可见,正常显示View.INVISIBLE
View不可见,但是在进行布局时仍然会考虑,并占据一定区域View.GONE
View不可见,并且进行布局时也不会考虑,不占据认可区域