4.4.1 自定义 View 的分类
自定义 View 可以分为 4 类。
1.继承 View 重写 onDraw 方法
这种方法主要用于实现一些不规则的效果,既要重写 onDraw 方法,这种方式需要自己支持 wrap_content,并且 padding 也需要自己处理。
2.继承 ViewGroup 派生特殊的 Layout
这种方法主要用于实现自定义布局,这种方式稍微复杂一些,需要合适的处理 ViewGroup 的测量、布局这两个过程,并同事处理子元素的测量和布局过程。
3.继承特定的 View(比如 TextView)
这种方法比较常见,一般用于扩展某种已有的 View 的功能,这种方法不需要自己支持 wrap_content 和 padding 等。
4.继承特定的 ViewGroup(比如 LiearLayout)
这种方法也比较常见,这种方法不需要自己处理 ViewGroup 的测量和布局。
4.4.2 自定义 View 须知
1. 让 View 支持 wrap_content
这是因为直接继承 View 或者 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做处理,在布局中使用 wrap_content 将达不到预期效果。解决方法在:View 的工作原理(上)4.3.1 measure
2. 如果有必要,让你的 View 支持 padding
这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding,那么 padding 将无效。另外,继承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响。
3. 尽量不要在 View 中使用 Handler,没必要
这是因为 View 内部本身就提供了 post 方法,当然除非你明确的要使用 Handler 来发送消息。
4. 如果有线程或者动画,需要及时停止,参考 View # onDetachedFromWindow
onDetachedFromWindow 在包含此 View 的 Activity 退出或者当前 View 被 remove 时调用, 可以在 该方法中停止线程和动画。
5. View 带有滑动嵌套情形时,需要处理好滑动冲突
滑动冲突肯定要处理。
4.4.3
1.继承 View 重写 onDraw 方法
简单的写一个圆:
public class CircleView extends View {
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width / 2, height / 2);
canvas.drawCircle(width/2,height/2, radius,mPaint);
}
}
如图可见,在没做 padding 和 wrap_content 处理时,这两个设定是不生效的,而 margin 是由父容器控制的。再对做 padding 和 wrap_content 处理。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);//单位是 px
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthMeasureSpec, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingRight = getPaddingRight();
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2 + paddingLeft, height / 2 + paddingTop, radius, mPaint);
}
最后,为了让我们的 View 更加容易使用,我们需要为其提供自定义属性。在 values 中新建一个 XML 。
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//如果没有设置,默认为黄色
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.BLUE);
a.recycle();//解析完自定义属性之后要回收
init();
}
private void init() {
mPaint.setColor(mColor);
}
2.继承 ViewGroup 派生特殊的 layout
直接看 HorizontalScrollViewEx.java 的 onMesure 方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
//测量子元素
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
//如果设置了 wrap_content
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
//获取第一个子元素
final View childView = getChildAt(0);
//假设每一个资源的宽和高都是一样的,直接乘以子元素数量就是总宽度
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
上述代码逻辑:先判断是否有子元素,;然后在判断是否使用了 wrap_content。没有考虑 padding 属性并且当没有子元素时宽高直接设为 0 (应当根据 LayoutParams 中的宽高来做处理)。
再看 HorizontalScrollViewEx.java 的 onLayout 方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
//判断子元素是否 GONE
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
//给子元素放置到合适的顶点位置 参数:(左,上,右,下)
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
//从左往右放置
childLeft += childWidth;
}
}
}
上述代码作用是遍历子元素并完成子元素的定位, childLeft += childWidth,实现子元素从左往右放置。
HorizontalScrollViewEx.java 这个自定义 View 包含了 measure、layout、滑动冲突的实现方法,得掌握之!
4.4.4 自定义 View 的思想
自定义 View 尤其关键的必须要掌握基本功: 比如 View 的弹性滑动、滑动冲突、绘制原理等。