天天看点

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不可见,并且进行布局时也不会考虑,不占据认可区域