1. 程式人生 > >自定義帶進度的Button

自定義帶進度的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();
    }
}