重学 Android 自定义 View 系列(六):环形进度条

目标

自定义一个环形进度条,可以自定义其最大值、当前进度、背景色、进度色,宽度等信息。

最终效果如下(GIF展示纯色有点问题):
在这里插入图片描述

1. 结构分析


  • 背景圆环:表示进度条的背景。
  • 进度圆环:表示当前进度,根据进度值动态绘制圆环。
  • 进度值文本:在圆环中间展示进度。

2. 实现思路


  1. 定义自定义属性:在 res/values/attrs.xml 中定义自定义属性,以便通过 XML 配置自定义的视图样式。
  2. 初始化视图元素:在构造函数中,根据传入的属性初始化各种画笔、尺寸和视图元素。
  3. 绘制视图内容:重写 onDraw 方法,使用画笔绘制背景圆环、进度圆环和文本。
  4. 支持动态更新进度:提供 setProgress 方法,允许外部动态设置进度,触发视图重绘。

3. 关键技术点解析


我们首先要知道,画圆的基础是有个正方形,或者说有一个正方形的坐标,还要考虑到画笔的宽度,所以我们首先就要确定矩形的大小:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int padding = circleWidth / 2;
        rectF.set(padding, padding, w - padding, h - padding);
    }

circleWidth 为圆环的宽,也为圆环画笔的宽。

canvas.drawArc是用于绘制弧形(圆弧)的一种方法,具体参数在前几篇文章已介绍,再次不做赘述。这里主要自定义了进度开始的角度,即 startAngle 属性。

textPaint.setTextAlign(Paint.Align.CENTER)

重点关注下文本画笔的这个属性,在前几篇文章中都是通过计算基线的方式确保文本水平居中的,本篇文章的实现方式不同,先来看下setTextAlign 的作用:

textPaint.setTextAlign(Paint.Align.CENTER) 是 Paint 类中的一个设置文本对齐方式的方法。它用于控制文本在指定位置绘制时的对齐方式,具体来说,它影响了绘制文本时文本的起始点(x,y 坐标)如何被确定。

作用:

Paint.Align 是一个枚举,定义了文本绘制时如何对齐相对于给定的 xy 坐标。常用的对齐方式有三个选项:

  1. Paint.Align.LEFT

    • 文本绘制时,文本的起始位置在 x 坐标上。
    • 也就是说,文本的左边缘会对齐到 x 坐标。
  2. Paint.Align.CENTER

    • 文本绘制时,文本的中心位置与 x 坐标对齐。
    • 这意味着文本的中点(水平中心)会与 x 坐标对齐,从而实现水平居中。
  3. Paint.Align.RIGHT

    • 文本绘制时,文本的右边缘会对齐到 x 坐标。
    • 也就是说,文本的右边缘会对齐到指定的 x 坐标,x 坐标即为文本的终点。

所以说现在我们不用计算基线,而是给drawText一个中心的 x 坐标,文字就能做到水平居中了!

4. 定义自定义属性


  <declare-styleable name="CircularProgressBar">
        <!-- 进度条的最大值 -->
        <attr name="maxProgress" format="integer"/>
        <!-- 当前进度 -->
        <attr name="progress" format="integer"/>
        <!-- 环形进度条的背景色 -->
        <attr name="circleBackgroundColor" format="color"/>
        <!-- 进度条的颜色 -->
        <attr name="progressColor" format="color"/>
        <!-- 进度条的宽度 -->
        <attr name="circleWidth" format="dimension"/>
        <!-- 显示进度文本 -->
        <attr name="showProgressText" format="boolean"/>
        <!-- 进度文本的颜色 -->
        <attr name="progressTextColor" format="color"/>
        <!-- 进度文本的大小 -->
        <attr name="progressTextSize" format="dimension"/>
        <!-- 开始角度 -->
        <attr name="startAngle" format="enum">
            <enum name="angle0" value="0"/>
            <enum name="angle90" value="90"/>
            <enum name="angle180" value="180"/>
            <enum name="angle270" value="270"/>
        </attr>
    </declare-styleable>

5. 完整代码


public class CircularProgressBar extends View {

    private Paint backgroundPaint;
    private Paint progressPaint;
    private Paint textPaint;
    private RectF rectF;

    private int maxProgress = 100;
    private int progress = 0;
    private int circleBackgroundColor = Color.GRAY;
    private int progressColor = Color.GREEN;
    private int circleWidth = 20;
    private boolean showProgressText = true;
    private int progressTextColor = Color.BLACK;
    private int progressTextSize = 50;
    private int startAngle = 0; // 起始角度,默认从0开始

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

    public CircularProgressBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        // 初始化自定义属性
        if (attrs != null) {
            TypedArray typedArray = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.CircularProgressBar,
                    0, 0
            );

            try {
                maxProgress = typedArray.getInt(R.styleable.CircularProgressBar_maxProgress, 100);
                progress = typedArray.getInt(R.styleable.CircularProgressBar_progress, 0);
                circleBackgroundColor = typedArray.getColor(R.styleable.CircularProgressBar_circleBackgroundColor, Color.GRAY);
                progressColor = typedArray.getColor(R.styleable.CircularProgressBar_progressColor, Color.GREEN);
                circleWidth = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBar_circleWidth, 20);
                showProgressText = typedArray.getBoolean(R.styleable.CircularProgressBar_showProgressText, true);
                progressTextColor = typedArray.getColor(R.styleable.CircularProgressBar_progressTextColor, Color.BLACK);
                progressTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBar_progressTextSize, 50);

                // 获取自定义的起始角度
                int angleValue = typedArray.getInt(R.styleable.CircularProgressBar_startAngle, 0);
                switch (angleValue) {
                    case 90:
                        startAngle = 90;
                        break;
                    case 180:
                        startAngle = 180;
                        break;
                    case 270:
                        startAngle = 270;
                        break;
                    default:
                        startAngle = 0;
                        break;
                }
            } finally {
                typedArray.recycle();
            }
        }

        // 设置画笔属性
        backgroundPaint = new Paint();
        backgroundPaint.setColor(circleBackgroundColor);
        backgroundPaint.setStyle(Paint.Style.STROKE);
        backgroundPaint.setStrokeWidth(circleWidth);
        backgroundPaint.setAntiAlias(true);

        progressPaint = new Paint();
        progressPaint.setColor(progressColor);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(circleWidth);
        progressPaint.setStrokeCap(Paint.Cap.ROUND);//圆角
        progressPaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setColor(progressTextColor);
        textPaint.setTextSize(progressTextSize);
        textPaint.setTextAlign(Paint.Align.CENTER);//文本对齐方式
        textPaint.setAntiAlias(true);

        rectF = new RectF();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int padding = circleWidth / 2;
        rectF.set(padding, padding, w - padding, h - padding);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘制背景圆环
        canvas.drawArc(rectF, 0f, 360f, false, backgroundPaint);

        // 绘制进度圆环
        float sweepAngle = 360f * progress / (float) maxProgress;

        // 根据起始角度调整起始位置
        float adjustedStartAngle = 0f;
        switch (startAngle) {
            case 90:
                adjustedStartAngle = 0f;
                break;
            case 180:
                adjustedStartAngle = 90f;
                break;
            case 270:
                adjustedStartAngle = 180f;
                break;
            default:
                adjustedStartAngle = -90f; // 默认从0度(顶部)开始
                break;
        }

        canvas.drawArc(rectF, adjustedStartAngle, sweepAngle, false, progressPaint);

        // 绘制进度文本
        if (showProgressText) {
            String progressText = progress + "%";

            // 使用视图的中心来计算x和y坐标
            float x = getWidth() / 2f;
            float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;

            // 绘制文本
            canvas.drawText(progressText, x, y, textPaint);
        }
    }

    // 设置进度
    public void setProgress(int progress) {
        if (progress > maxProgress) {
            this.progress = maxProgress;
        } else {
            this.progress = Math.max(progress, 0);
        }
        invalidate(); // 重新绘制
    }


    // 获取当前进度
    public int getProgress() {
        return progress;
    }
}

6. 使用示例


xml:

<com.xaye.example.CircularProgressBar
        android:id="@+id/circularProgressBar"
        android:layout_width="140dp"
        android:layout_height="140dp"
        app:maxProgress="100"
        app:circleBackgroundColor="#DDDDDD"
        app:progressColor="#00B8D4"
        app:circleWidth="15dp"
        app:showProgressText="true"
        app:progressTextColor="#000000"
        app:progressTextSize="30sp"
        app:startAngle="angle0"/>

Activity:

val animator = ValueAnimator.ofInt(0, 80)
            animator.setDuration(2000)
            animator.interpolator = LinearInterpolator()
            animator.addUpdateListener { animation ->
                val value = animation.animatedValue as Int
                mBind.circularProgressBar.setProgress(value)
            }
            animator.start()

7. 最后


本篇没什么难度,主要是介绍点新的东西,再熟悉熟悉手感,再会。

另外给喜欢记笔记的同学安利一款好用的云笔记软件,对比大部分国内的这个算还不错的,免费好用:wolai

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/915115.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

⚙️ 如何调整重试策略以适应不同的业务需求?

调整 Kafka 生产者和消费者的重试策略以适应不同的业务需求&#xff0c;需要根据业务的特性和容错要求来进行细致的配置。以下是一些关键的调整策略&#xff1a; 业务重要性&#xff1a; 对于关键业务消息&#xff0c;可以增加重试次数&#xff0c;并设置较长的重试间隔&#x…

总结拓展十五:特殊采购业务——寄售采购

1、寄售采购的定义 寄售采购是指供应商提供物料&#xff0c;并将它们存储在你处&#xff0c;在贵公司将这些物料从寄售库存提取&#xff08;转自有&#xff09;之前&#xff0c;该供应商一直是这些物料法律上的所有者。只有当这些物料被贵司转自有领用后&#xff0c;供应商才会…

RK3568平台开发系列讲解(GPIO篇)GPIO的sysfs调试手段

🚀返回专栏总目录 文章目录 一、内核配置二、GPIO sysfs节点介绍三、命令行控制GPIO3.1、sd导出GPIO3.2、设置GPIO方向3.3、GPIO输入电平读取3.4、GPIO输出电平设置四、Linux 应用控制GPIO4.1、控制输出4.2、输入检测4.3、使用 GPIO 中断沉淀、分享、成长,让自己和他人都能有…

【算法】——二分查找合集

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 零&#xff1a;二分查找工具 1&#xff1a;最基础模版 2&#xff1a;mid落点问题 一&#xff1a;最…

JAVA学习日记(十五) 数据结构

一、数据结构概述 数据结构是计算机底层存储、组织数据的方式。 数据结构是指数据相互之间以什么方式排列在一起的。 数据结构是为了更加方便的管理和使用数据&#xff0c;需要结合具体的业务场景来进行选择。 二、常见的数据结构 &#xff08;一&#xff09;栈 特点&…

Windows快速部署并使用GitHub上Swift项目

1.科学上网 2.找到项目&#xff0c;release部分&#xff0c;下载最新版的ZIP文件&#xff0c;并且打开&#xff0c;解压。 3.打开cmd&#xff0c;使用你做项目用的虚拟环境&#xff0c;安装必须安装的包文件 pip install ms-swift[llm] -U 类似这样子唰唰唰一堆安装好之后&am…

C++ | Leetcode C++题解之第552题学生出勤记录II

题目&#xff1a; 题解&#xff1a; class Solution { public:static constexpr int MOD 1000000007;vector<vector<long>> pow(vector<vector<long>> mat, int n) {vector<vector<long>> ret {{1, 0, 0, 0, 0, 0}};while (n > 0) {…

【精读】Kinodynamic Trajectory Optimization and Control for Car-Like Robots

原来阅读这个板块是我用来写小说灵感和摘抄笔记的&#xff0c;但是CSDN总说我重复率太高&#xff0c;mad以后改用来精读论文了 每天都在写不同的文章&#xff01;为什么&#xff1f;主要还是自我的研究进度跟不上课题组的进度 先给自己点根蜡烛11.15就开组会了我还没读完 ho…

学Linux的第八天

目录 管理进程 概念 程序、进程、线程 进程分类 查看进程 ps命令 unix 风格 bsd风格 GNU风格 top命令 格式 统计信息区 进程信息区&#xff1a;显示了每个进程的运行状态 kill命令 作用 格式 管理进程 概念 程序、进程、线程 程序&#xff1a; 二进制文件&…

uniCloud云对象调用第三方接口,根据IP获取用户归属地的免费API接口,亲测可用

需求 在2022年5月初&#xff0c;网络上各大平台上&#xff0c;都开始展示用户IP属地&#xff0c;在某音、某手等小视频平台以及各主流网站应用中&#xff0c;都展示IP归属地&#xff0c;如下图所示&#xff1a; 解决办法 收费文档的肯定有很多&#xff0c;基本你百度搜“归…

Leetcode - 143双周赛

目录 一&#xff0c;3345. 最小可整除数位乘积 I 二&#xff0c;3346. 执行操作后元素的最高频率 I 1.差分数组 2.同向三指针 滑动窗口 三&#xff0c; 3348. 最小可整除数位乘积 II 一&#xff0c;3345. 最小可整除数位乘积 I 本题直接暴力枚举&#xff0c;题目求 >n…

VS2022项目配置笔记

文章目录 $(ProjectDir&#xff09;与 $(SolutionDir) 宏附加包含目录VC目录和C/C的区别 $(ProjectDir&#xff09;与 $(SolutionDir) 宏 假设有一个解决方案 MySolution&#xff0c;其中包含两个项目 ProjectA 和 ProjectB&#xff0c;目录结构如下&#xff1a; C:\Projects\…

ReactPress:深入解析技术方案设计与源码

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎提出宝贵的建议&#xff0c;欢迎一起共建&#xff0c;感谢Star。 ReactPress是一个基于React框架开发的开源发布平台&#xff0c;它不仅仅是一个简单的博客系统&#xff0c;更是一个功能全…

[编译报错]ImportError: No module named _sqlite3解决办法

1. 问题描述&#xff1a; 在使用python进行代码编译时&#xff0c;提示下面报错&#xff1a; "/home/bspuser/BaseTools/Source/Python/Workspace/WorkspaceDatabase.py", line 18, in <module>import sqlite3File "/usr/local/lib/python2.7/sqlite3/_…

信号量和线程池

1.信号量 POSIX信号量&#xff0c;用与同步操作&#xff0c;达到无冲突的访问共享资源目的&#xff0c;POSIX信号量可以用于线程间同步 初始化信号量 #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); sem&#xff1a;指向sem_t类…

泷羽sec学习打卡-Linux基础2

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 关于Linux的那些事儿-Base2 一、Linux-Base2linux有哪些目录呢&#xff1f;不同目录下有哪些具体的文件呢…

【Android、IOS、Flutter、鸿蒙、ReactNative 】约束布局

Android XML 约束布局 参考 TextView居中 TextView 垂直居中并且靠右 TextView 宽高设置百分比 宽和高的比例 app:layout_constraintDimensionRatio"h,2:1" 表示子视图的宽高比为2:1&#xff0c;其中 h表示保持宽度不变&#xff0c;高度自动调整。 最大宽度 设…

使用HTML、CSS和JavaScript创建动态圣诞树

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 ✨特色专栏&#xff1a…

golang分布式缓存项目 Day1 LRU 缓存淘汰策略

注&#xff1a;该项目原作者&#xff1a;https://geektutu.com/post/geecache-day1.html。本文旨在记录本人做该项目时的一些疑惑解答以及部分的测试样例以便于本人复习。 LRU缓存淘汰策略 三种缓存淘汰策略 FIFO&#xff08;First In, First Out&#xff09;先进先出 原理&…

面向对象的需求分析和设计(一)

[toc] 1. 引言 前一篇文章《我对需求分析的理解》提到了面向对象分析和设计&#xff0c;正好最近又重新有重点的读了谭云杰著的《Think in UML》&#xff0c;感觉有必要写把书中一些核心内容观点以及自己的想法整理出来&#xff0c;一是方便自己日后的复习&#xff0c;另外也…