在我们的日常开发中自定义控件还是用的挺多的,设计师或者产品为了更好的漂亮,美观,交互都会做一写牛逼的ui效果图,但是最后实现的还是我们程序员啊。
所以说 自定义view你还是得会的。 今天我们要实现的每一交互性的view,所以就继承子view。
自定义view的套路,套路很深
这么看来,自定义view的套路很清晰嘛。
我们看下今天的效果图
我们按照套路来。
一.自定义属性
1
2
3
4
5
6
7
8
9
|
<declare-styleable name=
"WaveProgressView"
>
</attr></attr></attr></attr></attr></attr></attr></declare-styleable>
|
看下效果图我们就知道因该需要哪些属性。就不说了。
然后就是获取我们的这些属性,就是用TypedArray来获取。当然是在构造中获取,一般我们会复写构造方法,少参数调用参数多的,然后走到参数最多的那个。
1
2
3
4
5
6
7
8
9
|
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.WaveProgressView, defStyleAttr, R.style.WaveProgressViewDefault);
radius = (
int
) a.getDimension(R.styleable.WaveProgressView_radius, radius);
textColor = a.getColor(R.styleable.WaveProgressView_progress_text_color,
0
);
textSize = a.getDimensionPixelSize(R.styleable.WaveProgressView_progress_text_size,
0
);
progressColor = a.getColor(R.styleable.WaveProgressView_progress_color,
0
);
radiusColor = a.getColor(R.styleable.WaveProgressView_radius_color,
0
);
progress = a.getFloat(R.styleable.WaveProgressView_progress,
0
);
maxProgress = a.getFloat(R.styleable.WaveProgressView_maxProgress,
100
);
a.recycle();
|
注: R.style.WaveProgressViewDefault是这个控件的默认样式。
二.onMeasure测量
我们重写这个方法主要是更具父看见的宽和高来设置自己的宽和高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
//计算宽和高
int
exceptW = getPaddingLeft() + getPaddingRight() +
2
* radius;
int
exceptH = getPaddingTop() + getPaddingBottom() +
2
* radius;
int
width = resolveSize(exceptW, widthMeasureSpec);
int
height = resolveSize(exceptH, heightMeasureSpec);
int
min = Math.min(width, height);
this
.width =
this
.height = min;
//计算半径,减去padding的最小值
int
minLR = Math.min(getPaddingLeft(), getPaddingRight());
int
minTB = Math.min(getPaddingTop(), getPaddingBottom());
minPadding = Math.min(minLR, minTB);
radius = (min - minPadding *
2
) /
2
;
setMeasuredDimension(min, min);
}
|
首先该控件的宽和高肯定是一样的,因为是个圆嘛。其实是宽和高与半径和内边距有关,这里的内边距,我们取上下左右最小的一个。宽和高也选择取最小的。
this.width = this.height = min; 包含左右边距。
resolveSize这个方法很好的为我们实现了我们想要的宽和高我慢看下源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public
static
int
resolveSizeAndState(
int
size,
int
measureSpec,
int
childMeasuredState) {
final
int
specMode = MeasureSpec.getMode(measureSpec);
final
int
specSize = MeasureSpec.getSize(measureSpec);
final
int
result;
switch
(specMode) {
case
MeasureSpec.AT_MOST:
if
(specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
}
else
{
result = size;
}
break
;
case
MeasureSpec.EXACTLY:
result = specSize;
break
;
case
MeasureSpec.UNSPECIFIED:
default
:
result = size;
}
return
result | (childMeasuredState & MEASURED_STATE_MASK);
}
|
如果我们自己写也是这样写。
最后通过setMeasuredDimension设置宽和高。
三.onDraw绘制
关于绘制有很多android 提供了很多API,这里就不多说了。
绘制首先就是一些画笔的初始化。
需要提一下绘制path路径的画笔设置为PorterDuff.Mode.SRC_IN模式,这个模式只显示重叠的部分。
1
2
3
4
|
pathPaint =
new
Paint(Paint.ANTI_ALIAS_FLAG);
pathPaint.setColor(progressColor);
pathPaint.setDither(
true
);
pathPaint.setXfermode(
new
PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
我们要将所有的绘制 绘制到一个透明的bitmap上,然后将这个bitmap绘制到canvas上。
1
2
3
4
|
if
(bitmap ==
null
) {
bitmap = Bitmap.createBitmap(
this
.width,
this
.height, Bitmap.Config.ARGB_8888);
bitmapCanvas =
new
Canvas(bitmap);
}
|
为了方便计算和绘制,我将坐标系平移padding的距离
1
2
3
4
5
|
bitmapCanvas.save();
//移动坐标系
bitmapCanvas.translate(minPadding, minPadding);
// .... some thing
bitmapCanvas.restore();
|
3.1绘制圆
1
2
|
<code> bitmapCanvas.drawCircle(radius, radius, radius, circlePaint);
</code>
|
3.2绘制PATH 路径.
一是要实现波纹的左右飘,和上下的振幅慢慢的减小
绘制这个之前我们需要知道二阶贝塞尔曲线的大致原理。
简单的说就是知道:P1起始点,P2是终点,P1是控制点.利用塞尔曲线的公式就可以得道沿途的一些点,最后把点连起来就是喽。
下面这个图片来于网络:
在android-sdk里提供了绘制贝塞尔曲线的函数rQuadTo方法
1
|
<code>
public
void
rQuadTo(
float
dx1,
float
dy1,
float
dx2,
float
dy2) </code>
|
dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标,可为负值,正值表示相加,负值表示相减; dy1:控制点Y坐标,相对上一个终点Y坐标的位移坐标。同样可为负值,正值表示相加,负值表示相减; dx2:终点X坐标,同样是一个相对坐标,相对上一个终点X坐标的位移值,可为负值,正值表示相加,负值表示相减; dy2:终点Y坐标,同样是一个相对,相对上一个终点Y坐标的位移值。可为负值,正值表示相加,负值表示相减;
这四个参数都是传递的都是相对值,相对上一个终点的位移值。
要实现振幅慢慢的减小我们可以调节控制点的y坐标即可,即:
float percent=progress * 1.0f / maxProgress;
就可以得到[0,1]的
一个闭区间,[0,1]这货好啊,我喜欢,可以来做很多事情。
这样我们就可以根据percent来调节控制点的y坐标了。
1
2
3
4
5
6
7
8
|
<code>
//根据直径计算绘制贝赛尔曲线的次数
int
count = radius *
4
/
60
;
//控制-控制点y的坐标
float
point = (
1
- percent) *
15
;
for
(
int
i =
0
; i < count; i++) {
path.rQuadTo(
15
, -point,
30
,
0
);
path.rQuadTo(
15
, point,
30
,
0
);
}</code>
|
要实现左右波纹只需要控制闭合路径的左上角的x坐标即可,当然也是根据percent喽。
大家可以结合下面这个图来理解下上面的话。
path绘制的完整代码片段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<code>
//绘制PATH
//重置绘制路线
path.reset();
float
percent=progress *
1
.0f / maxProgress;
float
y = (
1
- percent) * radius *
2
;
//移动到右上边
path.moveTo(radius *
2
, y);
//移动到最右下方
path.lineTo(radius *
2
, radius *
2
);
//移动到最左下边
path.lineTo(
0
, radius *
2
);
//移动到左上边
// path.lineTo(0, y);
//实现左右波动,根据progress来平移
path.lineTo(-(
1
-percent) * radius*
2
, y);
if
(progress !=
0
.0f) {
//根据直径计算绘制贝赛尔曲线的次数
int
count = radius *
4
/
60
;
//控制-控制点y的坐标
float
point = (
1
- percent) *
15
;
for
(
int
i =
0
; i < count; i++) {
path.rQuadTo(
15
, -point,
30
,
0
);
path.rQuadTo(
15
, point,
30
,
0
);
}
}
//闭合
path.close();
bitmapCanvas.drawPath(path, pathPaint);</code>
|
3.3绘制进度的文字
这个就比较简单了,绘制在控件的中间即可。关于文字的坐标计算还是很好理解的。
1
2
3
4
5
6
7
|
<code>
//绘制文字
String text = progress +
"%"
;
float
textW = textPaint.measureText(text);
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float
baseLine = radius - (fontMetrics.ascent + fontMetrics.descent) /
2
;
bitmapCanvas.drawText(text, radius - textW /
2
, baseLine, textPaint);
</code>
|
最后别忘了把我们的bitmap绘制到canvas上。
canvas.drawBitmap(bitmap, 0, 0, null);
哦,最后是实用方法,这里我们不用thread+handler,我们用属性动画。
你懂的!!!,like
1
2
3
4
5
|
<code> ObjectAnimator objectAnimator0 = ObjectAnimator.ofFloat(waveProgressView_0,
"progress"
, 0f, 100f);
objectAnimator0.setDuration(
3300
);
objectAnimator0.setInterpolator(
new
LinearInterpolator());
objectAnimator0.start();
</code>
|
至此,也就实现了我们的效果。
最后给出源码的下载地址: