在android开发中,常会遇到一些设计是现有控件没有的效果,这时候通常需要我们自定义view来解决问题。先给出设计稿:
看上去有些像progressbar,不过android系统自带的progressbar并不能在滑块上显示当前数值,还是要用自定义view才能实现。
自定义view的三个方法:
函数 | 作用 |
---|---|
onMeasure() | 测量,测量View的宽高 |
onLayout() | 布局,计算当前View以及子View的位置;只有ViewGroup调用该方法 |
onDraw() | 绘制,根据设计稿绘制控件的实际效果 |
自定义view最关键的实现onMeasure()、onDraw(),本例继承自View类无需覆写onLayout()。最后加上事件处理才能完成完整的自定义view。
姑且把这个自定义控件称为CustomizedSeekBar吧。
自定义view的构造函数有四个。一般来说,自定义view的构造函数调用前两个就行,这里调用前三个,在第二个构造函数里显示调用第三个构造函数。
public CustomizedSeekBar(Context context) {
this(context, null);
}
public CustomizedSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomizedSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
……
initAttrs(context, attrs);
}
private void initAttrs(Context context, AttributeSet attrs) {
// Attribute initialization
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomizedSeekBar);
//省略获取style属性代码
a.recycle();
……
}
特别说明一下,TypedArray为什么要调用recycle()
方法。TypedArray类没有公共的构造函数,只提供静态方法获取实例。实际上 array 是从一个 array pool的池中获取;framework层维护了一个同步栈结构的对象池,从而避免在程序运行期间频繁创建属性值TypedArray对象,维护TypedArray对象池的大小默认为5,调用recyle()方法将不用的对象返回至对象池来达到重用的目的。
在来看看CustomizedSeekBar的style属性:
<declare-styleable name="CustomizedSeekBar">
<attr name="minValue" format="float" />
<attr name="maxValue" format="float" />
<attr name="showNum" format="boolean" />
<attr name="showThumb" format="boolean" />
<attr name="progress" format="integer" />
……
declare-styleable>
style属性里定义了各种CustomizedSeekBar的属性。
protected synchronized void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
width = MeasureSpec.getSize(widthMeasureSpec);
}
if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
setMeasuredDimension(width, height);
}
在MeasureSpec当中一共存在三种测量模式:UNSPECIFIED、EXACTLY 和
AT_MOST。
模式 | 意义 | 对应 |
---|---|---|
EXACTLY | 精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size | match_parent |
AT_MOST | 最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值 | wrap_content |
UNSPECIFIED | 无限制,View对尺寸没有任何限制,View设置为多大就应当为多大 | 一般系统内部使用 |
如果是继承子View类的onMeasure()方法比较套路化,只要设置好width 、height 最后调用 setMeasuredDimension()方法即可。
根据设计稿,可以将这个控件分解为以下三部分:
onDraw()方法可以根据以上三个步骤来绘制:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制背景
drawSeekBarBackground(canvas);
//绘制滑过的部分
drawSeekBarActiveOneWayLine(canvas);
//绘制滑块
drawThumb(canvas);
//绘制进度数字
drawProgress(canvas);
}
以下分别说明每个绘制步骤。
原理比较简单,绘制一个圆角矩形即可。
private void drawSeekBarBackground(Canvas canvas) {
RectF rectF = new RectF(0, 0, getWidth(), getHeight());
paint.setColor(mBgColor);
canvas.drawRoundRect(rectF, mRadius, mRadius, paint);
}
根据设计稿的效果,滑块滑过的部分是一个渐变色,只需要绘制出一个渐变的圆角矩形即可,android系统已经定义了各种各样的渐变类这里采用LinearGradient。
private void drawSeekBarActiveOneWayLine(Canvas canvas) {
RectF rectF = new RectF(0, 0, getWidth(), getHeight());
……
LinearGradient shader = new LinearGradient(
rectF.left, 0, rectF.right, getHeight(),
colors,
positions,
Shader.TileMode.MIRROR);
//gradient
gradientPaint.setShader(shader);
canvas.drawRoundRect(rectF, mRadius, mRadius, gradientPaint);
}
private void drawThumb(Canvas canvas) {
……
canvas.drawRoundRect(rectF, mRadius, mRadius, paint);
}
private void drawProgress(Canvas canvas) {
String progressText;
……
canvas.drawText(progressText,
x,
y,
textPaint);
}
以上,CustomizedSeekBar的主体代码就完成了。
绘制完控件,最重要的事情就是处理控件的事件了。就CustomizedSeekBar来说,即拖动设置当前进度值。
public boolean onTouchEvent(MotionEvent event) {
int pointerIndex;
final int action = event.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
isThumbPressed = true;
invalidate();
attemptClaimDrag();
break;
case MotionEvent.ACTION_MOVE:
if (isThumbPressed) {
//将屏幕像素值转换为归一化的滑块上的值
mProgress = ……;
setProgress(mProgress);
}
break;
case MotionEvent.ACTION_UP:
isThumbPressed = false;
//将屏幕像素值转换为归一化的滑块上的值
mProgress = ……;
setProgress(mProgress);
break;
case MotionEvent.ACTION_CANCEL:
isThumbPressed = false;
invalidate();
break;
default:
break;
}
return true;
}
/**
* Tries to claim the user's drag motion, and requests disallowing any
* ancestors from stealing events in the drag.
*/
private void attemptClaimDrag() {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
将屏幕像素值转换为归一化的滑块上的值思路其实就是计算在屏幕上滑动过的像素值与CustomizedSeekBar整体长度的比例,然后在取整。这里就不贴出代码了。
经过以上的步骤,一个自定义控件就完成了。给出了实现思路,实际在编码过程中,还需要大量的时间来调试以达到预期效果。在xml文件里,这样使用:
<com.package.view.CustomizedSeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:maxValue="10"
app:showNum="true"
app:showThumb="true"
app:thumbColor="@color/thumb_color"
app:thumbHeight="40dp"
app:thumbTextColor="@color/thumb_text_color"
app:thumbWidth="40dp" />