前段时间做了一个应用市场的项目,项目中需要一个带进度的Button,如下图:
可以观察到大致有三点要求:
1、Button会显示各种状态;
2、下载过程中要显示下载进度;
3、被进度覆盖的文字颜色与未被覆盖的文字颜色不同。
首先可以肯定的是必须通过自定义View来实现,那怎么实现了,我们来一点一点分析。
第一点,显示状态比较容易实现,直接忽略。
看第二点,如何实现进度?进度的计算倒不难,难的是如何将它画出来!如果Button外观是个矩形倒好办,但事实却是一个圆角矩形,画进度时左边要画圆角,右边要画直角。这种不规则的东西要怎么画呢?想到有几个方法:
方法一:使用Path来画
创建两个path,path1画一个矩形,宽度为进度对应的数字,path2画整个外部的圆角矩形,通过path.op方法取两个path的交集,然后把交集画出来,即为当前进度,代码如下
Path path1 = new Path();
//20为进度
RectF rectF1 = new RectF(0, 0, 20, getHeight());
path1.addRect(rectF1, Path.Direction.CW);
Path path2 = new Path();
path2.addRoundRect(rectF, radius, radius, Path.Direction.CW);
path1.op(path2, Path.Op.INTERSECT);
canvas.drawPath(path1, paint);
但是path.op方法只有android4.4及以上的系统支持,不能兼容低版本的系统。
Region也可以实现取交集,但是Region是通过绘制一个个Rect来组成最终的图形,这样绘制出来的圆弧边缘会有锯齿。只能换另外的方式,如下图:
path先moveTo到点1; 再arcTo到点3;点2 为arcTo的起始点,处于rectF的270角度线上;最后lineTo到点4,代码如下:
Path path = new Path();
//50为进度
path.moveTo(50 , 0);
RectF rectF = new RectF(0, 0, getHeight(), getHeight());
//sweepAngle 扫描角度,正数顺时针方向,负数逆时针方向
path.arcTo(rectF, 270, -180);
path.lineTo(50, rectF.bottom);
path.close();
貌似可以,但是这种方式对于上图中的进度可以画出来,对于下图中进度还在圆弧范围内的情况,arcTo就没办法画出来的,如下图:
因此使用Path的方法不可取。
方法二:使用Paint的PorterDuffXfermode
在方法一中,我们矩形和圆角矩形的交集,即为我们要画的进度,而Paint就可以通过setXfermode方法取两者的交集。我们来试试,代码如下:
//20为进度
Bitmap bitmap = Bitmap.createBitmap(20, getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas1 = new Canvas(bitmap);
canvas1.drawRoundRect(rectF, radius, radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas1.drawRect(0, 0, 20, getHeight(), paint);
canvas.drawBitmap(bitmap, 0, 0, null);
paint.setXfermode(null);
代码中,必须要新建一个bitmap来画进度,并且bitmap必须为Bitmap.Config.ARGB_8888,宽度必须为进度对应的值,不然会画不出来进度。另外这里使用PorterDuff.Mode.DST_IN或PorterDuff.Mode.SRC_IN都可以,因为Src和Dst的颜色都是一样的。
这种方法不管进度为多少都可以画出来,并且没有低版本兼容问题。但是使用Bitmap会导致占用的内存多一些。
方法三:使用线性渐变LinearGradient
LinearGradient 的构造方法中有一个positions[]参数,用于控制各个颜色分布的比重,如果传null,颜色会均匀分布。
在有进度的状态下,Button分为进度区域和非进度区域两部分,进度区域有颜色,非进度区域为透明,我们可以构造一个LinearGradient ,只包含进度颜色和透明颜色两种,并且使用positions[]来控制进度,第一个float值为progress,第二个float为0或者其他值都可以,代码如下:
LinearGradient progressGradient = new LinearGradient(0, 0, width, 0,
new int[]{blueColor, Color.TRANSPARENT},
new float[]{progress, 0},//两种颜色占的比重
LinearGradient.TileMode.CLAMP);
这里非进度区域必须为透明颜色或是Button的背景颜色,比重设置为0并不是不会画出这个颜色,而是非进度区域都会是这个颜色。
好了,进度的问题解决了,再看看第三个问题,怎么实现文字覆盖部分和非覆盖部分颜色的不同
有了解决第二个问题的方法,其实这个问题也很好实现,还是使用LinearGradient,绘制文字时,计算文字被覆盖的进度值,然后给paint设置LinearGradient即可。
好了,所有的问题都解决了,最后贴下源码:
public class DownloadButton extends View {
private Paint paint;
private int blueColor;
private int whiteColor;
private float baseLine;
private RectF rectF;
private String statusText;
private float progress;
public DownloadButton(Context context) {
this(context, null);
}
public DownloadButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DownloadButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DownloadButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
blueColor = getResources().getColor(R.color.colorPrimary);
whiteColor = getResources().getColor(R.color.gray_white);
int textSize = getResources().getDimensionPixelOffset(R.dimen.normal_text_size);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(textSize);
paint.setColor(blueColor);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
rectF = new RectF(1, 1, w - 1, h - 1);
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
baseLine = h / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float height = rectF.height();
float width = rectF.right;
if (height <= 0 || width <= 0) {
return;
}
paint.setShader(null);
paint.setColor(blueColor);
float radius = height / 2;
if (progress == 1) {
paint.setStyle(Paint.Style.FILL);
} else {
paint.setStyle(Paint.Style.STROKE);
}
canvas.drawRoundRect(rectF, radius, radius, paint);
paint.setStyle(Paint.Style.FILL);
if (progress > 0 && progress < 1) {
LinearGradient progressGradient = new LinearGradient(0, 0, width, 0,
new int[]{blueColor, Color.TRANSPARENT},
new float[]{progress, 0},//两种颜色占的比重
LinearGradient.TileMode.CLAMP);
paint.setShader(progressGradient);
canvas.drawRoundRect(rectF, radius, radius, paint);
paint.setShader(null);
}
if (TextUtils.isEmpty(statusText)) {
return;
}
rectF.right = width * progress;
float textWidth = paint.measureText(statusText);
float textLeft = width / 2 - textWidth / 2;
float textRight = width / 2 + textWidth / 2;
if (rectF.right >= textRight) {//进度完全覆盖了文字,文字不用计算进度,全部显示白色
paint.setColor(whiteColor);
} else if (rectF.right > textLeft) {//进度覆盖了文字,但是没有完全覆盖,计算文字进度
float textProgress = (rectF.right - textLeft) / textWidth;
LinearGradient textGradient = new LinearGradient(textLeft, 0, textRight, 0,
new int[]{whiteColor, blueColor},
new float[]{textProgress, 0},
LinearGradient.TileMode.CLAMP);
paint.setShader(textGradient);
}
canvas.drawText(statusText, width / 2, baseLine, paint);
rectF.right = width;
}
public void setProgress(@FloatRange(from = 0.0f, to = 1.0f) float progress) {
this.progress = progress;
invalidate();
}
public void setStatusText(@StringRes int resid) {
statusText = getResources().getString(resid);
invalidate();
}
public void setProgressAndText(@FloatRange(from = 0.0f, to = 1.0f) float progress, @StringRes int resid) {
this.progress = progress;
statusText = getResources().getString(resid);
invalidate();
}
}