自定義帶進度的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就沒辦法畫出來的,如下圖:
方法二:使用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();
}
}