安卓中自定义View就是根据需求自己设计一个组件,这就包括继承View或View的派生类,然后去重写内部方法。
通常来说自定义View分为三种:
继承View,自定义组件;继承View的派生类,即系统提供的组件;组合型控件,自定义组件中包含了其他组件。1.测量 onMeasure(),即确定控件的长和宽;
2.布局 onLayout(),即确定控件的摆放位置;
3.绘画onDraw(),即确定控件的样式。
在自定义ViewGroup中,一般只要重写onMeasure()和onLayout(),而在自定义View中一般只要重写onMeasure()和onDraw() 。
本文要实现的是一个流式布局,就像这样:
具体代码展示:
public class FlowLayout extends ViewGroup { //保存每行子View集合 private List<List<View>> list; //保存每行的行高 private List<Integer> lineHeights; //view之间的间距 int spacing = 30; public FlowLayout(Context context) { super(context); } public FlowLayout(Context context, AttributeSet attrs) { super(context, attrs); } public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } //初始化集合 private void init(){ if(list != null){ list.clear(); lineHeights.clear(); }else { list = new ArrayList<>(); lineHeights = new ArrayList<>(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //一定要在该方法里面初始化,因为该方法可能运行不止一次 init(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); //一行有多少子view,记录一下,为layout做准备 List<View> lineViews = new ArrayList<>(); //一行中已经记录了的宽度 int lineWidth = 0; //一行中的最高高度 int lineHeight = 0; //他父类给的宽度 int parentWidth = MeasureSpec.getSize(widthMeasureSpec); int parentHeight = MeasureSpec.getSize(heightMeasureSpec); //通过子view测量出的宽度 int width = 0; int height = 0; for (int i = 0;i < getChildCount();i++){ View childView = getChildAt(i); //获取子View属性 //LayoutParams childLP = childView.getLayoutParams(); //获取子View的外边距,直接无法获取到,要重写generateLayoutParams() MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); //测量宽度 // int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft + paddingRight,childLP.width); // int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop + paddingBottom,childLP.height); // //设置子View的测量结果 // childView.measure(childWidthMeasureSpec,childHeightMeasureSpec); //设置子View的测试结果 measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0); //获取子View宽度 int childWidth = childView.getMeasuredWidth(); int childHeight = childView.getMeasuredHeight(); //是否需要换行 if (lineWidth + childWidth + spacing + lp.leftMargin + lp.rightMargin > parentWidth){ //保存每行子View集合 list.add(lineViews); //保存行高 lineHeights.add(lineHeight); //得到最大宽度 width = Math.max(width,lineWidth); //计算高度 height = height + lineHeight + spacing; //换行要进行初始化 lineViews = new ArrayList<>(); lineHeight = 0; lineWidth = 0; } //保存子view lineViews.add(childView); lineWidth = lineWidth + childWidth + spacing + lp.leftMargin + lp.rightMargin; lineHeight = Math.max(lineHeight,childHeight + lp.topMargin + lp.bottomMargin); //计算最后一行的宽高,防止遗漏 if (i == getChildCount() - 1){ //保存每行子View集合 list.add(lineViews); lineHeights.add(lineHeight); width = Math.max(width,lineWidth); height = height + lineHeight + spacing; } } //获取测量模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //判断是用通过子view计算出的宽高还是父类给予的宽高 int realWidth = (widthMode == MeasureSpec.EXACTLY) ? parentWidth : width + paddingLeft + paddingRight; int realHeight = (heightMode == MeasureSpec.EXACTLY) ? parentHeight : height + paddingTop + paddingBottom; setMeasuredDimension(realWidth,realHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int line = lineHeights.size(); //给子View进行布局 for(int i = 0; i < line; i++){ for(int j = 0; j < list.get(i).size(); j++){ View childView = list.get(i).get(j); MarginLayoutParams lp = (MarginLayoutParams)childView.getLayoutParams(); //计算出位置信息 int left = paddingLeft + lp.leftMargin; int top = paddingTop + lp.topMargin; int right = left + childView.getMeasuredWidth(); int bottom = top + childView.getMeasuredHeight(); //然后进行布局 childView.layout(left,top,right,bottom); paddingLeft = right + spacing; } //换行后要重新计算left和top paddingLeft = getPaddingLeft(); paddingTop = paddingTop + lineHeights.get(i) + spacing; } } //重写该方法 @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(),attrs); } }自定义布局代码注释写的很清楚,不在过多解释了。
但是其中比较重要的就是测量子View的宽高,在计算出布局的宽高。比如这个方法:
//设置子View的测试结果 measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0);我们可以看一下这个方法:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //测量子View的宽高 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); //设置子View的测量结果 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }继续查看这个方法:
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); int resultSize = 0; int resultMode = 0; switch (specMode) { // 当父类的宽或者高,是确定值时如:match_parent,100dp case MeasureSpec.EXACTLY: //子View宽或者高是自己设置的值,如100dp if (childDimension >= 0) { //那么子View的宽或者高就是自己设定的值和精确模式 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //子view宽度或者高度是最大值时,那么子view就是最大值和精确模式 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View的宽或者高是自适应时,宽或者高最大不能超过父类给的最大值, //模式也是不确定的,即自适应模式。 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当父类的宽或者高是不确定,如WRAP_CONTENT时 case MeasureSpec.AT_MOST: if (childDimension >= 0) { // 子view是自己设置的确定值时,宽或者高就是自己设置的值,模式就是精确的 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 子view是最大值,宽或者高就是最大值,模式就是不确定的 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //子View的宽或者高是自适应时,宽或者高最大不能超过父类给的最大值, //模式也是不确定的,即自适应模式。 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当父类不设限模式,如ListView,他的内容可以超过屏幕限制,一般用不到 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // 子view是自己设置的确定值时,宽或者高就是自己设置的值,模式就是精确的 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 父类对子View大小不设限制 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //保存测量模式和长度 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }这里涉及MeasureSpec,这个很重要。
1.MeasureSpec = SpecMode + SpecSize,即测量模式和测量长度构成。
测量模式分为三种:
MeasureSpec.EXACTLY ,精确模式,当控件的layout_width和layout_height属性指定为具体数值或match_parent时。MeasureSpec.AT_MOST,不确定模式,当控件的layout_width和layout_height属性指定为wrap_content时。MeasureSpec.UNSPECIFIED,不设限模式,父类不对子类大小进行限制。2.LayoutParams 组件的属性,子 View 自身的测量状态
如:layout_width和layout_height属性,MeasureSpec可以认为是父View对子View大小的限制,而LayoutParams是子View自身对父View尺寸大小的请求。通过父控件的MeasureSpec和子View的LayoutParams可以确定子View的MeasureSpec,即测量的大小。
就如上面的方法,getChildMeasureSpec(int spec, int padding, int childDimension)
要确定子View的宽或者高的测量结果就需要父View的MeasureSpec和子View的LayoutParams。
这样就实现了上面演示的流式布局效果。