Android自定义View-矩形圆角进度条

Android自定义View——矩形圆角扇形进度View

概述

最近的项目需求中,有一个显示下载进度的需求。大概是这样子的,一个圆角矩形ImageView作为背景图,以这个矩形的中心作为圆心,圆角矩形的外切圆上的某个点为起点,进度为矩形上某个点与起点的角度。位于进度范围内的圆角矩形部分为透明,其它部分为半透明遮罩。效果如下图所示(因为背景图片并不是规则矩形,所以效果有点差):
Android自定义View-矩形圆角进度条_第1张图片

矩形圆角扇形进度View的需求

如上图,剩余进度是半透明遮罩,完成进度部分为透明,可以设置进度,圆角,半透明遮罩的颜色,还有一个起始角度。相对来说需求很简单,和iOS的app升级时桌面上的升级进度效果基本一致。完成这个需求,自然而然地想到了自定义View,自定义一个遮罩层View,覆盖在ImageView上就可以了。

矩形圆角扇形进度View

  1. 新建attrs.xml定义属性文件,并增加相应的属性;
    <declare-styleable name="CircleProgress">
        <attr name="circleProgress" format="integer"/> 
        <attr name="startAngle" format="integer"/> 
        <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绘图详解(图文)

你可能感兴趣的:(android)