跳至主要內容

自定义View | 跑道View

guodongAndroid大约 10 分钟Androidandroidview

完成效果:

completed effect

核心原理

如下图:

辅助矩形

重点:上图三个辅助矩形框应该是同一个中心点。这是左侧的辅助矩形,右侧的同理。

有了上述辅助矩形,就可以使用 Path 来绘制跑道环了,以绘制外环为例:

  1. 首先,使用 Path.moveTo(x, y) 方法指定路径的起点,X坐标为左侧外环辅助矩形的 RectF.centerX(),Y坐标为左侧外环辅助矩形的 RectF.top
  2. 然后,使用 Path.arcTo(oval, startAngle, sweepAngle) 方法绘制外环顶部的线与右侧的圆弧,startAngle 参数为 -90FsweepAngle 参数为 180F
  3. 最后,使用 Path.arcTo(oval, startAngle, sweepAngle) 方法绘制外环底部的线与左侧的圆弧,startAngle 参数为 90FsweepAngle 参数为 180F

计算路径

View.onSizeChanged() 方法中计算跑道路径:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    mOuterRingPath.reset();
    mInnerRingPath.reset();
    mProgressPath.reset();

    computeOuterRing(w, h);
    computeInnerRing(h);
    computeProgressRing();
}

外环路径

private void computeOuterRing(int width, int height) {
    mOuterRingLeftArcRectF.setEmpty();
    mOuterRingRightArcRectF.setEmpty();

    // 直径
    int diameter = height - getPaddingTop() - getPaddingBottom();

    // 左边的圆弧
    float leftArcLeft = getPaddingStart() + OUTER_RING_OFFSET;
    float leftArcTop = getPaddingTop() + OUTER_RING_OFFSET;
    float leftArcRight = leftArcLeft + diameter;
    float leftArcBottom = height - getPaddingBottom() - OUTER_RING_OFFSET;
    mOuterRingLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

    // 右边的圆弧
    float rightArcLeft = width - getPaddingEnd() - diameter - OUTER_RING_OFFSET;
    float rightArcTop = getPaddingTop() + OUTER_RING_OFFSET;
    float rightArcRight = rightArcLeft + diameter;
    float rightArcBottom = height - getPaddingBottom() - OUTER_RING_OFFSET;
    mOuterRingRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

    // 顶部的线的起始坐标
    mOuterRingPath.moveTo(mOuterRingLeftArcRectF.centerX(), mOuterRingLeftArcRectF.top);

    // 顶部的线与右边的圆弧
    mOuterRingPath.arcTo(mOuterRingRightArcRectF, -90F, 180F);

    // 底部的线与左边的圆弧
    mOuterRingPath.arcTo(mOuterRingLeftArcRectF, 90F, 180F);
}
  1. 外环圆弧的直径为 View 的 height 减去设置的 padding
  2. 在没有设置 padding 的情况下,为了圆弧不紧贴着 View 的边缘,主动增加一个 OUTER_RING_OFFSET 偏移,
  3. 根据核心原理中的分析,即可计算出外环路径。

内环路径

private void computeInnerRing(int height) {
    mInnerRingLeftArcRectF.setEmpty();
    mInnerRingRightArcRectF.setEmpty();

    // 半径
    float radius = (height - getPaddingTop() - getPaddingBottom()) * 1.0F / 4;

    // 左边的圆弧
    float leftArcLeft = mOuterRingLeftArcRectF.centerX() - radius;
    float leftArcTop = mOuterRingLeftArcRectF.centerY() - radius;
    float leftArcRight = mOuterRingLeftArcRectF.centerX() + radius;
    float leftArcBottom = mOuterRingLeftArcRectF.centerY() + radius;
    mInnerRingLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

    // 右边的圆弧
    float rightArcLeft = mOuterRingRightArcRectF.centerX() - radius;
    float rightArcTop = mOuterRingRightArcRectF.centerY() - radius;
    float rightArcRight = mOuterRingRightArcRectF.centerX() + radius;
    float rightArcBottom = mOuterRingRightArcRectF.centerY() + radius;
    mInnerRingRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

    // 顶部的线的起始坐标
    mInnerRingPath.moveTo(mInnerRingLeftArcRectF.centerX(), mInnerRingLeftArcRectF.top);

    // 顶部的线与右边的圆弧
    mInnerRingPath.arcTo(mInnerRingRightArcRectF, -90F, 180F);

    // 底部的线与左边的圆弧
    mInnerRingPath.arcTo(mInnerRingLeftArcRectF, 90F, 180F);
}
  1. 内环圆弧我们计算圆弧的半径:View 实际高度的四分之一作为圆弧的半径,
  2. 由于三个辅助矩形是同一个中心点,根据上一步计算出来的外环辅助矩形可得中心点坐标,现在有了内环圆弧的半径后,根据中心点坐标即可得到内环辅助矩形,
  3. 根据核心原理中的分析,即可计算出内环路径。

进度环路径

private void computeProgressRing() {
    mProgressLeftArcRectF.setEmpty();
    mProgressRightArcRectF.setEmpty();

    // 左边的圆弧
    float leftArcLeft = (mOuterRingLeftArcRectF.left + mInnerRingLeftArcRectF.left) / 2;
    float leftArcTop = (mOuterRingLeftArcRectF.top + mInnerRingLeftArcRectF.top) / 2;
    float leftArcRight = (mOuterRingLeftArcRectF.right + mInnerRingLeftArcRectF.right) / 2;
    float leftArcBottom = (mOuterRingLeftArcRectF.bottom + mInnerRingLeftArcRectF.bottom) / 2;
    mProgressLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

    // 右边的圆弧
    float rightArcLeft = (mOuterRingRightArcRectF.left + mInnerRingRightArcRectF.left) / 2;
    float rightArcTop = (mOuterRingRightArcRectF.top + mInnerRingRightArcRectF.top) / 2;
    float rightArcRight = (mOuterRingRightArcRectF.right + mInnerRingRightArcRectF.right) / 2;
    float rightArcBottom = (mOuterRingRightArcRectF.bottom + mInnerRingRightArcRectF.bottom) / 2;
    mProgressRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

    // 顶部的线的起始坐标
    mProgressPath.moveTo(mProgressLeftArcRectF.centerX(), mProgressLeftArcRectF.top);

    // 顶部的线与右边的圆弧
    mProgressPath.arcTo(mProgressRightArcRectF, -90F, 180F);

    // 底部的线与左边的圆弧
    mProgressPath.arcTo(mProgressLeftArcRectF, 90F, 180F);
}
  1. 进度环圆弧的辅助矩形介于外环和内环的辅助矩形的中间,
  2. 根据核心原理中的分析,即可计算出进度环路径。

runway-angular

进度环使用颜色扫描渐变,以路径起点为渐变起点,而扫描渐变以 0 度作为起始角度,因此需要调整扫描渐变的起始角度:

private void computeProgressRing() {
    // 省略代码
    
	mPathMeasure.setPath(mProgressPath, false);

    mProgressPathBounds.setEmpty();
    mProgressPath.computeBounds(mProgressPathBounds, false);

    Shader progressShader = new SweepGradient(
            mProgressPathBounds.centerX(),
            mProgressPathBounds.centerY(),
            new int[]{mProgressStartColor, mProgressCenterColor, mProgressEndColor},
            new float[]{0F, 0.6F, 1F});

    Matrix matrix = new Matrix();
    // 计算起始点与路径框中心点的夹角角度
    double theta = Math.atan2(mProgressLeftArcRectF.top - mProgressPathBounds.centerY(), mProgressLeftArcRectF.centerX() - mProgressPathBounds.centerX());
    float angle = (float) (theta * (180 / Math.PI));
    matrix.setRotate(angle - 0.5F, mProgressPathBounds.centerX(), mProgressPathBounds.centerY());
    progressShader.setLocalMatrix(matrix);

    mProgressPaint.setShader(progressShader);
}

绘制

protected void onDraw(@NonNull Canvas canvas) {
    int width = getWidth();
    int height = getHeight();

    drawOuterRing(canvas);
    drawInnerRing(canvas);
    drawProgressRing(canvas);
    drawProgressIndicator(canvas);
    drawText(canvas, width, height);
}

private void drawOuterRing(Canvas canvas) {
    canvas.drawPath(mOuterRingPath, mOuterRingPaint);
}

private void drawInnerRing(Canvas canvas) {
    canvas.drawPath(mInnerRingPath, mInnerRingPaint);
}

private void drawProgressRing(Canvas canvas) {
    mCurrentProgressPath.reset();
    float progress = mProgress * 1.0F / mMaxProgress;
    float distance = mPathMeasure.getLength() * progress;
    if (mPathMeasure.getSegment(0F, distance, mCurrentProgressPath, true)) {
        canvas.drawPath(mCurrentProgressPath, mProgressPaint);
    }
}

private void drawProgressIndicator(Canvas canvas) {
    float distance = mPathMeasure.getLength() * (mProgress * 1.0F / mMaxProgress);
    float[] pos = new float[2];
    mPathMeasure.getPosTan(distance, pos, null);

    float cx = pos[0];
    float cy = pos[1];

    mCircleIndicatorPaint.setColor(mProgressIndicatorShadowColor);
    canvas.drawCircle(cx, cy, mProgressIndicatorRadius, mCircleIndicatorPaint);

    mCircleIndicatorPaint.setColor(mProgressIndicatorColor);
    canvas.drawCircle(cx, cy, mProgressIndicatorRadius - 1F, mCircleIndicatorPaint);
}

private void drawText(Canvas canvas, int width, int height) {

    float baseline = getPaddingTop() + (height - getPaddingTop() - getPaddingBottom()) * 1.0F / 2;

    // 坡度数据
    String slopeText = format(mSlope);
    float slopeTextWidth = mValueTextPaint.measureText(slopeText);
    canvas.drawText(slopeText, mInnerRingLeftArcRectF.right - slopeTextWidth / 2, baseline - mValueTextBaseline, mValueTextPaint);

    // 坡度文本
    canvas.drawText(FIXED_TEXT_SLOPE, mInnerRingLeftArcRectF.right + slopeTextWidth / 2 + 5, baseline - mValueTextBaseline, mFixedTextPaint);

    // 圈数数据
    String lapsText = String.valueOf(mLaps);
    float lapsTextWidth = mValueTextPaint.measureText(lapsText);
    canvas.drawText(lapsText, width * 1.0F / 2 - lapsTextWidth / 2, baseline, mValueTextPaint);

    // 圈数文本
    float fixedLapsTextWidth = mFixedTextPaint.measureText(FIXED_TEXT_LAPS);
    canvas.drawText(FIXED_TEXT_LAPS, width * 1.0F / 2 - fixedLapsTextWidth / 2, baseline - mFixedTextPaint.ascent() + 5, mFixedTextPaint);

    // 速度数据
    String speedText = format(mSpeed);
    float speedTextWidth = mValueTextPaint.measureText(speedText);
    canvas.drawText(speedText, mInnerRingRightArcRectF.left - speedTextWidth, baseline - mValueTextBaseline, mValueTextPaint);

    // 速度文本
    canvas.drawText(FIXED_TEXT_SPEED, mInnerRingRightArcRectF.left + 5, baseline - mValueTextBaseline, mFixedTextPaint);
}

private String format(float value) {
    return String.format(Locale.CHINESE, "%.1f", value);
}

完整代码

public class RunWayView extends View {

    private static final String FIXED_TEXT_SLOPE = "坡度";

    private static final String FIXED_TEXT_LAPS = "圈数";

    private static final String FIXED_TEXT_SPEED = "速度";

    private static final float OUTER_RING_OFFSET = 5F;

    private Paint mOuterRingPaint;

    private Paint mInnerRingPaint;

    private Paint mProgressPaint;

    private Paint mCircleIndicatorPaint;

    private Paint mAxisPaint;

    private TextPaint mValueTextPaint;

    private TextPaint mFixedTextPaint;

    private final PathEffect mPathEffect = new CornerPathEffect(10);

    private final Path mOuterRingPath = new Path();

    private final RectF mOuterRingLeftArcRectF = new RectF();

    private final RectF mOuterRingRightArcRectF = new RectF();

    private final Path mInnerRingPath = new Path();

    private final RectF mInnerRingLeftArcRectF = new RectF();

    private final RectF mInnerRingRightArcRectF = new RectF();

    private final Path mProgressPath = new Path();

    private final RectF mProgressPathBounds = new RectF();

    private final Path mCurrentProgressPath = new Path();

    private final PathMeasure mPathMeasure = new PathMeasure();

    private final RectF mProgressLeftArcRectF = new RectF();

    private final RectF mProgressRightArcRectF = new RectF();

    private int mOuterRingColor = Color.parseColor("#1aFFFFFF");

    private int mInnerRingColor = Color.parseColor("#1aFFFFFF");

    private float mOuterRingWidth = 1.0F;

    private float mInnerRingWidth = 1.0F;

    private float mProgressRingWidth = 1.0F;

    private int mProgressStartColor = Color.parseColor("#ffEE6767");

    private int mProgressCenterColor = Color.parseColor("#ffF03072");

    private int mProgressEndColor = Color.parseColor("#FFD03D3D");

    private int mProgressIndicatorShadowColor = Color.WHITE;

    private int mProgressIndicatorColor = Color.parseColor("#ffEE6767");

    private float mProgressIndicatorRadius = 3;

    private int mValueTextColor = Color.WHITE;

    private float mValueTextSize = 20F;

    private int mValueTextStyle = 0;

    private float mValueTextBaseline;

    private int mFixedTextColor = Color.parseColor("#ffEE6767");

    private float mFixedTextSize = 8F;

    private int mFixedTextStyle = 0;

    /**
     * 坡度
     */
    private float mSlope = 0.0F;

    /**
     * 圈数
     */
    private int mLaps = 0;

    /**
     * 速度
     */
    private float mSpeed = 0.0F;

    private int mProgress = 0;

    private int mMaxProgress = 100;

    public RunWayView(Context context) {
        this(context, null);
    }

    public RunWayView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RunWayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public RunWayView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context, attrs);
        initPaint();
    }

    private void initPaint() {
        mOuterRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mOuterRingPaint.setStrokeWidth(mOuterRingWidth);
        mOuterRingPaint.setColor(mOuterRingColor);
        mOuterRingPaint.setStyle(Paint.Style.STROKE);
        mOuterRingPaint.setPathEffect(mPathEffect);

        mInnerRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerRingPaint.setStrokeWidth(mInnerRingWidth);
        mInnerRingPaint.setColor(mInnerRingColor);
        mInnerRingPaint.setStyle(Paint.Style.STROKE);
        mInnerRingPaint.setPathEffect(mPathEffect);

        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mProgressPaint.setStrokeWidth(mProgressRingWidth);
        mProgressPaint.setStyle(Paint.Style.STROKE);
        mProgressPaint.setPathEffect(mPathEffect);
        mProgressPaint.setStrokeCap(Paint.Cap.ROUND);

        mCircleIndicatorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCircleIndicatorPaint.setStyle(Paint.Style.FILL);

        mAxisPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mAxisPaint.setStyle(Paint.Style.STROKE);

        mValueTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mValueTextPaint.setStyle(Paint.Style.FILL);
        mValueTextPaint.setColor(mValueTextColor);
        mValueTextPaint.setTextSize(mValueTextSize);
        mValueTextPaint.setTypeface(Typeface.create((Typeface) null, mValueTextStyle));
        mValueTextBaseline = (mValueTextPaint.ascent() + mValueTextPaint.descent()) / 2;

        mFixedTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mFixedTextPaint.setStyle(Paint.Style.FILL);
        mFixedTextPaint.setColor(mFixedTextColor);
        mFixedTextPaint.setTextSize(mFixedTextSize);
        mFixedTextPaint.setTypeface(Typeface.create((Typeface) null, mFixedTextStyle));
    }

    private void initView(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RunWayView);

        mOuterRingColor = ta.getColor(R.styleable.RunWayView_rw_outerRingColor, mOuterRingColor);
        mOuterRingWidth = ta.getDimension(R.styleable.RunWayView_rw_outerRingWidth, mOuterRingWidth);

        mInnerRingColor = ta.getColor(R.styleable.RunWayView_rw_innerRingColor, mInnerRingColor);
        mInnerRingWidth = ta.getDimension(R.styleable.RunWayView_rw_innerRingWidth, mInnerRingWidth);

        mProgressStartColor = ta.getColor(R.styleable.RunWayView_rw_progressRingStartColor, mProgressStartColor);
        mProgressCenterColor = ta.getColor(R.styleable.RunWayView_rw_progressRingCenterColor, mProgressCenterColor);
        mProgressEndColor = ta.getColor(R.styleable.RunWayView_rw_progressRingEndColor, mProgressEndColor);
        mProgressRingWidth = ta.getDimension(R.styleable.RunWayView_rw_progressRingWidth, mProgressRingWidth);

        mProgressIndicatorShadowColor = ta.getColor(R.styleable.RunWayView_rw_progressIndicatorShadowColor, mProgressIndicatorShadowColor);
        mProgressIndicatorColor = ta.getColor(R.styleable.RunWayView_rw_progressIndicatorColor, mProgressIndicatorColor);
        mProgressIndicatorRadius = ta.getDimension(R.styleable.RunWayView_rw_progressIndicatorRadius, mProgressIndicatorRadius);

        mProgress = ta.getInt(R.styleable.RunWayView_rw_progress, mProgress);
        mMaxProgress = ta.getInt(R.styleable.RunWayView_rw_max_progress, mMaxProgress);

        mSlope = ta.getFloat(R.styleable.RunWayView_rw_slope, 0.0F);
        mLaps = ta.getInt(R.styleable.RunWayView_rw_laps, 0);
        mSpeed = ta.getFloat(R.styleable.RunWayView_rw_speed, 0.0F);

        mValueTextColor = ta.getColor(R.styleable.RunWayView_rw_valueTextColor, mValueTextColor);
        mValueTextSize = ta.getDimension(R.styleable.RunWayView_rw_valueTextSize, mValueTextSize);
        mValueTextStyle = ta.getInt(R.styleable.RunWayView_rw_valueTextStyle, mValueTextStyle);

        mFixedTextColor = ta.getColor(R.styleable.RunWayView_rw_fixedTextColor, mFixedTextColor);
        mFixedTextSize = ta.getDimension(R.styleable.RunWayView_rw_fixedTextSize, mFixedTextSize);
        mFixedTextStyle = ta.getInt(R.styleable.RunWayView_rw_fixedTextStyle, mFixedTextStyle);

        ta.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightSize > widthSize) {
            throw new IllegalArgumentException("height must be not greater than width.");
        }

        if (isInEditMode()) {
//            widthMeasureSpec = MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY);
//            heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        mOuterRingPath.reset();
        mInnerRingPath.reset();
        mProgressPath.reset();

        computeOuterRing(w, h);
        computeInnerRing(h);
        computeProgressRing();
    }

    private void computeProgressRing() {
        mProgressLeftArcRectF.setEmpty();
        mProgressRightArcRectF.setEmpty();

        // 左边的圆弧
        float leftArcLeft = (mOuterRingLeftArcRectF.left + mInnerRingLeftArcRectF.left) / 2;
        float leftArcTop = (mOuterRingLeftArcRectF.top + mInnerRingLeftArcRectF.top) / 2;
        float leftArcRight = (mOuterRingLeftArcRectF.right + mInnerRingLeftArcRectF.right) / 2;
        float leftArcBottom = (mOuterRingLeftArcRectF.bottom + mInnerRingLeftArcRectF.bottom) / 2;
        mProgressLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

        // 右边的圆弧
        float rightArcLeft = (mOuterRingRightArcRectF.left + mInnerRingRightArcRectF.left) / 2;
        float rightArcTop = (mOuterRingRightArcRectF.top + mInnerRingRightArcRectF.top) / 2;
        float rightArcRight = (mOuterRingRightArcRectF.right + mInnerRingRightArcRectF.right) / 2;
        float rightArcBottom = (mOuterRingRightArcRectF.bottom + mInnerRingRightArcRectF.bottom) / 2;
        mProgressRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

        // 顶部的线的起始坐标
        mProgressPath.moveTo(mProgressLeftArcRectF.centerX(), mProgressLeftArcRectF.top);

        // 顶部的线与右边的圆弧
        mProgressPath.arcTo(mProgressRightArcRectF, -90F, 180F);

        // 底部的线与左边的圆弧
        mProgressPath.arcTo(mProgressLeftArcRectF, 90F, 180F);

        mPathMeasure.setPath(mProgressPath, false);

        mProgressPathBounds.setEmpty();
        mProgressPath.computeBounds(mProgressPathBounds, false);

        Shader progressShader = new SweepGradient(
                mProgressPathBounds.centerX(),
                mProgressPathBounds.centerY(),
                new int[]{mProgressStartColor, mProgressCenterColor, mProgressEndColor},
                new float[]{0F, 0.6F, 1F});

        Matrix matrix = new Matrix();
        // 计算起始点与路径框中心点的夹角角度
        double theta = Math.atan2(mProgressLeftArcRectF.top - mProgressPathBounds.centerY(), mProgressLeftArcRectF.centerX() - mProgressPathBounds.centerX());
        float angle = (float) (theta * (180 / Math.PI));
        matrix.setRotate(angle - 0.5F, mProgressPathBounds.centerX(), mProgressPathBounds.centerY());
        progressShader.setLocalMatrix(matrix);

        mProgressPaint.setShader(progressShader);
    }

    private void computeInnerRing(int height) {
        mInnerRingLeftArcRectF.setEmpty();
        mInnerRingRightArcRectF.setEmpty();

        // 半径
        float radius = (height - getPaddingTop() - getPaddingBottom()) * 1.0F / 4;

        // 左边的圆弧
        float leftArcLeft = mOuterRingLeftArcRectF.centerX() - radius;
        float leftArcTop = mOuterRingLeftArcRectF.centerY() - radius;
        float leftArcRight = mOuterRingLeftArcRectF.centerX() + radius;
        float leftArcBottom = mOuterRingLeftArcRectF.centerY() + radius;
        mInnerRingLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

        // 右边的圆弧
        float rightArcLeft = mOuterRingRightArcRectF.centerX() - radius;
        float rightArcTop = mOuterRingRightArcRectF.centerY() - radius;
        float rightArcRight = mOuterRingRightArcRectF.centerX() + radius;
        float rightArcBottom = mOuterRingRightArcRectF.centerY() + radius;
        mInnerRingRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

        // 顶部的线的起始坐标
        mInnerRingPath.moveTo(mInnerRingLeftArcRectF.centerX(), mInnerRingLeftArcRectF.top);

        // 顶部的线与右边的圆弧
        mInnerRingPath.arcTo(mInnerRingRightArcRectF, -90F, 180F);

        // 底部的线与左边的圆弧
        mInnerRingPath.arcTo(mInnerRingLeftArcRectF, 90F, 180F);
    }

    private void computeOuterRing(int width, int height) {
        mOuterRingLeftArcRectF.setEmpty();
        mOuterRingRightArcRectF.setEmpty();

        // 直径
        int diameter = height - getPaddingTop() - getPaddingBottom();

        // 左边的圆弧
        float leftArcLeft = getPaddingStart() + OUTER_RING_OFFSET;
        float leftArcTop = getPaddingTop() + OUTER_RING_OFFSET;
        float leftArcRight = leftArcLeft + diameter;
        float leftArcBottom = height - getPaddingBottom() - OUTER_RING_OFFSET;
        mOuterRingLeftArcRectF.set(leftArcLeft, leftArcTop, leftArcRight, leftArcBottom);

        // 右边的圆弧
        float rightArcLeft = width - getPaddingEnd() - diameter - OUTER_RING_OFFSET;
        float rightArcTop = getPaddingTop() + OUTER_RING_OFFSET;
        float rightArcRight = rightArcLeft + diameter;
        float rightArcBottom = height - getPaddingBottom() - OUTER_RING_OFFSET;
        mOuterRingRightArcRectF.set(rightArcLeft, rightArcTop, rightArcRight, rightArcBottom);

        // 顶部的线的起始坐标
        mOuterRingPath.moveTo(mOuterRingLeftArcRectF.centerX(), mOuterRingLeftArcRectF.top);

        // 顶部的线与右边的圆弧
        mOuterRingPath.arcTo(mOuterRingRightArcRectF, -90F, 180F);

        // 底部的线与左边的圆弧
        mOuterRingPath.arcTo(mOuterRingLeftArcRectF, 90F, 180F);
    }

    @Override
    protected void onDraw(@NonNull Canvas canvas) {
        if (isInEditMode()) {
//            canvas.drawColor(Color.parseColor("#ff132855"));
        }

        int width = getWidth();
        int height = getHeight();

        drawOuterRing(canvas);
        drawInnerRing(canvas);
        drawProgressRing(canvas);
        drawProgressIndicator(canvas);
        drawText(canvas, width, height);

//        drawAxis(canvas, width, height);
    }

    private void drawAxis(Canvas canvas, int width, int height) {
        mAxisPaint.setColor(Color.RED);
        DashPathEffect effect = new DashPathEffect(new float[]{5F, 2F}, 0);

        float offset = 5F;
        float centerX = mProgressPathBounds.centerX();
        float centerY = mProgressPathBounds.centerY();

        // X轴
        mAxisPaint.setStrokeWidth(2F);
        mAxisPaint.setPathEffect(effect);
        canvas.drawLine(0F, centerY, width, centerY, mAxisPaint);

        mAxisPaint.setPathEffect(null);
        canvas.drawLine(width, centerY, width - offset, centerY - offset, mAxisPaint);
        canvas.drawLine(width, centerY, width - offset, centerY + offset, mAxisPaint);

        // Y轴
        mAxisPaint.setPathEffect(effect);
        canvas.drawLine(centerX, 0F, centerX, height, mAxisPaint);

        mAxisPaint.setPathEffect(null);
        canvas.drawLine(centerX, height, centerX - offset, height - offset, mAxisPaint);
        canvas.drawLine(centerX, height, centerX + offset, height - offset, mAxisPaint);

        // 中心点与起始点连线
        mAxisPaint.setColor(Color.YELLOW);
        mAxisPaint.setPathEffect(effect);
        Path linePath = new Path();
        linePath.moveTo(mProgressPathBounds.centerX(), mProgressPathBounds.centerY());
        linePath.lineTo(mProgressLeftArcRectF.centerX(), mProgressLeftArcRectF.top);
        canvas.drawPath(linePath, mAxisPaint);

        // 夹角线
        PathMeasure measure = new PathMeasure(linePath, false);
        float[] pos = new float[2];
        measure.getPosTan(measure.getLength() * 0.15F, pos, null);

        Path angularPath = new Path();
        float offsetX = 90F;
        angularPath.moveTo(mProgressPathBounds.centerX() + offsetX, mProgressPathBounds.centerY());
        angularPath.quadTo((mProgressPathBounds.centerX() + offsetX + pos[0]) / 2, (mProgressPathBounds.centerY() + pos[1]) / 4, pos[0], pos[1]);
        canvas.drawPath(angularPath, mAxisPaint);
    }

    private void drawText(Canvas canvas, int width, int height) {

        float baseline = getPaddingTop() + (height - getPaddingTop() - getPaddingBottom()) * 1.0F / 2;

        // 坡度数据
        String slopeText = format(mSlope);
        float slopeTextWidth = mValueTextPaint.measureText(slopeText);
        canvas.drawText(slopeText, mInnerRingLeftArcRectF.right - slopeTextWidth / 2, baseline - mValueTextBaseline, mValueTextPaint);

        // 坡度文本
        canvas.drawText(FIXED_TEXT_SLOPE, mInnerRingLeftArcRectF.right + slopeTextWidth / 2 + 5, baseline - mValueTextBaseline, mFixedTextPaint);

        // 圈数数据
        String lapsText = String.valueOf(mLaps);
        float lapsTextWidth = mValueTextPaint.measureText(lapsText);
        canvas.drawText(lapsText, width * 1.0F / 2 - lapsTextWidth / 2, baseline, mValueTextPaint);

        // 圈数文本
        float fixedLapsTextWidth = mFixedTextPaint.measureText(FIXED_TEXT_LAPS);
        canvas.drawText(FIXED_TEXT_LAPS, width * 1.0F / 2 - fixedLapsTextWidth / 2, baseline - mFixedTextPaint.ascent() + 5, mFixedTextPaint);

        // 速度数据
        String speedText = format(mSpeed);
        float speedTextWidth = mValueTextPaint.measureText(speedText);
        canvas.drawText(speedText, mInnerRingRightArcRectF.left - speedTextWidth, baseline - mValueTextBaseline, mValueTextPaint);

        // 速度文本
        canvas.drawText(FIXED_TEXT_SPEED, mInnerRingRightArcRectF.left + 5, baseline - mValueTextBaseline, mFixedTextPaint);
    }

    private void drawProgressRing(Canvas canvas) {
        mCurrentProgressPath.reset();
        float progress = mProgress * 1.0F / mMaxProgress;
        float distance = mPathMeasure.getLength() * progress;
        if (mPathMeasure.getSegment(0F, distance, mCurrentProgressPath, true)) {
            canvas.drawPath(mCurrentProgressPath, mProgressPaint);
        }
    }

    private void drawProgressIndicator(Canvas canvas) {
        float distance = mPathMeasure.getLength() * (mProgress * 1.0F / mMaxProgress);
        float[] pos = new float[2];
        mPathMeasure.getPosTan(distance, pos, null);

        float cx = pos[0];
        float cy = pos[1];

        mCircleIndicatorPaint.setColor(mProgressIndicatorShadowColor);
        canvas.drawCircle(cx, cy, mProgressIndicatorRadius, mCircleIndicatorPaint);

        mCircleIndicatorPaint.setColor(mProgressIndicatorColor);
        canvas.drawCircle(cx, cy, mProgressIndicatorRadius - 1F, mCircleIndicatorPaint);
    }

    private void drawInnerRing(Canvas canvas) {
        canvas.drawPath(mInnerRingPath, mInnerRingPaint);
    }

    private void drawOuterRing(Canvas canvas) {
        canvas.drawPath(mOuterRingPath, mOuterRingPaint);
    }

    public void setProgress(int progress) {
        mProgress = progress;
        if (mProgress >= mMaxProgress) {
            aLapCompleted();
        }
        invalidate();
    }

    public int getProgress() {
        return mProgress;
    }

    public void setSlope(float slope) {
        mSlope = slope;
        invalidate();
    }

    public float getSlope() {
        return mSlope;
    }

    public void setLaps(int laps) {
        mLaps = laps;
        invalidate();
    }

    public int getLaps() {
        return mLaps;
    }

    public void setSpeed(float speed) {
        mSpeed = speed;
        invalidate();
    }

    public float getSpeed() {
        return mSpeed;
    }

    public void reset() {
        mSlope = 0.0F;
        mLaps = 0;
        mSpeed = 0.0F;
        mProgress = 0;
        invalidate();
    }

    private void aLapCompleted() {
        mProgress = 0;
        mLaps++;
    }

    private String format(float value) {
        return String.format(Locale.CHINESE, "%.1f", value);
    }
}
<declare-styleable name="RunWayView">
    <attr name="rw_outerRingColor" format="color|reference"/>
    <attr name="rw_outerRingWidth" format="dimension"/>

    <attr name="rw_innerRingColor" format="color|reference"/>
    <attr name="rw_innerRingWidth" format="dimension"/>

    <attr name="rw_progressRingStartColor" format="color|reference"/>
    <attr name="rw_progressRingCenterColor" format="color|reference"/>
    <attr name="rw_progressRingEndColor" format="color|reference"/>
    <attr name="rw_progressRingWidth" format="dimension"/>

    <attr name="rw_progressIndicatorShadowColor" format="color|reference"/>
    <attr name="rw_progressIndicatorColor" format="color|reference"/>
    <attr name="rw_progressIndicatorRadius" format="dimension"/>

    <attr name="rw_progress" format="integer"/>
    <attr name="rw_max_progress" format="integer"/>
    <attr name="rw_slope" format="float"/>
    <attr name="rw_laps" format="integer"/>
    <attr name="rw_speed" format="float"/>

    <attr name="rw_valueTextColor" format="color|reference"/>
    <attr name="rw_valueTextSize" format="dimension"/>
    <attr name="rw_valueTextStyle">
        <flag name="normal" value="0" />
        <flag name="bold" value="1" />
        <flag name="italic" value="2" />
    </attr>

    <attr name="rw_fixedTextColor" format="color|reference"/>
    <attr name="rw_fixedTextSize" format="dimension"/>
    <attr name="rw_fixedTextStyle">
        <flag name="normal" value="0" />
        <flag name="bold" value="1" />
        <flag name="italic" value="2" />
    </attr>
</declare-styleable>