View工作原理之实现等分布局

Posted by Chejdj Blog on May 11, 2018

View的工作原理,也就是View的measure,layout,draw三个过程,为了巩固学习,写代码实现“等分布局”,下面就介绍“等分布局”的实现,不过之前,需要重新学习一下View的三个过程。

View大致工作流程

View的绘制流程,从RootView的performTraversals开始,经过measure,layout,draw三个过程才将View绘制而出。measure用于测量View的大小,layout用于定位View在父容器中的放置相对位置,draw方法则是将View绘制在屏幕上。
performTraversals依次调用performMeasure,performLayout和performDraw,他们就是完成三个过程,performMeasuer会调用measure方法,measure又调用onMeasure,onMeasuer对所有的子元素进行measure过程,子元素又调用onMeasure又测量所有的子元素,直到所有的View测量完。(layout和draw也是如此)

View的测量

View的测量分为两种,DecorView(顶级View)和普通View,DecorView的测量只能根据自己属性和窗口的大小决定,而普通的View则是根据自己的属性和父容器的大小决定(LayoutParams和父容器的MeasureSpec决定)。

MeasureSpec

在测量的过程中,MesureSpec决定了一个View的尺寸规格,它是View大小的一个决定性因数,系统就是根据View的MeasureSpec来决定一个View的测量大小。 它本身是一个32位int值,高2位代表SpecMode,低30位代表SpecSize。 SpecMode有3中模式:
(1) UNSPECIFIED : 父容器不对View有任何限制
(2) EXACTLY : View具有精确大小 (width=10dp height=10dp)
(3) AT_MOST : 不能超过父容器的大小
关于如何确定自己的MeasureSpec这里就不说了,主要看一下getChildMeasureSpec源码就行,有一个对应关系

Measure过程

一般来说,都是由ViewGroup开始,我们看ViewGroup方法没有measure方法,但是ViewGroup继承自View,看出来measure是final类型,不可以复写

1
2
3
4
5
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ...
  onMeasure(widthMeasureSpec, heightMeasureSpec);
  ...
}

调用onMeasure来最终确定自己的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }

...
如果View的没有设置背景就是 android:minWidth指定值,有背景就是他们之间的最大值,setMesauredDimension就是确定了自己测量值

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

.....

UNSPECIFIED模式就是 result值(无限制)  
其他就是messureSpec的值
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;
 }

上面几个步骤就最终完成了自己的测量,但是ViewGroup还需要对于child进行测量,measureChildren方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
            }
        }
}

循环调用子child
... 

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);
    }
过渡到了child的measure方法,其中getChildMeasureSpec就是根据child的layoutparams和父类的MesaureSpec来决定子View的MeasureSpec,然后又去调用onMeasure()方法

layout过程

Layout的作用是确定自己的位置,layout中又会调用onLayout方法去调用自己子类的layout方法去确定子类View的方法,onlayout是抽象方法需要自己实现

1
2
3
4
protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

child.layout(width*i,0,width*i+child.getMeasuredWidth(),child.getMeasuredHeight());

draw过程

Draw将View绘制到屏幕上面,绘制步骤
(1) 绘制背景 background.draw(canvas)
(2) 绘制自己 (onDraw)
(3) 绘制children(dispatchDraw)
(4) 绘制装饰(onDrawScrollBars)

如何实现等分布局呢??

首先要确定子View的大小,测量的时候确定View等分,在ViewGroup决定子View测量的是measurechild函数,默认match_parent和warp_content 子View的大小就是parentSzie我们修改一下传入等分的parentSpec,然后就是定位,修改onLayout方法,在里面传入等分坐标就能够实现等分 效果图如下:
图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class three_equal_layout  extends ViewGroup {
    public three_equal_layout(Context context) {
        super(context);
    }

    public three_equal_layout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public three_equal_layout(Context context, AttributeSet attrs) {
        super(context, attrs,0);
    }
    //修改measureChildren方法
  //获取MeasureSpec 使用makeMeasureSpec方法
    @Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        int count=getChildCount();
        Log.e("Layout","子view的数量为"+count);
        Log.e("Layout","测量的高度为"+MeasureSpec.getSize(heightMeasureSpec));
        widthMeasureSpec=MeasureSpec.makeMeasureSpec((MeasureSpec.getSize(widthMeasureSpec))/count,MeasureSpec.getMode(widthMeasureSpec));
        super.measureChildren(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec,heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count =getChildCount();
        Log.e("Layout","子view的数量为"+count);
        int width=getMeasuredWidth()/count;
           for(int i=0;i<count;i++){
               final View child=getChildAt(i);
               child.layout(width*i,0,width*i+child.getMeasuredWidth(),child.getMeasuredHeight());
           }
    }

}