基本概念 ViewGroup
继承 View
,但是用来作为一个容器,装载各种 View
以及对它们做 UI
布局,比如高、宽、对齐方式等等,布局文件中凡是以 layout_
开头的属性,都是传递给 ViewGroup
来解析和使用的。ViewGroup
主要是计算子 View
的测量高宽并决定他们的位置。 重写 LayoutParams
可以自定义子 View
的特定参数,比如 weight
等。
框架和层级结构 View
和 ViewGroup
的绘制流程框架:
层级结构如下:
重要 API
onMeasure 测量自己的高宽;测量所有子 View
的高宽
onLayout 抽象函数,必须重写。自定义子 View
的排列规则
自定义 ViewGroup
步骤 自定义布局属性及 LayoutParams
同样在 res/values/attr.xml
文件中定义 ViewGroup
的属性及样式。
1 2 3 4 5 6 7 8 9 <attr name ="custom_orientation" > <enum name ="horizontal" value ="0" /> <enum name ="vertical" value ="1" /> </attr > <declare-styleable name ="CustomLayout" > <attr name ="custom_orientation" /> <attr name ="custom_margin" format ="integer" /> </declare-styleable >
在布局文件使用时,示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <com.***.view.CustomLayout android:id ="@+id/view_event_custom_layout" android:layout_width ="300dp" android:layout_height ="300dp" // 自定义Layout 的属性1 app:custom_orientation ="vertical" > <TextView android:id ="@+id/view_event_textview" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="@string/view_event_textview" // 自定义Layout 的属性2 app:custom_margin ="@dimen/custom_margin_text" /> </com.***.view.CustomLayout >
获取自定义布局属性 在 ViewGroup
或者自定义 LayoutParams
的构造方法中获取自定义属性值。
1 2 3 4 5 6 7 8 9 public CustomLayout (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomLayout); mOrientation = a.getInt(R.styleable.CustomLayout_custom_orientation, HORIZONTAL); }
重写 LayoutParams
相关方法 自定义类 LayoutParams
继承 ViewGroup.LayoutParams
,并定义布局所需的几个变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static class LayoutParams extends ViewGroup .LayoutParams { public int left = 0 ; public int top = 0 ; public int right = 0 ; public int bottom = 0 ; public int custom_margin = 0 ; public LayoutParams (Context c, AttributeSet attrs) { super (c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayout); custom_margin = a.getDimensionPixelSize( R.styleable.CustomLayout_custom_margin, 0 ); } ... }
如果自定义了 LayoutParams
,必须重写下面四个方法,确保能做类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public LayoutParams generateLayoutParams (AttributeSet attrs) { return new CustomLayout.LayoutParams(getContext(), attrs); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams () { return new CustomLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected boolean checkLayoutParams (ViewGroup.LayoutParams p) { return p instanceof CustomLayout.LayoutParams; } @Override protected ViewGroup.LayoutParams generateLayoutParams (ViewGroup.LayoutParams p) { return new CustomLayout.LayoutParams(p); }
注意 :如果没有重写这四个方法,会导致子 View
获取的 LayoutParams
转换为自定义时抛出异常:CustomLayout.LayoutParams lp = (CustomLayout.LayoutParams) childView.getLayoutParams();
转换失败异常 Log
打印如下:java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to com.***.view.CustomLayout$LayoutParams
重写 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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { super .onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int measureWidth = 0 , measureHeight = 0 ; if (widthMode != MeasureSpec.AT_MOST){ measureWidth = widthSize; } if (heightMode != MeasureSpec.AT_MOST){ measureHeight = heightSize; } int totalLeft = 0 , totalTop = 0 ; int count = getChildCount(); for (int i = 0 ; i < count; i++){ View childView = getChildAt(i); measureChild(childView, widthMeasureSpec, heightMeasureSpec); int measureChildWidth = childView.getMeasuredWidth(); int measureChildHeight = childView.getMeasuredHeight(); LayoutParams lp = (LayoutParams) childView.getLayoutParams(); if (mOrientation == VERTICAL) { lp.left = 0 ; lp.top = totalTop + lp.custom_margin; lp.right = measureChildWidth; lp.bottom = lp.top + measureChildHeight; totalTop = lp.bottom; if (widthMode == MeasureSpec.AT_MOST){ measureWidth = Math.max(measureWidth, measureChildWidth); } if (heightMode == MeasureSpec.AT_MOST){ measureHeight = lp.bottom; } } ... } setMeasuredDimension(measureWidth, measureHeight); }
重写 onLayout
计算子 View
的具体布局位置
1 2 3 4 5 6 7 8 9 10 11 @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { int count = getChildCount(); for (int i = 0 ; i < count; i++){ View childView = getChildAt(i); CustomLayout.LayoutParams lp = (CustomLayout.LayoutParams) childView.getLayoutParams(); childView.layout(lp.left, lp.top, lp.right, lp.bottom); } }
自定义 ViewGroup
中,至少需要重写 onMeasure
和 onLayout
总结
onMeasure
主要计算 wrap_content
模式下的测量高宽,包含自己和所有的子 View
onLayout
主要计算子 View
布局的具体位置
onDraw
绘制自己,展现需要显示的内容
自定义 ViewGroup
主要计算自身和子 View
的测量高宽,以及子 View
布局的具体位置。 自定义 View
主要计算自身的测量高宽,以及绘制自己。
问题 在 Log
跟踪中发现 onLayout
和 onMeasure
会被调用执行两次
目标
自定义 ViewGroup
常见流程
必须重写 onLayout
及需要实现那些功能
是否处理事件分发流程
参考文档
http://www.jianshu.com/p/3d2c49315d68
http://blog.csdn.net/lmj623565791/article/details/38339817/
http://www.jianshu.com/p/138b98095778