Android自定義View——矩形圓角扇形進度View
概述
最近的項目需求中,有一個顯示下載進度的需求。大概是這樣子的,一個圓角矩形ImageView作爲背景圖,以這個矩形的中心作爲圓心,圓角矩形的外切圓上的某個點爲起點,進度爲矩形上某個點與起點的角度。位於進度範圍內的圓角矩形部分爲透明,其它部分爲半透明遮罩。效果如下圖所示(因爲背景圖片並不是規則矩形,所以效果有點差):
矩形圓角扇形進度View的需求
如上圖,剩餘進度是半透明遮罩,完成進度部分爲透明,可以設置進度,圓角,半透明遮罩的顏色,還有一個起始角度。相對來說需求很簡單,和iOS的app升級時桌面上的升級進度效果基本一致。完成這個需求,自然而然地想到了自定義View,自定義一個遮罩層View,覆蓋在ImageView上就可以了。
矩形圓角扇形進度View
- 新建attrs.xml定義屬性文件,並增加相應的屬性;
<declare-styleable name="CircleProgress">
<attr name="circleProgress" format="integer"/> <!-- 進度0-100 -->
<attr name="startAngle" format="integer"/> <!-- 開始的角度0-360 -->
<attr name="circleCorner" format="dimension" /> <!-- 圓角 -->
<attr name="backgroundColor" format="color"/> <!-- 背景的顏色 -->
</declare-styleable>
- 繼承View並繪製遮罩層,實現自定義View,代碼如下:
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Region;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.bottle.app.R;
/**
* 原理就是利用一個圓角矩形A,和一個扇形B,裁切一塊區域(A-B),然後把顏色繪製在這塊區域,
* 被裁掉區域的中心角度爲進度。
* 難度就在於如何裁剪出這一塊區域:
* 1. 圓角矩形背景A:取View 的 padding 範圍內的View空間即可;
* 2. 進度扇形背景B:以View的中心爲圓心,矩形對角線爲直徑,起始角度爲起點a,
* 得到圓上起點,進度角度b,得到圓上第二點,然後畫弧度得到扇形;
* 3. 取A-B得到背景,然後畫一個矩形即可。
*/
public class CircleProgress extends View {
public static final int PI_RADIUS = 180; // pi弧度對應的角度
private int mProgress; // 進度,取值範圍: 0-100
private int mCorner; // 圓角,如果是矩形,取一半的話可以是圓
private int mStartAngle; // 百分比進度的起始值,0-n,其中0度與x軸方向一致
private int mBackgroundColor; // 覆蓋部分,也就是除進度外部分的顏色
private int width;
private int height;
private PointF mCenter; // View的中心
private PointF mStart; // 起始點角度在圓上對應的橫座標
private float mRadius; // View的外切圓的半徑
private RectF mBackground; // 被裁剪的底層圓角矩形
private Path mClipArcPath = new Path(); // 要裁剪掉的扇形部分 B
private Path mClipBgPath = new Path(); // 整個View的背景 A,繪製部分爲: A-B
private RectF mEnclosingRectF; // 這是整個View的外切圓的外切矩形,忽略padding的話它比View的尺寸大
private Paint mPaint = new Paint();
public void setProgress(int progress) {
mProgress = progress;
invalidate();
}
public CircleProgress(Context context) {
super(context);
}
public CircleProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public CircleProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
mProgress = typedArray.getInt(R.styleable.CircleProgress_circleProgress, 0);
mCorner = typedArray.getDimensionPixelOffset(R.styleable.CircleProgress_circleCorner, 0);
mStartAngle = typedArray.getInt(R.styleable.CircleProgress_startAngle, 315);
mBackgroundColor = typedArray.getColor(R.styleable.CircleProgress_backgroundColor,
Color.argb(90, 90, 90, 90));
typedArray.recycle();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
mPaint.setColor(mBackgroundColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
float rw = (width - getPaddingStart() - getPaddingEnd()) / 2f;
float rh = (height - getPaddingTop() - getPaddingBottom()) / 2f;
mRadius = (float) Math.sqrt(rw * rw + rh * rh);
mCenter = new PointF(getPaddingStart() + rw, getPaddingTop() + rh);
mStart = new PointF((float) (mCenter.x + mRadius * Math.cos(mStartAngle * Math.PI / PI_RADIUS)),
(float) (mCenter.y + mRadius * Math.sin(mStartAngle * Math.PI / PI_RADIUS)));
mBackground = new RectF(getPaddingStart(),
getPaddingTop(),
width - getPaddingEnd(),
height - getPaddingBottom());
mEnclosingRectF = new RectF(mCenter.x - mRadius, mCenter.y - mRadius,
mCenter.x + mRadius, mCenter.y + mRadius);
mClipBgPath.reset();
mClipBgPath.addRoundRect(mBackground, mCorner, mCorner, Path.Direction.CW);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.clipPath(mClipBgPath);
canvas.clipPath(getSectorClip(360 * mProgress / 100f + mStartAngle), Region.Op.DIFFERENCE);
canvas.drawRoundRect(mBackground, mCorner, mCorner, mPaint);
canvas.restore();
}
private Path getSectorClip(float sweepAngle) {
mClipArcPath.reset();
mClipArcPath.moveTo(mCenter.x, mCenter.y);
mClipArcPath.lineTo(mStart.x, mStart.y);
mClipArcPath.lineTo((float) (mCenter.x + mRadius * Math.cos(sweepAngle * Math.PI / PI_RADIUS)),
(float) (mCenter.y + mRadius * Math.sin(sweepAngle * Math.PI / PI_RADIUS)));
mClipArcPath.close();
mClipArcPath.addArc(mEnclosingRectF, mStartAngle, sweepAngle - mStartAngle);
return mClipArcPath;
}
}
- 測試
在佈局文件添加一個FrameLayout,底層是一個ImageView,上面是自定義CircleProgress(這個名字有點不合主題了)
<FrameLayout
android:layout_width="150dp"
android:layout_height="150dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/ic_launcher"/>
<com.bottle.app.widget.CircleProgress
android:id="@+id/circleProgress"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:backgroundColor="#9000ee00"
app:startAngle="315"
app:circleProgress="10"
app:circleCorner="60dp"/>
</FrameLayout>
在Activity中實例化後,模擬動態改變進度
private CircleProgress mCircleProgress;
private int power;
private Runnable mAction = new Runnable() {
@Override
public void run() {
int pow = power++ % 100;
mCircleProgress.setProgress(pow);
mBatteryView.postDelayed(mAction, 50);
}
};
總結
原理就是利用一個圓角矩形A,和一個扇形B,裁切一塊區域(A-B),然後把顏色繪製在這塊區域,被裁掉區域的中心角度爲進度。難度就在於如何裁剪出這一塊區域:
- 圓角矩形背景A:取View 的 padding 範圍內的View空間即可;
- 進度扇形背景B:以View的中心爲圓心,矩形對角線爲直徑,起始角度爲起點a,
得到圓上起點,進度角度b,得到圓上第二點,然後畫弧度得到扇形; - 取A-B得到背景,然後畫一個矩形即可。
相對來說不難,只需要簡單的三角函數sin和cos,還有就是Canvas的裁剪,繪製圓角矩形,扇形等簡單操作。(再次感受到自己數學基礎不好,就這麼一個三角函數還花了大半天才搞明白)