《Android开发艺术探索笔记》Part 4、View的工作原理

View与ViewGroupAndroid中的View与我们以前理解的“视图”不同。在Android中,View比视图具有更广的含义,它包含了用户交互和显示,更像Windows操作系统中的window。ViewGroup是View的子类,所以它也具有View的特性,但它主要用来充当View的容器,将其中的View视作自己的孩子,对它的子View进行管理,当然它的孩子也可以是ViewGroup类型。ViewGroup和它的孩子们(View和ViewGroup)以树形结构形成了一个层次结构,View类有接受和处理消息的功能,android系统所产生的消息会在这些ViewGroup和
View之间传递。

界面永远离不开各种各样的控件,而这些控件,无论是TextView,Button,ImageView,甚至ListView等等,他们都有一个共同的基类,那就是View。但是,哪怕有了如此多的控件,有时候依旧满足不了我们设计师的胃口,时不时会冒出各种各样酷炫吊炸天的界面,这时候就需要我们自己去自定义View了。例如说,绘制一个圆形头像,绘制图片的加载进度条,或者实现上拉刷新下拉加载的操作等等,这些都是通过自定义View的实现。想要自定义View,那么首先就要先了解View:

1、ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

View的绘制流程是从ViewRoot和performTraversals开始的,它经过measure,layout,draw三个过程才能最终将一个view绘制出来,其中measure测量view的宽高,layout用来确定View在父容器中的放置位置,而draw则负责将view绘制在屏幕中。

measure--------->  [performMeasure -> measure -> onMeasure]
layout --------->  [performLayout  -> layout  -> onLayout ]
draw   --------->  [performDraw    -> draw    -> onDraw   ]

如图所示,performTraversals会依次调用performMeasure,performLayout和performDraw三个方法,这三个方法分别完成顶级view的measure,layout,draw三个流程,其中performMeasure会调用measure方法,measure中在调用onMeasure方法,在onMeasure中会对所有的子元素进行measure过程,这个时候measure流程会从父容器传递到子元素中,就完成了依次measure。接着子元素会重复父容器的measure过程,如此反复的完成对整个树的遍历。layout和draw同理。

measure过程决定了view的宽高,measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法获取view测量后的宽高。

layout过程决定了view的四个顶点的坐标和实际的view的宽高,完成之后,可以通过getTop,getBottom,getLeft,getRight来拿到View的四个顶点的位置,并可以通过getWidth和getHeight方法获取view的最终宽高。

draw过程则是决定了View的展示,只有draw方法完成之后,view的内容才能呈现到屏幕上。

DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的Linearlayout,在这个Linearlayout里面有上下两个部分,上面是标题,下面是内容。在Activity中通过setContentView所设置的布局文件,其实就是加载到内容之中。而内容栏的id为content,所以可以理解为将setContentView所设置的视图添加到了id为content的Framelayout中。可以通过findViewById(android.R.id.content)获取到内容栏的ViewGroup对象。

View绘制 和 ViewGroup绘制的区别

 

2、理解MeasureSpec

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode表示测量模式,SpecSize是指在某种测量模式下的规则大小。

SpecMode有三类,每一类都有特殊的含义:

UNSPECIFIED

父容器不对View有任何限制,要多大有多大,这种情况一般用于系统内部,表示一种测量的状态。

EXACTLY

父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的大小。它对应于LayoutParams中的match_parent和具体的数值这两种模式。

AT_MOST

父容器指定一个可用大小即SpecSize,
View的大小不能大于这个值,具体是什么值要看不同View的具体情况,它对应于LayoutParams的Wrap_content。

系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后在根据这个MeasureSpec来确定View测量后的宽高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。另外,对于顶级View(即DecorView)和普通View来说,MeasureSpec的转换过程不同。对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器和自身的LayoutParams来共同决定。MeasureSpec一旦确定,onMeasure中就可以确定其View的宽高。

当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精准模式(EXACTLY),并且其大小遵循LayoutParams的大小。当View的宽高是match_parenmt时,如果父容器的模式是精准模式(exactly),那么View也是精准模式并且大小是父容器的剩余空间;如果父容器是最大模式(at_most),那么view也是最大模式,并且其大小不会超过父容器的剩余空间。当view的宽高是wrap_content时,不管父容器的模式是最大模式还是精准模式,View的模式都是最大化并且大小不能超过父容器的剩余空间。

 View ViewGroupMeasure onMeasure onMeasureLayout onLayoutDraw onDraw 

一、位置,尺寸:

3、View的工作流程

View的工作流程主要指Measure、Layout、draw这三大流程。

当继承一个ViewGroup的时候 必须重写一个方法 onLayout 传入参数

对于Android系统中的每一个View都会在界面中占据一块矩形的区域,自然也就包括left,top,right,bottom四个属性,我们可以使用相应的get方法进行获取,具体几个方法如下:

3-1、measure过程

View的Measure流程

View的Measure过程是由其measure方法来完成,measure方法是一个final类型,不可重写。在View的measure方法中回去调用View的onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

在onMeasure中setMeasuredDimension方法是设置回传测量获得的宽高值。在onMeasure中测量宽高调用了getDefaultSize(int
size, int measureSpec),

getSuggestedMinimumWidth
方法可以看出,如果View没有设置背景的话,那么宽度就是mMinWidth,如果设置了背景的话,则取背景Drawable的原始宽度和mMinWidth的大的值。

ViewGroup的Measure流程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历取去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup中没有重写onMeasure方法,他提供了一个measureChildren的方法。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到measureChildren方法会对每一个子元素进行遍历进行measureChild,
而在measureChild中调用了getChildMeasureSpec方法获取MeasureSpec,之后每个子元素调用自身的measure方法计算宽高。在ViewGroup中没有定义测量的具体过程,它本身是一个抽象类,其测量过程“onMeasure“`需要各个子类去具体实现。

如果进行过android开发应该知道,我们在onCreate,onStart和onResume这些生命周期中,想要获取到view或者ViewGroup的宽高,得到的都是0。下面列出来几种获取方法。

  • 1、Activity/View#onWindowFocusChanged

当Activity的窗口得到焦点或者是去焦点都会调用,具体来说当Activity继续执行或者暂停执行时,onWindowFocusChanged都会执行。典型代码:

public void onWindowFocusChanged(boolean hasFocus){
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int width = view.getMeasureWidth();
        int height = view.getMeasureHeight();
    }
}
  • 2、View.post(runnable);

通过post可以将一个runnable投递到消息队列的尾部,然后等待Loop调用次runnable的时候,View已经初始化好了,典型代码:

protected void onStart(){
    super.onStart();
    view.post(Void->{
        int width = view.getMeasureWidth();
        int height = view.getMeasureHeight();
    });
}
  • 3、ViewTreeObserver

OnGlobalLayoutListener,在View树的状态发生变化或者View树内部的View可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取View的宽/高一个很好的时机。典型代码:

protected void onStart(){
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
        @override
        public void onGlobalLayout(){
            view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}
  • 4、view.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对view进行measure来获取view的宽高。

match_parent

如果是这种方式,是不能获取到具体的宽高的。

具体的值

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);

wrap_content

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) -1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) -1, MeasureSpec.AT_MOST)
view.measure(widthMeasureSpec, heightMeasureSpec);

(boolean changed, int l, int t, int r, int b)含义

getLeft():获取view的left边相对于父view的距离,左上角的横坐标。

3-2、layout流程

Layout的作用是确定ViewGroup的位置,当ViewGroup的位置确认之后,它在onLayout中遍历所有的子元素并调用其Layout方法,在layout方法中,onLayout方法又会被调用。

layout的大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft,mRight,mTop,mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置。

view跟父容器的距离 l 左边距离父容器左边的距离 t
view的上边距离父容器上边的距离

getTop():获取view的top边相对于父View的距离,左上角的纵坐标。

3-3、draw流程

Draw过程作用就是讲View绘制到屏幕上面,View的绘制过程遵循如下几步:

  • 1、绘制背景 background.draw(canvas)
  • 2、绘制自己 onDraw
  • 3、绘制children dispatchDraw
  • 4、绘制装饰 onDrawScrollBars

View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递下去了。

view有一个特殊的方法setWillNotDraw。如果一个view不需要绘制任何内容,那么设置这个标记为true,系统会自动做相应的优化。默认情况下View是没有这个优化标志的。在ViewGroup中,如果我们需要调用onDraw绘制内容的话,可以关闭这个优化标志位即可。

r view的右边距离父容器左边的距离 b view的下边距离父容器上边的距离

getRight():获取view的right边相对于父View的距离,右下角的横坐标。

4、自定义View

从写了这个onLayout之后 需要调用它的每一个子view的layout方法
为这个子View分配一个现实的空间 如果不调用子view的layout方法

getBottom():获取view的bottom边相对于父View的距离,右下角的纵坐标。

4-1、自定义view的分类

  • 1、继承View重写onDraw方法
  • 2、继承ViewGroup派生特殊的Layout
  • 3、继承特定的View(比如TextView)
  • 4、继承特定的ViewGroup(比如Linearlayout)

这个子View在Viewgroup中没有显示

而view的尺寸是以宽度和高度来表达的,事实上一个view拥有两组宽和高的值。一组是measured
width和measured
height,可以使用getMeasuredWidth()和getMeasuredHeight()来获取,这组尺寸指的是view想要在父布局内是多大。第二组尺寸是width和height,这组尺寸定义了view在屏幕上绘制时候的实际尺寸,可以使用getWidth()和getHeight()方法获取,两组尺寸大多数情况下一样。两组尺寸大多数情况下一样,那么时候不一样呢?等到接下来再说。为了测量尺寸,view通常需要将padding也要考虑进去,如果有必要的话,其实在自定义view的onDraw()方法里也应该处理padding,不然padding是无法起到任何作用的。而margin则是只有我们自定义ViewGroup的时候才会去考虑。

4-2、自定义view须知

  • 1、让view支持wrap_content
  • 2、如果有必要,让你的view支持padding
  • 3、尽量不要在view中使用handler,没必要
  • 4、view中如果有线程或者动画,需要及时停止,参考View#onDetchedFromWindow
  • 5、View带有滑动嵌套情形时,需要处理好滑动冲突

只有当子View的layout方法调用之后 才可以通过view.getHeight 和
view.getWidth获取这个view的宽高

二、view的绘制过程:

这个宽高 是父容器在layout阶段设置的值

view的绘制流程依次是measure过程,layout过程和draw过程。其实我们稍微一想也就知道这个大概思路了,得首先进行measure测量过程,知道了view的宽度和高度;之后layout布局过程,由父布局安排view的位置;最后进行draw过程,将view绘制到屏幕上。

如果在layout阶段之前想获取view的测量高度和宽度

1、measure过程

需要主动调用view.measure方法 view.measure

view的measure是通过调用measure()这个方法来实现的,当测量过程结束,measure()方法返回之后,就可以获取measuredWidth和measuredHeight了,通过getMeasuredWidth()和getMeasuredHeight()来获取。

测完之后 可以获取到 测量高度和测量宽度

 

view.getMeasuredHeight()

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

view.getMeasuredWidth();

而measure()方法,很明显是个final方法,这代表子类不能重写这个方法,而在measure()方法内部,则会去调用onMeasure()方法,确切的测量工作也都是在onMeasure()这个方法里执行的。而我们通常自定义View的时候,需要重写的也就是onMeasure()方法。

自定义一个MyViewGroup extends ViewGroup:

 

package test.pgl.com.a_onlayout;import android.content.Context;import android.util.AttributeSet;import android.view.ViewGroup;/** * Created by Administrator on 2017/5/28. */public class MyViewGroup extends ViewGroup { public MyViewGroup(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { }}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

布局代码:

我们在onMeasure()方法中可以看到widthMeasureSpec和heightMeasureSpec这两个参数,也是measure()方法传递进来的。这里就不得不提MeasureSpec,虽然已经有很多博客仔细研究过,可能你们都厌烦了,可是它确实不可或缺,我还是要在这里好好说一遍。MeasureSpec是一个32的int值,高2位代表的SpecMode,低30位代表的是SpecSize。SpecMode有以下三种:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android: android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="vertical" tools:context="test.pgl.com.a_onlayout.MainActivity"> <TextView android:layout_width="wrap_content" android:textSize="20dp" android:layout_height="wrap_content" android:text="Hello World!" /> <test.pgl.com.a_onlayout.MyViewGroup android:background="#999" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:textColor="#ff00" android:textSize="20dp" android:layout_height="wrap_content" android:text="Hello --World!" /> </test.pgl.com.a_onlayout.MyViewGroup></LinearLayout>

 

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图