自定义View梳理

有点顾不上Blog,一直在忙项目和琐事,还想着学其他语言..自定义View一直在用,但是缺少系统的总结,开发过程中也难免google,很多细节容易混淆。在此理一遍流程和需要注意的点。

UI

绘制UI,继承自View/ViewGroup后,一般还需要复写最基本的二/三个方法(除onLayout)–onMeasure,onLayout,onDraw。作用分别为:
onMeasure,用于本View宽高的测量,布局复杂时可能触发多次。官档所说如下:这是一个parentViews告知该View能够使用多大空间的时间点,也是该View有机会向父布局告知自己desireSize的时机。此处应有用户自行协商并调用setMeasuredDimension确定实际大小。ViewGroup的onMeasure则负责处理它children的测量工作。
onLayout,常复写于viewGroup的自定义子类。它有责任对它内部所有children进行处理,告知childrenView的ltrb,以正确摆放。
onDraw,UI最终呈现的过程,用户使用Paint(What to draw)、Canvas(How to draw)两个类完成自定义画面。

onMeasure

官方教程对onMeasure给了示例,tips给了以下三点:

1. resolveSizeAndState() help method.
2. remember call setMeasuredDimension().
3. should consider padding.

一个一个分析,首先使用View类中提供的工具方法resolveSizeAndState,可以较好的实现“父类的限制”和“本子类需求”协商过程。初学自定义View时最头疼的就是onMeasure中measureSpec类以及它的三种模式。其实看看resolveSizeAndState方法,或stackoverflow上大牛的回答(参考http://stackoverflow.com/questions/12266899/onmeasure-custom-view-explanation),不难发现协商测量过程的规律。MeasureSpec就是一个工具类,利用位运算的特点将size和mode整合在一起。父亲对子类有限制,无限制和强制,分别对应AT_MOST、UNSPECIFIED、EXACTLY。需要注意的是resolveSizeAndState方法第三个参数childMeasuredState,找了些资料也没能完全明白它的用途,应该是自定义viewGroup时才使用用于记录children测量状态的,一般自定义View传0即可(参考http://stackoverflow.com/questions/13650903/whats-the-utility-of-the-third-argument-of-view-resolvesizeandstate)。
onMeasure方法没有返回值,所以测量的结果应该通过setMeasuredDimension方法告知系统。
最后关于padding。网上讲自定义view padding的并不多,与margin不同,padding是属于本View的属性,不同于margin(不需要自定义时做处理系统就能很好的使用margin),所以要在测量绘图时考虑它。我通常这么处理:

  • 测量时:desireSize=实际所需size+相应方向的padding。
  • 绘图时:考虑padding,做相应的位移。

一段简单的示例代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    LogUtils.d(this, "parent wMode=" + MeasureSpec.getMode(widthMeasureSpec) + ", parent hMode=" + MeasureSpec.getMode(heightMeasureSpec));
    LogUtils.d(this, "parent wSize=" + MeasureSpec.getSize(widthMeasureSpec) + ", parent hSize=" + MeasureSpec.getSize(heightMeasureSpec));

    //third param. usually 0. http://stackoverflow.com/questions/13650903/whats-the-utility-of-the-third-argument-of-view-resolvesizeandstate
    int w = resolveSizeAndState(getDesireW(), widthMeasureSpec, 0);
    int h = resolveSizeAndState(getDesireH(), heightMeasureSpec, 0);
    LogUtils.d(this, "resolve W=" + MeasureSpec.getSize(w) + ", resolve H=" + MeasureSpec.getSize(h));

    setMeasuredDimension(MeasureSpec.getSize(w), MeasureSpec.getSize(h));
}

Draw时考虑padding:

canvas.drawRect(getPaddingLeft(), getPaddingTop(), getMeasuredWidth() - getPaddingRight(), getMeasuredHeight() - getPaddingBottom(), mPaint);

onLayout

ViewGroup中onLayout是抽象方法必须复写,这是children位置能正确摆放的保证。依靠mLeft,mTop,mRight,mBottom这四个值,以坐上为原点,这四个值分别为对应边到原点的距离。最后和onMeasure一样,记得调用child.layout()方法。

onDraw

一般都是在该View构造时创建画笔paint,因为如果在onDraw中新建过多对象会严重影响绘制速度。paint决定了画的风格:实心、空心、颜色、抗锯齿,这个具体看paint类。Canvas可以画矩形、圆形、椭圆、线、路径等,还可以画Bitmap,画字体。其中字体的绘制大有门道,包括自定义typeface(assets中读取font资源)、字体对齐、字体居中等,(参考http://blog.csdn.net/hursing/article/details/18703599),不在此赘述。
注意,绘制动态View时往往要多次调用重绘invalidate。所以onDraw方法应足够解耦和完整,保证一次invalidate能够正常工作,仅依赖少量可变的参数。

其他方法

官档一张图很能说明问题。

自定义Attributes

Google提供了自定义属性方便自定义View更好的定制。个人感觉这部分代码多为复制粘贴,记住有这么个路子并能够熟练使用就好,尤其自定义控件开发时自定义属性的抽取优先级应当放的最低。

Touch处理

自定义View的时候也经常需要考虑点击事件的分发,所以打算把TouchEvent的处理注意事项也写下来,便于加强记忆。见下篇。

其他

Mask

看View源码时发现使用掩码位运算的例子很多,其实学过来用在自己的项目中能够提升一定的效率和逼格。
Flag多是多种状态的集合,杂乱且不具有可读性。必须借助Mask的筛选,来分离出感兴趣的位置,从而得知状态。这部分笔记比较乱,待整理TODO。

(mViewFlags & VISIBILITY_MASK) == GONE

flag和mask的基本用法。位与后知道当前关心的某属性。

int old = mViewFlags;
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
int changed = mViewFlags ^ old;

这里是setFlags(int flags, int mask)的方法。
(mViewFlags & ~mask):取当前flag的mask之外所有位。
(flags & mask):mask过滤取当前作用位。最后或运算合并。
changed:异或,将修改的位置1。

if ((changed & DRAWING_CACHE_QUALITY_MASK) != 0)

mask保证筛选出自己关心的标志位,然后根据是否修改来更新View。

总之位运算性能优,而且只需一个flag便可知晓所有state,便于统一管理。比如我在项目中曾经需要封装sdk,从调用者的Actvity-A打开sdk中的Activity-B,遇到错误时Bfinish掉,并返回错误代码在onActivityResult中处理。这时将出错逻辑交给调用者处理最佳,但是发现错误代码+其他应返回的有用信息零零散散一堆,最终封成一个flag返回,结合mask解出有用信息即可。

eg:

public boolean isEnabledNextPtrAtOnce() {
    return (mFlag & FLAG_ENABLE_NEXT_PTR_AT_ONCE) > 0;
}

public void setEnabledNextPtrAtOnce(boolean enable) {
    if (enable) {
        mFlag = mFlag | FLAG_ENABLE_NEXT_PTR_AT_ONCE;
    } else {
        mFlag = mFlag & ~FLAG_ENABLE_NEXT_PTR_AT_ONCE;
    }
}