一、自定义View的步骤
1、自定义属性,将外部设置的参数定义为自定义属性,方便使用
2、重写onMeasure方法,View大小为wrap_content时根据View的特点计算宽高(注意如果支持padding测量时也要处理padding),match_parent和具体值时使用传入的widthMeasureSpec和heightMeasure的尺寸。通过setMeasureDimension设置具体的测量宽高。
3、重写onDraw方法,绘制界面(图片、文字、圆形、方形、横线等),如果要支持padding需要处理padding。
4、有滑动的重写onTouchEvent方法,处理滑动。处理完后调用invalidate方法触发onDraw重新绘制界面,这样达到滑动的效果。
5、定义Listener接口,外部通过接口知道View的状态。
注意:margin不用在View中考虑,是父View处理margin。padding需要在View中考虑即绘制时位置需要减去padding值,否则padding没有效果。
二、自定义ToggleBotton
下面以自定义ToggleBotton为例子介绍自定义View的详细流程。系统ToggleButton通过设置背景图片也可以达到常用的效果,但是没有滑动的效果。效果如下:
1、自定义属性
1)、在values目录下新建attrs.xml文件
2)、在attrs.xml中添加declare-styleable,一个declare-styleable就表示一个View的自定义属性,代码如下:
<declare-styleable name="ToggleButton">
<attr name="open_color" format="color"/>
<attr name="close_color" format="color"/>
<attr name="init_status" format="boolean"/>
</declare-styleable>
其中name是自定义View的类名,<attr name 就是属性名
3)、使用自定义属性。先定义命名空间
xmlns:app="http://schemas.android.com/apk/res-auto"
使用如下:
app:open_color="@android:color/holo_red_light"
4)、在View中如何获取布局文件中定义的自定义属性?
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ToggleButton);
mOpenColor = a.getColor(R.styleable.ToggleButton_open_color, Color.GREEN);
mCloseColor = a.getColor(R.styleable.ToggleButton_close_color, Color.WHITE);
mIsOpen = a.getBoolean(R.styleable.ToggleButton_init_status, true);
a.recycle();
attrs是构造函数中传入的。注意属性获取完了后要调用a.recycle()
2、重写onMeasure
在宽高为wrap_content时自己根据View的特点设置测量宽高,match_parent和具体值时使用父View传入的measureSpec。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode != MeasureSpec.EXACTLY) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultWidth + getPaddingLeft() + getPaddingRight(), MeasureSpec.EXACTLY);
}
if (heightSpecMode != MeasureSpec.EXACTLY) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mDefaultHeight + getPaddingTop() + getPaddingBottom(), MeasureSpec.EXACTLY);
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
3、重写onDraw
主要绘制三部分内容:
1)、绘制背景,默认是关闭时是白色,打开时是绿色
2)、绘制圆形按钮,和关闭时颜色一致
3)、关闭时绘制边框,因为关闭时是白色,可能和其父容器颜色一样,用边框加以区分
代码如下:
protected void onDraw(Canvas canvas) {
// 绘制背景
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mIsOpen ? mOpenColor : mCloseColor);
RectF rect = new RectF(left, top, right, bottom);
canvas.drawRoundRect(rect, mRroundConerRadius, mRroundConerRadius, mPaint);
// 绘制圆形按钮
mPaint.setColor(mCloseColor);
if (mIsSliding == false) {
mCircleButtonX = mIsOpen ? measureWidth - mRroundConerRadius : mRroundConerRadius;
mCircleButtonX += getPaddingLeft();
}
canvas.drawCircle(mCircleButtonX, mCircleButtonY, mCircleButtonRadius, mPaint);
// 关闭时绘制边框
if (mIsOpen == false) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dp2px(1));
mPaint.setColor(0XffDDDDDD);
canvas.drawRoundRect(rect, mRroundConerRadius, mRroundConerRadius, mPaint);
canvas.drawCircle(mCircleButtonX, mCircleButtonY, mCircleButtonRadius, mPaint);
}
}
重写onDraw实际就是用canvas绘制图形,绘制自定义View需要的图形。上面绘制了圆角矩形、圆形以及边框。注意绘制边框时要设置paint的style为STROKE,默认是FILL填充内部。
4、滑动处理
为了实现圆形按钮的滑动效果,我们需要重写onTouchEvent方法,获取每次滑动的距离,根据这个距离重写绘制View即重新回调onDraw(调用invalidate就会回调onDraw),从而达到滑动的效果。代码如下:
private float actionDownX;
private long actionDownTime;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actionDownX = event.getX();
actionDownTime = System.currentTimeMillis();
Log.d(TAG, "downx:" + actionDownX + ",downy:" + event.getY());
break;
case MotionEvent.ACTION_MOVE:
mIsSliding = true;
float moveX = event.getX() - actionDownX;
actionDownX = event.getX();
mCircleButtonX += moveX;
// 圆形按钮的边界限制
if (mCircleButtonX > measureWidth - mRroundConerRadius) {
mCircleButtonX = measureWidth - mRroundConerRadius;
} else if (mCircleButtonX < mRroundConerRadius) {
mCircleButtonX = mRroundConerRadius;
}
invalidate();
break;
case MotionEvent.ACTION_UP:
boolean oldIsOpen = mIsOpen;
if (System.currentTimeMillis() - actionDownTime < 300) {
// 单击按钮的情况
mIsOpen = !mIsOpen;
} else {
// 滑动时位置超过一半打开,否则关闭
if (mCircleButtonX > measureWidth * 0.5f) {
mIsOpen = true;
} else {
mIsOpen = false;
}
}
invalidate();
mIsSliding = false;
break;
}
return true;
}
一个完整的点击事件回调如下:ACTION_DOWN->ACTION_UP或者ACTION_DOWN->ACTION_MOVE(一个或者多个)->ACTION_UP。
因为只能在x轴上滑动,所以我们只获取了在x轴上滑动的距离。这里有两种情况,一种是点击,一种是滑动。可以通过该次点击事件的时间区分是滑动还是点击,时间小于300ms时认为是点击,大于时认为是滑动。
还要注意滑动边界的处理。
5、设置Listener
对外提供接口监听ToggleButton开关状态的切换
Listener接口类:
public interface ToggleButtonStatusListener {
/**
* 状态改变时回调
*/
void onChange(boolean isOpen);
}
View中开关状态改变时回调监听器:
case MotionEvent.ACTION_UP:
boolean oldIsOpen = mIsOpen;
if (System.currentTimeMillis() - actionDownTime < 300) {
// 单击按钮的情况
mIsOpen = !mIsOpen;
} else {
// 滑动时位置超过一半打开,否则关闭
if (mCircleButtonX > measureWidth * 0.5f) {
mIsOpen = true;
} else {
mIsOpen = false;
}
}
invalidate();
// 状态改变时回调
if (oldIsOpen != mIsOpen && mToggleButtonStatusListener != null) {
mToggleButtonStatusListener.onChange(mIsOpen);
}
mIsSliding = false;
break;