重学 Android 自定义 View 系列(十):带指针的渐变环形进度条

news2025/1/5 21:49:13

前言

该篇文章根据前面 重学 Android 自定义 View 系列(六):环形进度条 拓展而来。

最终效果如下:
在这里插入图片描述

1. 扩展功能


  1. 支持进度顺时针或逆时针显示
  2. 在进度条末尾添加自定义指针图片
  3. 使用线性渐变为进度条添加颜色效果

2. 关键技术点解析


2.1 进度方向控制的实现

通过添加一个 direction 属性,设置角度的正负,决定进度条是顺时针还是逆时针绘制:

public static final int CLOCKWISE = 1;
public static final int COUNTERCLOCKWISE = -1;
private int direction = COUNTERCLOCKWISE; // 默认逆时针

// 在 onDraw 方法中计算扫过的角度时,加入方向
float sweepAngle = 360f * progress / maxProgress;
sweepAngle *= direction;

// 绘制进度条
canvas.drawArc(rectF, startAngle, sweepAngle, false, progressPaint);

2.2 自定义指针图片的绘制

在环形进度条的末尾,绘制一张 Bitmap 图片作为指针,图片可由你传入,但指针的方向要和demo中的一致,如下:

在这里插入图片描述

绘制指针的步骤:

  1. 调整指针的绘制半径:确保指针贴合圆环外侧,加入一个 outerSize 参数用于控制指针漏出圆环的长度。
  2. 计算指针位置:使用三角函数计算图片中心点坐标。
  3. 旋转画布并绘制图片(关键):将画布旋转到指定角度后,再绘制指针图片。

用到的三角函数原理如下,再重温一下学校的知识:),因为在Java中 Math 函数计算三角函数用的是弧度而不是角度,所以代码使用了Math.toRadians进行了角度转弧度。
在这里插入图片描述

核心代码实现:

private void drawPointer(Canvas canvas, float angle) {
    // 调整半径,使指针图片紧贴圆环外部
    float adjustedRadius = radius + backgroundPaint.getStrokeWidth() / 2 + outerSize;

    // 计算指针的中心点位置
    float rightCenterX = centerX + adjustedRadius * (float) Math.cos(Math.toRadians(angle));
    float rightCenterY = centerY + adjustedRadius * (float) Math.sin(Math.toRadians(angle));

    // 计算Bitmap左上角位置
    float left = rightCenterX - bitmapWidth;
    float top = rightCenterY - bitmapHeight / 2;

    // 保存画布状态,旋转画布
    canvas.save();
    canvas.rotate(angle, rightCenterX, rightCenterY);

    // 绘制指针Bitmap
    canvas.drawBitmap(pointerBitmap, left, top, null);

    // 恢复画布状态
    canvas.restore();
}

重点是角度的计算 angle = startAngle + sweepAngle,和指针的位移与旋转,结合三角函数计算坐标,并通过旋转画布保持图片对齐,使指针始终指向圆心位置。

2.3 渐变颜色的实现

为进度条添加线性渐变效果使用了 LinearGradient 着色器,实际效果按需求自定义,重点是 计算渐变起点和终点,因为有起始角度的存在,需要用到 圆心坐标、半径和起始角度计算:

private void updateGradient() {
    // 计算圆上的起点和终点坐标
    double startAngleRadians = Math.toRadians(startAngle);
    float startX = centerX + (float) (radius * Math.cos(startAngleRadians));
    float startY = centerY + (float) (radius * Math.sin(startAngleRadians));
    float endX = centerX - (float) (radius * Math.cos(startAngleRadians));
    float endY = centerY - (float) (radius * Math.sin(startAngleRadians));

    //线性渐变,从一个点渐变到另一个点,因为渐变的距离是圆的直径 所以,TileMode 在这里实际无意义
    gradientShader = new LinearGradient(
            startX, startY, endX, endY,
            progressColors, null,
            Shader.TileMode.CLAMP
    );
    progressPaint.setShader(gradientShader);
}

4. 定义自定义属性


<declare-styleable name="CircularProgressBarEx">
        <!-- 进度条的最大值 -->
        <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>
        <!-- 进度方向 -->
        <attr name="direction">
            <enum name="clockwise" value="1"/>
            <enum name="counterclockwise" value="-1"/>
        </attr>
        <!-- 指针漏出的长度 -->
        <attr name="outerSize" format="dimension"/>

    </declare-styleable>

5. 完整代码


public class CircularProgressBarEx extends View {

    private Paint backgroundPaint;
    private Paint progressPaint;
    private Paint textPaint;
    private RectF rectF;
    private Bitmap pointerBitmap; // 指针图标
    private float bitmapWidth; // Bitmap的宽度
    private float bitmapHeight; // Bitmap的高度
    private int outerSize = 5; //让指针漏出圆环的长度
    private int maxProgress = 100;
    private int progress = 0;
    private int circleBackgroundColor = Color.GRAY;
    private int[] progressColors = {Color.GREEN, Color.BLUE}; // 渐变颜色
    private int circleWidth = 20;
    private boolean showProgressText = true;
    private int progressTextColor = Color.BLACK;
    private int progressTextSize = 50;
    private int startAngle = 0; // 默认从左边开始
    private float centerX, centerY;
    private float radius;

    public static final int CLOCKWISE = 1;
    public static final int COUNTERCLOCKWISE = -1;
    private int direction = COUNTERCLOCKWISE; // 默认顺时针

    private LinearGradient gradientShader;

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

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

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

        if (attrs != null) {
            TypedArray typedArray = context.getTheme().obtainStyledAttributes(
                    attrs,
                    R.styleable.CircularProgressBarEx,
                    0, 0
            );
            try {
                maxProgress = typedArray.getInt(R.styleable.CircularProgressBarEx_maxProgress, 100);
                progress = typedArray.getInt(R.styleable.CircularProgressBarEx_progress, 0);
                circleBackgroundColor = typedArray.getColor(R.styleable.CircularProgressBarEx_circleBackgroundColor, Color.GRAY);
                circleWidth = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_circleWidth, 20);
                showProgressText = typedArray.getBoolean(R.styleable.CircularProgressBarEx_showProgressText, true);
                progressTextColor = typedArray.getColor(R.styleable.CircularProgressBarEx_progressTextColor, Color.BLACK);
                progressTextSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_progressTextSize, 50);
                startAngle = typedArray.getInt(R.styleable.CircularProgressBarEx_startAngle, 0);
                direction = typedArray.getInt(R.styleable.CircularProgressBarEx_direction, CLOCKWISE);
                outerSize = typedArray.getDimensionPixelSize(R.styleable.CircularProgressBarEx_outerSize, 5);
            } finally {
                typedArray.recycle();
            }
        }

        backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        backgroundPaint.setColor(circleBackgroundColor);
        backgroundPaint.setStyle(Paint.Style.STROKE);
        backgroundPaint.setStrokeWidth(circleWidth);

        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setStrokeWidth(circleWidth);
        //progressPaint.setStrokeCap(Paint.Cap.ROUND);

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setColor(progressTextColor);
        textPaint.setTextSize(progressTextSize);
        textPaint.setTextAlign(Paint.Align.CENTER);

        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 + outerSize;
        rectF.set(padding, padding, w - padding, h - padding);

        centerX = rectF.centerX();
        centerY = rectF.centerY();

        radius = rectF.width() / 2;
        updateGradient();
    }

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

        // 绘制背景圆环
        //canvas.drawArc(rectF, 0f, 360f, false, backgroundPaint);

        // 绘制背景圆环
        canvas.drawCircle(centerX, centerY, radius, backgroundPaint);

        // 计算进度角度
        float sweepAngle = 360f * progress / maxProgress;
        sweepAngle *= direction;

        // 绘制进度
        canvas.drawArc(rectF, startAngle, sweepAngle, false, progressPaint);

        // 绘制进度文本
        if (showProgressText) {
            String progressText = progress + "%";
            float x = getWidth() / 2f;
            float y = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;
            canvas.drawText(progressText, x, y, textPaint);
        }

        // 绘制指针
        if (pointerBitmap != null) {
            drawPointer(canvas, startAngle + sweepAngle);
        }

    }

    /**
     * 绘制指针
     *
     * @param angle 指针的角度(画布坐标系下的角度)
     */
    private void drawPointer(Canvas canvas, float angle) {

        //Log.d("drawPointer", "angle: " + angle);
        // 计算调整后的半径,使Bitmap边缘紧贴圆环最外部
        float adjustedRadius = radius + backgroundPaint.getStrokeWidth() / 2 + outerSize;

        // 确保Bitmap的右边上的中心点在圆上
        float rightCenterX = centerX + adjustedRadius * (float) Math.cos(Math.toRadians(angle));
        float rightCenterY = centerY + adjustedRadius * (float) Math.sin(Math.toRadians(angle));

        // 计算Bitmap左上角的位置,使得右边上的中心点位于计算出的坐标
        float left = rightCenterX - bitmapWidth;
        float top = rightCenterY - bitmapHeight / 2;

        // 保存画布状态
        canvas.save();

        // 将画布旋转,使得Bitmap对齐到半径上
        canvas.rotate(angle, rightCenterX, rightCenterY);

        // 绘制指针Bitmap
        canvas.drawBitmap(pointerBitmap, left, top, null);

        // 恢复画布状态
        canvas.restore();
    }


    // 更新渐变
    private void updateGradient() {
        float centerX = rectF.centerX();
        float centerY = rectF.centerY();
        float radius = rectF.width() / 2;

        double startAngleRadians = Math.toRadians(startAngle);

        float startX = centerX + (float) (radius * Math.cos(startAngleRadians));
        float startY = centerY + (float) (radius * Math.sin(startAngleRadians));
        float endX = centerX - (float) (radius * Math.cos(startAngleRadians));
        float endY = centerY - (float) (radius * Math.sin(startAngleRadians));

        //Log.d("updateGradient", "startX: " + startX + ", startY: " + startY + ", endX: " + endX + ", endY: " + endY);

        //线性渐变,从一个点渐变到另一个点,因为渐变的距离是圆的直径 所以,TileMode 在这里实际无意义
        gradientShader = new LinearGradient(
                startX, startY, endX, endY,
                progressColors, null,
                Shader.TileMode.CLAMP
        );
        progressPaint.setShader(gradientShader);
    }

    // 设置进度
    public void setProgress(int progress) {
        this.progress = Math.max(0, Math.min(progress, maxProgress));
        invalidate();
    }

    // 设置指针图标
    public void setPointerBitmap(Bitmap bitmap) {
        this.pointerBitmap = bitmap;
        bitmapWidth = bitmap.getWidth();
        bitmapHeight = bitmap.getHeight();
        invalidate();
    }


    // 设置渐变颜色
    public void setProgressColors(int[] colors) {
        this.progressColors = colors;
        updateGradient();
        invalidate();
    }

    // 设置绘制方向
    public void setDirection(int direction) {
        this.direction = direction;
        invalidate();
    }

    // 设置开始角度
    public void setStartAngle(int angle) {
        this.startAngle = angle;
        updateGradient();
        invalidate();
    }

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

6. 使用示例


xml:

    <com.xaye.diyview.view.progressEx.CircularProgressBarEx
        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="20sp"
        app:startAngle="angle0"
        app:direction="clockwise"
        app:outerSize="10dp" />

Activity:

        mBind.circularProgressBar.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))
        mBind.circularProgressBar2.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))
        mBind.circularProgressBar3.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))
        mBind.circularProgressBar4.setPointerBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.point))
        mBind.btnStartAnimation.clickNoRepeat {
            val animator = ValueAnimator.ofInt(0, 95)
            animator.setDuration(2000)
            animator.interpolator = LinearInterpolator()
            animator.addUpdateListener { animation ->
                val value = animation.animatedValue as Int
                mBind.circularProgressBar.setProgress(value)
                mBind.circularProgressBar2.progress = value
                mBind.circularProgressBar3.progress = value
                mBind.circularProgressBar4.progress = value
            }

            animator.start()
        }
    }

7. 最后


本篇文章由网友 Fas 的评论拓展而来,相比于之前那一篇还是稍稍有点难度的,哈哈。

源码及更多自定义View已上传Github:DiyView

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【北京迅为】iTOP-4412全能版使用手册-第七十章 Linux内核移植

iTOP-4412全能版采用四核Cortex-A9&#xff0c;主频为1.4GHz-1.6GHz&#xff0c;配备S5M8767 电源管理&#xff0c;集成USB HUB,选用高品质板对板连接器稳定可靠&#xff0c;大厂生产&#xff0c;做工精良。接口一应俱全&#xff0c;开发更简单,搭载全网通4G、支持WIFI、蓝牙、…

CG顶会论文阅读|《科技论文写作》硕士课程报告

文章目录 一、基本信息1.1 论文基本信息1.2 课程基本信息1.3 博文基本信息 二、论文评述&#xff08;中英双语&#xff09;2.1 研究问题&#xff08;Research Problem&#xff09;2.2 创新点&#xff08;Innovation/Contribution&#xff09;2.3 优点&#xff08;Why this pape…

.NET周刊【12月第4期 2024-12-22】

国内文章 dotnet 简单使用 ICU 库进行分词和分行 https://www.cnblogs.com/lindexi/p/18622917 本文将和大家介绍如何使用 ICU 库进行文本的分词和分行。 dotnet 简单聊聊 Skia 里的 SKFontMetrics 的各项属性作用 https://www.cnblogs.com/lindexi/p/18621674 本文将和大…

git 问题解决记录

在用git上传文件到仓库中出现了2个问题 第一个问题&#xff1a; 需要修改git的代理端口与电脑自己的代理服务器设置中的端口和VPN的端口保持一致&#xff0c; 比如我的端口是7897&#xff0c;就设置 git config --global http.proxy http://127.0.0.1:7897 git config --glo…

XML结构快捷转JSON结构API集成指南

XML结构快捷转JSON结构API集成指南 引言 在当今的软件开发世界中&#xff0c;数据交换格式的选择对于系统的互操作性和效率至关重要。JSON&#xff08;JavaScript Object Notation&#xff09;和XML&#xff08;eXtensible Markup Language&#xff09;是两种广泛使用的数据表…

Oracle 创建本地用户,授予权限,创建表并插入数据

目录 一. 用户的种类二. 切换session为PDB三. 创建用户并授予权限四. 创建表空间五. 为用户分配默认表空间并指定表空间配额六. 通过创建的用户进行登录七. 创建脚本&#xff0c;简化登录八. 查看用户信息九. 创建表&#xff0c;并插入数据9.1 查看当前用户的schema9.2 插入数据…

系统设计——大文件传输方案设计

摘要 大文件传输是指通过网络将体积较大的文件从一个位置发送到另一个位置的过程。这些文件可能包括高清视频、大型数据库、复杂的软件安装包等&#xff0c;它们的大小通常超过几百兆字节&#xff08;MB&#xff09;甚至达到几个吉字节&#xff08;GB&#xff09;或更大。大文…

【老白学 Java】简单位移动画

简单位移动画 文章来源&#xff1a;《Head First Java》修炼感悟。 上一篇文章中&#xff0c;老白利用内部类的特性完成了多个事件的处理&#xff0c;感觉还不错。 为了更深入理解内部类&#xff0c;本篇文章继续使用内部类创建一个画板&#xff0c;完成简单的位移动画&#x…

彻底解决 Selenium ChromeDriver 不匹配问题:Selenium ChromeDriver 最新版本下载安装教程

在 Python 的 Selenium 自动化测试中&#xff0c;ChromeDriver 是不可或缺的工具。它作为代码与 Chrome 浏览器的桥梁&#xff0c;但如果版本不匹配&#xff0c;就会导致各种报错&#xff0c;尤其是以下常见问题&#xff1a; selenium.common.exceptions.SessionNotCreatedExc…

[CTF/网络安全] 攻防世界 warmup 解题详析

查看页面源代码&#xff0c;发现source.php 得到一串代码&#xff0c;进行代码审计&#xff1a; <?phpclass emmm{public static function checkFile(&$page){$whitelist ["source">"source.php","hint">"hint.php"];…

基于fMRI数据计算脑脊液(CSF)与全脑BOLD信号的时间耦合分析

一、前言 笔者之前的文章《基于Dpabi和spm12的脑脊液(csf)分割和提取笔记》,介绍了如何从普通的fMRI数据中提取CSF信号。首先是基础的预处理,包括时间层校正、头动校正,再加上0.01-0.1Hz的带通滤波。接着用SPM12分割出CSF区域,设置一个比较严格的0.9阈值,确保提取的真是…

游泳溺水识别数据集,对25729张图片进行YOLO,COCO JSON, VOC XML 格式的标注,溺水平均识别率在89.9%

游泳溺水识别数据集&#xff0c;对25729张图片进行YOLO&#xff0c;COCO JSON, VOC XML 格式的标注&#xff0c;溺水识别率在92&#xff05; 训练结果 数据集和标签 验证 游泳测试视频 根据测试的视频来获取检测结果&#xff1a; 游泳测试视频的置信度设置60% 检测结果如下&…

STM32 拓展 电源控制

目录 电源控制 电源框图 VDDA供电区域 VDD供电区域 1.8V低电压区域 后备供电区域 电压调节器 上电复位和掉电复位 可编程电压检测器(PVD) 低功耗 睡眠模式(只有CUP(老板)睡眠) 进入睡眠模式 退出睡眠模式 停机(停止)模式(只留核心区域(上班)) 进入停…

Mac M2 Pro安装MySQL 8.4.3

絮絮叨叨 MacBook Pro&#xff0c;芯片&#xff1a;Apple M2 Pro, macOS: Sonoma 14.0一直知道很多软件对Mac M1或M2的支持不好&#xff0c;但没想到在安装MySQL 8.x上也让我吃尽了苦头本文除了介绍如何安装MySQL 8.4.3外&#xff0c;还会记录笔者遇到的一些问题以及解决方法 …

闻泰科技涨停-操盘训练营实战-选股和操作技术解密

如上图&#xff0c;闻泰科技&#xff0c;今日涨停&#xff0c;这是前两天分享布局的一个潜伏短线的标的。 选股思路&#xff1a; 1.主图指标三条智能辅助线粘合聚拢&#xff0c;即将选择方向 2.上图红色框住部分&#xff0c;在三线聚拢位置&#xff0c;震荡筑底&#xff0c;…

ts总结一下

ts基础应用 /*** 泛型工具类型*/ interface IProps {id: string;title: string;children: number[]; } type omita Omit<IProps, id | title>; const omitaA: omita {children: [1] }; type picka Pick<IProps, id | title>; const pickaA: picka {id: ,title…

Linux:各发行版及其包管理工具

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 Debian 包管理工具&#xff1a;dpkg&#xff08;低级包管理器&#xff09;、apt&#xff08;高级包管理器&#xff0c;建立在dpkg基础上&#xff09;包格式&#xff1a;…

2024秋语法分析作业-B(满分25分)

特别注意&#xff1a;第17条产生式改为 17) Stmt → while ( Cond ) Stmt 【问题描述】 本次作业只测试一个含简单变量声明、赋值语句、输出语句、if语句和while语句的文法&#xff1a; 0) CompUnit → Block 1) Block → { BlockItemList } 2) BlockItemList → BlockItem…

Tomcat优化指南

以下是一份详细的Tomcat优化指南&#xff1a; 一、JVM&#xff08;Java虚拟机&#xff09;优化 内存设置 堆内存&#xff08;Heap Memory&#xff09; 调整-Xms&#xff08;初始堆大小&#xff09;和-Xmx&#xff08;最大堆大小&#xff09;参数。一般来说&#xff0c;将初始…

【我的 PWN 学习手札】IO_FILE 之 劫持vtable

vtable帮助C实现了类似于多态的效果&#xff0c;然而其中的大量函数指针&#xff0c;一旦被劫持修改&#xff0c;就会产生巨大的危害。 前言 【我的 PWN 学习手札】IO_FILE相关几个基本函数的调用链源码-CSDN博客 【我的 PWN 学习手札】IO_FILE 之 stdin任意地址写-CSDN博客…