自定义View之圆形进度条的实现,带有心跳动画效果!

今天我们一起来实现一个带有缺口的自定义view进度条,带有心跳动画效果

看一下截图是什么效果
自定义View之圆形进度条的实现,带有心跳动画效果!

说到自定义view我们就需要实现onDraw() 方法, 去操作其携带的Canvas参数,通过drawXXX()系列方法来绘制出自己想要的布局样式。

怎样实现一个缺口向下同时带有心跳动画的进度条呢?

思路

1. 通过drawArc()方法绘制两个空心的扇型圆

第一个扇型是未加载的样式,第二个扇型是进度条样式

 RectF rect = new RectF(left, top, right, bottom);
        canvas.drawArc(rect, startPosition, endPosition, false, paint);
        canvas.save(); // 保存一下。
        //
        paint.setColor(Color.parseColor(progressColor));
        //paint.setDither(true);
        if(!TextUtils.isEmpty(shadowColor))
        paint.setShadowLayer(shadowSize,1,1,Color.parseColor(shadowColor));
        canvas.drawArc(rect, startPosition, progress, false, paint);
        canvas.save(); // 保存一下。

需要注意的是startPosition & endPosition 这两个参数是通过计算得来, 首先要了解canvas.drawArc这几个参数的意思:

第一个参数rect是确定位置,RectF的几个位置信息参数分别代表:

left= 最左边view视图边界到绘制画布最左边的距离;
top=最顶部view视图的边界到绘制画布最上边的距离;
right=最左边view视图的边界到绘制画布最右边的距离;
bottom=最顶部view视图的边界到绘制画布最底部的距离

第二个参数开始画扇形的角度, 这个角度是以3点钟方向为0角度,顺时针旋转360度再回到3点钟的方向。

第三个参数是结束扇形的角度。

第四个参数是否要画一条线与中心点相连,这个还是好理解的。

第五个参数就是画笔paint,专门用来设定一些画笔属性。

现在来计算缺口的位置,计算方式也很简单,假设我们的缺口大小是30度,并且缺口的位置是垂直向下的,我们需要先确定垂直向下的角度为90度,也就是6点钟方向(3点为起始位置,到6点方向为90度),这条线将我们需要展示缺口一分为二,左右两边各占15度角,那么开始扇形的位置的计算方式就是90.0f + (30/2)。 确定了开始画扇形的位置,就可以计算出结束扇形的角度位置了,缺口大小为30度,从105度的地方顺时针画360度就是一个完整的圆了, 当然我们不需要一个完整的圆, 那么我们用360度减去缺口大小,就是缺口结束的位置。

通过计算, 就可以提供下列方法,抛出设置缺口大小的方法。

   /**
     *  计算缺口位置
     * @param gap
     */
    private void setGapPosition(float gap){
        startPosition = 90.0f + (gap/2);
        endPosition = 360 - gap;
    }

思路:通过改变圆的半径实现心跳动画

看onDraw()方法中,先确定圆的半径,这里(l+levelOffset)代表在动态计算圆的半径,要实现心跳动画,就必须要动态改变圆的半径。

 float r = getMeasuredWidth() / (l+levelOffset)-5; //

(l+levelOffset)中的 “l” 是 圆的等级大小,最低等级是2,2是最大的圆,与布局一样大,等级越大,画的圆就越小:

    // 圆的大小,默认是最小规格的
    private int level = XXXXXX;
    // XX = 控件 / 2 = 圆的半径
    public static final int XX = 2;
    // XXX = 控件 / 3 = 圆的半径
    public static final int XXX = 3;
    // XXXX = 控件 / 4 = 圆的半径
    public static final int XXXX = 4;
    // XXXXX = 控件 / 5 = 圆的半径
    public static final int XXXXX = 5;
    // XXXXXX = 控件 / 6 = 圆的半径
    public static final int XXXXXX = 6;
    // 修改画笔的宽度,进度条的粗细设置

levelOffset的计算方式:

   /**
     *  计算偏移量/ 动态控制圆的大小
     */
    public void reconOffset(){
        if(isStarAnimation){
            if(levelOffset <= 0f || isAdd){
                // 增加
                isAdd = true;
                levelOffset +=0.02f;
                if(levelOffset>=0.3)isAdd=false;
            }else if(levelOffset>=0f && !isAdd){
                //减小
                isAdd = false;
                levelOffset -=0.02f;
            }
        }else{
            levelOffset = 0;
        }
    }

以上就介绍了实现的思路与计算方式,关于reconOffset()方法,可以自己尝试修改一下数值, 跑一下,看是什么效果。

完整代码如下:

package com.gongjiebin.drawtest;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;

import java.text.DecimalFormat;

/**
 * @author gongjiebin
 */
public class JJumpProgress extends View {

    public JJumpProgress(Context context) {
        super(context);
        initView();
    }

    public JJumpProgress(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public JJumpProgress(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

    }


    public void initView(){
        setGapPosition(this.gap);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        /**
         *  得到view的宽度模式
         *  取值-  MeasureSpec.UNSPECIFIED view的大小没有限制,可以是任意大小
         *  取值-  MeasureSpec.EXACTLY 当前的尺寸大小就是view的大小 相当于match_parent 如果是具体的值也是这个
         *  取值-  MeasureSpec.AT_MOST View能取的尺寸大小不能超过当前值大小 相当于wrap_content
         */
//        int widthSize = getSize(100, widthMeasureSpec);
        int heightSize = getSize(200, heightMeasureSpec);

        int widthSize = getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec);
        int heightSize = getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec);
//        int widthSize =  MeasureSpec.getSize(widthMeasureSpec);
//        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // Log.i("GJB", "widthSize=" + widthSize + "  heightSize=" + heightSize);

        // 填充大小
        setMeasuredDimension(widthSize, heightSize);

    }


    /**
     * 通过默认值和widthMeasureSpec/heightMeasureSpec获取控件的最终宽度
     *
     * @param defSize 默认值
     */
    public int getSize(int defSize, int measureSpec) {
        /**
         *  得到控件大小。 确定模式
         */
        int widthModel = MeasureSpec.getMode(measureSpec);

        int size = defSize;
        switch (widthModel) {
            case MeasureSpec.UNSPECIFIED: //撑满布局。
                // 没有指定大小。则使用默认大小
                size = defSize;
                break;
            case MeasureSpec.EXACTLY:
                size = MeasureSpec.getSize(measureSpec);
                break;
            case MeasureSpec.AT_MOST:// 内容包裹
                // 指定大小了,返回指定的大小-固定值了也取当前view 的大小
                size = MeasureSpec.getSize(measureSpec);

        }

        return size;
    }


    /**
     * 进度条
     */
    private float progress = 0f;
    // 缺口结束位置
    private float endPosition;
    // 缺口开始位置
    private float startPosition;
    // 进度文字
    private String progressTxt = "";
    // 偏移量
    private float offset = 5f;
    // 缺口 。设置圆缺口的大小,默认30度
    private float gap = 30;
    // 进度条的颜色 #000000 - 十六进制表达
    private String progressColor;
    // 进度条的颜色 #000000 - 背景圆的颜色
    private String bg_color;
    // 阴影的颜色
    private String shadowColor;
    // 阴影的大小
    private float shadowSize = 5;
    // 字体颜色 -- 默认黑色的。
    private String textColor = "#000000";
    // 默认显示百分比 / false 不显示
    private boolean isShowPercentage;
    // 字体大小
    private float textSize;

    // 圆的大小,默认是最小规格的
    private int level = XXXXXX;
    // 等级动画偏移量
    private float levelOffset=0;
    // XX = 控件 / 2 = 圆的半径
    public static final int XX = 2;
    // XXX = 控件 / 3 = 圆的半径
    public static final int XXX = 3;
    // XXXX = 控件 / 4 = 圆的半径
    public static final int XXXX = 4;
    // XXXXX = 控件 / 5 = 圆的半径
    public static final int XXXXX = 5;
    // XXXXXX = 控件 / 6 = 圆的半径
    public static final int XXXXXX = 6;
    // 修改画笔的宽度,进度条的粗细设置
    private float strokeWidth=10;

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

    private boolean isStarAnimation;

    public boolean isStarAnimation() {
        return isStarAnimation;
    }

    public void setStarAnimation(boolean starAnimation) {
        isStarAnimation = starAnimation;
    }

    public void setLevel(int level) {
        this.level = level;
    }


    public void setProgressTxt(String progressTxt) {
        this.progressTxt = progressTxt;
    }

    public int getLevel() {
        return level;
    }

    public void setTextSize(int textSize) {
        this.textSize = textSize;
    }

    public void setShowPercentage(boolean percentage) {
        this.isShowPercentage = percentage;
    }



    public void setTextColor(String textColor) {
        this.textColor = textColor;
    }

    public void setShadowSize(float shadowSize) {
        this.shadowSize = shadowSize;
    }

    public void setShadowColor(String shadowColor) {
        this.shadowColor = shadowColor;
    }

    public void setBg_color(String bg_color) {
        this.bg_color = bg_color;
    }

    public void setProgressColor(String progressColor) {
        // 关闭硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        this.progressColor = progressColor;
    }

    public void setGap(float gap) {
        this.gap = gap;
        setGapPosition(this.gap);
    }


    /**
     *  切换圆的大小
     * @param level
     */
    public void changeLevel(int level){
        this.setLevel(level);
        // 切换的时候不要动画
        this.setStarAnimation(false);

        // 刷新Ui
        invalidate();
    }


    /**
     *  计算缺口位置
     * @param gap
     */
    private void setGapPosition(float gap){
        startPosition = 90.0f + (gap/2);
        endPosition = 360 - gap;
    }

    /**
     *  设置每次的偏移量。。越小动画执行的越慢, 越大动画执行越快
     * @param offset
     */
    public void setOffset(float offset) {
        this.offset = offset;
    }


    /**
     * 设置进度
     *
     * @param progress 取值 : 0-100
     */
    public void setProgress(final float progress) {
        // 计算当前的值
        this.progress = endPosition / 100 * progress;
        //刷新view
        invalidate();
    }


    /**
     * 有动画效果的执行
     *
     * @param progress 需要到达的进度条位置
     */
    public void animationSetProgress(float progress) {
        // 换算成view能够认识的数值
        float jd = progress * (endPosition / 100);
        if(jd == 0){
            setProgress(jd);
        }else{
            animationStart(jd, this.offset,progress);
        }
    }


    /**
     * @param progress 需要到达的进度条位置
     */
    public void animationSetProgress(float progress,boolean isStarAnimation) {
        this.setStarAnimation(isStarAnimation);
        this.animationSetProgress(progress);
    }


    /**
     *  更改文字
     * @param offset
     */
    public void setTextCenter(float progress,float offset,float thasProgress){
        if(!isShowPercentage)return;
        //Log.i("TAG",isShowPercentage+"");
        float b =  (offset / progress * thasProgress);
        DecimalFormat df = new DecimalFormat("#.00");
        String t = df.format(b);
        progressTxt = t+"%";
    }

    /**
     * @param progress 总进度
     * @param offset   偏移量 每次偏移的角度
     */
    private void animationStart(final float progress, float offset,float thasProgress) {
        // 得到百分比
        float tPro =  (offset / progress * thasProgress);
        if(offset == progress){
            this.progress = offset;
            setTextCenter(progress,offset,thasProgress);
            invalidate();
            isStarAnimation = false;
            if(onChangeListener!=null)onChangeListener.onProgress(tPro);
            return;
        }

        // 换算百分比。
        if(offset >= progress){
            // 修改参数
            offset = progress;
            this.progress = offset;
            setTextCenter(progress,offset,thasProgress);
        }else{
            this.progress = offset;
            setTextCenter(progress,offset,thasProgress);
           //("GJB","offset" + offset +"progress =" +progress);
            offset+=this.offset;
        }
        //
        if(onChangeListener!=null)onChangeListener.onProgress(tPro);
        invalidate();
        Message message = new Message();
        float[] sets = new float[]{progress,offset,thasProgress};
        message.obj = sets;
        handler.sendMessage(message);
    }

   Handler handler = new Handler(new Handler.Callback() {
       @Override
       public boolean handleMessage(Message msg) {
           float set[] = (float[]) msg.obj;
           animationStart(set[0],set[1],set[2]);
           return false;
       }
   });



    public boolean isAdd = true;

    /**
     *  计算偏移量/ 控制圆的大小
     */
    public void reconOffset(){
        if(isStarAnimation){
            if(levelOffset <= 0f || isAdd){
                // 增加
                isAdd = true;
                levelOffset +=0.02f;
                if(levelOffset>=0.3)isAdd=false;
            }else if(levelOffset>=0f && !isAdd){
                //减小
                isAdd = false;
                levelOffset -=0.02f;
            }
        }else{
        //不需要动画
            levelOffset = 0;
        }
    }



    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float l = Float.valueOf(level);
        // 计算半径的偏移大小
        this.reconOffset();
        // 计算圆的半径
        float r = getMeasuredWidth() / (l+levelOffset)-5; //
      //  Log.i("GJB","r = "+r + " levelOffset" + levelOffset);
        // x坐标 中间位置
        int c_x = getLeft() + (getMeasuredWidth() / 2);
        // y坐标 中间位置
        int c_y = getTop() + (getMeasuredHeight() / 2);

        // 创建画笔
        Paint paint = new Paint();
        // 画笔的颜色 -
        paint.setColor(Color.parseColor(bg_color));
        // **当绘图样式为的时候,setStrokeJoin这个方法用于指定线条连接拐角样式: 相当于圆角
        //paint.setStrokeJoin(Paint.Join.ROUND);
        // 画笔的形状 Paint.Cap.Round 圆 ,Cap.SQUARE 方
        paint.setStrokeCap(Paint.Cap.ROUND);
        //画笔样式。 Paint.Style.STROKE为一条线。Paint.Style.FILL是从起点开始。一直到终点为止,形成一扇形的绘制区。Paint.Style.FILL_AND_STROKE 为扇形区再加上一个圈
        paint.setStyle(Paint.Style.STROKE); // 空心
        paint.setStrokeWidth(strokeWidth);
        paint.setAntiAlias(true); // 设置锯齿效果 取值true 代表

        // 画一个红色的圆在空心的中间位置
        float left = c_x - r;
        float top = c_y - r;
        float right = c_x + r;
        float bottom = c_y + r;
        // RectF - left计算的是从左边边界到扇形最左边的位置,
        // RectF -right是计算的是从左边边界到扇形最右边的位置
        // RectF -top 是计算的是从顶部边界到扇形最上边的位置
        // RectF -bottom 是计算的是从顶部边界到扇形最下边的位置
        RectF rect = new RectF(left, top, right, bottom);
        canvas.drawArc(rect, startPosition, endPosition, false, paint);
        canvas.save(); // 保存一下。
        //
        paint.setColor(Color.parseColor(progressColor));
        //paint.setDither(true);
        if(!TextUtils.isEmpty(shadowColor))
        paint.setShadowLayer(shadowSize,1,1,Color.parseColor(shadowColor));
        canvas.drawArc(rect, startPosition, progress, false, paint);
        canvas.save(); // 保存一下。


        if(!TextUtils.isEmpty(progressTxt)){
            // 在圆心画一个文字。
            Paint textPaint = new Paint();
            // 画笔的颜色

            textPaint.setColor(Color.parseColor(textColor));
            textPaint.setAntiAlias(true); // 设置锯齿效果 取值true 代表
            // 计算textView的字体大小随着布局的变化而变化
            float thasSize = 0;
            if(textSize == 0){
                thasSize = r / 6;
            }else{
                thasSize = textSize;
            }
           // Log.i("TAG",progressTxt+" progressTxt");
            textPaint.setTextSize(sp2px(thasSize));

            // 字体加粗
            textPaint.setFakeBoldText(true);
            // 测量一下文字的宽度
            float textSize = textPaint.measureText(progressTxt); // 测量文字的大小
            float center = c_x - (textSize/2);
            /**
             * 其中 y 表示文字的基线(baseline )所在的坐标,说白了就是我们小学写字用的那种带有横线的本子(一般都是按照一条基线来写字是吧?),
             * 用于规范你写的字是否成一条直线,否则很多人写着写着就往上飘了。而 x 坐标就是文字绘制的起始水平坐标,但是每个文字本身两侧都有一定的间隙,
             * 故实际文字的位置会比 x 的位置再偏右侧一些。
             */
            canvas.drawText(progressTxt,center,c_y,textPaint);
            canvas.save();
        }
    }


    private int sp2px(float spValue) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
    }


    private OnChangeListener onChangeListener;

    public void setOnChangeListener(OnChangeListener onChangeListener) {
        this.onChangeListener = onChangeListener;
    }

    public interface OnChangeListener{
        /**
         *  当前进度回调
         * @param progress
         */
        void onProgress(float progress);
    }
}

MainActivity完整代码如下(使用方法)

package com.gongjiebin.drawtest;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private JJumpProgress jumpProgress;

    private Button btn_pull;

    private Button btn_level;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ViewGroup rootView = (ViewGroup) LayoutInflater.from(getBaseContext()).inflate(R.layout.activity_main, null);
        setContentView(rootView);

        btn_pull = rootView.findViewById(R.id.btn_pull);
        btn_level = rootView.findViewById(R.id.btn_level);


        jumpProgress = rootView.findViewById(R.id.dv_jd);
        jumpProgress.setGap(30f);// 设置缺口的大小。
        jumpProgress.setOffset(5f); // 数值越大,动画效果越快
        jumpProgress.setBg_color("#E9E9E9"); 
        jumpProgress.setProgressColor("#FFDF2C32");
        jumpProgress.setShadowColor("#FFDF2C32");
        jumpProgress.setShadowSize(8);
        jumpProgress.setShowPercentage(true);
//        dv_jd.setTextSize(8);
        jumpProgress.setTextColor("#FFDF2C32");
//        dv_jd.setStrokeWidth(20);
        btn_pull.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                jumpProgress.animationSetProgress(100f, true);
            }
        });

        btn_level.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int level = jumpProgress.getLevel();
                if (level == JJumpProgress.XX) {
                    level = JJumpProgress.XXX;
                } else if (jumpProgress.getLevel() == JJumpProgress.XXX) {
                    level = JJumpProgress.XXXX;
                } else if (jumpProgress.getLevel() == JJumpProgress.XXXX) {
                    level = JJumpProgress.XXXXX;
                } else if (jumpProgress.getLevel() == JJumpProgress.XXXXX) {
                    level = JJumpProgress.XXXXXX;
                } else if (jumpProgress.getLevel() == JJumpProgress.XXXXXX) {
                    level = 10;
                } else if (jumpProgress.getLevel() == 10) {
                    level = 15;
                } else if (jumpProgress.getLevel() == 15) {
                    level = JJumpProgress.XX;
                }

                // 切换进度条圆的大小
                jumpProgress.changeLevel(level);
            }
        });

        initListener();
    }





    public void initListener() {
        jumpProgress.setOnChangeListener(new JJumpProgress.OnChangeListener() {
            @Override
            public void onProgress(float progress) {
                // 在这里可以显示你想显示的文本, 如果你已经开启了默认打开百分比请先关闭。setShowPercentage(false)
//                float price = progress / 100 * countPrice;
//                Log.i("TAG", "text=" + price + "progress =" + progress);
//                DecimalFormat df = new DecimalFormat("#.0");
//                String t = df.format(price);
//                dv_jd.setProgressTxt("¥" + t);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 完成百分之90。
        jumpProgress.animationSetProgress(90f, true);
    }


    @Override
    protected void onPause() {
        super.onPause();
    }
}


XML中使用:

  <com.gongjiebin.drawtest.JJumpProgress
        android:id="@+id/dv_jd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
/>

需要注意的是, 在使用时,可以看看代码中的注释,还是写的比较清楚的。 比如一些参数的设置,找到对应的set方法就可以啦。

 //进度
    private float progress = 0f;
    // 缺口结束位置
    private float endPosition;
    // 缺口开始位置
    private float startPosition;
    // 进度文字
    private String progressTxt = "";
    // 偏移量
    private float offset = 5f;
    // 缺口 。设置圆缺口的大小,默认30度
    private float gap = 30;
    // 进度条的颜色 #000000 - 十六进制表达
    private String progressColor;
    // 背景圆的颜色 #000000 - 十六进制表达
    private String bg_color;
    // 阴影的颜色  #000000 - 十六进制表达
    private String shadowColor;
    // 阴影的大小
    private float shadowSize = 5;
    // 字体颜色 -- 默认黑色的。
    private String textColor = "#000000";
    // 默认显示百分比 / false 不显示
    private boolean isShowPercentage;
    // 字体大小
    private float textSize;
    // 圆的大小,默认是最小规格的
    private int level = XXXXXX;
    // 修改画笔的宽度,进度条的粗细设置
    private float strokeWidth=10;

中间的文字默认按照百分比显示, 如果想显示其它文字,需要按照下列方式进行设置。

 jumpProgress.setShowPercentage(false);

动态改变文字的监听方法

jumpProgress.setOnChangeListener(new JJumpProgress.OnChangeListener() {
            @Override
            public void onProgress(float progress) {
                // 在这里可以显示你想显示的文本, 如果你已经开启了默认打开百分比请先关闭。setShowPercentage(false)
                float price = progress / 100 * countPrice;
                Log.i("TAG", "text=" + price + "progress =" + progress);
                DecimalFormat df = new DecimalFormat("#.0");
                String t = df.format(price);
                jumpProgress.setProgressTxt("¥" + t);
            }
        });

说明

关于textSize的设置, 如果设置了这个参数,那字体的动画效果就会消失了。不设置会根据进度条的大小变化而变化。 这个view的动画效果适合已经知道百分比的情况下使用(类似查看今天的步数,展示统计数据类), 当需要实时更新数据的进度条并不适合这个动画,或者说效果不是很好,但我们可以调用setProgress完成更新,只是不会出现心跳动画了。

   /**
     * 设置进度
     *
     * @param progress 取值 : 0-100
     */
    public void setProgress(final float progress) {
        // 计算当前的值
        this.progress = endPosition / 100 * progress;
        //刷新view
        invalidate();
    }
上一篇:Flutter:教你用CustomPaint画一个自定义的CircleProgressBar


下一篇:js switch 用法