自定义View_一个带悬浮窗的ProgressBar(中)
在上篇内容【自定义View_一个带悬浮窗的ProgressBar(上)】里,我们已经完成了自定义的进度条效果,那么本篇内容我们来介绍悬浮窗如何实现,先来看一下我们今天要实现的效果:
效果分析
-
悬浮窗有边框,可以使用Path绘制。
-
悬浮窗有填充色,可以使用Path绘制,Path填充。
-
悬浮窗有文字,进度百分比,可以使用drawText绘制。
-
悬浮窗的宽度可以随文字动态变化,这里我们就把文字的宽度作为悬浮窗的宽度(不考虑两端圆弧)。
-
悬浮窗底部有箭头,并且箭头可以根据百分比进行位置偏移。
经过上面对效果的拆解分析,我们已经大概有了实现方案,那就是使用Path进行开发。那么Path该如何绘制?从哪里作为起点开始绘制,需要绘制那些片段,是我们需要分析重中之重。
Path轨迹
由于悬浮窗底部箭头的位置是需要根据百分比动态偏移的,它影响整个Path轨迹的绘制,也就是说它影响着较多的其他变量的变化,那么我们就把箭头的位置作为起点开始绘制,绘制过程示意如下:
动作分解
我们可以把上面的动图进行分解成下面7个片段:
①、从起点开始,向左上方画一条线段
②、紧接着往左方形画一条线段
③、再继续画一个半圆弧
④、往右画一条线段
⑤、再继续画一个半圆弧
⑥、继续往左画一条线段
⑦、闭合
如何开始?
下面的箭头,我们可以把它当成一个等边三角形,并且给它一个初始高度mArrowHeight。
再仔细看下悬浮窗的动效可以发现,底部箭头是需要根据当前的百分比而发生位置偏移的,如下:
当0%的时候,箭头在最左侧,随着百分比的增大,箭头逐渐移动到最右边:
我们需要知道,整个过程中箭头移动的总距离是多少,才能控制其偏移量。
已知箭头的高度是mArrowHeight,悬浮窗左右两个半圆弧的半径是radius。
假设底部起点坐标是(firstX, firstY),那么在上面这个过程中,我们需要明确firstX移动的范围,示意图如下:
根据示意图,也就得到了firstX移动范围是:[raidus+c , getWidth() - radius - c]
移动的总距离是:
// 箭头活动范围,对应的距离长度是:distance
float distance = getWidth() - 2 * radius - 2 * c
上面的计算中,还需要求出c的值,那么如何求出c可以使用三角函数公式:
根据:
tan30° = c / mArrowHeight
可以求出:
c =tan30° * mArrowHeight
即:
double rad = 30 * Math.PI / 180.0; // 角度转成弧度
c = (float) (Math.tan(rad) * mArrowHeight);
计算坐标,绘制
我们知道,起点坐标是动态偏移的,是和当前百分比(currentPercent)相关联的,那么当百分比在某个数值时,我们需要计算线段②的长度,即下图中的②:
上图中④的长度是等于文字的宽度的,即④=textWidth,那么②(leftDistance)就等于:
// 箭头左边的线段的长度
float leftDistance = (textWidth - 2 * c) * currentPercent;
其中,文字宽度计算如下:
// 获取当前文字
private String getText() {
return "进度 " + df.format(this.currentPercent* 100) + " %";
}
// 计算文字的宽度
private float getTextWidth() {
String text = getText();
return paintProgress.measureText(text, 0, text.length());
}
接下来,我们就可以逐个计算出坐标了。
第1个坐标,即起点坐标:
// 1
float firstX = radius + c + (currentPercent* distance);
float firstY = getHeight();
path.moveTo(firstX, firstY);
第2个坐标:
// 2
float secondX = firstX - c;
float secondY = getHeight() - mArrowHeight;
path.lineTo(secondX, secondY);
第3个坐标:
// 3
float thirdX = firstX - c - leftDistance;
float thirdY = getHeight() - mArrowHeight;
path.lineTo(thirdX, thirdY);
第4个坐标:
// 4
float left = firstX - c - leftDistance - radius;
float top = 0;
float right = firstX - c - leftDistance + radius;
float bottom = getHeight() - mArrowHeight;
leftRect.set(left, top, right, bottom);
path.arcTo(leftRect, 90, 180);
第5个坐标:
// 5
float fourthX = firstX - c - leftDistance + textWidth;
float fourthY = 0;
path.lineTo(fourthX, fourthY);
第6个坐标:
//6
float left2 = firstX - c - leftDistance + textWidth - radius;
float top2 = 0;
float right2 = firstX - c - leftDistance + textWidth + radius;
float bottom2 = getHeight() - mArrowHeight;
rightRect.set(left2, top2, right2, bottom2);
path.arcTo(rightRect, -90, 180);
第7个坐标:
// 7
float fifthX = firstX + c;
float fifthY = getHeight() - mArrowHeight;
path.lineTo(fifthX, fifthY);
path.close();
绘制轨迹和文字:
//绘制轨迹-填充
canvas.drawPath(path, paintFill);
//绘制轨迹-边框
canvas.drawPath(path, paintStroke);
// 绘制文字,百分比
Paint.FontMetricsInt fontMetricsInt = paintProgress.getFontMetricsInt();
float dy2 = (fontMetricsInt.bottom - fontMetricsInt.top) / 2f - fontMetricsInt.bottom;
float baseLine = (getHeight() - mArrowHeight) / 2f + dy2;
canvas.drawText(getText(), thirdX, baseLine, paintProgress);
加动画:
/**
* 设置当前进度的数据,带动画
*
* @param progress 百分比
*/
public void setProgressWithAnim(float progress) {
ValueAnimator animator = ValueAnimator.ofFloat(0, progress).setDuration(ANIMATION_DURATION);
animator.addUpdateListener(animation -> {
currentPercent = (float) animation.getAnimatedValue() / 100.0f;
invalidate();
});
animator.start();
}
至此,我们已经完成了今天要实现的效果。