Android自定義View-矩形圓角進度條

Android自定義View——矩形圓角扇形進度View

概述

最近的項目需求中,有一個顯示下載進度的需求。大概是這樣子的,一個圓角矩形ImageView作爲背景圖,以這個矩形的中心作爲圓心,圓角矩形的外切圓上的某個點爲起點,進度爲矩形上某個點與起點的角度。位於進度範圍內的圓角矩形部分爲透明,其它部分爲半透明遮罩。效果如下圖所示(因爲背景圖片並不是規則矩形,所以效果有點差):
在這裏插入圖片描述

矩形圓角扇形進度View的需求

如上圖,剩餘進度是半透明遮罩,完成進度部分爲透明,可以設置進度,圓角,半透明遮罩的顏色,還有一個起始角度。相對來說需求很簡單,和iOS的app升級時桌面上的升級進度效果基本一致。完成這個需求,自然而然地想到了自定義View,自定義一個遮罩層View,覆蓋在ImageView上就可以了。

矩形圓角扇形進度View

  1. 新建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>
  1. 繼承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;
    }

}

  1. 測試
    在佈局文件添加一個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),然後把顏色繪製在這塊區域,被裁掉區域的中心角度爲進度。難度就在於如何裁剪出這一塊區域:

  1. 圓角矩形背景A:取View 的 padding 範圍內的View空間即可;
  2. 進度扇形背景B:以View的中心爲圓心,矩形對角線爲直徑,起始角度爲起點a,
    得到圓上起點,進度角度b,得到圓上第二點,然後畫弧度得到扇形;
  3. 取A-B得到背景,然後畫一個矩形即可。
    相對來說不難,只需要簡單的三角函數sin和cos,還有就是Canvas的裁剪,繪製圓角矩形,扇形等簡單操作。(再次感受到自己數學基礎不好,就這麼一個三角函數還花了大半天才搞明白)

參考

  1. Android Canvas 繪製 剪切 clip 與 幾何變換
  2. Android Canvas繪圖詳解(圖文)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章