当前位置: 首页 > article >正文

Android自定义View实现彩虹进度条(带动画)

前言

最近在写项目的时候用自定义View写了一个带动画的彩虹进度条(如下所示),这篇博客用来做个分享。

在这里插入图片描述

注意:本文并未详解Android自定义View的知识,后续会补一篇博客用于详解。

分析

首先可以将这个彩虹进度条拆解为以下几个组成:

  • 蓝色进度条
  • 黄色进度条
  • 红色进度条

那么我们要做的就是以下几件事:绘制蓝色进度条的底层的半圆弧(浅蓝色的部分),绘制蓝色进度条的顶层圆弧(深蓝色的部分),给蓝色进度条的顶层圆弧加上动画,另外两个相同。OK接下来正式开始吧。

注意:要完成这个,需要具备一定的canvas基础和paint基础。

基础

  • 自定义View的第一步都是先创建出一个类,让它继承自View并重写它的几个构造方法
public class RainbowView extends View {
    // 当View是new出来的时候,调用这个方法
    public RainbowView(Context context) {
        super(context);
    }

    // 当View是在.xml文件里声明的时候,调用这个方法
    public RainbowView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    // 这个也是当在.xml文件里声明View时调用,且这个方法允许传入一个样式资源ID,这样View就会根据传入的样式资源设置属性
    public RainbowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 这个也是当在.xml文件里声明View时调用,与上面不同的是多传入了一个样式属性
    public RainbowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}
  • 去.xml文件中使用,为了方便看出位置,给LinearLayout加个背景颜色
<LinearLayout
        android:background="#FFEB3B"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <com.example.canvastest.RainbowView
            android:layout_width="400dp"
            android:layout_height="200dp"/>
    </LinearLayout>

初始化画笔

我们要画三个进度条的底部和顶部,显然我们需要六个画笔,所以接下来我们要对这六个画笔进行初始化:

一般我们初学对画笔进行初始化都是如下:

paint.setFlags(Paint.ANTI_ALIAS_FLAG);//设为抗锯齿,可以让绘画更丝滑
paint.setColor(Color.BLACK);
paint.setStrokeWidth(100);//设置宽度
paint.setStrokeCap(Paint.Cap.ROUND);//设置端点形状,ROUND可以看起来更丝滑,适用于绘制曲线和弧线
paint.setStyle(Paint.Style.STROKE);//设置绘制样式,STROKE表示描边不填充内部,FILL表示填充内部

可以看到这里将颜色和宽度在初始化时写死,这样没问题,但面对不同的需求时就少了些灵活性,所以这里我们要对画笔的属性进行自定义,这样我们就可以在布局文件中手动设置我们想要的颜色、宽度等。

  • 在res/values包下创建attr.xml文件,写入以下内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="RainbowView">
        <attr name="bottomWidth" format="float"></attr>
        <attr name="topWidth" format="float"></attr>
        <attr name="bottomColor1" format="color"></attr>
        <attr name="bottomColor2" format="color"></attr>
        <attr name="bottomColor3" format="color"></attr>
        <attr name="topColor1" format="color"></attr>
        <attr name="topColor2" format="color"></attr>
        <attr name="topColor3" format="color"></attr>
    </declare-styleable>
</resources>

<declare-styleable name=“RainbowView”>中的RainbowView就是我们自定义View的名字。

  • 在布局文件中定义属性
<com.example.canvastest.RainbowView
            app:bottomColor1="#C1F0F6"
            app:bottomColor2="#F4EAC8"
            app:bottomColor3="#F6B2B2"
            app:topColor1="#26B4F4"
            app:topColor2="#FBD665"
            app:topColor3="#F66A6A"
            app:bottomWidth="100"
            app:topWidth="100"
            android:layout_width="400dp"
            android:layout_height="200dp"/>
  • 接下来在RainbowView中使用TypedArray获取这些属性
public RainbowView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray= context.obtainStyledAttributes(attrs, R.styleable.SportHalfCircleView);
        bottomWidth=typedArray.getFloat(R.styleable.RainbowView_bottomWidth,30f);
        topWidth=typedArray.getFloat(R.styleable.RainbowView_topWidth,30f);
        bottomColor1=typedArray.getColor(R.styleable.RainbowView_bottomColor1, Color.BLACK);
        bottomColor2=typedArray.getColor(R.styleable.RainbowView_bottomColor2, Color.BLACK);
        bottomColor3=typedArray.getColor(R.styleable.RainbowView_bottomColor3, Color.BLACK);
        topColor1=typedArray.getColor(R.styleable.RainbowView_topColor1,Color.WHITE);
        topColor2=typedArray.getColor(R.styleable.RainbowView_topColor2,Color.WHITE);
        topColor3=typedArray.getColor(R.styleable.RainbowView_topColor3,Color.WHITE);
    
    	typedArray.recycle();
    }

获取属性时第一个参数是根据你在attr.xml文件中来的,如R.styleable.RainbowView_bottomWidth表示RainbowView的bottomWidth属性。

  • 最后我们就可以使用这些获得的属性对画笔进行初始化了

截至目前,RainbowView完整代码如下:

public class RainbowView extends View {
    //蓝色进度条底层圆圈
    private Paint paintBottom1=new Paint();
    //黄色进度条底层圆圈
    private Paint paintBottom2=new Paint();
    //红色进度条底层圆圈
    private Paint paintBottom3=new Paint();
    //蓝色进度条顶层圆弧
    private Paint paintTop1=new Paint();
    private Paint paintTop2=new Paint();
    private Paint paintTop3=new Paint();

    //底层半圆圈的宽度
    float bottomWidth;
    //顶层圆弧的宽度
    float topWidth;
    int bottomColor1;
    int bottomColor2;
    int bottomColor3;
    int topColor1;
    int topColor2;
    int topColor3;

    public RainbowView(Context context) {
        super(context);
    }

    public RainbowView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray= context.obtainStyledAttributes(attrs, R.styleable.RainbowView);
        bottomWidth=typedArray.getFloat(R.styleable.RainbowView_bottomWidth,30f);
        topWidth=typedArray.getFloat(R.styleable.RainbowView_topWidth,30f);
        bottomColor1=typedArray.getColor(R.styleable.RainbowView_bottomColor1, Color.BLACK);
        bottomColor2=typedArray.getColor(R.styleable.RainbowView_bottomColor2, Color.BLACK);
        bottomColor3=typedArray.getColor(R.styleable.RainbowView_bottomColor3, Color.BLACK);
        topColor1=typedArray.getColor(R.styleable.RainbowView_topColor1,Color.WHITE);
        topColor2=typedArray.getColor(R.styleable.RainbowView_topColor2,Color.WHITE);
        topColor3=typedArray.getColor(R.styleable.RainbowView_topColor3,Color.WHITE);

        typedArray.recycle();
        initPaint();
    }

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

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

    public void initPaint(){
        paintBottom1.setFlags(Paint.ANTI_ALIAS_FLAG);//设为抗锯齿
        paintBottom1.setColor(bottomColor1);
        paintBottom1.setStrokeWidth(bottomWidth);
        paintBottom1.setStrokeCap(Paint.Cap.ROUND);
        paintBottom1.setStyle(Paint.Style.STROKE);

        paintBottom2.setFlags(Paint.ANTI_ALIAS_FLAG);
        paintBottom2.setColor(bottomColor2);
        paintBottom2.setStrokeWidth(bottomWidth);
        paintBottom2.setStrokeCap(Paint.Cap.ROUND);
        paintBottom2.setStyle(Paint.Style.STROKE);

        paintBottom3.setFlags(Paint.ANTI_ALIAS_FLAG);
        paintBottom3.setColor(bottomColor3);
        paintBottom3.setStrokeWidth(bottomWidth);
        paintBottom3.setStrokeCap(Paint.Cap.ROUND);
        paintBottom3.setStyle(Paint.Style.STROKE);

        paintTop1.setFlags(Paint.ANTI_ALIAS_FLAG);
        paintTop1.setColor(topColor1);
        paintTop1.setStrokeWidth(topWidth);
        paintTop1.setStrokeCap(Paint.Cap.ROUND);
        paintTop1.setStyle(Paint.Style.STROKE);

        paintTop2.setFlags(Paint.ANTI_ALIAS_FLAG);
        paintTop2.setColor(topColor2);
        paintTop2.setStrokeWidth(topWidth);
        paintTop2.setStrokeCap(Paint.Cap.ROUND);
        paintTop2.setStyle(Paint.Style.STROKE);

        paintTop3.setFlags(Paint.ANTI_ALIAS_FLAG);
        paintTop3.setColor(topColor3);
        paintTop3.setStrokeWidth(topWidth);
        paintTop3.setStrokeCap(Paint.Cap.ROUND);
        paintTop3.setStyle(Paint.Style.STROKE);
    }
}

绘制底层圆圈

接下来就是进行绘制了,首先我们绘制三个底层的圆弧。

重写onDraw()方法:

	@Override
    protected void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawArc(50,50,850,850,180,180,false,paintBottom1);
        canvas.drawArc(200,200,700,700,180,180,false,paintBottom2);
        canvas.drawArc(350,350,550,550,180,180,false,paintBottom3);
    }

drawArc()方法表示画一个圆弧,前四个参数是规定了一个矩形,我们在这个矩形中进行圆弧的绘画。后面两个参数表示起始角度以及绘制角度,绘制角度这里就先指定为180°,后面动态指定。

运行项目得到如下效果:

在这里插入图片描述

绘制上层圆弧

接下来就是绘制上层圆弧了,因为每个上层圆弧都与一个下层的圆弧对应,所以它们的矩形也是一一对应,在onDraw()方法中加入以下代码:

canvas.drawArc(50,50,850,850,180,180,false,paintTop1);
canvas.drawArc(200,200,700,700,180,180,false,paintTop2);
canvas.drawArc(350,350,550,550,180,180,false,paintTop3);

重新运行项目如下所示:

在这里插入图片描述

可以看到颜色确实变深了,就表示我们的上层弧形绘制好了

添加动画

我们可以在RainbowView中提供一个方法,用于供外部调用展示动画

public void startProgress(int progress){
        ValueAnimator valueAnimator=ValueAnimator.ofFloat(0,100f);//设置进度为0到100
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                float cur=(float) valueAnimator.getAnimatedValue();
                //计算弧度
                topAngle=cur/100*180*progress/100;//记得在前面定义
                invalidate();//刷新
            }
        });
        valueAnimator.setDuration(1500);//动画时长
        valueAnimator.setInterpolator(new LinearInterpolator());//设置动画匀速
        valueAnimator.start();
    }

接着记得将绘制顶层圆弧的sweepAngle改为topAngle,如下:

canvas.drawArc(50,50,850,850,180,topAngle,false,paintTop1);

接着我们在外部Activity中创建RainbowView接着调用startProgress()方法即可。(这里就演示了一个进度条的动画,另外两个同理)

在MainActivity中:

RainbowView rainbowView=findViewById(R.id.rainbowView);
rainbowView.startProgress(90);//设置进度为90

调整位置以及宽高

我们在绘制圆弧时,那前面的四个参数都是随意取写死的,这样显然不对。接着我们分析一下,四个参数不就应该分别对应的圆心横坐标-半径-圆弧宽度、圆心纵坐标-半径-圆弧宽度、圆心横坐标+半径+圆弧宽度、圆心纵坐标+半径+圆弧宽度,如下图所示:

在这里插入图片描述

所以我们修改一下onDraw()方法:

@Override
    protected void onDraw(@NonNull Canvas canvas) {
        super.onDraw(canvas);
        //取到圆心(getWidth()表示取到父布局的宽、getHeight()表示取到父布局的高)
        int centerX=getWidth()/2;//因为这种图形就是宽要大于高,所以宽要除2,高不需要
        int centerY=getHeight();
        //取两值的较小值作为半径这样我们获得的就是最大半内切圆
        float radius1=Math.min(centerX,centerY)-paintBottom1.getStrokeWidth();
        
        //三个圆弧之间的间隔
        int step=30;
        canvas.drawArc(centerX-radius1,centerY-radius1,centerX+radius1,centerY+radius1,180,180,false,paintBottom1);
        float radius2=Math.min(centerX,centerY)-paintBottom1.getStrokeWidth()-step-paintBottom2.getStrokeWidth();
        canvas.drawArc(centerX-radius2,centerY-radius2,centerX+radius2,centerY+radius2,180,180,false,paintBottom2);
        float radius3=Math.min(centerX,centerY)-paintBottom1.getStrokeWidth()-step-paintBottom2.getStrokeWidth()-step-paintBottom3.getStrokeWidth();
        canvas.drawArc(centerX-radius3,centerY-radius3,centerX+radius3,centerY+radius3,180,180,false,paintBottom3);

        canvas.drawArc(centerX-radius1,centerY-radius1,centerX+radius1,centerY+radius1,180,topAngle,false,paintTop1); 
    }

另外,我们在布局文件中已经定义好了RainbowView的具体宽高,如果宽高有任意一方是wrap_content,那么就可能造成出乎意料的错误,这部分我会在后面详解自定义View进行解释,现在只需要知道,我们需要重写一个onMeasure()方法来应对这种情况:

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

        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);

        //如果长宽有任意一个为wrap_content,即将默认值(100)设置进去
        if(widthMode==MeasureSpec.AT_MOST && heightMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(100,100);
        }else if(widthMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(100,heightMeasureSpec);
        }else if(heightMeasureSpec==MeasureSpec.AT_MOST){
            setMeasuredDimension(widthMeasureSpec,100);
        }
    }

分享到此结束,谢谢阅读!


http://www.kler.cn/a/287034.html

相关文章:

  • Java Web开发高级——单元测试与集成测试
  • OSI5GWIFI自组网协议层次对比
  • 目标跟踪算法发展简史
  • 游戏引擎学习第80天
  • .Net Core微服务入门全纪录(五)——Ocelot-API网关(下)
  • 亲测有效!如何快速实现 PostgreSQL 数据迁移到 时序数据库TDengine
  • 完美解决Jenkins重启后自动杀掉衍生进程(子进程)问题
  • ​哈哈题库​邀请书
  • [Day 68] 區塊鏈與人工智能的聯動應用:理論、技術與實踐
  • 如何在 CentOS 6 上安装 Nagios
  • 噪音消除模块调研
  • selenium(一)基于java、元素定位
  • 697.数组的度
  • 超级会员卡积分收银系统源码,一站式解决方案,可以收银的小程序 带完整的安装代码包以及搭建部署教程
  • 探讨Facebook开户广告起充多少:全球标准与优势解析
  • 0基础学习Python路径(41)paramiko模块
  • 性能优化:提升TMS运行效率的策略
  • 【HuggingFace Transformers】LlamaModel源码解析
  • AI写的不用游标派发明细数量例子
  • Kettle发送邮件功能如何配置以实现自动化?
  • 七. 部署YOLOv8检测器-load-save-tensor
  • C#——类与结构
  • 后端输出二进制数据,前端fetch接受二进制数据,并转化为字符输出
  • Etl加工建模方式分类使用
  • BITCN合集(BITCN 、BITCN-GRU、BITCN-BIGRU、BITCN-LSTM、BITCN-BILSTM、BITCN-SVM)
  • HTML5 全屏API讲解