Android运动健康血氧自定义控件



/**
 *
 * 日图表
 * zrj 2020/9/1
 */
class BODayChart(context: Context, attrs: AttributeSet?) : View(context, attrs) {
    //屏幕宽高
    private var scrWidth = 0f
    private var scrHeight = 0f
    private var xData: Array<String> = arrayOf("00:00", "06:00", "12:00", "18:00", "00:00")
    private var yData: Array<Int> = arrayOf(100, 90, 85, 80, 70)
    private var boData = mutableListOf<Int>()
    private lateinit var paintLine: Paint
    private lateinit var paintGradientLine: Paint
    private lateinit var paintXText: Paint
    private lateinit var paintYText: Paint
    private lateinit var paintPillar: Paint
    private lateinit var paintRound: Paint
    private lateinit var paintBessel: Paint

    private var animDuration = 500L
    private var anim: ValueAnimator? = null
    private var mPercent = 0f //动画进度
    private var xSlider = 0f //滑块的x轴位置

    private var mPath: Path
    private val curveCircleRadius = 12f.dp

    // the coordinates of the first curve
    private val mFirstCurveStartPoint = Point()
    private val mFirstCurveEndPoint = Point()
    private val mFirstCurveControlPoint1 = Point()
    private val mFirstCurveControlPoint2 = Point()

    //the coordinates of the second curve
    private var mSecondCurveStartPoint = Point()
    private val mSecondCurveEndPoint = Point()
    private val mSecondCurveControlPoint1 = Point()
    private val mSecondCurveControlPoint2 = Point()


    init {
        setLayerType(LAYER_TYPE_SOFTWARE, null)
        mPath = Path()
        initPaint()
    }

    /**
     * 初始化画笔
     */
    private fun initPaint() {

        paintLine = Paint()
        paintLine.style = Paint.Style.STROKE
        paintLine.strokeWidth = 1f
        paintLine.color = context.colorCompat(R.color.e6e6e6_2e2e2e)

        paintGradientLine = Paint()
        paintGradientLine.style = Paint.Style.STROKE
        paintGradientLine.strokeWidth = 2f

        paintXText = Paint()
        paintXText.isAntiAlias = true
        paintXText.strokeWidth = 1f
        paintXText.textSize = 12f.sp
        paintXText.textAlign = Paint.Align.CENTER
        paintXText.color = context.colorCompat(R.color.color_on_surface)

        paintYText = Paint()
        paintYText.isAntiAlias = true
        paintYText.textSize = 12f.sp
        paintYText.strokeWidth = 1f
        paintYText.textAlign = Paint.Align.RIGHT
        paintYText.color = context.colorCompat(R.color.secondary_666666_808080)

        paintPillar = Paint()
        paintPillar.style = Paint.Style.FILL
        paintPillar.isAntiAlias = true
        paintPillar.color = context.colorCompat(R.color.fc355c_fc3159)

        paintRound = Paint()
        paintRound.style = Paint.Style.FILL
        paintRound.isAntiAlias = true
        paintRound.color = context.colorCompat(R.color.ffffff_6e6e6e)

        paintBessel = Paint()
        paintBessel.style = Paint.Style.FILL
        paintBessel.isAntiAlias = true
        paintBessel.color = context.colorCompat(R.color.f2f2f2_1d1d1d)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        parent.requestDisallowInterceptTouchEvent(true)
        return super.dispatchTouchEvent(ev)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        scrWidth = width.toFloat()
        scrHeight = height.toFloat()
        ySpacing = scrHeight / 10f //y轴分10份

        //底部圆滑块可以滑动的范围
        xWithStart = margin + paintXText.measureText(xData[0]) / 2
        xWithEnd = scrWidth - margin - paintYText.measureText(yData[0].toString()) * 2.5f

        xTextSpacing = (xWithEnd - xWithStart) / (xData.size - 1)
        xSpacing = xTextSpacing / 36 //x轴等分4份   144

    }

    private var mDownX = 0f
    private var mDownY = 0f
    private var isSlider = false

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mDownX = event.x
                mDownY = event.y
                isSlider = abs(event.x - xSlider) < 60f && abs(event.y - ySpacing * 9) < 60f
            }
            MotionEvent.ACTION_MOVE ->
                if (abs(event.y - mDownY) < abs(event.x - mDownX)) {
                    if (isSlider) {
                        xSlider = event.x
                        invalidate()
                    }
                }

            MotionEvent.ACTION_UP -> {
                if (isSlider) {
                    if (xSlider < xWithStart) {
                        xSlider = xWithStart
                        invalidate()
                    }
                    if (xSlider > xWithEnd) {
                        xSlider = xWithEnd
                        invalidate()
                    }
                    boData.forEachIndexed { index, _ ->
                        val x = xWithStart + xSpacing * index
                        val dis = abs(x - xSlider)
                        if (dis < xSpacing / 2) {
                            xSlider = x
                            invalidate()
                            return@forEachIndexed
                        }
                    }
                } else {
                    if (abs(event.x - mDownX) > xSpacing) {
                        onDayMoveListener?.invoke(event.x > mDownX)
                    } else {
                        boData.forEachIndexed { index, _ ->
                            val x = xWithStart + xSpacing * index
                            val dis = abs(x - event.x)
                            if (dis < xSpacing) {
                                xSlider = x
                                invalidate()
                                return@forEachIndexed
                            }
                        }
                    }
                }
            }
        }
        return true
    }

    private val margin = 20f.dp //左右两边距离
    private var xWithStart = 0f //x轴的起始点
    private var xWithEnd = 0f  //x轴结束点
    private var ySpacing = 0f //高度分割份数后间距
    private var xSpacing = 0f //x轴柱子分割份数后间距
    private var xTextSpacing = 0f //x轴文字分割份数后间距

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //画y轴方向横线与文字
        drawY(canvas)
        //垂直渐变线
        drawGradientLine(canvas)
        //画柱子
        drawPillar(canvas)
        //底部
        drawBessel(canvas)
        //画x轴方向文字
        drawX(canvas)
    }

    private fun drawX(canvas: Canvas) {
        xData.forEachIndexed { index, s ->
            val x = xWithStart + xTextSpacing * index
            val dis = abs(x - xSlider)
            var y = ySpacing * 9 - 10f
            if (dis < xTextSpacing / 2) {
                paintXText.typeface = Typeface.DEFAULT_BOLD
                y -= 40f * (1 - dis / xTextSpacing)
            } else {
                paintXText.typeface = Typeface.DEFAULT
            }
            canvas.drawText(s, x, y, paintXText)
        }
    }

    private fun drawPillar(canvas: Canvas) {
        boData.forEachIndexed { index, i ->
            if (xSlider < xWithStart + xSpacing * index + xSpacing / 2 && xSlider > xWithStart + xSpacing * index - xSpacing / 2) {
                onDaySelectListener?.invoke(index, i)
            }

            if (i > 89) {
                paintPillar.color = Color.parseColor("#00d656")
            } else {
                paintPillar.color = Color.parseColor("#fdc221")
            }

            if (i > 0) {
                canvas.drawRoundRect(
                    RectF(
                        xWithStart + xSpacing * index - xSpacing / 2,
                        ySpacing + ySpacing * ((100 - i) / 5f) * mPercent,
                        xWithStart + xSpacing * index + xSpacing / 2,
                        ySpacing * 7
                    ), 2f, 2f, paintPillar
                )
            }
        }
    }

    private fun drawY(canvas: Canvas) {
        var k = 1
        for (i in 0..4) {
            if (i > 0) {
                k = i + 2
            }
            if (i == 4) {
                k = 7
            }
            canvas.drawLine(
                margin,
                ySpacing * k,
                scrWidth - margin,
                ySpacing * k,
                paintLine
            )

            canvas.drawText(
                "${yData[i]}%",
                scrWidth - margin,
                ySpacing * k - 10f,
                paintYText
            )
        }
    }

    private fun drawBessel(canvas: Canvas) {
        // 第一条曲线开始点
        mFirstCurveStartPoint[(xSlider - curveCircleRadius * 3).toInt()] = (ySpacing * 9).toInt()
        // 第一条曲线结束点
        mFirstCurveEndPoint[xSlider.toInt()] =
            (ySpacing * 9 - curveCircleRadius - curveCircleRadius / 4).toInt()
        // 第二条开始点
        mSecondCurveStartPoint = mFirstCurveEndPoint
        mSecondCurveEndPoint[(xSlider + curveCircleRadius * 3).toInt()] = (ySpacing * 9).toInt()

        // 第一条控制点
        mFirstCurveControlPoint1[(mFirstCurveStartPoint.x + curveCircleRadius + curveCircleRadius / 4).toInt()] =
            mFirstCurveStartPoint.y
        mFirstCurveControlPoint2[(mFirstCurveEndPoint.x - curveCircleRadius * 2 + curveCircleRadius).toInt()] =
            mFirstCurveEndPoint.y
        // 第二条控制点
        mSecondCurveControlPoint1[(mSecondCurveStartPoint.x + curveCircleRadius * 2 - curveCircleRadius).toInt()] =
            mSecondCurveStartPoint.y
        mSecondCurveControlPoint2[(mSecondCurveEndPoint.x - curveCircleRadius - curveCircleRadius / 4).toInt()] =
            mSecondCurveEndPoint.y
        mPath.reset()
        mPath.moveTo(0f, ySpacing * 9)
        mPath.lineTo(mFirstCurveStartPoint.x.toFloat(), mFirstCurveStartPoint.y.toFloat())
        mPath.cubicTo(
            mFirstCurveControlPoint1.x.toFloat(), mFirstCurveControlPoint1.y.toFloat(),
            mFirstCurveControlPoint2.x.toFloat(), mFirstCurveControlPoint2.y.toFloat(),
            mFirstCurveEndPoint.x.toFloat(), mFirstCurveEndPoint.y.toFloat()
        )
        mPath.cubicTo(
            mSecondCurveControlPoint1.x.toFloat(), mSecondCurveControlPoint1.y.toFloat(),
            mSecondCurveControlPoint2.x.toFloat(), mSecondCurveControlPoint2.y.toFloat(),
            mSecondCurveEndPoint.x.toFloat(), mSecondCurveEndPoint.y.toFloat()
        )
        mPath.lineTo(scrWidth, ySpacing * 9)
        mPath.lineTo(scrWidth, scrHeight)
        mPath.lineTo(0f, scrHeight)
        mPath.close()

        //底部灰色
        canvas.drawPath(mPath, paintBessel)
        //底部滑块
        canvas.drawCircle(xSlider, ySpacing * 9 + 5f, curveCircleRadius, paintRound)
    }

    fun setValue(value: MutableList<Int>, time: Int): BODayChart {
        boData.clear()
        boData.addAll(value)
        xSlider = xSpacing * time + xWithStart
        startAnimation()
        return this
    }

    private fun startAnimation() {
        anim = ValueAnimator.ofObject(AngleEvaluator(), 0f, 1f)
        anim?.interpolator = AccelerateDecelerateInterpolator()
        anim?.addUpdateListener { animation ->
            mPercent = animation.animatedValue as Float
            postInvalidate()
        }
        anim?.duration = animDuration
        anim?.start()
    }

    private fun drawGradientLine(canvas: Canvas) {
        val mLinearGradient = LinearGradient(
            xSlider, ySpacing, xSlider, ySpacing * 8,
            intArrayOf(
                context.colorCompat(R.color.ffffff_262626),
                context.colorCompat(R.color.fc355c_fc3159),
                context.colorCompat(R.color.ffffff_262626)
            ), null, Shader.TileMode.MIRROR
        )
        paintGradientLine.shader = mLinearGradient

        if (ySpacing > 0) {
            canvas.drawLine(xSlider, ySpacing, xSlider, ySpacing * 8, paintGradientLine)
        }
    }

    private var onDaySelectListener: ((index: Int, value: Int) -> Unit)? = null

    fun setOnDaySelectListener(l: ((index: Int, value: Int) -> Unit)): BODayChart {
        this.onDaySelectListener = l
        return this
    }

    private var onDayMoveListener: ((isPre: Boolean) -> Unit)? = null

    fun setOnDayMoveListener(l: ((index: Boolean) -> Unit)): BODayChart {
        this.onDayMoveListener = l
        return this
    }
}



转自https://juejin.cn/post/6944670773520891912?from=search-suggest

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

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

相关文章

数造科技亮相第26届高交会并接受媒体采访,以数据智能赋能未来

11 月 14 日至 16 日&#xff0c;第二十六届中国国际高新技术成果交易会&#xff08;简称“高交会”&#xff09;在深圳成功举办。本届大会以“科技引领发展&#xff0c;产业融合聚变”为主题&#xff0c;汇聚了全球最新的科技成果&#xff0c;打造了一场科技界的盛大聚会。 在…

Facebook广告投放如何提高过审率?

在Facebook进行广告投放活动时&#xff0c;如何让广告过审应该是让很多人头疼的事情&#xff0c;前期花时间准备文案素材等&#xff0c;结果广告不过审&#xff0c;等于一切的前期准备都打水漂了&#xff0c;特别是黑五类的一些产品。许多独立站会架设斗篷&#xff0c;根据市场…

springBoot插件打包部署

打包插件spring-boot-maven-plugin 不使用插件&#xff0c;运行时&#xff0c;异常信息为“没有主清单属性” 本地部署 杀进程

VSCode+ESP-IDF开发ESP32-S3-DevKitC-1(2)第一个工程 LED心跳灯

VSCodeESP-IDF开发ESP32-S3-DevKitC-1&#xff08;2&#xff09;第一个工程 LED心跳灯 前言1.新建工程2.编写控制LED代码3.LED控制独立成.c和.h文件 前言 实际开发中很多时候我们需要有一个类似心跳灯或运行指示灯的灯以不同的状态闪烁以表示程序的运行状态&#xff0c;所以第…

【金融风控项目-06】:风控建模流程

文章目录 2 风控建模流程2.1 ABC评分卡简介2.2 机器学习模型工作的完整流程2.3 项目准备期2.3.1 明确需求 2.4 模型设计2.4.1 业务抽象成分类/回归问题2.4.2 模型算法2.4.3 模型输入2.4.4 Y标签定义2.4.5 样本选取2.4.6 样本采样2.4.7 观察期和表现期2.4.8 Y标签阈值确定2.4.9 …

Gartner发布中国PAM特权访问管理创新洞察:PAM的8个主要目标和国内9个主要提供商

特权账户是攻击者的主要目标&#xff0c;对每个组织来说都是重大的安全风险。安全和风险管理领导者可以利用这项研究来了解技术前景并降低特权访问风险。 主要发现 合规在推动中国采用特权访问管理 (PAM) 工具方面发挥着重要作用。然而&#xff0c;这些工具的实施经常遭到IT管理…

mayo介绍和QTqmake编译基于Opencascade开发的mayo工程-小白配置

目录: 一、mayo介绍:zap: 最新功能&#xff08;截止7.8.2&#xff09;在这里插入图片描述 二、编译准备三、编译过程3.1QT Creator打开源码的pro工程3.2修改几处pro配置3.3复制所需的动态链接库3.4编译完成 一、mayo介绍 1️⃣mayo是一个基于opencascade开源库开发的一个开源CA…

ISUP协议视频平台EasyCVR私有化部署视频平台如何实现RTMP推流将大疆无人机的视频画面回传?

在现代视频监控和流媒体技术领域&#xff0c;EasyCVR视频融合云平台以其卓越的性能和灵活性&#xff0c;成为了跨区域、网络化视频监控综合管理的理想选择。作为TSINGSEE青犀视频“云边端”架构体系中的核心组件&#xff0c;私有化部署视频平台EasyCVR不仅能够实现视频数据的集…

如何高效实现汤臣倍健营销云数据集成到SQLServer

新版订单同步-&#xff08;Life-Space&#xff09;江油泰熙&#xff1a;汤臣倍健营销云数据集成到SQL Server 在企业信息化建设中&#xff0c;数据的高效集成和管理是提升业务运营效率的关键。本文将分享一个实际案例——如何通过新版订单同步方案&#xff0c;将汤臣倍健营销云…

OpenHarmony-2.DeviceInfo适配

DeviceInfo适配说明 1.启动子系统设备信息说明 2.OHOS 2.1.OHOS 固定值参数适配 OHOS 固定值参数: const.ohos.version.security_patch const.ohos.releasetype const.ohos.apiversion const.ohos.fullname适配说明&#xff1a; OHOS 固定值参数由OHOS系统填充&#xff0…

Java实现两数交换

文章目录 实现两数交换方法一、&#xff08;数组的方式进行交换&#xff09;方法二、&#xff08;对象的方式进行交换&#xff09;总结 实现两数交换 实现两数交换&#xff0c;没有办法通过直接传递数字达到交换的结果&#xff0c;定义的int型变量是被存储在栈空间上的&#xf…

uniapp 购物弹窗组件 (微信小程序)

效果图&#xff0c;暂时只适应单规格&#xff0c;居中弹出和下方弹出&#xff0c;如需求不满足&#xff0c;请自行修改代码 &#xff08;更新于24/11/15) 居中显示效果 下方弹出效果 html <template><view class"" v-if"show":class"mod…

革新车间照明,分布式IO模块引领智能制造新纪元

在智能制造的浪潮中&#xff0c;每一个细节的优化都是推动生产效率与能耗管理迈向新高度的关键。车间照明系统&#xff0c;作为生产环境中不可或缺的一环&#xff0c;其智能化升级正成为众多企业转型升级的重要着力点。 一、从传统到智能&#xff1a;照明系统的变革之旅 传统…

Java基于微信小程序+SSM的校园失物招领小程序

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

微软Office 2021 24年11月授权版

概述 Microsoft Office LTSC 2021 专业增强版是微软公司推出的一款专为企业客户设计的办公软件套件。该版本于2024年11月进行了批量许可版更新推送&#xff0c;旨在为企业用户提供更加稳定、高效的办公体验。 主要特点 LOGO设计趋势强化&#xff1a;新版Office将棱角改为圆角…

uniapp实现中英文切换

home.js const data {ZH: {content1: "苹果",},EN: {content1: “Apple”,} } export default dataindex.js import home from "./home.js" export default {home }en.js import part1 from ./data/part1/index.js const en {language: {name: "…

go-zero(七) RPC服务和ETCD

go-zero 实现 RPC 服务 在实际的开发中&#xff0c;我们是通过RPC来传递数据的&#xff0c;下面我将通过一个简单的示例&#xff0c;说明如何使用go-zero框架和 Protocol Buffers 定义 RPC 服务。 一、生成 RPC项目 在这个教程中&#xff0c;我们根据user.api文件&#xff0…

C#.Net筑基-字符串超全总结

字符串是日常编码中最常用的引用类型了&#xff0c;可能没有之一&#xff0c;加上字符串的不可变性、驻留性&#xff0c;很容易产生性能问题&#xff0c;因此必须全面了解一下。 01、字符与字符编码 1.1、字符Char 字符 char 表示为 Unicode字符&#xff0c;在C#中用 UTF-16 …

ROS Action

在 ROS 中&#xff0c;Action 是一种支持长时间异步任务的通信机制。与 Service 不同&#xff0c;Action 允许客户端发起一个请求&#xff0c;并在任务执行的过程中不断接收反馈&#xff0c;直到任务完成。这种机制非常适用于可能需要较长时间来完成的任务&#xff0c;比如机器…

23.UE5删除存档

2-25 删除存档制作_哔哩哔哩_bilibili 按照自己的风格制作删除按钮 这样该行的存档就被从存档列表中删除了&#xff0c;并且实际存档&#xff08;我的存档蓝图&#xff09;中也被删除了 但是存在一个问题&#xff0c;如果存档数据中存在索引为: 0 1 2 3的存档&#xff0c;当索…