Android View 系统 3

来源:互联网 发布:hr工资计算软件 编辑:程序博客网 时间:2024/06/06 15:03

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.javapublic 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.javaprivate 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_widthlayout_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_widthlayout_height两个属性的值可以为3种:

  • match_parent/fill_parent: 大小为父View允许的最大值(fill_parent 为 Android2.3 之前使用)
  • wrap_content: 大小为该View实际需要的大小
  • 固定大小: 大小固定为某个具体值,可以使用的单位有 dp/dippxptinmm单位参考

如下为使用上面三种类型指定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方法的两个参数widthMeasureSpecheightMeasureSpec 是父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系统提供的LinearLayoutRelativeLayout等布局类,可以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进行布局
  • 其他系统提供的布局类如GridLayoutListView等也可以在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不可见,并且进行布局时也不会考虑,不占据认可区域
原创粉丝点击