Android 开发实践 自定义控件 详解

来源:互联网 发布:物理数据模型转换为sql 编辑:程序博客网 时间:2024/06/16 21:50

转载请注明出处:http://blog.csdn.net/smartbetter/article/details/52194663

要想成为一名合格的 Android 开发者,掌握自定义控件是必不可少的,Android 系统中的控件是每个 Android App 都必不可少的部分,无论是使用系统控件,还是使用自定义控件,这些控件,为我们组成了每个精美的界面,本篇将从 Android 控件架构入手,然后到 View 和 ViewGroup 的测量与绘制,最后分析了一个很重要的知识点,事件的拦截机制,总体来说还是很丰富的。

1.Android控件架构

Android 中控件大致被分为 ViewGroup 控件 (例如RelativeLayout、LinearLayout、FrameLayout等控件) 和 View 控件 (例如TextView、Button等控件),ViewGroup 作为父控件可以包含多个 View 控件,并管理其包含的 View 控件,通过 ViewGroup,整个界面上的控件就形成了一个树形结构,也就是我们常说的控件树,上层控件负责下层子控件的测量与绘制,并传递交互事件。我们在 Activity 中使用的 findViewById() 方法就是控件树中以树的深度优先遍历来查找对应元素。

View树结构

2.View的测量与绘制

1.View的测量

Android 在绘制 View 之前必须对 View 进行测量,这个过程由 View 类中的 onMeasure() 方法来执行。

/** * onMeasure()方法默认只支持EXACTLY模式,如果想让支持wrap_content,则需要重写该方法 * * @param widthMeasureSpec * @param heightMeasureSpec */@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    // 此时宽高测量条件相同,如不相同则分开写自定义测量值方法    setMeasuredDimension(measure(widthMeasureSpec), measure(heightMeasureSpec));}/** * 自定义测量值 * 通过MeasureSpec类帮助我们测量View * 测量的模式有三种: EXACTLY:精确值模式; AT_MOST:wrap_content; UNSPECIFIED:不指定测量模式,想多大就多大. * * @param measureSpec * @return */private int measure(int measureSpec) {    int result = 0;    int specMode = MeasureSpec.getMode(measureSpec);    int specSize = MeasureSpec.getSize(measureSpec);    //    if (specMode == MeasureSpec.EXACTLY) {        result = specSize;    } else {        result = 200;        if (specMode == MeasureSpec.AT_MOST) {            result = Math.min(result, specSize);        }    }    return result;}

之后就可以在布局文件中引用该View,并通过 android:layout_width 属性和 android:layout_height 属性来控制大小了。

2.View的绘制

当测量好之后,就可以重写View 类中的 onDraw() 方法,并在 Canvas (系统2D绘图API所必须使用到的对象,就像一个画板,使用 Paint 就可以在上面作画了) 对象上来绘制所需要的图形了。

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);}

onDraw() 方法中有一个参数就是 Canvas 对象,使用这个 Canvas 对象就可以进行绘图了,而在其他地方,通常需要使用代码创建一个 Canvas 对象:

Canvas canvas = new Canvas(bitmap);

传入的 bitmap 与通过这个 bitmap 创建的 Canvas 画布是紧紧关联在一起的,这个过程我们称之为 装载画布,这个 bitmap 用来存储所有绘制在 Canvas 上的像素信息。所以当你通过这种方式创建了 Canvas 对象后,后面调用的 Canvas.drawXXX() 方法都发生在这个 bitmap 上。

Canvas 的一些常用绘制方法:

方法名 说明 drawText() 绘制文本 drawCircle() 绘制圆 drawLine() 画线 drawArc() 绘制弧形 drawRect() 绘制矩形 drawPoint() 画点 drawBitmap() 绘制Bitmap图片

3.View的重绘

View 中的 invalidate() 方法是用来重绘 View 的,必须是在 UI 线程中进行工作。比如在修改某个 View 的显示时,invalidate() 的调用是把之前的旧的 View 从 UI 线程队列中pop掉。

有时为了防止使用 invalidate() 刷新过快反而影响效果,可以使用 postInvalidateDelayed(300); 进行 View 的延迟重绘。

3.ViewGroup的测量与绘制

1.ViewGroup的测量

ViewGroup 会管理其子 View,其中一个管理项目就是负责子 View 的显示大小,当 ViewGroup 的大小为 wrap_content 时,ViewGroup 就会对子 View 进行遍历,调用子 View 的 Measure 方法来获取每一个子 View 的测量结果 (前面说的对View的测量,就是在这里进行的),以便获得所有子 View 的大小,从而确定自己的大小,而其他模式下则会通过具体的指定值来设置自身的大小。

在子 View 测量完毕后,就需要将子 View 放到合适的位置,这个过程就是 Layout 过程,ViewGroup 在执行 Layout 过程时,同样是使用遍历来调用子 View 的 Layout 方法,并指定其具体的位置,从而确定其布局位置。

在自定义 ViewGroup 时,通常会重写 onLayout() 方法来控制其子 View 显示位置的逻辑,同样,如果需要支持 wrap_content 属性,那么它还必须重写 onMeasure 方法,这点与 View 是相同的。

2.ViewGroup的绘制

ViewGroup 通常情况下不需要绘制,因为它本身就没有需要绘制的东西,如果不是指定了 ViewGroup 的背景颜色,那么 ViewGroup 的 onDraw() 方法都不会被调用。

4.自定义View

在自定义 View 时,我们通常会去重写 onDraw() 方法来绘制 View 的显示内容,如果该 View 还需要使用 wrap_content 属性,那么还需要重写 onMeasure() 方法。另外,通过自定义 attrs 属性还可以设置新的属性配置项。

在 View 中 有以下一些比较重要的回调方法:

方法名 说明 onFinishInflate() 从 XML 加载组件后回调 onSizeChanged() 组件大小改变时回调 onMeasure() 回调该方法来进行测量 onLayout() 回调该方法来确实显示的位置 onTouchEvent() 监听触摸事件 onDraw() 绘制 View 时回调

通常有三种方法实现自定义控件:对现有控件进行拓展;通过组合来实现新的控件;重写 View 来实现全新的控件。

1.对现有控件进行拓展

以 TextView 为例,在构造方法初始化画笔并重写其 onDraw() 方法:

mPaint = new Paint();mPaint.setColor(Color.BLUE);mPaint.setStyle(Paint.Style.FILL);

重写其 onDraw() 方法:

@Overrideprotected void onDraw(Canvas canvas) {    // 在回调父类方法前实现逻辑,对TextView来说即是在绘制文本内容前    canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);    canvas.save();    canvas.translate(10, 0); // 平移10像素    super.onDraw(canvas);    // 在回调父类方法后实现逻辑,对TextView来说即是在绘制文本内容后    canvas.restore();}

2.重写View来实现全新的控件

创建一个自定义View难点在于绘制控件和实现交互。通常需要继承 View 类,并重写它的 onDraw() 、onMeasure() 等方法来实现绘制逻辑,同时通过重写 onTouchEvent() 等触摸事件来实现交互逻辑,当然还可以像实现组合控件的方式,通过引入自定义属性,丰富自定义 View 的可定制性。

5.自定义ViewGroup

ViewGroup 存在的目的就是为了对其子 View 进行管理,为其子 View 添加显示、响应的规则。因此,自定义 ViewGroup 需要重写 onMeasure() 来对子 View进行测量,重写 onLayout() 方法来确定子 View 的位置,重写 onTouchEvent() 方法增加响应事件。

/** * 使用遍历的方式来通知子View对自身进行测量 */@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int count = getChildCount();    for (int i=0; i<count; ++i) {        View childView = getChildAt(i);        measureChild(childView, widthMeasureSpec, heightMeasureSpec);    }}/** * 确定子View的位置 */@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {    int childCount = getChildCount();    // 设置ViewGroup的高度    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();    mlp.height = mScreenHeight * childCount;    setLayoutParams(mlp);    for (int i=0; i<childCount; i++) {        View child = getChildAt(i);        if (child.getVisibility()!=View.GONE) {            child.layout(l, i*mScreenHeight, r, (i+1)*mScreenHeight);        }    }}/** * 增加响应事件,MotionEvent是Android为触摸事件封装的类 */@Overridepublic boolean onTouchEvent(MotionEvent event) {    int y = (int) event.getY();    switch (event.getAction()) {        // 初次触摸        case MotionEvent.ACTION_DOWN:            // 记录触摸起点            mLastY = y;            mStart = getScaleY();            // 添加其他逻辑            break;        // 滑动        case MotionEvent.ACTION_MOVE:            break;        // 抬起        case MotionEvent.ACTION_UP:            // 记录触摸终点            mEnd = getScaleY();            int dScrollY = mEnd - mStart;            // 添加其他逻辑            break;    }    postInvalidate();    return true;}

下来我们对比一下 自定义View 和 自定义ViewGroup 的区别:

- 自定义View 自定义ViewGroup 绘制流程 onMeasure() -> onDraw() onMeasure() -> onLayout() 注意事项 不重写 onLayout() 方法 不重写 onDraw() 方法

6.事件的拦截机制分析

1.组成事件传递机制的三个方法

对于 ViewGroup 来说,可重写如下三个方法:

// 拦截事件,事件拦截的核心方法@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {    return super.onInterceptTouchEvent(ev);}// 处理事件@Overridepublic boolean onTouchEvent(MotionEvent ev) {    return super.onTouchEvent(ev);}// 分发事件@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    return super.dispatchTouchEvent(ev);}

对于 View 来说,可重写如下两个方法:

// 处理事件@Overridepublic boolean onTouchEvent(MotionEvent ev) {    return super.onTouchEvent(ev);}// 分发事件@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    return super.dispatchTouchEvent(ev);}

初始情况下,返回值都是 false,返回 true 则拦截。

2.深入事件传递机制的逻辑

首先有我们的一个 viewgroupA,在它里面有 viewgroupB,在 viewgroupB 里面有 viewC,当我们触摸 viewC 的时候就会触发事件,其事件传递机制的流程我们用如下表示:

这里写图片描述

最后送大家一句话:只有站在一个设计者的角度上,才可以更好地创建自定义View。

10 0