自定义带进度的Button

前段时间做了一个应用市场的项目,项目中需要一个带进度的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();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值