[Android自定义View] 水波纹进度球

1. CircularProgressBar.java

package com.example.myapplication.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DrawFilter;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import com.example.myapplication.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 水波纹效果的进度球
 * @author 
 */
public class CircularProgressBar extends View {

    // 默认波纹颜色
    private static final int WAVE_PAINT_COLOR = 0xC815BC83;
    // 默认外环颜色
    private static final int OUTER_RING_COLOR = 0xF800838F;
    // todo y = Asin(wx+b)+h 水波纹的振幅因子
    private static final float STRETCH_FACTOR_A = 16;
    // 第一条水波移动速度
    private static final int TRANSLATE_X_SPEED_ONE = 5;
    // 第二条水波移动速度
    private static final int TRANSLATE_X_SPEED_TWO = 3;
    // todo 小球颜色
    private static final int BALL_COLOR = Color.BLUE;
    // todo 小球半径
    private static final float BALL_RADIUS = 7.0f;

    protected Context mContext;
    // 上中下位置的文本、大小、颜色
    private String centerText;
    private float centerTextSize;
    private int centerTextColor, wavePaintColor, strokeColor, submergedTextColor;
    // 线条宽度
    private float strokeWidth;
    private float waveHeight;
    private int max, progress;

    // 显示进度条百分比的位置
    private @BindingText int bindingText;

    // 各部分的绘笔
    private Paint mWavePaint, centerPaint, outerRingPaint, mBallPaint;

    // 过滤和调整绘图效果
    private DrawFilter mDrawFilter;

    // 绘制圆形路径
    private Path cirPath;

    // 文本矩形框检测
    private Rect textRect;

    private int width, height;
    private float[] mYPositions, mResetOneYPositions, mResetTwoYPositions;
    // 控制不同分辨率上的速度一致
    private int mXOffsetSpeedOne, mXOffsetSpeedTwo;
    private int mXOneOffset, mXTwoOffset;
    // 小球的当前位置
    private float ballAngle = 0;

    @IntDef({NONE, TOP_TEXT, CENTER_TEXT, BOTTOM_TEXT})
    @Retention(RetentionPolicy.SOURCE)
    private @interface BindingText {
    }

    public static final int NONE = 0;
    public static final int TOP_TEXT = 1;
    public static final int CENTER_TEXT = 2;
    public static final int BOTTOM_TEXT = 3;

    public CircularProgressBar(Context context) {
        super(context);
        init(context, null);
    }

    public CircularProgressBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public CircularProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs) {
        this.mContext = context;
        // 用于从 XML 布局中获取自定义视图的属性
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.WaveProgressView);
        bindingText = array.getInt(R.styleable.WaveProgressView_bindingText, NONE);
        wavePaintColor = array.getColor(R.styleable.WaveProgressView_wave_color, WAVE_PAINT_COLOR);
        submergedTextColor = array.getColor(R.styleable.WaveProgressView_submerged_textColor, 0xffffff0);
        max = array.getInt(R.styleable.WaveProgressView_max, 100);
        waveHeight = array.getDimension(R.styleable.WaveProgressView_wave_height, STRETCH_FACTOR_A);
        // todo 外环的线条粗度
        strokeWidth = array.getDimension(R.styleable.WaveProgressView_stroke_width, getResources().getDimension(R.dimen.stroke_width));
        strokeColor = array.getColor(R.styleable.WaveProgressView_stroke_color, OUTER_RING_COLOR);

        centerText = array.getString(R.styleable.WaveProgressView_center_text);
        centerTextColor = array.getColor(R.styleable.WaveProgressView_center_textColor, Color.BLACK);
        centerTextSize = array.getDimension(R.styleable.WaveProgressView_center_textSize, getResources().getDimension(R.dimen.center_text_size));

        if (centerText == null) centerText = "";

        // 外圈绘制
        outerRingPaint = new Paint();
        outerRingPaint.setAntiAlias(true); // 启用抗锯齿
        outerRingPaint.setStrokeWidth(strokeWidth);
        outerRingPaint.setColor(strokeColor);
        outerRingPaint.setStyle(Paint.Style.STROKE);
        // 波纹绘制
        mWavePaint = new Paint();
        mWavePaint.setAntiAlias(true);
        mWavePaint.setStyle(Paint.Style.FILL);
        mWavePaint.setColor(wavePaintColor);
        // 设置锯齿和位图滤镜
        mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        // 文本绘制
        centerPaint = new Paint();
        centerPaint.setAntiAlias(true);
        centerPaint.setTextSize(centerTextSize);

        // 小球绘制
        mBallPaint = new Paint();
        mBallPaint.setAntiAlias(true);
        mBallPaint.setColor(BALL_COLOR);
        mBallPaint.setStyle(Paint.Style.FILL);

        int progress = array.getInt(R.styleable.WaveProgressView_progress, 0);
        setProgress(progress);
        textRect = new Rect();
        cirPath = new Path();

        array.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            int size = dp2px(mContext, 150);
            setMeasuredDimension(size, size);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(dp2px(mContext, 150), MeasureSpec.getSize(heightMeasureSpec));
        } else if (heightMode == MeasureSpec.AT_MOST) {
            int size = MeasureSpec.getSize(widthMeasureSpec);
            setMeasuredDimension(size, size);
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w < h)
            width = height = w;
        else
            width = height = h;
        // 用于保存原始波纹的y值
        mYPositions = new float[width];
        // 用于保存波纹一的y值
        mResetOneYPositions = new float[width];
        // 用于保存波纹二的y值
        mResetTwoYPositions = new float[width];
        //
        float mCycleFactorW = (float) (2 * Math.PI / width);
        // 根据view总宽度得出所有对应的y值
        for (int i = 0; i < width; i++) {
            mYPositions[i] = (float) (waveHeight * Math.sin(mCycleFactorW * i) - waveHeight);
        }
        // 圆心,半径,顺时针(指定填充的部分)
        // 0.3f用于微调
        // TODO 调整水波填充的区域 与圆形一致
        // todo 应该是改这个
        cirPath.addCircle(width / 2.0f, height / 2.0f, height / 4.0f - strokeWidth + 0.3f, Path.Direction.CW);
        // 将dp转化为px,用于控制不同分辨率上移动速度基本一致
        mXOffsetSpeedOne = dp2px(mContext, TRANSLATE_X_SPEED_ONE) * width / dp2px(mContext, 330);
        mXOffsetSpeedTwo = dp2px(mContext, TRANSLATE_X_SPEED_TWO) * width / dp2px(mContext, 330);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.setDrawFilter(mDrawFilter);

        // 画水波 用save和restore保存状态
        canvas.save();
        canvas.clipPath(cirPath);
        // 绘制水波纹
        resetPositionY();
        // todo 应该是改这个
        float proHeight = height - (float) progress / max * (height / 2.0f - strokeWidth)  - height/4.0f -strokeWidth;
        Log.d("Circle","proHeight: "+proHeight);
        for (int i = 0; i < width; i++) {
            // 绘制第一条水波纹
            canvas.drawLine(i, proHeight - mResetOneYPositions[i], i, height, mWavePaint);
            // 绘制第二条水波纹
            canvas.drawLine(i, proHeight - mResetTwoYPositions[i], i, height, mWavePaint);
        }
        canvas.restore();

        // 绘制文本
        float centerTextY = height / 2.0f + textRect.height() / 2.0f;
        centerPaint.setColor(centerTextY > proHeight + textRect.height() ? (submergedTextColor == 0xffffff0 ? centerTextColor : submergedTextColor) : centerTextColor);
        centerPaint.getTextBounds(centerText, 0, centerText.length(), textRect);
        canvas.drawText(centerText, width / 2.0f - textRect.width() / 2.0f, centerTextY, centerPaint);

        // 绘制外环
        // TODO 调整圆环的位置大小
        if (strokeWidth > 0)
            canvas.drawCircle(width / 2.0f, height / 2.0f, width / 4.0f - strokeWidth / 2, outerRingPaint); // 绘制外环

        // 绘制小球
        // todo 调整小球的半径
        float ballRadius = width / 2.6f - strokeWidth * 3 / 2;
        float ballX = (float) (width / 2.0f + ballRadius * Math.cos(ballAngle));
        float ballY = (float) (height / 2.0f + ballRadius * Math.sin(ballAngle));
        canvas.drawCircle(ballX, ballY, BALL_RADIUS, mBallPaint);

        // 更新波纹和小球位置
        mXOneOffset += mXOffsetSpeedOne;
        mXTwoOffset += mXOffsetSpeedTwo;
        if (mXOneOffset >= width)
            mXOneOffset = 0;
        if (mXTwoOffset > width)
            mXTwoOffset = 0;

        ballAngle += 0.05f; // 调整旋转速度
        if (ballAngle >= 2 * Math.PI) {
            ballAngle -= 2 * Math.PI;
        }

        if (waveHeight > 0)
            postInvalidate();
    }


    private void resetPositionY() {
        // mXOneOffset代表当前第一条水波纹要移动的距离
        int oneInterval = mYPositions.length - mXOneOffset;
        // 重新填充第一条波纹的数据
        System.arraycopy(mYPositions, mXOneOffset, mResetOneYPositions, 0, oneInterval);
        System.arraycopy(mYPositions, 0, mResetOneYPositions, oneInterval, mXOneOffset);

        int twoInterval = mYPositions.length - mXTwoOffset;
        System.arraycopy(mYPositions, mXTwoOffset, mResetTwoYPositions, 0, twoInterval);
        System.arraycopy(mYPositions, 0, mResetTwoYPositions, twoInterval, mXTwoOffset);
    }

    private int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, context.getResources().getDisplayMetrics());
    }

    public int getMax() {
        return max;
    }

    public void setMax(int max) {
        this.max = max;
        invalidate();
    }

    public int getProgress() {
        return progress;
    }

    /**
     * 可在子线程中刷新
     * @param progress 进度
     */
    @SuppressLint("DefaultLocale")
    public void setProgress(int progress) {
        this.progress = progress;
        String content = String.format("%.2f", (float) progress / max * 100) + "%";
        switch (bindingText) {
            case CENTER_TEXT:
                centerText = content;
                break;

            case NONE:
                break;
        }
        postInvalidate();
    }

    public int getBindingText() {
        return bindingText;
    }

    public void setBindingText(@BindingText int bindingText) {
        this.bindingText = bindingText;
    }

    public String getCenterText() {
        return centerText;
    }

    public void setCenterText(String centerText) {
        this.centerText = centerText == null ? "" : centerText;
        invalidate();
    }

    public float getCenterTextSize() {
        return centerTextSize;
    }

    public void setCenterTextSize(float centerTextSize) {
        centerPaint.setTextSize(centerTextSize);
        invalidate();
    }

    public int getCenterTextColor() {
        return centerTextColor;
    }

    public void setCenterTextColor(int centerTextColor) {
        this.centerTextColor = centerTextColor;
        invalidate();
    }

    public float getStrokeWidth() {
        return strokeWidth;
    }

    public void setStrokeWidth(float strokeWidth) {
        this.strokeWidth = strokeWidth;
        invalidate();
    }

    public int getStrokeColor() {
        return strokeColor;
    }

    public void setStrokeColor(int strokeColor) {
        outerRingPaint.setColor(strokeColor);
        invalidate();
    }

    public int getWaveColor() {
        return wavePaintColor;
    }

    public void setWaveColor(int wavePaintColor) {
        mWavePaint.setColor(wavePaintColor);
        invalidate();
    }
}

2. 推进度

package com.example.myapplication.view;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.SeekBar;

import androidx.appcompat.app.AppCompatActivity;

import com.example.myapplication.R;

public class MainActivity extends AppCompatActivity {
    private CircularProgressBar progressBar;
    private Handler handler = new Handler();
    private int progress = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar = findViewById(R.id.circularProgressBar);
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (progress <= 100) {
                    progressBar.setProgress(progress);
                    progress += 2;
                    handler.postDelayed(this, 200);
                } else {
                    // 当进度达到100%时跳转到下一个Activity
                    Intent intent = new Intent(MainActivity.this, NextActivity.class);
                    startActivity(intent);
                    finish();
                }
            }
        }, 200);


    }
}

3. attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="bindingText">
        <enum name="none" value="0"/>
        <enum name="top_text" value="1" />
        <enum name="center_text" value="2" />
        <enum name="bottom_text" value="3" />
    </attr>

    <declare-styleable name="WaveProgressView">
        <attr name="top_text" format="string" />
        <attr name="top_textSize" format="dimension" />
        <attr name="top_textColor" format="color" />
        <attr name="center_text" format="string" />
        <attr name="center_textSize" format="dimension" />
        <attr name="center_textColor" format="color" />
        <attr name="bottom_text" format="string" />
        <attr name="bottom_textSize" format="dimension" />
        <attr name="bottom_textColor" format="color" />
        <attr name="max" format="integer" />
        <attr name="progress" format="integer" />
        <attr name="bindingText" />
        <attr name="wave_color" format="color" />
        <attr name="wave_height" format="dimension" />
        <attr name="stroke_color" format="color" />
        <attr name="stroke_width" format="dimension" />
        <attr name="submerged_textColor" format="color" />
    </declare-styleable>
</resources>

4. dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="top_text_size">18sp</dimen>
    <dimen name="center_text_size">22sp</dimen>
    <dimen name="bottom_text_size">16sp</dimen>
    <dimen name="stroke_width">4dp</dimen>
</resources>

5.colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <!-- res/values/colors.xml -->
        <color name="waveColor">#87CEFA</color> <!-- 浅蓝色 -->
        <color name="backgroundColor">#FFFFFF</color> <!-- 白色 -->

</resources>

6. thems.xml

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/white</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_700</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

7. layout.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.myapplication.view.CircularProgressBar
        android:id="@+id/circularProgressBar"
        android:layout_width="381dp"
        android:layout_height="384dp"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentBottom="true"
        android:layout_marginStart="15dp"
        android:layout_marginTop="93dp"
        android:layout_marginEnd="15dp"
        android:layout_marginBottom="254dp"
        app:bindingText="center_text"
        app:stroke_color="#87CEFA"
        app:wave_color="#87CEFA" />


</RelativeLayout>

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值