当自定义ViewGroup时,主要需要重写onMeasure计算高度和宽度,重写onLayout为每个子View设置位置。 在onMeasure中设置的宽度和高度时,需要注意的是这个高度和宽度应该是包括padding的;在onLayout中为每个子View设置的位置应该是不包含每个子View的左右上下margin的。 另外需要注意的是,如果需要提供LayoutParams,需要重写generateLayoutParams(AttributeSet attrs)方法返回一个LayoutParams,这个参数就是其子View调用getLayoutParams返回的LayoutParams,重写这个这个方法时,如果所有的布局均是在xml中完成的,那么不会出现问题,而如果一旦调用addView方法,则会抛出异常,如果需要支持addView方法,那么需要重写generateDefaultLayoutParams()方法返回一个默认的LayoutParams。 下面是一个标签流式布局的示例:
/** * 标签布局 * Created by Xingfeng on 2016-10-20. */public class TagLayout extends ViewGroup { public TagLayout(Context context) { super(context); } public TagLayout(Context context, AttributeSet attrs) { super(context, attrs); } public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @TargetApi(Build.VERSION_CODES.LOLLipOP) public TagLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } /** * 计算高度和宽度 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override PRotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int count = getChildCount(); int lineHeight = 0; int lineWidth = 0; int width = 0; int height = 0; //遍历子View for (int i = 0; i < count; i++) { View child = getChildAt(i); //测量子View measureChild(child, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //得到子View占据的宽度和高度,包括marigin int w = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int h = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //如果一行宽度超过了TagLayout的宽度,不包括左右padding if (lineWidth + w > widthSize - getPaddingLeft() - getPaddingRight()) { //高度加上上一行的高度 height += lineHeight; //高度取所有行中最宽的 width = Math.max(width, lineWidth); lineHeight = h; lineWidth = w; } //不需要换行 else { //每一行的高度以最大的高度为准 lineHeight = Math.max(lineHeight, h); lineWidth += w; } //如果是最后一个View,因为可能没有转行,所以要对宽度做个判断,有可能最后一行就是最宽的,总的高度需要加上最后一行的高度 if (i == count - 1) { width = Math.max(width, lineWidth); height += lineHeight; } } //确定宽高,如果是确定的,则使用约束的,否则使用计算得到值 int w = widthMode == MeasureSpec.EXACTLY ? widthSize : width + getPaddingLeft() + getPaddingRight(); int h = heightMode == MeasureSpec.EXACTLY ? heightSize : height + getPaddingTop() + getPaddingBottom(); //千万记得调用该方法 setMeasuredDimension(w, h); } /** * 对子View进行位置安放 * @param changed * @param l * @param t * @param r * @param b */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //安放的起始位置是左上角,去除左和上padding部分 int left = getPaddingLeft(); int top = getPaddingTop(); int lineWidth = 0; int lineHeight = 0; int width = getWidth(); int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int vWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; int vHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; //换行 if (vWidth + lineWidth > width - getPaddingRight() - getPaddingLeft()) { //计算下一行的高度 top += lineHeight; lineWidth = vWidth; //下一行左边的开始 left = getPaddingLeft(); } else { //每一行高度取最大的一个 lineHeight = Math.max(lineHeight, vHeight); //行宽 lineWidth += vWidth; } child.layout(left + lp.leftMargin, top + lp.topMargin, left + +lp.leftMargin + child.getMeasuredWidth(), top + lp.topMargin + child.getMeasuredHeight()); //左起始 left += vWidth; } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } /** * 用于支持addView方法 * @return */ @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(200,700); }}自定义View一般分为三种: 1. 组合View,利用基本View进行组合,得到一个新的View 2. 继承现有View,增加新功能 3. 继承View,自定义内容的绘制以及事件的处理
下面以三个例子分为介绍这三种情况。
组合View一个典型的例子,就是应用的顶部类似ActionBar的一个例子,比如说左边一个返回按钮,中间是标题,最右边又是一个按钮。下面就以这个为例,介绍一下组合View的步骤:
XML布局如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:orientation="horizontal"> <Button android:id="@+id/left_btn" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="返回" /> <TextView android:id="@+id/middle_title" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="4" android:background="#DD0000" android:gravity="center" android:text="组合View" /> <Button android:id="@+id/right_btn" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="更多" /></LinearLayout>虽然是组合View,但依然要依附于某个View,不然就不能在XML中使用了,这里我们使TabBar继承自FrameLayout,然后将上面的XML文件加载到FrameLayout里,这样就可以在XML中使用了,而XML中的每个控件也可以找到了,如下:
public class TabBar extends FrameLayout { private Button leftBtn, rightBtn; private TextView titleTv; public TabBar(Context context, AttributeSet attrs) { super(context, attrs); //使上面的XML文件加载为FrameLayout的子View LayoutInflater.from(context).inflate(R.layout.tabbar_layout, this); leftBtn = (Button) findViewById(R.id.left_btn); titleTv = (TextView) findViewById(R.id.middle_title); rightBtn = (Button) findViewById(R.id.right_btn); }}当然,TabBar还可以写出一些接口供用户设置按钮属性等等,这儿就不介绍了,想了解的朋友可以到最下面的源码查看。 效果如下:
下面以一个CircleView为例介绍下继承已有View,CircleView继承自TextView,主要就是用于没有设置背景时,在文本后绘制一个圆形的背景。效果如下图: 代码如下,主要就是重写onDraw方法,在调用TextView的onDraw方法之前首先绘制一个尽可能大的圆。
@Override protected void onDraw(Canvas canvas) { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(Color.BLUE); int width = getWidth(); int height = getHeight(); int radius = width > height ? height / 2 : width / 2; canvas.drawCircle(width / 2, height / 2, radius, paint); super.onDraw(canvas); }当组合View和继承已有View不能满足我们的需求时,那么需要继承View,一步一步实现自定义View。主要有如下几步: 1. 编写attrs.xml文件定义属性,这些属性是可以直接在XML中指定了,就像layout_width等等 2. 继承View,在构造方法中获取到XML文件中的各属性以及赋值 3. 重写onMeasure方法处理高宽为wrap_content的情况 4. 重写onDraw绘制内容
下面以一个ArcProgress(弧形进度条)为例,模仿魅族5.0.1的垃圾清理进度条。
attrs.xml文件位于res/value目录下,主要需要定义中间文字尺寸、颜色、当前进度值、弧形的宽度、颜色、底部标题的文字、颜色和尺寸。
<declare-styleable name="ArcProgress"> <attr name="progress_text_size" format="dimension|reference"></attr> <attr name="progress_text_color" format="color|reference"></attr> <attr name="arc_progress" format="integer|reference"></attr> <attr name="arc_stroke_width" format="dimension|reference"></attr> <attr name="arc_color" format="color|reference"></attr> <attr name="arc_bottom_text" format="string|reference"></attr> <attr name="arc_bottom_text_color" format="color|reference"></attr> <attr name="arc_bottom_text_size" format="dimension|reference"></attr> </declare-styleable>在View的构造方法中使用TypedArray进行获取属性值并赋值,如下:
public ArcProgress(Context context) { this(context, null); } public ArcProgress(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ArcProgress(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initDefaultValues(context); initAttrs(context, attrs); initPaints(); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ArcProgress(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initDefaultValues(context); initAttrs(context,attrs); initPaints(); } private void initDefaultValues(Context context){ DEFAULT_COLOR= Color.parseColor(DEFAULT_COLOR_STR); DEFAULT_STROKE_WIDTH= Utils.dp2px(context.getResources(), DEFAULT_STROKE_WIDTH_F); DEFAULT_TEXT_COLOR=Color.parseColor(DEAULT_TEXT_COLOR_STR); DEFAULT_TEXT_SIZE=Utils.sp2px(context.getResources(), DEFAULT_TEXT_SIZE_F); DEFAULT_INSIDE_STORKE_WIDTH= Utils.dp2px(getResources(),DEFAULT_INSIDE_STORKE_WIDTH_F); } private void initAttrs(Context context,AttributeSet attrs){ TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.ArcProgress); arcColor=ta.getColor(R.styleable.ArcProgress_arc_color,DEFAULT_COLOR); strokeWidth=ta.getDimension(R.styleable.ArcProgress_arc_stroke_width, DEFAULT_STROKE_WIDTH); progressTextColor=ta.getColor(R.styleable.ArcProgress_progress_text_color, DEFAULT_COLOR); progressTextSize=ta.getDimension(R.styleable.ArcProgress_progress_text_size,DEFAULT_TEXT_SIZE); bottomText=ta.getString(R.styleable.ArcProgress_arc_bottom_text); bottomTextColor =ta.getColor(R.styleable.ArcProgress_arc_bottom_text_color, DEFAULT_TEXT_COLOR); bottomTextSize=ta.getDimension(R.styleable.ArcProgress_arc_bottom_text_size, DEFAULT_TEXT_SIZE); progress=ta.getInt(R.styleable.ArcProgress_arc_progress, 0); ta.recycle(); } private void initPaints(){ arcPaint=new Paint(Paint.ANTI_ALIAS_FLAG); arcPaint.setColor(arcColor); arcPaint.setStyle(Paint.Style.STROKE); textPaint=new Paint(Paint.ANTI_ALIAS_FLAG); }}在构造方法内完成初始化操作,包括属性的默认值,获取属性值以及Paint的设置。
当需要处理wrap_content属性时,需要重写该方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode=MeasureSpec.getMode(widthMeasureSpec); int heightMode=MeasureSpec.getMode(heightMeasureSpec); int widthSize=MeasureSpec.getSize(widthMeasureSpec); int heightSize=MeasureSpec.getSize(heightMeasureSpec); //如果宽度是wrap_content,使用建议的最小宽度 if(widthMode==MeasureSpec.AT_MOST){ widthSize=getSuggestedMinimumWidth(); } //如果高度是wrap_content,使用建议的最小高度 if(heightMode==MeasureSpec.AT_MOST){ heightSize=getSuggestedMinimumHeight(); } setMeasuredDimension(widthSize, heightSize); } @Override protected int getSuggestedMinimumHeight() { return (int) Utils.dp2px(getResources(),MINIMUM_HEIGHT); } @Override protected int getSuggestedMinimumWidth() { return (int)Utils.dp2px(getResources(),MINIMUM_WIDTH); }从上面的代码可以看到,当使用wrap_content时,使用建议的最小值,重写了建议值,默认值是80dp。
在完成了整个编写工作后,还可以给该View提供一些额外的设置方法。首先看一下效果: 上图模仿了一个下载动作,进度从0变成了100。 该部分关于ArcProgress的代码可以查看ArcProgress源码 至此,三种自定义View的方式就都介绍了,具体应该使用哪种方式视情况而定。
本篇博客主要介绍了自定义View。首先介绍了自定义ViewGroup,需要注意的是onMeasure方法和onLayout方法,以及需要返回LayoutParams对象;自定义View时,有三种选择,可以组合View,可以继承已有View,也可以直接继承View,自己编写逻辑。
关于本篇博客中的所有代码均在我的Github地址。
新闻热点
疑难解答