Android实现一个可拖拽带有坐标尺的进度条

news2024/12/24 8:52:54

拿到上边的UI效果图,给我的第一印象就是这实现起来也太简单了吧,SeekBar轻轻松松就搞定了,换个thumb,加个渐变不就完成了,说搞就搞,搞着搞着就抑郁了,底部坐标尺还能搞,等比例分割后,在SeekBar下面多设置几个TextView就行了,中间的等比例小分割线怎么搞?而且滑动前滑动后都需要有,并且,左右的分割线还要留出一小段间距,渐变颜色要跟着滑动的距离进行展示,而不是整个宽度展示,在多种条件下,SeekBar就很难满足这个需求了,怎么办?只能自定义了。

还是按照惯例,粗略的列一个大纲:

1、分析要素,确定实现方案

2、主要代码进行刨析

3、开源地址及使用方式

4、总结

一、分析要素,确定实现方案

Canvas绘制这样的一个可拖拽坐标尺,基本上可以拆分出四部分,第一部分就是背景和默认的离散间隔,第二部分是移动的背景和离散间隔,第三部分是移动的图片也就是thumb,最后一部分是底部的文字坐标。

四部分基本上就绘制出来了,但是除了绘制之外,还需要考虑一下其他的因素,比如高度,比如手指的移动事件等。

1、设置默认高度

设置默认高度的原因,是为了让View更好的展示一个合适的尺寸,不至于设置wrap_content时不展示,具体的设置可以根据当前设置的模式来控制,关于模式呢,有三种,这个在之前的文章中介绍过,这里就不详细介绍了,当控件设置wrap_content时,此时的模式为MeasureSpec.AT_MOST,在这个模式下,我们就要给一个默认的高度。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        var windowHeight = heightMeasureSpec
        if (heightMode == MeasureSpec.AT_MOST) {
            windowHeight = mDefaultHeight.toInt()//默认的高度
        }
        setMeasuredDimension(widthMeasureSpec, windowHeight)
    }

2、拖动事件

实现拖动效果,我们就需要监听用户的手指移动事件了,也就是在自定义View中我们要重写onTouchEvent方法,在这个方法里,需要针对手指的按下、抬起、移动做相应的处理。

在onTouchEvent里我做了如下处理,一是直接返回,不执行事件的消费,目的是让自定义View可实现静态展示和动态展示两种效果,通过一个变量mProgressIsIntercept来控制;第二个是解决与父View的滑动冲突事件,在有横向或者纵向滑动事件时,在拖动的时候,难免会有冲突,那么就需要通知父View不要消费事件,也就是执行requestDisallowInterceptTouchEvent方法。

所有的拖拽效果,都是在move事件,不断的改变坐标执行更新UI的方式实现的,mMoveProgress就是手指移动的坐标。

onTouchEvent(event: MotionEvent?): Boolean {
        super.onTouchEvent(event)
        //如果为true直接返回,不进行拖拽
        if (mProgressIsIntercept) {
            return mProgressIsIntercept
        }
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(mDisallowIntercept)
                val downX = getChangeX(event.x)
                val startX = mMoveOldX - mProgressMarginLeftRight
                val endX = mMoveOldX + mProgressMarginLeftRight
                return downX in startX..endX
            }
            MotionEvent.ACTION_MOVE -> {
                //移动
                var moveX = getChangeX(event.x)
                //滑动至最右边
                //计算最后边的坐标
                val viewWidth = getViewWidth()

                if (moveX >= viewWidth) {
                    moveX = viewWidth
                }

                mMoveProgress = moveX

                invalidate()
            }
            MotionEvent.ACTION_UP -> {
                //手指谈起
                mMoveOldX = getChangeX(event.x)

                val viewWidth = getViewWidth()

                if (mMoveOldX >= viewWidth) {
                    mMoveOldX = viewWidth
                }
            }
        }
        return true
    }

二、主要代码进行刨析

1、绘制背景

背景没什么好说的,就是一个简单的圆角矩形,使用drawRoundRect绘制即可,需要确定的是左上右下的间距。

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制背景
     */
    private fun canvasBackground(canvas: Canvas) {
        mPaint!!.color = mProgressBackground
        val rect = RectF().apply {
            left = mProgressMarginLeftRight
            top = mProgressMarginTopBottom
            right = width.toFloat() - mProgressMarginLeftRight
            bottom = mProgressHeight + mProgressMarginTopBottom
        }
        canvas.drawRoundRect(rect, mProgressRadius, mProgressRadius, mPaint!!)
    }

2、绘制离散间隔

离散间隔,需要确定,间隔数,然后根据间隔数量,动态的计算每个间隔的位置,可以使用drawLine绘制一个小小的竖线,竖线也需要确定距离上下的距离和自身的宽度;特殊情况下,离散间隔,在滑动前后的颜色是不一样的,所以这里也做了一个动态改变颜色的判断。

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制离散间隔
     */
    private fun canvasIntervalLine(canvas: Canvas, isCanvas: Boolean) {
        val rect =
            (width - mProgressMarginLeftRight * 2 - mIntervalParentLeftRight * 2) / mIntervalSize
        if (isCanvas) {
            mPaint!!.color = mIntervalSelectColor
        } else {
            mPaint!!.color = mIntervalColor
        }

        mPaint!!.strokeWidth = mIntervalWidth
        for (a in 0..mIntervalSize) {
            val x = (rect * a) + mProgressMarginLeftRight + mIntervalParentLeftRight
            val y = mIntervalMarginTopBottom + mProgressMarginTopBottom
            canvas.drawLine(
                x,
                y,
                x,
                mProgressHeight + mProgressMarginTopBottom - mIntervalMarginTopBottom,
                mPaint!!
            )
        }
    }

3、绘制移动thumb

关于thumb,首先要确定的就是大小,如果设置了宽高,那么就需要使用Bitmap重新设置高度,改变thumb的坐标,只需要不断的改变图片的left坐标点即可,也就是通过上述的Move事件的中移动坐标来设置。

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制移动的图标
     */
    private fun canvasMoveIcon(canvas: Canvas) {
        mProgressThumb?.let {
            var decodeResource = BitmapFactory.decodeResource(resources, it)
            mProgressThumbWidth = decodeResource.width
            if (mThumbWidth != 0f) {
                val height: Int = decodeResource.height
                // 设置想要的大小
                val newWidth = mThumbWidth
                val newHeight = mThumbHeight
                // 计算缩放比例
                val scaleWidth = newWidth / width
                val scaleHeight = newHeight / height
                // 取得想要缩放的matrix参数
                val matrix = Matrix()
                matrix.postScale(scaleWidth, scaleHeight)
                // 得到新的图片
                decodeResource =
                    Bitmap.createBitmap(decodeResource, 0, 0, width, height, matrix, true)

            }

            var mThumpLeft = mMoveProgress
            if (mThumpLeft < (mProgressThumbWidth / 2 - mIntervalParentLeftRight + mProgressThumbSpacing)) {
                mThumpLeft =
                    mProgressThumbWidth / 2 - mIntervalParentLeftRight + mProgressThumbSpacing
            }

            if (mThumpLeft > (getViewWidth() - mIntervalParentLeftRight + mProgressThumbSpacing)) {
                mThumpLeft = getViewWidth() - mIntervalParentLeftRight + mProgressThumbSpacing
            }

            canvas.drawBitmap(
                decodeResource, mThumpLeft, mThumbMarginTop, mIconPaint!!
            )
        }
    }

4、绘制移动进度

移动的进度,和背景的绘制是一样的,只不过需要按照手指的坐标一点一点的移动距离,也就是不断的改变右边的坐标值,同样的,也是通过Move事件中的mMoveProgress进度来动态的计算。进度的渐变比较简单,使用的是画笔的shader属性,当前使用的是横向的线性渐变LinearGradient。

/**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制进度
     */
    private fun canvasMoveProgress(canvas: Canvas) {
        //为空
        if (mColorArray.isEmpty()) {
            mColorArray = intArrayOf(
                ContextCompat.getColor(context, R.color.text_ff3e3e93),
                ContextCompat.getColor(context, R.color.text_ff8548d2),
            )
        }
        val linearShader = LinearGradient(
            0f,
            0f,
            mMoveProgress + mProgressMarginLeftRight,
            mProgressHeight,
            mColorArray,
            floatArrayOf(0f, 1f),
            Shader.TileMode.CLAMP
        )
        mProgressPaint!!.shader = linearShader


        //等于0时
        val rect = RectF()
        rect.left = mProgressMarginLeftRight
        rect.top = mProgressMarginTopBottom
        rect.right = mMoveProgress + mProgressMarginLeftRight
        rect.bottom = mProgressHeight + mProgressMarginTopBottom
        canvas.drawRoundRect(rect, mProgressRadius, mProgressRadius, mProgressPaint!!)


        //计算比例
          
        mGraduationResult =
            ((mMoveProgress / getViewWidth()) * mMaxProgress).roundToInt()//(endProgress * mMaxProgress).roundToInt()

        if (mGraduationResult < 1) {
            mGraduationResult = if (mGraduationSectionZero) {
                0
            } else {
                1
            }
        }
        if (mGraduationResult >= mMaxProgress) {
            mGraduationResult = mMaxProgress
        }

        mMoveProgressCallback?.invoke(mGraduationResult)

    }

5、绘制文字刻度

其实大家可以发现,离散间隔和底部的坐标文字刻度,其实是一一对应的,既然是相互关联,我们直接放到一起就可以,也就是在遍历离散间隔的时候,我们直接绘制底部的坐标尺刻度。

坐标刻度,有四种效果,第一种是不要刻度值,第二种是只要开始和结尾刻度值,第三种是展示所有的刻度值,第四种是刻度值是从0还是从1开始。

mIsGraduation是用于判断是否需要刻度值的变量,为true则需要绘制,否则就不绘制,也就是不需要刻度值。mHideGraduationSectionCenter为隐藏中间刻度的变量,为true隐藏,否则为不隐藏,具体的代码如下:

 /**
     * AUTHOR:AbnerMing
     * INTRODUCE:绘制离散间隔
     */
    private fun canvasIntervalLine(canvas: Canvas, isCanvas: Boolean) {
        val rect =
            (width - mProgressMarginLeftRight * 2 - mIntervalParentLeftRight * 2) / mIntervalSize
        if (isCanvas) {
            mPaint!!.color = mIntervalSelectColor
        } else {
            mPaint!!.color = mIntervalColor
        }

        mPaint!!.strokeWidth = mIntervalWidth
        for (a in 0..mIntervalSize) {
            val x = (rect * a) + mProgressMarginLeftRight + mIntervalParentLeftRight
            val y = mIntervalMarginTopBottom + mProgressMarginTopBottom
            canvas.drawLine(
                x,
                y,
                x,
                mProgressHeight + mProgressMarginTopBottom - mIntervalMarginTopBottom,
                mPaint!!
            )
            //绘制刻度值
            if (mIsGraduation && isCanvas) {

                if (mHideGraduationSectionCenter && (a != 0 && a != mIntervalSize)) {
                    //隐藏中间
                    continue
                }

                var graduation = a * mGraduationSection
                //是否从0开始记录
                if (graduation == 0 && !mGraduationSectionZero) {
                    graduation = 1
                }

                //如果移动到了,改变颜色


                if (mGraduationResult >= graduation && mGraduationResult < graduation + mGraduationSection) {
                    mGraduationPaint?.color = mGraduationSelectTextColor
                } else {
                    mGraduationPaint?.color = mGraduationTextColor
                }


                val text = graduation.toString()
                val rectText = Rect()
                mGraduationPaint!!.getTextBounds(text, 0, text.length, rectText)
                val textWidth = rectText.width()
                val textHeight = rectText.height()
                canvas.drawText(
                    text,
                    x - textWidth / 2,
                    mProgressHeight + mProgressMarginTopBottom * 2 + textHeight + mGraduationMarginTop,
                    mGraduationPaint!!
                )

            }

        }
    }

三、开源地址及使用方式

目前已经上传到了Github,本身就一个简单的类,没多少东西,需要的铁子,可以直接查看源码即可。

地址:https://github.com/AbnerMing888/MoveProgress

如果懒得下载源码,想直接使用,没得问题,我已经上传到了远程Maven,大家可以依赖使用。

1、在你的根项目下的build.gradle文件下,引入maven。

allprojects {
    repositories {
        maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
    }
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。

dependencies {
    implementation 'com.vip:moveprogress:1.0.0'
}

3、XML引入即可

 <com.vip.moveprogress.MoveProgress
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:ms_graduation_hide_center="true" />

相关属性

属性

类型

概述

ms_height

dimension

View视图的高度

ms_progress_height

dimension

进度条的高度

ms_progress_thumb

reference

进度条的Icon

ms_progress_margin_top_bottom

dimension

进度条距离icon的上下距离

ms_progress_margin_left_right

dimension

进度条距离左右的边距

ms_progress_radius

dimension

进度条的圆角

ms_progress_background

color

进度条的背景颜色

ms_interval_color

color

间隔线颜色

ms_interval_select_color

color

间隔线选中颜色

ms_interval_parent_margin_left_right

dimension

间隔线距离父左右

ms_interval_size

integer

间隔线数量

ms_interval_width

dimension

间隔线宽度

ms_interval_margin_top_bottom

dimension

间隔线上下边距

ms_progress_move_color

reference

定义的移动颜色

ms_progress_max

integer

最大进度

ms_progress_default

integer

默认进度

ms_is_graduation

boolean

是否显示刻度尺

ms_graduation_text_size

dimension

刻度尺文字大小

ms_graduation_text_color

color

刻度尺文字颜色

ms_graduation_select_text_color

color

刻度尺文字选中颜色

ms_graduation_section

integer

刻度值段

ms_graduation_section_zero

boolean

刻度值段从零开始

ms_graduation_hide_center

boolean

刻度值段中间是否隐藏

ms_graduation_margin_top

dimension

刻度值距离上边的距离

ms_progress_thumb_width

dimension

icon的宽

ms_progress_thumb_height

dimension

icon的高

ms_progress_thumb_margin_top

dimension

icon距离上边的高度

ms_progress_thumb_spacing

dimension

icon的内边距

ms_progress_disallow_intercept

boolean

是否拦截

ms_progress_is_intercept

boolean

是否禁止拖拽

相关方法

方法

参数

概述

getProgress

无参

返回当前进度

changeProgress

Int

改变当前进度

getMoveProgress

返回Int

回调函数

setProgressIsIntercept

Boolean

设置是否进行拦截

四、总结

关于渐变,需要注意,渐变的范围不是默认的从左到右固定的距离,而是从左到手指滑动的距离,这一点需要注意,也就是在设置渐变的时候,终止的X坐标需要根据手势的左边动态设置。

从这个简单的拖拽进度条,我们可以了解到,canvas绘制线,圆角矩形,图片以及和手势结合的相关知识点,本身并没有难点。

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

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

相关文章

Springboot开发微信小游戏后台-玩家登录流程

最近使用Springboot开发了一个微信小游戏的后台服务&#xff0c;为小游戏提供接口&#xff0c;其中登录需要前后端与微信服务端配合。 注意使用自己开发的服务作为小游戏后端&#xff0c;前提条件是必须要有域名证书&#xff0c;提供https服务&#xff0c;否则在微信正式环境下…

QT Creator写一个简单的电压电流显示器

前言 本文主要涉及上位机对接收的串口数据处理&#xff0c;LCD Number控件的使用。之前的一篇写一个简单的LED控制主要是串口发出数据&#xff0c;这里再看一下怎么接收数据处理数据&#xff0c;这样基本就对串口上位机有简单的认识了。 LCD Number显示时间 这一小节通过用一…

从实现到原理,我总结了11种延迟任务的实现方式

延迟任务在我们日常生活中比较常见&#xff0c;比如订单支付超时取消订单功能&#xff0c;又比如自动确定收货的功能等等。 所以本篇文章就来从实现到原理来盘点延迟任务的11种实现方式&#xff0c;这些方式并没有绝对的好坏之分&#xff0c;只是适用场景的不大相同。 DelayQu…

【python】js逆向基础案例——有道翻译

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 课程亮点: 1、爬虫的基本流程 2、反爬的基本原理 3、nodejs的使用 4、抠代码基本思路 环境介绍: python 3.8 pycharm 2022专业版 >>> 免费使用教程文末名片获取 requests >>> pip install req…

Vue 配置正向代理的使用

浏览器对于用户的安全考虑&#xff0c;设置了同源策略。同源策略就是指协议、域名、端口都要相同的情况下&#xff0c;才能请求资源。 跨域&#xff1a; 跨域指的是&#xff1a;在浏览器中&#xff0c;从一个域名去请求另一个域名的资源时&#xff0c;如果协议、域名、端口任意…

深入理解 SpringBoot 日志框架:从入门到高级应用——(六)Log4j2 输出日志到 QQ邮箱

文章目录 获取 QQ 邮箱授权码添加依赖编写 SMTPAppender运行结果 要实现将 log4j2 输出日志到 QQ 邮箱&#xff0c;需按照以下步骤进行&#xff1a; 在 QQ 邮箱中设置 SMTP 服务&#xff0c;开启 POP3/SMTP 服务&#xff0c;获取 SMTP 服务地址、端口号、登录邮箱账号和密码。 …

拿捏指针(二)---对指针的进阶认识(中级)

文章目录 字符指针指针数组数组指针数组指针的定义&数组名与数组名的区别数组指针的使用 数组参数、指针参数一维数组传参二维数组传参一级指针传参二级指针传参 字符指针 我们知道&#xff0c;在指针的类型中有一种指针类型叫字符指针char * 。 字符指针的一般使用方法为…

Pytest自动化测试的三种运行方式

目录 1、主函数模式 2、命令行模式 3、通过读取pytest ini配置文件运行 &#xff08;最主要运用的方式&#xff09; 总结&#xff1a; Pytest 运行方式共有三种&#xff1a; 1、主函数模式 运行所有 pytest.main() 指定模块 pytest.main([-vs],,./testcase/test_day1.py)…

组合逻辑电路设计---多路选择器

目录 1、多路选择器简介 2、硬件设计 3、实验任务 4、程序设计 4.1、模块设计 4.2、绘制波形图 4.3、编写代码 &#xff08;1&#xff09;assign 中条件运算符&#xff08;三目运算符&#xff09;实现方法&#xff1a; &#xff08;2&#xff09;always 语句块中使用 …

逍遥自在学C语言 | 指针的基础用法

前言 在C语言中&#xff0c;指针是一项重要的概念&#xff0c;它允许我们直接访问和操作内存地址。 可以说&#xff0c;指针是C语言一大优势。用得好&#xff0c;你写程序如同赵子龙百万军中取上将首级&#xff1b;用得不好&#xff0c;则各种问题层出不穷&#xff0c;有种双…

.gitignore 忽略文件和目录

1. .gitignore 简介2. .gitignore 注释3. / 开头或结尾的忽略4. glob 模式匹配忽略5. .gitignore 全局忽略6. 忽略已提交到远程仓库的内容7. 使用各种框架下的忽略规则 1. .gitignore 简介 .gitignore 文件的作用就是告诉 git 哪些文件不需要添加到版本管理中&#xff08;定义…

Python如何制作图标点选验证码

本文讲解如何使用python中的opencv库来制作图标点选验证码 图标点选验证码制作起来非常简单,你只需要准备两部分数据集,数据集数量都不用很多,背景图我选择了20个左右,大小为(300, 500)左右,图标我抓取了100多个,图标大小为(40,40)左右,图标由不同大小的透明度构成…

html实现好看的个人介绍,个人主页模板1(附源码)

文章目录 1.设计来源1.1 主界面1.2 关于我界面1.3 自我介绍界面1.4 项目演示界面1.5 个人成就界面1.6 联系我界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/13125310…

01-Maven 安装

一. 下载 apache官网下载 Maven&#xff1a;Maven – Download Apache Maven &#xff0c;根据需要下载不同压缩包。 二. 安装和配置 因为是压缩包不是可执行文件&#xff0c;直接将压缩包进行解压即可&#xff0c;最好放在无中文目录下解压。 1. 配置maven本地仓库 打开解压…

自然语言处理从入门到应用——静态词向量预训练模型:神经网络语言模型(Neural Network Language Model)

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 《自然语言处理从入门到应用——自然语言处理的语言模型&#xff08;Language Model&#xff0c;LM&#xff09;》中介绍了语言模型的基本概念&#xff0c;以及经典的基于离散符号表示的N元语言模型&#xff08;N-gram…

每日一道算法---数组中出现次数超过一半的数字

数组中出现次数超过一半的数字 1.题目2.思路3.代码 1.题目 链接: 数组中出现次数超过一半的数字 2.思路 【解题思路1】&#xff1a; 思路一&#xff1a;数组排序后&#xff0c;如果符合条件的数存在&#xff0c;则一定是数组中间那个数。这种方法虽然容易理解&#xff0c;但…

gcov的使用

什么是代码覆盖率&#xff1f; 代码覆盖率是对整个测试过程中被执行的代码的衡量&#xff0c;它能测量源代码中的哪些语句在测试中被执行&#xff0c;哪些语句尚未被执行。 代码覆盖率的指标种类 代码覆盖率工具通常使用一个或多个标准来确定你的代码在被自动化测试后是否得…

SQL注入第一章节

SQL注入第一章节 1.1 什么是SQL注入 SQL 注入&#xff08;Injection&#xff09; 概述 SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严&#xff0c;攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句&#xff0c;在管理员不知情…

合并表格的指定列按序号排序

这里有一个Excel需求&#xff1a; 如下图所示&#xff0c;需要在序号那一列自动排序下去。 但是是合并的行&#xff0c;而且合并的行数还是不确定的&#xff0c;那怎么给他自动排序下去呢&#xff1f; 解决方法可供参考&#xff1a;使用筛选和COUNT函数完成。 1.第一步筛选 首…

Collection集合

Collection集合面试题 导学 这次课程主要涉及到的是List和Map相关的面试题&#xff0c;比较高频就是 ArrayList LinkedList HashMap ConcurrentHashMap ArrayList底层实现是数组LinkedList底层实现是双向链表HashMap的底层实现使用了众多数据结构&#xff0c;包含了数组、…