一、前言
前面我们讲了怎么自定义简单View,其实原理都很简单,思路就那几步,难的是怎么定义大型的复杂的View,因为那往往意味着大量的内部逻辑和有着非常高的数学要求,但也不用怕,自定义之路其实就是这样,平时多多练习,多思考,多接触一些别人写的开源控件,能力就会慢慢的自然而然的提升上来的,而我们今天说的ViewGroup也同样如此。
二、相关介绍
ViewGroup就是一个盛放childView(可能是一个ViewGroup,也可能是View)的容器,我们在xml中经常使用的RelativeLayout、LinearLayout、FrameLayout、AbsoluteLayout都是ViewGroup的子类,它负责给childView计算出建议的宽高尺寸和测量模式,并且确定childView在其所提供区域内的位置,Android API中是这样介绍ViewGroup的
前两句跟我们上面说的是一个意思,但是API在后面还着重提到了ViewGroup.LayoutParams ,其又是何许东西也?
看了API介绍之后是不是有点理解了,LayoutParams被childView用来告诉它们的容器它们该如何布局,因为每个childView不同,所以需求就有可能不同,其编写的属性就会存在差异,就如我们常用的RelativeLayout、LinearLayout中的layout_centerInParent和layout_weight属性各自独有一样。所以这也就是为什么ViewGroup让其childView自己实现自己所需要的LayoutParams的原因,ViewGroup.LayoutParams只给我们提供了最基本的layout_width和layout_height,因为这是每个ViewGroup所通用的layout_属性。
三、自定义ViewGroup
首先我们先总结一下一般自定义ViewGroup的步骤,让我们在写的时候能有一个大致的方向:
1.继承一个ViewGroup或者一个我们需要的ViewGroup的子类,并添加构造方法
2.重写onMeasure方法
3.重写onLayout方法
4.在xml中自定义ViewGroup属性
关于定义ViewGroup属性的顺序,我原来已经说过查看,看个人习惯吧。闲话不多说了,我们直接来拿一个流式布局的小例子来从头到尾的实现一下吧,效果图如下:
这是我们自定义的ViewGroup类,我们添加了不同参数的构造方法
public MyFlowLayout(Context context) {
this(context, null);
}
public MyFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
我们要重写onMeasure方法,ViewGroup不仅要给childView传入测量值和测量模式,而且还要求我们自己去测量尺寸并提供给父ViewGroup让其给我们提供期望大小的区域,其中测量模式分为3种:
1.EXACTLY:提供的尺寸就是当前view所应该取的尺寸,一般设置了明确的固定大小或者match_parent
2.AT_MOST:提供的尺寸就是当前view所能取的最大尺寸,一般设置了wrap_content
3.UNSPECIFIED:父view对当前view的尺寸没有限制,当前view想要多大都可以,一般很少用到
来看看我们的onMeasure方法,对于layout_width和layout_height的不同情况做了相应的处理
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//计算出所有child的宽度和高度
measureChildren(widthMeasureSpec, heightMeasureSpec);
//计算child的个数
int childCount = getChildCount();
//viewGroup最终要提供的宽度和高度
int maxWidth = 0;
int maxHeight = 0;
int widthModle = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightModle = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthModle) {
//当宽度为wrap_content
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
int childWidth = 0;
for(int i = 0; i< childCount; i++){
//将每个child的宽度累加
childWidth += getChildAt(i).getMeasuredWidth();
//如果一行放不下,则另起一行算起
if(childWidth > widthSize){
//则viewGrop提供的宽度只需要是每行最大的宽度即可
maxWidth = Math.max(childWidth - getChildAt(i).getMeasuredWidth(),maxWidth);
//宽度从头开始
childWidth = getChildAt(i).getMeasuredWidth();
}
else{
maxWidth = Math.max(childWidth,maxWidth);
}
}
break;
case MeasureSpec.EXACTLY:
//当宽度全屏或者固定尺寸时候
maxWidth = widthSize;
break;
}
switch (heightModle) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
int childWidth = 0;
int childHeight = 0;
int beforeMaxHeight = 0;
for(int i = 0; i< childCount; i++){
//将每个child的宽度累加
childWidth += getChildAt(i).getMeasuredWidth();
//获取当前child的高度
childHeight = getChildAt(i).getMeasuredHeight();
//如果一行放不下,则另起一行算起
if(childWidth > widthSize){
//则viewGrop提供的宽度只需要是每行最大的宽度即可
maxWidth = Math.max(childWidth - getChildAt(i).getMeasuredWidth(),maxWidth);
//宽度从头开始
childWidth = getChildAt(i).getMeasuredWidth();
//开始下行之前,记录下当前的高度
beforeMaxHeight = maxHeight;
//换行的时候,高度也要变成多行的高度
maxHeight += childHeight;
}
else{
//每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
}
}
break;
case MeasureSpec.EXACTLY:
//当高度全屏或者固定尺寸时候
maxHeight = heightSize;
break;
}
//给viewGroup设置最终提供的大小
setMeasuredDimension(maxWidth, maxHeight);
}
重写onLayout方法,给childView确定位置,其实核心方法就是childView.layout(l, t, r, b),原理非常简单,难的还是那句话,是每个childView区域坐标的求法。
@Override
protected void onLayout(boolean f, int l, int t, int r, int b) {
//计算child的个数
int childCount = getChildCount();
int maxHeight = 0;
int childWidth = 0;
int childHeight = 0;
int beforeMaxHeight = 0;
for(int i = 0; i< childCount; i++){
View childView = getChildAt(i);
//将每个child的宽度累加
childWidth += childView.getMeasuredWidth();
//获取当前child的高度
childHeight = childView.getMeasuredHeight();
//如果一行放不下,则另起一行算起
if(childWidth > getWidth()){
//宽度从头开始算
childWidth = childView.getMeasuredWidth();
//开始下行之前,记录下当前的高度
beforeMaxHeight = maxHeight;
//换行的时候,高度也要变成多行的高度
maxHeight += childHeight;
//换行的时候给child确定位置
childView.layout(0, beforeMaxHeight, childWidth, maxHeight);
}
else{
//每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
//给每个child确定位置
childView.layout(childWidth - childView.getMeasuredWidth(), beforeMaxHeight, childWidth, childHeight+beforeMaxHeight);
}
}
}
好了,到这里你会发现ViewGroup已经完成了,但是这里没有写自定义属性,有兴趣的可以查看上一篇文章了解一下,其实在开发中为了更灵活的使用ViewGroup,childView往往都需要支持一些布局属性,最常见的就是margin,所以下面我们就来看下怎么让childView支持margin属性,其实其他属性也大同小异啦。
四、支持Margin
我们需要支持什么属性,只需要去继承相应的LayoutParams即可,在这里我们要实现margin效果,所以只需要继承MarginLayoutParams,因为在ViewGroup源码中已经给我们提供了静态内部类MarginLayoutParams,所以我们只需要重写
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
重写onMeasure方法,思路跟上面一样,只不过这次在计算时需要加上childView可能存在的margin外边距,所以这就告诉我们,在需求下来之后,一定要好好考虑要不要支持边距,如果对方也不确定,那就加上,要不等弄好之后还得梳理代码,那是真的超烦唉。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//计算child的个数
int childCount = getChildCount();
//计算出所有child的宽度和高度
measureChildren(widthMeasureSpec, heightMeasureSpec);
//viewGroup最终要提供的宽度和高度
int maxWidth = 0;
int maxHeight = 0;
//外边距的margin
MarginLayoutParams params = null;
int widthModle = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightModle = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthModle) {
//当宽度为wrap_content
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
int childWidth = 0;
for(int i = 0; i< childCount; i++){
View view = getChildAt(i);
params = (MarginLayoutParams) view.getLayoutParams();
//计算内边距和外边距
int excessWidth = params.leftMargin + params.rightMargin;
//将每个child的宽度累加
childWidth += view.getMeasuredWidth() + excessWidth;
//如果一行放不下,则另起一行算起
if(childWidth > widthSize){
//则viewGrop提供的宽度只需要是每行最大的宽度即可
maxWidth = Math.max(childWidth - view.getMeasuredWidth() - excessWidth, maxWidth);
//宽度从头开始
childWidth = view.getMeasuredWidth() + excessWidth;
}
else{
maxWidth = Math.max(childWidth,maxWidth);
}
}
break;
case MeasureSpec.EXACTLY:
//当宽度全屏或者固定尺寸时候
maxWidth = widthSize;
break;
}
switch (heightModle) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
int childWidth = 0;
int childHeight = 0;
int beforeMaxHeight = 0;
for(int i = 0; i< childCount; i++){
View view = getChildAt(i);
params = (MarginLayoutParams) view.getLayoutParams();
//计算内边距和外边距
int excessWidth = params.leftMargin + params.rightMargin;
int excessHeight = params.topMargin + params.bottomMargin;
//将每个child的宽度累加
childWidth += view.getMeasuredWidth() + excessWidth;
//获取当前child的高度
childHeight = view.getMeasuredHeight() + excessHeight;
//如果一行放不下,则另起一行算起
if(childWidth > widthSize){
//则viewGrop提供的宽度只需要是每行最大的宽度即可
maxWidth = Math.max(childWidth - view.getMeasuredWidth() - excessWidth, maxWidth);
//宽度从头开始
childWidth = view.getMeasuredWidth() + excessWidth;
//开始下行之前,记录下当前的高度
beforeMaxHeight = maxHeight;
//换行的时候,高度也要变成多行的高度
maxHeight += childHeight;
}
else{
//每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
}
}
break;
case MeasureSpec.EXACTLY:
//当高度全屏或者固定尺寸时候
maxHeight = heightSize;
break;
}
//给viewGroup设置最终提供的大小
setMeasuredDimension(maxWidth, maxHeight);
}
重写onLayout方法,如果你懂得了原理,最好按着自己的思路写一下,因为这就像小学时的应用题,不同的解题过程和思路都能得到相同的正确结果,所以没有必要非得跟着别人的思路走,但是如果别人的方法确实简单好用,也要懂的吸收借鉴哦。
@Override
protected void onLayout(boolean f, int l, int t, int r, int b) {
//计算child的个数
int childCount = getChildCount();
//外边距的margin
MarginLayoutParams params = null;
int maxHeight = 0;
int childWidth = 0;
int childHeight = 0;
int beforeMaxHeight = 0;
for(int i = 0; i< childCount; i++){
View childView = getChildAt(i);
params = (MarginLayoutParams) childView.getLayoutParams();
//将每个child的宽度累加
childWidth += childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
//获取当前child的高度
childHeight = childView.getMeasuredHeight() + params.topMargin + params.bottomMargin;
//如果一行放不下,则另起一行算起
if(childWidth > getWidth()){
//宽度从头开始算
childWidth = childView.getMeasuredWidth() + params.leftMargin + params.rightMargin;
//开始下行之前,记录下当前的高度
beforeMaxHeight = maxHeight;
//换行的时候,高度也要变成多行的高度
maxHeight += childHeight;
//换行的时候给child确定位置
childView.layout(childWidth - childView.getMeasuredWidth() - params.rightMargin
, beforeMaxHeight + params.topMargin
, childWidth - params.rightMargin
, maxHeight - params.bottomMargin);
}
else{
//每行的高度以最大的高度为准,多行的时候记得加上原来高度比较,单行原来高度为0
maxHeight = Math.max(maxHeight, childHeight+beforeMaxHeight);
//给每个child确定位置
childView.layout(childWidth - childView.getMeasuredWidth() - params.rightMargin
, beforeMaxHeight + params.topMargin
, childWidth - params.rightMargin
, childHeight + beforeMaxHeight - params.bottomMargin);
}
}
}
好了,到这里就已经都介绍完了,Activity代码和xml布局我就不贴了,有兴趣的可以下载demo看下,运行结果如下
五、总结
到此我已经基本介绍完自定义简单viewGroup的流程了,但其中涉及到的很多知识点,都没有具体的去深入介绍,充其量只是有个使用方法罢了,如果你感兴趣可以通过切入一个知识点,单独的再去查资料深入剖析一下。