华为电量分段图表实现过程

news2024/11/30 0:28:31

d5e551afb28c46d2b99bd6fb0c39a53d.jpg36cb908ec0e149b89fe0afd787bfe276.jpg

以前一直是改的MPAndroidChart,但最近看到华为手机的电池图表发现一旦设计不符合常规图表逻辑实现起来就很困难,

考虑过path相减(areaPath.op(-,- Path.Op.DIFFERENCE))、图像混合(paint.setXfermode)、裁剪区域(clipRigion)均不满足需求,因为他这个一段包含多个点且Y不相等,就算是我柱状图和折线图混合,然后混合也不行

因为底层逻辑就不一样,结合一下常见图表说明一下不方便修改的点,和我们重点复刻的内容,本文多为提供实现思路

复刻成果预览

1、需求提取

1.1 分段的fill

根据上升和降低分段变色,而常规chart是整个fill

1.2 范围选中逻辑

折线图逻辑只能选择一个点,柱状图可以选择一个范围但其Y是相等的,也就是说我们得实现 Y不相等的多个点,同时选择变色

1.3 网格线和标签等其他问题 

=看似简单 但仔细一看不符合框架 修改mp也很困难 比如网格线突出,虚线实线混合,x,y label 特殊摆放位置

2、实现思路思考过程

针对以上问题评估,再加上通常只有图表没有几个集成框架也比较重,所以我们试着从零开始复刻,重点讲解下核心实现

思路如下

2.1 提取最小模型

经过思考提取以下最重要部分,即一段为两条线加两个填充,分别可以自定义颜色

2.2 构建数据

在自定义view数据构建尤为重要,数据控制页面

一个小时内的数据

public class HourChartData {
    public List<ChartEntry> chartEntries;
}

 每个点的数据,为简单理解,我们会将真是数据转化为 Y百分比 我们将24小时分为48个段(具体可以根据你实际需求)(x = 0 ~ 48  y= 0 ~ 100)

public class ChartEntry {
    public float x;
    public float y;
//    public Object object ; // 方便后续拓展
//    public int hour;
//    public int upColor;
//    public int downColor;
}

2.3 自定义简单实现最小模型

class ChartView : View {
    lateinit var mPaint: Paint
    lateinit var mPaintDown: Paint
    lateinit var mPaintArea: Paint
    lateinit var mPaintAreaDown: Paint


private fun init() {

        mPaint = Paint()

        mPaint.run {
            color = Color.BLACK
            strokeWidth = 10f
            style = Paint.Style.STROKE
            flags = Paint.ANTI_ALIAS_FLAG
        }

        mPaintDown = Paint()
        mPaintDown.run {
            color = Color.RED
            strokeWidth = 10f
            style = Paint.Style.STROKE
            flags = Paint.ANTI_ALIAS_FLAG

        }

        mPaintArea = Paint()
        mPaintArea.run {
            color = Color.parseColor("#5900BEBE")
            style = Paint.Style.FILL
            flags = Paint.ANTI_ALIAS_FLAG

        }

        mPaintAreaDown = Paint()
        mPaintAreaDown.run {
            color = Color.parseColor("#59123456")
            style = Paint.Style.FILL
            flags = Paint.ANTI_ALIAS_FLAG
        }
}

private fun initTestData() {
        mData.clear()
        val listEntry = arrayListOf<ChartEntry>()

        // 随机24小时 49个点的数据 存在两个0点
        for (i in 0 until count + 1) {
            listEntry.add(ChartEntry(i * 1.0f, (0..10).random() * 10.0f))
        }


        listEntry.forEachIndexed { index, chartEntry ->
            if (index % 2 == 0 && index + 2 < listEntry.size) {
                val chartEntry2 = listEntry[index + 1]
                val chartEntry3 = listEntry[index + 2]
                mData.add(getOneHourData(chartEntry, chartEntry2, chartEntry3))
            }
        }

    }

private fun getOneHourData(
        chartEntry1: ChartEntry,
        chartEntry2: ChartEntry,
        chartEntry3: ChartEntry
    ): HourChartData {
        val tesData = HourChartData()
        val chartEntries: MutableList<ChartEntry> = ArrayList()
        chartEntries.add(ChartEntry(chartEntry1.x, chartEntry1.y))
        chartEntries.add(ChartEntry(chartEntry2.x, chartEntry2.y))
        chartEntries.add(ChartEntry(chartEntry3.x, chartEntry3.y))
        tesData.chartEntries = chartEntries
        return tesData
    }

}

2.3.1 绘制

    private fun drawHalfHourChart(canvas: Canvas, hourChartData: HourChartData, wSpace: Float) {
        for (i in hourChartData.chartEntries.indices) {
            if (i < hourChartData.chartEntries.size - 1) {
                val start = hourChartData.chartEntries[i]
                val end = hourChartData.chartEntries[i + 1]
                val path = Path()
                val moveX = wSpace * start.x
                // view坐标系和图表坐标系Y轴相反 0.8为留出 4/5间距
                val moveY = height * 0.8f * ((100f - start.y) / 100f)
                path.moveTo(moveX, moveY)
                val lineX = wSpace * end.x
                val lineY = height * 0.8f * ((100f - end.y) / 100f)
                path.lineTo(lineX, lineY)
                // 先画背景再画线 遮挡关系
                if (end.y > start.y) {
                    canvas.drawPath(path, mPaint)
                } else {
                    canvas.drawPath(path, mPaintDown)
                }

}

}

2.3.2 填充逻辑

选中小时内变色 松开全部变色

       if (selectPosition < 0 || isSelectCurr) {
                    val areaPath = Path(path)
                    val rectF = RectF()
                    // 0.8为留出 4/5间距
                    areaPath.computeBounds(rectF, true)
                    areaPath.lineTo(rectF.right, height.toFloat() * 0.8f)
                    areaPath.lineTo(rectF.left, height.toFloat() * 0.8f)
                    Log.i("testchart", "start ${start}")
                    Log.i("testchart", "end ${end}")
                    if (end.y >= start.y) {
                        canvas.drawPath(areaPath, mPaintArea)
                    } else {
                        canvas.drawPath(areaPath, mPaintAreaDown)
                    }
                }

2.3.3 选中逻辑

1.判断x是否在第几小时

2.小时内哪些点符合

3.松开则全部充满

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> {
                val x = event.x
                selectPosition = (x / (width / 48)).toInt()
                Log.i("testchart", "ACTION_DOWN selectPosition $selectPosition")

                invalidate() //更新视图
                return true
            }
            MotionEvent.ACTION_UP -> {
                selectPosition = -1
                mOnSelectListener?.select(-1, 0f, 0f, "")
                Log.i("testchart", "ACTION_UP selectPosition $selectPosition")
                invalidate()
                return true
            }
        }
        return super.onTouchEvent(event)
    }
       // 根据x的范围 每两个点的数据一组 24部分选中
                var isSelectCurr: Boolean
                if (selectPosition % 2 == 0) {
                    isSelectCurr = selectPosition == start.x.toInt()
                            || selectPosition + 1 == start.x.toInt()


                } else {
                    isSelectCurr = selectPosition == (start.x.toInt())
                            || selectPosition - 1 == start.x.toInt()
                }

                if (isSelectCurr && start.x.toInt() % 2 == 0) {
                    // 前半小时回调 显示在 一个小时 柱子范围中间位置
                    val des = "${start.x.toInt() / 2}:00 - ${start.x.toInt() / 2 + 1}:00 "
                    mOnSelectListener?.select(start.x.toInt(), lineX, lineY,des)

                }

2.3.4 网格线

    private fun drawGridDashLine(canvas: Canvas) {
        val dashPathEffect = DashPathEffect(
            floatArrayOf(
                10f, 5f
            ), 0f
        )
        mPaintLine.pathEffect = dashPathEffect
        val wSpace = width * 1.0f / 4

        val path = Path()
        for (i in 0 until 5) {
            path.moveTo(i * wSpace, 0f)
            path.lineTo(i * wSpace, height.toFloat())
        }
        canvas.drawPath(path, mPaintLine)


        val hSpace = height * 1.0f / 5

        // 留出最后一条线
        mPaintLine.pathEffect = null
        val pathH = Path()
        for (i in 0 until 5) {
            pathH.moveTo(0f, i * hSpace)
            pathH.lineTo(width.toFloat(), i * hSpace)
        }
        canvas.drawPath(pathH, mPaintLine)
    }

2.3.5 MarkView位置和显示

经典clipChildren解决边界处被遮挡的问题

.mOnSelectListener = object : ChartView.OnSelectListener {
            override fun select(position: Int, x: Float, y: Float, des: String) {

                // 开启透视 爷爷布局生效 分内部透视 和使用透视 也可以根局部直接使用
                this@View.let{
                    (it as ViewGroup).clipChildren = false
                }
                this@View.parent?.let {
                    (it as ViewGroup).clipChildren = false
                }

                tvMarkView.text = des
                if (position >= 0) {
                    markView.x = x - markView.width / 2.0f
                    markView.y = y - markView.height
                    markView.visibility = View.VISIBLE
                } else {
                    markView.visibility = View.GONE
                }
            }

        }

2.3.6 XY轴 数值显示

这一块建议大家摆烂 ,虽然绘制也不难,就是得把间距都留出来,类似文中0.8多留出一个网格线,反正一个项目就几个图,也不做通用控件,如果一起绘制调位置都不方便,反正就一个图用view补随意显示改位置🤣,像MPAndroid legend和label 封装起来了 改个位置麻烦的很

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context=".ChartActivity">

    <com.rex.demo.chart.SettingChartView
        android:id="@+id/chartView"
        android:layout_width="240dp"
        android:layout_height="240dp" />

    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/chartView"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="00:00" />


        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="06:00" />


        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="12:00" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="18:00" />

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

    </LinearLayout>


 <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="240dp"
        android:layout_marginStart="10dp"
        android:layout_toEndOf="@+id/chartView"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_margin="-10dp"
            android:layout_weight="1"
            android:gravity="top"
            android:text="100%" />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="2"
            android:gravity="center_vertical"
            android:text="50%" />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="bottom"
            android:text="0%" />


        <TextView
            android:layout_width="wrap_content"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="bottom"
            android:text="24:00" />


    </LinearLayout>

  <RelativeLayout
        android:id="@+id/markView"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:background="@drawable/marker2"
        android:visibility="gone"
        tools:ignore="Overdraw">

        <TextView
            android:id="@+id/tvMarkView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_marginLeft="5dp"
            android:layout_marginTop="7dp"
            android:layout_marginRight="5dp"
            android:ellipsize="end"
            android:gravity="center"
            android:singleLine="true"
            android:text="markView"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:textColor="@android:color/white"
            android:textSize="12sp" />

    </RelativeLayout>
</RelativeLayout>

本文主要提供复刻思路暂时没有源码,建议手撸一遍,因为这类需求必须理解每个细节才能方便改动,比如以前发过的断点不连续绘制 mp新版好像又不行了,等会看看

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

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

相关文章

学生HTML个人网页作业作品下载 动漫主题网页设计制作 大学生个人网站作业模板 dreamweaver简单个人网页制作

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…

vue3面试题

文章目录一、vue3有了解过吗&#xff1f;能说说跟vue2的区别吗&#xff1f;1.vue3介绍2.vue3的新特性&#xff1a;2.1速度更快2.2体积更小2.3更易维护2.4更好的Typescript支持2.5编译器重写2.6更接近原生2.7更易使用3.vue3新增特性framentsTeleportcreateRenderercomposition A…

JVM区域划分

概述 JVM在运行代码时&#xff0c;他使用多块内存空间&#xff0c;不同的内存空间用来放不同的数据&#xff0c;然后配合代码流程&#xff0c;让系统运行起来。 存放类加载信息 举个最简单的例子&#xff0c;比如现在知道了JVM会加载类到内存里来供后续运行&#xff0c;所以…

java后端web前端10套项目开发案例源码,毕设,期末作业

项目包括 基础的html,css,js,jquery期末作业项目,毕设 1.古风诗意主题的背单词项目 2.模仿考虫软件实现的考拉背单词网页 3.模仿不背单词软件实现的背单词项目 html,css,js,jquery,瀑布流&#xff0c;懒加载的前端毕设 仿京东网站实现的前端平台 java基础作业 1.纯后端学生管…

初识Nodejs -- nodejs简介

视频链接&#xff1a;黑马程序员Node.js全套入门教程 文章目录1. 初识Nodejs1.1 思考为什么JavaScript可以在浏览器中被执行为什么JavaScript可以操作DOM和BOM&#xff1f;浏览器中的JavaScript运行环境1.2 Nodejs简介1.2.1 Nodejs中的JavaScript运行环境1.2.2 Node.js可以做什…

元数据管理-解决方案调研三:元数据管理解决方案——开源解决方案

开源解决方案 3.1、Apache Atlas Atlas 是一组可扩展和可扩展的核心基础治理服务——使企业能够有效和高效地满足其在 Hadoop 中的合规性要求&#xff0c;并允许与整个企业数据生态系统集成。 Apache Atlas 为组织提供开放的元数据管理和治理功能&#xff0c;以构建其数据资产…

没有公网IP,怎么远程访问ERP/NAS?

当前&#xff0c;国内疫情形势不容乐观&#xff0c;企业的经营发展再一次面临巨大挑战。虽然“远程办公”早已不是新鲜词&#xff0c;但依然有大量企业没有做好随时切换到远程办公的准备。如遇疫情风险&#xff0c;企业运营很容易陷入瘫痪。 企业一般在内网部署服务器、视频监…

2分钟看懂OA与ERP

ERP (Enterprise Resource Planning)即企业资源计划&#xff0c;最早是一种供应链的管理思想。它汇合了商贸代理行业的各项特点&#xff0c;旨在协调企业各管理部门围绕市场导向&#xff0c;更加灵活或柔性地开展业务活动&#xff0c;实时地响应市场需求。 从功能来看&#xf…

AVL树左旋转算法思路与图解

AVL树左旋转算法思路与图解 对于数列{4, 3, 6, 5, 7, 8}, 当我们插入8的时候, rightHeight() - leftHeight() > 1成立(也就是当前AVL树中的根节点的BF(平衡因子)> 1了), 此时这个AVL树已经不再是平衡的了, 也就是已经不是一个AVL树了, 所以我们要经过处理之后让其重新平…

【C】输入一行字符,分别统计出其中英文字母、数字、空格和其他字符的个数

C语言没有字符串形式&#xff0c;所以可以用 char[] 数组来代替&#xff0c;但需要指定分配空间&#xff0c;所以可以采用单字符读取的形式&#xff1a; #include<stdio.h> int main(){char ch;int space0,number0,character0,other0;chgetchar(); …

WSO2安装使用的全过程详解

WSO2安装使用的全过程详解1. Install1.1 Docker Install 1.2 Uninstall2. User Guide3. Big Data -- Postgres4. Awakening1. Install 1.1 Docker Install 显示镜像 docker images/docker image ls拉去镜像/版本 docker pull wso2/wso2am 1.2 Uninstall 查看正在运行的容器 d…

流体动力润滑(轴承油膜承载机理)

目录 1.流体动力润滑 2.流体动力润滑的应用&#xff1a;楔形效应承载机理 3.承载量的计算 4.轴承动压油膜 1.流体动力润滑 定义&#xff1a;两个相对运动物体的摩擦表面&#xff0c;用于借助相对速度而产生的黏性流体膜将两摩擦面完全隔离开&#xff0c;由流体膜产生的压力…

那些下载不了的视频,Python只用1行代码就能直接下载

前言 现在有很多网站都并不支持直接下载的&#xff0c;例如我们常去的B站里面的视频&#xff0c;在页面是没有下载按钮的&#xff0c;还有的视频需要我们下载客户端才能下载…虽然这并不能拦住多少人&#xff0c;有些聪明的小伙伴就会去下载一些第三方软件去下载&#xff0c;比…

apache-atlas-hbase-bridge-源码分析

元数据类型 Hbase元数据类型, 包括命令空间、表、列族、列 public enum HBaseDataTypes {// ClassesHBASE_NAMESPACE,HBASE_TABLE,HBASE_COLUMN_FAMILY,HBASE_COLUMN;public String getName() {return name().toLowerCase();} }Hbase元数据采集实现 1&#xff09;批量采集HBa…

MyBatis基于XML的使用——缓存

1、介绍 MyBatis 内置了一个强大的事务性查询缓存机制&#xff0c;它可以非常方便地 配置和定制。 为了使它更加强大而且易于配置&#xff0c;我们对 MyBatis 3 中的缓存实现进行了许多改进。 默认情况下&#xff0c;只启用了本地的会话缓存&#xff0c;它仅仅对一个会话中的数…

【Java】异常处理

异常本质上是程序上的错误&#xff0c;包括程序逻辑错误和系统错误。比如使用空的引用、数组下标越界、内存溢出错误等. 错误在我们编写程序的过程中会经常发生&#xff0c;包括编译期间和运行期间的错误&#xff0c;在编译期间出现的错误有编译器帮助我们一起修正&#xff0c;…

使用java代码向mysql数据库插入100万条数据

使用java代码向mysql数据库插入100万条数据 使用springboot集成Mysql数据库&#xff0c;并使用java代码循环向msql数据库插入100万条数据&#xff0c;并测试插入时间 目录结构使用java代码向mysql数据库插入100万条数据一、使用工具二、项目结构图创建springboot项目启动类创建…

高并发-防止雪崩与穿透

一、DB查询前加锁 /** * 本地堆内缓存&#xff0c;优先级最高 */ ON_HEAP(1), /** * 本地堆外缓存&#xff0c;不影响GC&#xff0c;可以管理比堆内缓存更多的数据 * 数据get/set涉及序列化&#xff0c;性能次于本地堆内缓存 */ OFF_HE…

手撕红黑树

目录 一、概念 二、红黑树的插入操作 第一步: 按照二叉搜索树的规则插入新节点 第二步: 插入后检测性质是否造到破坏&#xff0c;若遭到破坏则进行调整 情况一: cur为红&#xff0c;parent为红&#xff0c;grandfather为黑&#xff0c;uncle存在且为红 情况二: cur为红&a…

JLink 添加新设备用于下载/调试固件

新驱动的安装目录结构如下&#xff1a; 可以看出新版本的 JLink 驱动中已经没有 Devices 目录和 JLinkDevices.xml 文件了&#xff0c;即旧的方法已经不能在新的驱动中使用了。 如果需要继续使用旧的方式添加新设备&#xff0c;则需要下载 JLink_V770d 之前的版本。 在新驱动…