概述 View 是由视图和属性组成的控件,实现一个 View 的自定义,主要包括定义它自身的尺寸,以及绘制它的内容,并且在自身属性发生改变时能正确更新其绘制的内容。
ViewGroup 是 View 的容器类型,内部可容纳多个 View,并对子 View 的位置进行规划,根据自身特性的不同,子 View 的摆放特点也各不相同。由于 ViewGroup 本身也是 View 的子类,所以 ViewGroup 本身也可作为子View 出现。自定义 ViewGroup 时,更着重于对子 View 位置的处理,并且尺寸由子 View 的尺寸和摆放位置决定,在一些复杂的自定义 View 中,内容的绘制和子 View 位置的处理可能显的同等重要。
MeasureSpec MeasureSpec 负责生成 View 的 onMeasure
方法中所传递的 measureSpec 参数和从中提取信息,measureSpec 参数将携带父 View 提供的尺寸和测量模式信息,作为确定 View 自身尺寸和 View 继续子View的测量参考。
measureSpec 参数是一个32位int型值,其中高两位将保存测量模式的值,低30位保存尺寸大小,MeasureSpec 提供了生成 measureSpec 参数的方法,它使用位运算将模式和尺寸信息保存至 measureSpec 参数中。
MeasureSpec 部分源码
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 private static final int MODE_SHIFT = 30 ;private static final int MODE_MASK = 0x3 << MODE_SHIFT;private static final int UNSPECIFIED = 0 << MODE_SHIFT;private static final int EXACTLY = 1 << MODE_SHIFT;private static final int AT_MOST = 2 << MODE_SHIFT;public static int makeMeasureSpec (int size, int mode) { if (isUseBrokenMakeMeasureSpec){ return size + mode; }else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode (int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize (int measureSpec) { return (measureSpec & ~MODE_MASK); }
当实现View的 onMeasure
方法时,可根据测量模式确定View的尺寸,下面是3种测量模式。
模式
描述
对应布局参数
AT_MOST
父容器指定了 View 最大可用大小
一般对应 WRAP_CONTENT
EXACTLY
父容器检测到 View 精确大小
MATCH_PARENT 或 固定 dp
UNSPECIFIED
父容器对 View 大小无限制
其他
每个 View 的 measureSpec 参数由父容器结合 View 自身的布局参数将要对 View 进行测量时生成,下面是 ViewGroup 的 getChildMeasureSpec
方法,负责生成子 View 的 measureSpec。
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 51 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) { case MeasureSpec.EXACTLY: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.AT_MOST: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break ; case MeasureSpec.UNSPECIFIED: if (childDimension >= 0 ) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break ; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
在 ViewGroup 的 measureChild
方法中有对 getChildMeasureSpec
的调用。
1 2 3 4 5 6 7 8 9 protected void measureChild (View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final ViewGroup.LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
自定义 View 属性 概述 一个合格的 View 是拥有多种可设置属性的,包括从 Java 代码和 XML 布局文件中设置两种方式,Android SDK 提供的原生 View 本身具有丰富的属性,这里实现自定义 View 的 XML 属性。
1 2 3 4 5 6 7 <?xml version="1.0" encoding="utf-8" ?> <resources> <declare-styleable name="TestView" > <attr name="attr name" format="value type" /> ... </declare-styleable> </resources>
使用 <declare-styleable/>
标签创建view的自定义属性,其中 name
最好指定为自定义View的名字,方便和 View 对应。在内部包含的 <attr/>
标签为该 View 支持的自定义属性列表,包含一个属性名字和引用的类型,定义后即可在布局中使用自定义 View 的属性,与系统属性所用的 android
命名空间不同,自定义属性需要使用 app
命名空间,自定义属性支持以下的所有 Format 类型。
format类型
描述
integer
指定一个整型数值
boolean
指定一个布尔型值
float
指定一个浮点型数值
string
指定一个字符串类型值
color
指定一个颜色的16进制数值或color资源的引用
dimension
指定Android中支持的尺寸类型(dp,sp,px…)或dimen资源的引用
fraction
指定一个百分比(20%,10%p),带p的支持分母因子
reference
指定一个引用类型,可以使任何资源的引用(drawable,color,dimen…)
flag
只能使用flag内定义的值,值的类型只能为整型
enum
和flag功能相同,但可以和integer类型混用,可指定定义外的int值
定义完属性后,即可在view的构造方法里,通过 AttributeSet
参数获取在xml中定义的属性值 。
1 2 3 4 5 6 7 8 9 public constrac (Context context, @Nullable AttributeSet attrs) { super (context, attrs); TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.AttrTestView); final int colorAttr = array.getColor(R.styleable.AttrTestView_colorAttr, 0 ); final xx attr = array.getXX(..); ... }
下面是每种类型定义以及获取的完整示例:
基本类型 基本类型 int,boolean,float,String。
1 2 3 4 5 6 7 <declare-styleable name ="AttrTestView" > <attr name ="intAttr" format ="integer" /> <attr name ="boolAttr" format ="boolean" /> <attr name ="floatAttr" format ="float" /> <attr name ="stringAttr" format ="string" /> ... </declare-styleable >
1 2 3 4 5 6 <com.example.viewtest.view.AttrTestView ... app:intAttr ="52" app:boolAttr ="true" app:floatAttr ="0.56" app:stringAttr ="hello" />
1 2 3 4 final int intAttr = array.getInt(R.styleable.AttrTestView_intAttr, 0 );final boolean boolAttr = array.getBoolean(R.styleable.AttrTestView_boolAttr, false );final float floatAttr = array.getFloat(R.styleable.AttrTestView_floatAttr, 0F );final String stringAttr = array.getString(R.styleable.AttrTestView_stringAttr);
色彩和尺寸类型
1 2 <attr name ="dimenAttr" format ="dimension" /> <attr name ="colorAttr" format ="color" />
使用,色彩类型可指定16进制颜色值或资源的引用,尺寸类型可指定支持的尺寸类型和引用
1 2 3 4 5 6 7 <com.example.viewtest.view.AttrTestView ... app:dimenAttr ="10dp" app:dimenAttr ="15px" app:dimenAttr ="@dimen/testDimen" app:colorAttr ="@color/testColor" app:colorAttr ="#ffaabbcc" />
1 2 final int colorAttr = array.getColor(R.styleable.AttrTestView_colorAttr, 0 );final float dimenAttr = array.getDimension(R.styleable.AttrTestView_dimenAttr, 0 );
引用类型
1 <attr name ="refAttr" format ="reference" />
1 2 3 4 5 6 <com.example.viewtest.view.AttrTestView ... app:refAttr ="@dimen/testDimen" app:refAttr ="@drawable/ic_test" app:refAttr ="@string/testString" app:refAttr ="@color/testColor" />
1 2 3 final Drawable refIcon = array.getDrawable(R.styleable.AttrTestView_refAttr);final int refColor = array.getColor(R.styleable.AttrTestView_refAttr, 0 );...
百分比类型
1 <attr name ="fractionAttr" format ="fraction" />
使用,有两种方式,默认(x%)和带有分母因子(x%p)的,在获取时有差异
1 2 3 4 <com.example.viewtest.view.AttrTestView ... app:fractionAttr ="10%" app:fractionAttr ="10%p" />
1 2 3 4 5 6 float getFraction (int index, int base, int pbase, float defValue)
1 2 3 4 5 6 7 8 9 10 array.getFraction(R.styleable.AttrTestView_fractionAttr, 1 , 2 , 0F ); array.getFraction(R.styleable.AttrTestView_fractionAttr, 2 , 2 , 0F ); array.getFraction(R.styleable.AttrTestView_fractionAttr, 2 , 1 , 0F ); array.getFraction(R.styleable.AttrTestView_fractionAttr, 2 , 2 , 0F );
flag 类型
定义,flag 可指定一组限制值,定义后,属性只能使用组内的数值,数值类型为数字
1 2 3 4 5 <attr name ="flagAttr" > <flag name ="FLAG_0" value ="0x01" /> <flag name ="FLAG_1" value ="0x02" /> ... </attr >
使用,使用时还可以使用按位或 |
运算符组合多个值
1 2 3 4 <com.example.viewtest.view.AttrTestView ... app:flagAttr ="FLAG_0|FLAG_1" app:flagAttr ="FLAG_0" />
1 final int flagAttr = array.getInt(R.styleable.AttrTestView_flagAttr, 0 );
枚举类型
定义,和 flag 定义形式相同,不过可以组合 integer 类型
1 2 3 4 5 <attr name ="enumAttr" format ="integer" > <flag name ="ENUM_0" value ="0" /> <flag name ="ENUM_1" value ="1" /> ... </attr >
当组合 Integer 类型时,可直接指定数字,否则和 flag 一样,只能使用组内定义的值
1 2 3 4 5 <com.example.viewtest.view.AttrTestView ... app:flagAttr ="123" app:flagAttr ="ENUM_0|ENUM_1" app:flagAttr ="ENUM_0" />
1 final int enumAttr = array.getInt(R.styleable.AttrTestView_enumAttr, 0 );
自定义 View
实现一个自定义 View,首先需要定义自身尺寸,然后绘制自身内容,最后提供自定义的 View 属性。
首先需要实现 onMeasure
测量方法,以指明 View 尺寸,根据 measureSpec 参数中的模式和基础尺寸,确定最终尺寸,下面提供一种通用的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { final int baseWidth = View.MeasureSpec.getSize(widthMeasureSpec); final int baseHeight = View.MeasureSpec.getSize(heightMeasureSpec); final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); final int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); final int width = widthMode == MeasureSpec.EXACTLY ? baseWidth : Math.min(baseWidth, getWrapWidth(baseWidth)); final int height = heightMode == MeasureSpec.EXACTLY ? baseHeight : Math.min(baseWidth, getWrapHeight(baseHeight)); super .onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), MeasureSpec.makeMeasureSpec(height, heightMode)); }
这里最后调用的父类的 onMeasure
方法,将会调用 setMeasuredDimension
方法设置自身尺寸,当然也可以直接调用这个方法
1 2 3 4 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
在 onMeasure
方法执行后,可以使用 getMeasuredWidth()
和 getMeasuredHeight()
获取自身宽高
实现 onDraw
方法,绘制 View 内容,使用 Canvas 和 Paint 组合进行绘制工作
1 2 3 4 5 6 @Override protected void onDraw (Canvas canvas) { super .onDraw(canvas); canvas.drawXX(...); ... }
下面编写一个非常简单的自定义View,模拟一个简单的按钮,当手触摸上去时,会变色,手指放开时,颜色还原。
首先建立一个空的 SimpleButtonTest 类,需要实现父类的构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 public class TestSimpleButton extends View {public TestSimpleButton (Context context) { this (context, null ); } public TestSimpleButton (Context context, @Nullable AttributeSet attrs) { this (context, attrs, 0 ); } public TestSimpleButton (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); } }
定义控件的尺寸,重写 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 @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { final int baseWidth = View.MeasureSpec.getSize(widthMeasureSpec); final int baseHeight = View.MeasureSpec.getSize(heightMeasureSpec); final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec); final int heightMode = View.MeasureSpec.getMode(heightMeasureSpec); int wrapWidth = getWrapWidth(); final int width = widthMode == MeasureSpec.EXACTLY ? baseWidth : Math.min(baseWidth, wrapWidth); int wrapHeight = getWrapHeight(); final int height = heightMode == MeasureSpec.EXACTLY ? baseHeight : Math.min(baseWidth, wrapHeight); super .onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode), MeasureSpec.makeMeasureSpec(height, heightMode)); } private int getWrapWidth () { return (int )TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 88F , getResources().getDisplayMetrics()); } private int getWrapHeight () { return (int )TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64F , getResources().getDisplayMetrics()); }
然后实现手指触摸时的颜色变化,首先绘制颜色,重写 onDraw
方法绘制内容
1 2 3 4 5 private int mColor;... @Override protected void onDraw (Canvas canvas) { canvas.drawColor(mBtnColor); }
定义两种状态时的色彩
1 2 3 4 5 6 7 8 9 10 private int mPressedColor;private int mBtnColor;... public TestSimpleButton (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); mColor = getResources().getColor(R.color.colorPrimary); mPressedColor = getResources().getColor(R.color.colorPrimaryDark); mBtnColor = mColor; }
实现触摸的监听需要重写 onTouchEvent
方法,为了使 View 的事件默认不被拦截,所以需要在构造器中设置
1 2 setClickable(true ); setFocusable(true );
最后实现 onTouchEvent
方法的内容
1 2 3 4 5 6 7 8 9 10 11 12 @Override public boolean onTouchEvent (MotionEvent event) { int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { mBtnColor = mPressedColor; invalidate(); } else if (actionMasked == MotionEvent.ACTION_UP) { mBtnColor = mColor; invalidate(); } return super .onTouchEvent(event); }
其中的 invalidate
方法为请求 View 重新绘制,他将会再次回调 onDraw
方法刷新绘制的内容,此时颜色改变,View 的显示自然发生变化。 以上就完成了一个简单 View 的自定义。
LayoutParams LayoutParams 是设置在 View 上的与父布局相关属性的映射。一个View在xml文件中设置的属性由两部分组成,一部分属于 View 自身的属性,交给 View 自身处理,另一部分属于 LayoutParams,交给所在的 ViewGroup 处理。
系统默认提供了 ViewGroup.LayoutParams
,它本身拥有丰富的属性,如果需要自定义自己的 LayoutParams,需要实现它的子类。
一般 LayoutParams 的属性都是带有 layout_
前缀的属性,例如 layout_width
,layout_Height
,自定义属性时也许需要遵循这个命名规范。
下面自定义一个简单的 LayoutParams,由于 LayoutParams 必须有关联的的 ViewGroup 的支持,所以这里先定义一个空的 ViewGroup。
1 public class TestViewGroup extends ViewGroup { ... }
然后和 View 的自定义属性一样,首先定义 attrs.xml
文件,一般 LayoutParams 的 styleable
的 name
属性定义为 ViewGroup 的名称加 _Layout
后缀。
1 2 3 4 5 6 7 8 <declare-styleable name ="TestViewGroup_Layout" > <attr name ="layout_gravity" > <flag name ="left" value ="0x0001" /> <flag name ="right" value ="0x0002" /> <flag name ="top" value ="0x0004" /> <flag name ="bottom" value ="0x0008" /> </attr > </declare-styleable >
这样就定义了一个简单的 layout_gravity
属性。 接下来在 TestViewGroup
实现一个 LayoutParams
来承载上面自定义的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static final class LayoutParams extends LayoutParams { ... public LayoutParams (Context c, AttributeSet attrs) { super (c, attrs); getGravity(c, attrs); } private void getGravity (Context c, AttributeSet attrs) { TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.TestViewGroup_Layout); gravity = array.getInt(R.styleable.TestViewGroup_Layout_layout_gravity, 0 ); array.recycle(); } }
最后一步,将这个 LayoutParams 与 ViewGroup 相关联,需要重写 ViewGroup 的几个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class TestViewGroup extends ViewGroup { ... @Override protected boolean checkLayoutParams (ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected LayoutParams generateDefaultLayoutParams () { return new LayoutParams(); } @Override protected LayoutParams generateLayoutParams (ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public LayoutParams generateLayoutParams (AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } }
实现了这些方法后,LayoutParams 就与 ViewGroup 建立了关联,在 ViewGroup 调用 addView
添加子 View 时,会使用上面重新的方法为每个子 View 建立一个 LayoutParams。
1 2 3 4 5 6 7 8 9 10 11 12 public void addView (View child, int index) { ... LayoutParams params = child.getLayoutParams(); if (params == null ) { params = generateDefaultLayoutParams(); if (params == null ) { throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null" ); } } addView(child, index, params); }
最后在 XML 布局中使用
1 2 3 4 5 6 7 8 9 10 <com.example.viewtest.view.TestViewGroup android:layout_width ="match_parent" android:layout_height ="match_parent" > <View app:layout_gravity ="left" android:layout_width ="match_parent" android:layout_height ="match_parent" /> </com.example.viewtest.view.TestViewGroup >
如果想让自定义的 LayoutParams 支持 Margin 属性, 可以继承 ViewGroup.MarginLayoutParams
。
自定义 ViewGroup
实现一个 ViewGroup,除了需要实现 View 的基本的尺寸定义和提供自定义属性,还需要实现对子 View 位置的控制,如果需要更为细致的针对每个子 View 有不同的处理,则需要实现自定义的 LayoutParams,
onLayout
方法为必须实现的抽象方法,它是 ViewGroup 的核心,负责控制所有子 View 的位置摆放,具体方法是通过计算子 View 的 left, right, top, bottom 的位置后,调用它们的 layout
方法为每个子 View 设置最终位置。
下面将会用一个实例来说明 ViewGroup 的自定义。
BorderLayoutSample 实例 下面要定义一个类似 Java中的 Border 布局的 ViewGroup,它拥有以下特性:
子 View 可使用 layout_gravity
属性设置自身的位置,基本位置属性值包括 left, right, top, bottom, centerX, centerY, center
,还可使用按位或 |
符号组合属性值使用,例如 left | bottom
代表左下方。
为了简单,一旦放入布局中,所有子 View 的尺寸将统一,默认使用最小宽度和最小高度的子View(宽高不为0dp)的尺寸作为统一子 View 尺寸,可以使用 requestWidth
和 requestHeight
请求统一的子 view 的宽和高,BorderLayout 的宽高为统一子 View 宽高的3倍(为了给其他方位的子View留出空间),如果默认和请求的子View尺寸的3倍超过尺寸限制(measureSpec包含的 size),则强制另统一子 View 的尺寸为限制尺寸的 1/3。
如果所有子 View 的宽高都为0,则 BorderLayout 的宽高为0。
最终效果如下:
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 <com.example.viewtest.view.BorderLayoutSample android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:orientation ="vertical" android:background ="#dadada" app:requestHeight ="60dp" app:requestWidth ="60dp" > <View android:layout_width ="60dp" android:layout_height ="60dp" android:background ="#ff7474" app:layout_gravity ="right" /> <View android:layout_width ="0dp" android:layout_height ="0dp" android:background ="#74ff74" app:layout_gravity ="center" /> <View android:layout_width ="60dp" android:layout_height ="60dp" android:background ="#74b0ff" app:layout_gravity ="centerY" /> <View android:layout_width ="60dp" android:layout_height ="60dp" android:background ="#ffe874" app:layout_gravity ="bottom|right" /> </com.example.viewtest.view.BorderLayoutSample >
实机测试
下面开始实现 BorderLayoutSample
首先建立 BorderLayoutSample
类,由于继承了 ViewGroup
,并且需要支持自定义属性,所以这里重写了三个构造器方法和一个 onLayout
方法的空实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class BorderLayoutSample extends ViewGroup { public BorderLayoutSample (Context context) { this (context, null ); } public BorderLayoutSample (Context context, AttributeSet attrs) { this (context, attrs, 0 ); } public BorderLayoutSample (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); } @Override protected void onLayout (boolean changed, int l, int t, int r, int b) {} }
然后是对两个自定义属性 requestWidth
和 requestHeight
的支持,新建 borderlayoutsample_attrs.xml
文件,定义如下属性:
1 2 3 4 5 6 <declare-styleable name ="BorderLayoutSample" > <attr name ="requestWidth" format ="dimension" /> <attr name ="requestHeight" format ="dimension" /> </declare-styleable >
在构造器中获取值,方便后面访问
1 2 3 4 5 6 7 8 9 10 11 12 ... public BorderLayoutSample (Context context, AttributeSet attrs, int defStyleAttr) { super (context, attrs, defStyleAttr); init(attrs); } private void init (AttributeSet attrs) { TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.BorderLayoutSample); mRequestWidth = array.getDimensionPixelOffset(R.styleable.BorderLayoutSample_requestWidth, -1 ); mRequestHeight = array.getDimensionPixelOffset(R.styleable.BorderLayoutSample_requestHeight, -1 ); array.recycle(); }
根据前面定义的 Border布局特性,实现 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 ... - private int mChildWidth = -1 ;private int mChild Height = -1 ;private int mRequestWidth;private int mRequestHeight;... @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); if (count == 0 ) { return ; } final int baseWidth = MeasureSpec.getSize(widthMeasureSpec); final int baseHeight = MeasureSpec.getSize(heightMeasureSpec); if (mRequestWidth != -1 && mRequestHeight != -1 ) { mChildWidth = mRequestWidth * 3 > baseWidth ? baseWidth / 3 : mRequestWidth; mChildHeight = mRequestHeight * 3 > baseHeight ? baseHeight / 3 : mRequestHeight; } else { int minWidth = Integer.MAX_VALUE; int minHeight = Integer.MAX_VALUE; for (int i = 0 ; i < count; i++) { View child = getChildAt(i); measureChild(child, widthMeasureSpec, heightMeasureSpec); if (child.getMeasuredWidth() == 0 || child.getMeasuredHeight() == 0 ) { continue ; } minWidth = Math.min(minWidth, child.getMeasuredWidth()); minHeight = Math.min(minHeight, child.getMeasuredHeight()); } if (minWidth == Integer.MAX_VALUE) { minWidth = 0 ; } if (minHeight == Integer.MAX_VALUE) { minHeight = 0 ; } mChildWidth = minWidth * 3 > baseWidth ? baseWidth / 3 : minWidth; mChildHeight = minHeight * 3 > baseHeight ? baseHeight / 3 : minHeight; } super .onMeasure(getMeasureSpec(widthMeasureSpec, mChildWidth * 3 ), getMeasureSpec(heightMeasureSpec, mChildHeight * 3 )); } private static int getMeasureSpec (int measureSpec, int wrapSize) { final int baseSize = View.MeasureSpec.getSize(measureSpec); final int mode = View.MeasureSpec.getMode(measureSpec); final int size = mode == View.MeasureSpec.AT_MOST ? Math.min(wrapSize, baseSize) : baseSize; return View.MeasureSpec.makeMeasureSpec(size, mode); }
其中使用了 measureChild
对子 View 进行测量,还可使用 measureChildWithMargins
测量子 View 尺寸,它会在测量时考虑子 View 的 margin 值。
接着实现核心方法 onLayout
,由于需要给子 View 提供 layout_gravity
的属性,这个属性将会影响每个子 View 自身所处位置,这里首先定义 LayoutParams 类来支持这个属性。
在刚才的 borderlayoutsample_attrs.xml
文件中加一个自定义属性
1 2 3 4 5 6 7 8 9 10 11 12 13 <declare-styleable name ="BorderLayoutSample_Layout" > <attr name ="layout_gravity" > <flag name ="left" value ="0x0001" /> <flag name ="right" value ="0x0002" /> <flag name ="top" value ="0x0004" /> <flag name ="bottom" value ="0x0008" /> <flag name ="centerX" value ="0x0010" /> <flag name ="centerY" value ="0x0020" /> <flag name ="center" value ="0x0030" /> </attr > </declare-styleable >
在定义 layout_gravity
的属性值的时候,由于需要支持 left|right
形式的组合值,所以需要对属性值进行设计,这里用的就是,0x0001, 0x0002, 0x0004...
,他们对应的二进制值分别为 0001,0010,0100,1000...
可以使用按位或运算进行组合和区分。
定义属性文件后,实现对应的 LayoutParams 类,这里先抽出一个对应 gravity
属性值的类 Gravity
,它包含 gravity 属性值的定义和计算。
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 public static final class Gravity { public static final int BASE = 0x0001 ; public static final int LEFT = BASE; public static final int RIGHT = BASE << 1 ; public static final int TOP = BASE << 2 ; public static final int BOTTOM = BASE << 3 ; public static final int CENTER_X = BASE << 4 ; public static final int CENTER_Y = BASE << 5 ; public static final int CENTER = CENTER_X | CENTER_Y; public static boolean hasLeft (int gravity) { return (gravity & LEFT) == LEFT; } public static boolean hasRight (int gravity) { return (gravity & RIGHT) == RIGHT; } public static boolean hasTop (int gravity) { return (gravity & TOP) == TOP; } public static boolean hasBottom (int gravity) { return (gravity & BOTTOM) == BOTTOM; } public static boolean hasCenterX (int gravity) { return (gravity & CENTER_X) == CENTER_X; } public static boolean hasCenterY (int gravity) { return (gravity & CENTER_Y) == CENTER_Y; } }
下面是 LayoutParams 类的实现
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 public static final class LayoutParams extends ViewGroup .LayoutParams { private int gravity = Gravity.LEFT | Gravity.TOP; public LayoutParams () { super (0 , 0 ); } public LayoutParams (ViewGroup.LayoutParams source) { super (source); if (source instanceof LayoutParams) { setGravity(((LayoutParams)source).gravity); } } public LayoutParams (Context c, AttributeSet attrs) { super (c, attrs); getGravity(c, attrs); } private void getGravity (Context c, AttributeSet attrs) { TypedArray array = c.obtainStyledAttributes(attrs, R.styleable.BorderLayoutSample_Layout); gravity = array.getInt(R.styleable.BorderLayoutSample_Layout_layout_gravity, 0 ); array.recycle(); } public void setGravity (int gravity) { this .gravity = gravity; } public int getGravity () { return gravity; } }
最后建立 BorderLayoutSample 的关联
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override protected boolean checkLayoutParams (ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected LayoutParams generateDefaultLayoutParams () { return new LayoutParams(); } @Override protected LayoutParams generateLayoutParams (ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public LayoutParams generateLayoutParams (AttributeSet attrs) { return new LayoutParams(getContext(), attrs); }
在完成了 LayoutParams 的定义后,就可以结合属性值进行 onLayout
的最终实现了。
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 @Override protected void onLayout (boolean changed, int l, int t, int r, int b) { if (mChildWidth == 0 || mChildHeight == 0 ) { return ; } int count = getChildCount(); for (int i = 0 ; i < count; i++) { View child = getChildAt(i); LayoutParams layoutParams = (LayoutParams)child.getLayoutParams(); layoutChild(child, layoutParams.getGravity()); } } private void layoutChild (View child, int gravity) { int left = 0 ; int right = left + mChildWidth; int top = 0 ; int bottom = top + mChildHeight; final int width = getMeasuredWidth(); final int height = getMeasuredHeight(); if (Gravity.hasLeft(gravity)) { left = 0 ; right = left + mChildWidth; } if (Gravity.hasRight(gravity)) { right = width; left = right - mChildWidth; } if (Gravity.hasTop(gravity)) { top = 0 ; bottom = top + mChildHeight; } if (Gravity.hasBottom(gravity)) { bottom = height; top = bottom - mChildHeight; } if (Gravity.hasCenterX(gravity)) { left = (width - mChildWidth) >> 1 ; right = left + mChildWidth; } if (Gravity.hasCenterY(gravity)) { top = (height - mChildHeight) >> 1 ; bottom = top + mChildHeight; } child.layout(left, top, right, bottom); }
以上就完成了一个 BorderLayoutSample 实例。
常用方法总结 以下记录一些自定义 View 时常用的辅助方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void setMeasuredDimension (int measuredWidth, int measuredHeight) ;void bringToFront () ;void invalidate () ;void postInvalidate () ;void requestLayout () ;void computeScroll () ;
参考