天天看点

自定义View、ViewGroup(温习&实战&笔记)基本的布局参数

自定义View、ViewGroup的最基本的支持要点

  • 自定义View - onMeasure、onDraw
  1. wrap_content;
  2. padding;
  • 自定义ViewGroup - onMeasure、onLayout
  1. wrap_content;
  2. padding;
  3. margin;

为啥View只需支持padding,而ViewGroup需要支持padding和margin?

这里注意了,margin属于父子View之间的布局关系,是用于父容器计算内部子View的显示位置(onLayout)的,为啥padding不需要父容器干涉?padding是View内部需要支持的属性,不属于父子View之间的布局关系。

自定义View、ViewGroup(温习&实战&笔记)基本的布局参数
  • 红色框线 - 父容器
  • 蓝色框线 - 子View的显示区域
  • 绿色框线 - 子View的绘制区域
  • A、B、C、D -  paddingTop、paddingLeft、paddingRight 、paddingBottom
  • E - 子View的绘制区域
  • 1、2、3、4 - marginTop、marginLeft、marginBottom、marginRight

其中,子View的显示区域 = 子View的绘制区域 (E)+ Padding(A、B、C、D),这才是子View实际的地盘。

而margin是父容器的地盘,有一句话叫各扫门前雪,所以子View处理好自己的Padding就好了。ViewGroup也是一个View(ps:可以包括子View的容器View),因此需要处理Padding,又因为它是一个容器,需要计算子View(在父容器中)的显示位置,这就需要将margin考虑在内了,因此必须处理margin。

自定义View、ViewGroup(温习&实战&笔记)基本的布局参数

父View的大小 = 子View大小总和 + 父View的padding总和 + 子View的margin总和;

那么wrap_content为啥也要处理?后面说。

自定义View

自定义一个View,只需处理测量(onMeasure)和重绘(onDraw)即可。

可以发现自定义View和自定义ViewGroup需要重写的方法是不同的,除了都需要重写onMeasure之外:

  • 自定义View只需额外重写onDraw即可
  • 自定义ViewGroup只需额外重写onLayout即可

于是就有了下面的对话:

父容器:你们(Views)在lz的地盘摆摊,摊位的位置老子定(onLayout),你们说了不算;

子View:好的,这个摊位咋个摆放货品,lz说的算(onDraw),你(父容器)管不着了;

父容器:好啊,但是这个摊位面积问题,咱俩得商量一下撒?不然我咋个知道!lz定完了(onLayout),会告诉你们。之后lz就不管了!

子View:可以啊,听起来你(父容器)很叼的样子!

其实,说白了,还是地盘问题~

看过View源码的同学都知道,View的onLayout是空实现,为啥呢?因为View不是容器,所以没有子View,自然onLayout是需要实现的。那么一个View到底要绘制到屏幕的位置呢?不用我们管么?确实,一般这块是不需要我们处理的。View的layout方法已经为我们实现好了。

public void layout(int l, int t, int r, int b) {
	......

    // 关键方法:setOpticalFrame 、 setFrame
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
	......
 
    }
           

 至于onLayout的四个参数,表示View在屏幕中的位置(左上右下),这四个参数的值是父容器的事情,由父容器计算子View该在屏幕的哪个位置显示,计算完成然后告诉子View(childView.layout(l,t,r,b)),然后子View就乖乖的在这个区域进行onDraw了。

  • onMeasure

注意:此处可以解答为啥要支持wrap_content

-----------View的onMeasure处理逻辑----------
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
           

正常情况下,如果我们自定义View或者ViewGroup不重写onMeasure方法的话,则会调用View的onMeasure方法。注意,在View或者ViewGroup中,测量完成都是通过setMeasuredDimension保存宽高尺寸的。

可以看到,在默认的处理逻辑中,View是通过getDefaultSize来计算宽高的。

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

这个方法是系统给我们的兜底尺寸,mMinWidth和mMinHeight没记错的话应该都是100px。继续向下看。

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;
    }
           

可以发现,系统默认的处理逻辑中,对于MATCH_PARENT和WRAP_CONTENT的处理是一毛一样的,这也是我们为啥不重写onMeasure方法,给View设置宽高属性值为wrap_content无效的原因。

so,自定义View和ViewGroup的要点说明就到此为止了。

MeasureSpec的生成规则

对于测量过程,这是一个三方协商的过程:开发者、父容器、子View。

开发者:我在XML文件设置好了我想要的尺寸,你俩(父容器和子View)看着办! -- 对应XML布局文件的各个ViewGroup和View的布局参数设置完毕;

父容器:对着各个子View说(measureChild Or measureChildren Or measureChildrenWithMargin):你们先开干吧,等你们都干完了,我的工作才能开始,同时将测量规格(测量规格可以理解为约束)依次告诉每个子View(MeasureSpec)。

子View:子View拿到测量规格,结合自己的实际需要尺寸,得出最终需要的尺寸,并通过setMeasuredDimension保存了起来;

父容器:既然你们都测量完了,我也就能通过你们各自的getMeasuredWidth和getMeasuredHeight获取测量的结果了,那轮到我开干了,我需要计算出我想要的尺寸(padding + Margin + 各个子View的高之和 以及 子View的最大宽度),现在我想要的尺寸我拿到手了,但是这就是最终尺寸么?很显然不是。为啥?老子也是有测量约束的人,想任性也不允许呀!最终,父容器会根据自己想要的尺寸和测量约束,得到最终的尺寸。

测量这块主要分两个部分需要掌握:

  • 测量规格的生成规则(这个系统已经帮我们办好了,我们需要做的是掌握该生成规则)

    MeasureSpec = View自身的LayoutParams + 父容器的MeasureSpec;

自定义View、ViewGroup(温习&实战&笔记)基本的布局参数

MeasureSpec的生成规则如下:

自定义View、ViewGroup(温习&实战&笔记)基本的布局参数
自定义View、ViewGroup(温习&实战&笔记)基本的布局参数

最终测量尺寸的生成逻辑

这里以宽度为例,View只是显示一个图片。

private int getWidthSize(int widthMeasureSpec) {
        int resultSize = 0;
        int mode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if (mode == MeasureSpec.EXACTLY) {// 精确测量,即ImgView的宽度是Match_Parent或者某个具体的测量值
            resultSize = size;
        } else {// wrap_content
            int wantWidth= bitmapWidth + getPaddingLeft() + getPaddingRight();
            // 实际所需需结合测量规格来算
            // -> 你的所需是有约束的,不是想要多少就是多少!凶残!
            resultSize = Math.min(wantWidth, size);
        }
        return resultSize;
    }
           

 绘制

可绘制的区域大小就是上面测量(onMeasure)给出的区域大小了!!

@Override
    protected void onDraw(Canvas canvas) {
        // FIXME 这里示范了一种错误的写法!!!
        // 原本的意思是想将图片绘制在屏幕的(sw / 2 - bitmap.getWidth() / 2 + getPaddingLeft(), sh / 2 - bitmap.getHeight() / 2 + getPaddingTop())位置
        // 但是忽略了一点:ImgView的大小并不是屏幕的大小,如果是屏幕的大小,这么绘制是没有问题的,但是ImgView的大小仅仅只是onMeasure中的测量的宽高那么大。
        // 因此,只需管好自己的一亩三分地就好了
        // canvas.drawBitmap(bitmap, sw / 2 - bitmap.getWidth() / 2 + getPaddingLeft(), sh / 2 - bitmap.getHeight() / 2 + getPaddingTop(), bitmapPaint);
        canvas.drawBitmap(bitmap, getPaddingLeft(), getPaddingTop(), bitmapPaint);
    }
           

自定义ViewGroup

自定义一个ViewGroup,只需处理测量(onMeasure)和布局(onLayout)即可。

自定义ViewGroup相对于单纯的自定义一个View,要复杂多了。除了完成自定义View的关于wrap_content、padding的工作外,还需要对margin进行支持。

测量

上面我们曾说过View的测量大小是由View和其父容器共同决定的,但是上述源码的分析中我们其实并没有体现,因为它们都在ViewGroup中,这里我们就要涉及ViewGroup中与测量相关的另外几个方法:measureChildren、measureChild和measureChildWithMargins还有getChildMeasureSpec,见名知意这几个方法都跟ViewGroup测量子元素有关,其中measureChildWithMargins和measureChildren类似只是加入了对Margins外边距的处理,ViewGroup提供对子元素测量的方法从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);
        }
    }
}
           

继续看measureChild

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    // 获取子元素的布局参数
    final LayoutParams lp = child.getLayoutParams();
 
    // 依据我们之前总结的测量规格的生成规则表格,生成子View的测量规格
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);
 
    // 告诉子View:测量规格给你了,可以开始测量你自己了
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
           

 子View的measure方法内部会调用我们重写的onMeasure方法,此处分析可以看上面第一部分的啦。

好了,子View测量工作完美的完成了,接下来就看父容器自己的啦!

这里我们自定义的ViewGroup是垂直分布的,类似Vertical LinearLayout - 为了支持margin,这里并没有直接采用measureChildren,而是采用了如下的逻辑:

{
        int childCount = getChildCount();
        if (childCount == 0) {
            return;
        }
        int parentDesiredWidth = 0;
        int parentDesiredHeight = 0;
        // 计算一个ViewGroup想要的尺寸
        for (int i = 0; i < childCount; i++) {
            // 额外考虑Margin
            View child = getChildAt(i);
            VerticalLayoutParams clp = (VerticalLayoutParams) child.getLayoutParams();
            // taking into account both the MeasureSpec requirements for this view and its padding and margins
            // ## The child must have MarginLayoutParams ##
            // 子View可以得到的最大规格尺寸是:specSize - parentPadding - 子View想要的margin
            // 如果采用measureChild,则子View可以得到的最大规格尺寸是:specSize - parentPadding
            // 可以看到:measureChildWithMargins与measureChild区别在于,前者在测量子View的时候,将Margin额外考虑进去了(padding的话,两者都有处理)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // 计算父容器尺寸
            parentDesiredWidth = Math.max(child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin, parentDesiredWidth);
            parentDesiredHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
        }
        // 别忘记了父容器自己的padding,这里为啥不考虑margin?margin的处理是父容器的事情,即ViewGroup也有自己的父容器,ViewGroup只处理自己的子View的margin就好了
        // 自己的margin是交给自己的父容器去处理的。
        parentDesiredWidth += getPaddingLeft() + getPaddingRight();
        parentDesiredHeight += getPaddingTop() + getPaddingBottom();
        // 得到想要的尺寸之后,需再结合widthMeasureSpec 或者 heightMeasureSpec,得出最终的尺寸
        // 方式1
        // setMeasuredDimension(getWidth(resultWidthSize, widthMeasureSpec), getHeight(resultHeightSize, heightMeasureSpec));
        // 方式2
        setMeasuredDimension(resolveSize(parentDesiredWidth, widthMeasureSpec), resolveSize(parentDesiredHeight, heightMeasureSpec));
    }
           

 建议使用方式2,为啥?系统已经给轮子了,我们不需要重新造轮子。

这里再看一下measureChild和measureChildWithMargins的区别!这两个方法都是父容器测量子View的方法,可以看出区别的是如下这个方法:(这两个方法内部在传递给子View的规则前,都调用了如下方法)

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);// 子View可得的最大规格
        ......
}
           

其实区别就一句话:告诉子View规格的时候,是否扣除了margin!!(padding两者都扣除了)

扣除后的才是最终给你的!!

与普通的自定义View相比,自定义ViewGroup除了需要完成自定义View的工作外,有如下两点不同:

  • 父容器的尺寸计算必须等到各个子View测量完成才能开始的呀,而且父容器的尺寸是各个子View的尺寸之和;
  • 需要额外考虑Margin,看看上面的VerticalLayoutParams是个什么鬼?

基本的布局参数

ViewGroup

有两个最基本的布局参数类,

ViewGroup.LayoutParams

ViewGroup.MarginLayoutParams

所有的自定义

ViewGroup

的布局参数类必须要直接或者间接继承自

ViewGroup.LayoutParams

类,因为你需要支持

layout_width

layout_height

布局属性。

如果你想要让自定义

ViewGroup

的布局参数属性支持

margin

,例如

layout_marginLeft

,那么自定义

ViewGroup

的布局参数类需要直接或者间接继承自

ViewGroup.MarginLayoutParams

,因为它有解析

margin

属性的功能。

至于为啥重写这些方法,详见:https://juejin.im/post/5dc1225c5188255f7b0fa42d#heading-6
这篇博文的分析,很详细
----------------VerticalViewGroup中重写如下方法 -----------------------  
注意:如果不需要在LayoutParams处理自己的逻辑,单纯地计算margins就没必要去实现一个自定义的MarginLayoutParams子类了,可以直接返回一个MarginLayoutParams的实例对象 
-----> 此处先已MarginLayoutParams子类为例子而已
    @Override
    protected VerticalLayoutParams generateDefaultLayoutParams() {
        return new VerticalLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    }
 
    @Override
    protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
        return new VerticalLayoutParams(p);
    }

 
    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new VerticalLayoutParams(getContext(), attrs);
    }

 
    @Override
    protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
        return p instanceof VerticalLayoutParams;
    }
           

 自定义一个ViewGroup.MarginLayoutParams的子类

// 用于VerticalViewGroup测量时,获取子View的Margin
public class VerticalLayoutParams extends ViewGroup.MarginLayoutParams {
    public VerticalLayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public VerticalLayoutParams(int width, int height) {
        super(width, height);
    }

    public VerticalLayoutParams(ViewGroup.MarginLayoutParams source) {
        super(source);
    }

    public VerticalLayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
           

注意:如果你没有额外添加对Margin的处理的话,却又想获取Margin(直接将LayoutParams强转成ViewGroup.MarginLayoutParams,然后通过xxxmargin属性获取属性值),你会收到一个“礼物”

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
           

布局

布局很简单,就是计算一下各个子View在父容器中的位置就可以了

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
 
	if (getChildCount() > 0) {
		// 声明一个临时变量存储高度倍增值
		int mutilHeight = 0;
 
		// 那么遍历子元素并对其进行定位布局
		for (int i = 0; i < getChildCount(); i++) {
			// 获取一个子元素
			View child = getChildAt(i);
 
			// 通知子元素进行布局
			child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
 
			// 改变高度倍增值
			mutilHeight += child.getMeasuredHeight();
		}
	}
           

以下内容节选自:教你步步为营掌握自定义View

我要改变这个View的行为,外观,肯定是覆写View类中的方法,但是怎么覆写,覆写哪些方法能够改变哪些行为? 

好了,View的位置和大小怎么确定我们都清楚了,现在,是时候开始自定义View了。

首先,关于View所要具备的一般功能,View类中都有了基本的实现,比如确定位置,它有layout方法,当然,这个只适用于ViewGroup,实现自己的ViewGroup时,才需要修改该方法。确定大小,它有onMeasure方法,如果你不满意默认的确认大小的方法,也可以自己定义。改变默认的绘制,就覆写onDraw方法。下面,我们通过一张图,来看看,自定义View时,我们最可能需要修改的方法是哪些:

自定义View、ViewGroup(温习&amp;实战&amp;笔记)基本的布局参数

把这些方法都搞明白了,你也就理解了View的生命周期了。

比如View被inflated出来后,系统会回调该View的onFinishInflate方法,你的View可以在这个方法中,做一些准备工作。

如果你的View所属的Window可见性发生了变化,系统会回调该View的onWindowVisibilityChanged方法,你也可以根据需要,在该方法中完成一定的工作,比如,当Window显示时,注册一个监听器,根据监听到的广播事件改变自己的绘制,当Window不可见时,解除注册,因为此时改变自己的绘制已经没有意义了,自己也要跟着Window变成不可见了。

当ViewGroup中的子View数量增加或者减少,导致ViewGroup给自己分配的屏幕区域大小发生变化时,系统会回调View的onSizeChanged方法,该方法中,View可以获取自己最新的尺寸,然后根据这个尺寸相应调整自己的绘制。

当用户在View所占据的屏幕区域发生了触摸交互,系统会将用户的交互动作分解成如DOWN、MOVE、UP等一系列的MotionEvent,并且把这些事件传递给View的onTouchEvent方法,View可以在这个方法中进行与用户的交互处理。

参考学习博文,感谢:

https://blog.csdn.net/aigestudio/article/details/42989325

https://blog.csdn.net/u012732170/article/details/55045472###

https://www.jianshu.com/p/d507e3514b65

https://juejin.im/post/5dc1225c5188255f7b0fa42d#heading-6