作者:波澜步惊
链接:https://www.jianshu.com/p/d906745f160c
做程序开发,基础很重要。同样是拧螺丝人家拧出来的可以经久不坏,你拧出来的遇到点风浪就开始颤抖,可见基本功的重要性。此系列,专门收录一些看似基础,但是没那么简单的小细节,同时提供权威解决方案。喜欢的同志们点个赞就是对我最大的鼓励!先行谢过!
网上可能有一些其他文章,提供了解决方案,但是要么就是没有提供可运行demo
,要么就是demo不够纯粹
,让人探索起来受到其他代码因素的影响,无法专注于当前这个知识点(比如,我只是想了解Activity
的生命周期,你把生命周期探究的过程混入到一个很复杂的大杂烩Demo
中,让人一眼就没有了阅读Demo代码
的欲望),所以我觉得有必要做一个专题,用最纯粹
的方式展示一个坑
的解决方案.
高级自定义view系列文章,都为学员波澜步惊在学习Android高级进阶课程的过程中学习的收获和笔记,如果你从事Android开发3-5年,处于瓶颈期需要寻找突破和进阶方向。那么我们一定会你有帮助和启发
高级自定义View系列思维脑图;
高级ViewGroup是高级开发者的必备技能,而且也有固定套路,标准范式。
要实现的效果很清晰,就是一行比一行缩进一定距离,并且考虑margin
和padding
.
注:这里不考虑viewGroup
自绘背景onDraw
神马的。
- 重写
onMeasure
- 在参数int值
int widthMeasureSpec
,int heightMeasureSpec
中,分离出size
和mode
- 遍历子
view
,把宽高信息传递给子view
,让子view
完成自己对自己的测量- 利用子
view
测量之后的宽高,计算viewGroup
的宽高- 将计算出来的
ViewGroup
宽高保存起来
- 重写
onLayout
- 遍历子
view
,计算子view
的left
,top
,right
,bottom
- 将计算出来的
left,top,right,bottom
用child.layout(left, top, right, bottom);
设置给子view
自定义ViewGroup
package study.hank.com.customviewgroup;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* 实现自己的ViewGroup,效果为:
* 所有子view,一律以纵向往下排布,每行一个的规律放置。
* 并且,每放置一个,下一个就往右边锁紧一定距离。
*/
public class MyViewGroup extends ViewGroup {
private static int OFFSET = 10;// 注意,源码中直接出现的距离,都是以px为单位,只有在定义了单位为dp的距离的时候,才是dp
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
OFFSET = Utils.dip2px(10, context);//这里直接把它变成10dp
}
/**
* 为了在121行 获取LayoutParam时 得到的是MarginLayoutParam,这里要重写此方法
*
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
/**
* 注意,这两个参数,是自身的MeasureSpec int值
*
* 如果考虑margin和padding的话,那具体指的就是 子view的margin,和自身的padding,不要弄错了。
*
*
* 自身的margin是自己的父去使用的,子view的padding是子View自己的。
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获得自身的padding,它最终影响自身的可绘制区域
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//从MeasureSpec中分离出size和mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//3、把宽高限制信息传给子,让子完成自己的测量
final int childCount = getChildCount();
View child;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
if (child.getVisibility() != GONE) {//如果子可见,才执行测量
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
//获取子测量之后的尺寸,然后根据子的尺寸决定 自己的尺寸
int width = 0, height = 0;
switch (widthMode) {//自身的测量mode
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
int xOffset = 0;
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
//由于我这里加了一个缩进,所以要计算缩进之后的距离
//child的左右margin也要计算在内,一起参与viewGroup的测量
int widthAddOffsetAddPadding = i * OFFSET + child.getMeasuredWidth() + paddingLeft + paddingRight;
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
xOffset += lp.leftMargin + lp.rightMargin;
int widthAddOffsetAddPaddingAddChildMargin = widthAddOffsetAddPadding + xOffset;
width = Math.max(width, widthAddOffsetAddPaddingAddChildMargin);//这里考虑到缩进也有可能是负数,但是自身的宽度不能容不下子,所以要max
}
break;
default:
break;
}
switch (heightMode) {//自身的测量mode
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
child = getChildAt(i);
height += child.getMeasuredHeight() + paddingTop + paddingBottom;
}
break;
default:
break;
}
//保存自身宽高
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
int topOffset = 0;//top偏移量
int leftOffset = 0;//left偏移量
for (int i = 0; i < childCount; i++) {//布局,就是要确定子的左上右下
View child = getChildAt(i);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int left = leftOffset + lp.leftMargin + paddingLeft;//为什么在这里获取的paddingLeft是空的?
int right = left + child.getMeasuredWidth();
int top = topOffset + paddingTop + lp.topMargin;
int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
topOffset += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
leftOffset += OFFSET + lp.leftMargin;//左偏移量也计算在内
}
}
static class Utils {
/**
* 屏幕dip值转换为像素值
*
* @param dipValue 屏幕dip值
* @return int 屏幕像素值
*/
public static int dip2px(float dipValue, Context activity) {
return (int) (dipValue * getScreenDensity(activity) + 0.5f);
}
private static float getScreenDensity(Context activity) {
try {
return activity.getResources().getDisplayMetrics().density;
} catch (Exception e) {
return 1;
}
}
}
}
xml
1 、如何处理自定义ViewGroup里的margin和padding?
答:所有ViewGroup的margin处理,都是子view的margin属性值;所有padding处理,都是viewGroup自身的。padding,可直接通过getPaddingLeft()这种方法处理,然后在测量和布局的时候考虑进去。Margin,则必须遍历子View,拿到子view的MaginLayouParam,再获得leftMargin这种属性,最后在测量和布局时考虑进去。
2、为什么有时候子view的LayoutParam强转成MarginLayoutParam会报错?
因为没有重写generateLayoutParam方法。这个方法将会对子view的attr进行封装,最后的结果就是,改变子view的LayoutParam类型,变为MarginLayoutParam。
持续更新 关注不迷路哦