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


    目录

    复刻成果预览

    1、需求提取

    1.1 分段的fill

    1.2 范围选中逻辑

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

    2、实现思路思考过程

    2.1 提取最小模型

    2.2 构建数据

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

    2.3.1 绘制

    2.3.2 填充逻辑

    2.3.3 选中逻辑

    2.3.4 网格线

    2.3.5 MarkView位置和显示

    2.3.6 XY轴 数值显示

    2.3.7 父布局竖滑动冲突


    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数据构建尤为重要,数据控制页面

    一个小时内的数据

    1. public class HourChartData {
    2. public List chartEntries;
    3. }

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

    1. public class ChartEntry {
    2. public float x;
    3. public float y;
    4. // public Object object ; // 方便后续拓展
    5. // public int hour;
    6. // public int upColor;
    7. // public int downColor;
    8. }

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

    1. class ChartView : View {
    2. lateinit var mPaint: Paint
    3. lateinit var mPaintDown: Paint
    4. lateinit var mPaintArea: Paint
    5. lateinit var mPaintAreaDown: Paint
    6. private fun init() {
    7. mPaint = Paint()
    8. mPaint.run {
    9. color = Color.BLACK
    10. strokeWidth = 10f
    11. style = Paint.Style.STROKE
    12. flags = Paint.ANTI_ALIAS_FLAG
    13. }
    14. mPaintDown = Paint()
    15. mPaintDown.run {
    16. color = Color.RED
    17. strokeWidth = 10f
    18. style = Paint.Style.STROKE
    19. flags = Paint.ANTI_ALIAS_FLAG
    20. }
    21. mPaintArea = Paint()
    22. mPaintArea.run {
    23. color = Color.parseColor("#5900BEBE")
    24. style = Paint.Style.FILL
    25. flags = Paint.ANTI_ALIAS_FLAG
    26. }
    27. mPaintAreaDown = Paint()
    28. mPaintAreaDown.run {
    29. color = Color.parseColor("#59123456")
    30. style = Paint.Style.FILL
    31. flags = Paint.ANTI_ALIAS_FLAG
    32. }
    33. }
    34. private fun initTestData() {
    35. mData.clear()
    36. val listEntry = arrayListOf()
    37. // 随机24小时 49个点的数据 存在两个0点
    38. for (i in 0 until count + 1) {
    39. listEntry.add(ChartEntry(i * 1.0f, (0..10).random() * 10.0f))
    40. }
    41. listEntry.forEachIndexed { index, chartEntry ->
    42. if (index % 2 == 0 && index + 2 < listEntry.size) {
    43. val chartEntry2 = listEntry[index + 1]
    44. val chartEntry3 = listEntry[index + 2]
    45. mData.add(getOneHourData(chartEntry, chartEntry2, chartEntry3))
    46. }
    47. }
    48. }
    49. private fun getOneHourData(
    50. chartEntry1: ChartEntry,
    51. chartEntry2: ChartEntry,
    52. chartEntry3: ChartEntry
    53. ): HourChartData {
    54. val tesData = HourChartData()
    55. val chartEntries: MutableList = ArrayList()
    56. chartEntries.add(ChartEntry(chartEntry1.x, chartEntry1.y))
    57. chartEntries.add(ChartEntry(chartEntry2.x, chartEntry2.y))
    58. chartEntries.add(ChartEntry(chartEntry3.x, chartEntry3.y))
    59. tesData.chartEntries = chartEntries
    60. return tesData
    61. }
    62. }

    2.3.1 绘制

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

    2.3.2 填充逻辑

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

    1. if (selectPosition < 0 || isSelectCurr) {
    2. val areaPath = Path(path)
    3. val rectF = RectF()
    4. // 0.8为留出 4/5间距
    5. areaPath.computeBounds(rectF, true)
    6. areaPath.lineTo(rectF.right, height.toFloat() * 0.8f)
    7. areaPath.lineTo(rectF.left, height.toFloat() * 0.8f)
    8. Log.i("testchart", "start ${start}")
    9. Log.i("testchart", "end ${end}")
    10. if (end.y >= start.y) {
    11. canvas.drawPath(areaPath, mPaintArea)
    12. } else {
    13. canvas.drawPath(areaPath, mPaintAreaDown)
    14. }
    15. }

    2.3.3 选中逻辑

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

    2.小时内哪些点符合

    3.松开则全部充满

    1. override fun onTouchEvent(event: MotionEvent): Boolean {
    2. when (event.action) {
    3. MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> {
    4. val x = event.x
    5. selectPosition = (x / (width / 48)).toInt()
    6. Log.i("testchart", "ACTION_DOWN selectPosition $selectPosition")
    7. invalidate() //更新视图
    8. return true
    9. }
    10. MotionEvent.ACTION_UP -> {
    11. selectPosition = -1
    12. mOnSelectListener?.select(-1, 0f, 0f, "")
    13. Log.i("testchart", "ACTION_UP selectPosition $selectPosition")
    14. invalidate()
    15. return true
    16. }
    17. }
    18. return super.onTouchEvent(event)
    19. }
    1. // 根据x的范围 每两个点的数据一组 24部分选中
    2. var isSelectCurr: Boolean
    3. if (selectPosition % 2 == 0) {
    4. isSelectCurr = selectPosition == start.x.toInt()
    5. || selectPosition + 1 == start.x.toInt()
    6. } else {
    7. isSelectCurr = selectPosition == (start.x.toInt())
    8. || selectPosition - 1 == start.x.toInt()
    9. }
    10. if (isSelectCurr && start.x.toInt() % 2 == 0) {
    11. // 前半小时回调 显示在 一个小时 柱子范围中间位置
    12. val des = "${start.x.toInt() / 2}:00 - ${start.x.toInt() / 2 + 1}:00 "
    13. mOnSelectListener?.select(start.x.toInt(), lineX, lineY,des)
    14. }

    2.3.4 网格线

    1. private fun drawGridDashLine(canvas: Canvas) {
    2. val dashPathEffect = DashPathEffect(
    3. floatArrayOf(
    4. 10f, 5f
    5. ), 0f
    6. )
    7. mPaintLine.pathEffect = dashPathEffect
    8. val wSpace = width * 1.0f / 4
    9. val path = Path()
    10. for (i in 0 until 5) {
    11. path.moveTo(i * wSpace, 0f)
    12. path.lineTo(i * wSpace, height.toFloat())
    13. }
    14. canvas.drawPath(path, mPaintLine)
    15. val hSpace = height * 1.0f / 5
    16. // 留出最后一条线
    17. mPaintLine.pathEffect = null
    18. val pathH = Path()
    19. for (i in 0 until 5) {
    20. pathH.moveTo(0f, i * hSpace)
    21. pathH.lineTo(width.toFloat(), i * hSpace)
    22. }
    23. canvas.drawPath(pathH, mPaintLine)
    24. }

    2.3.5 MarkView位置和显示

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

    1. .mOnSelectListener = object : ChartView.OnSelectListener {
    2. override fun select(position: Int, x: Float, y: Float, des: String) {
    3. // 开启透视 爷爷布局生效 分内部透视 和使用透视 也可以根局部直接使用
    4. this@View.let{
    5. (it as ViewGroup).clipChildren = false
    6. }
    7. this@View.parent?.let {
    8. (it as ViewGroup).clipChildren = false
    9. }
    10. tvMarkView.text = des
    11. if (position >= 0) {
    12. markView.x = x - markView.width / 2.0f
    13. markView.y = y - markView.height
    14. markView.visibility = View.VISIBLE
    15. } else {
    16. markView.visibility = View.GONE
    17. }
    18. }
    19. }

    2.3.6 XY轴 数值显示

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

    1. "1.0" encoding="utf-8"?>
    2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:tools="http://schemas.android.com/tools"
    4. android:layout_width="wrap_content"
    5. android:layout_height="wrap_content"
    6. tools:context=".ChartActivity">
    7. <com.rex.demo.chart.SettingChartView
    8. android:id="@+id/chartView"
    9. android:layout_width="240dp"
    10. android:layout_height="240dp" />
    11. <LinearLayout
    12. android:layout_width="300dp"
    13. android:layout_height="wrap_content"
    14. android:layout_alignBottom="@+id/chartView"
    15. android:orientation="horizontal">
    16. <TextView
    17. android:layout_width="0dp"
    18. android:layout_height="wrap_content"
    19. android:layout_weight="1"
    20. android:text="00:00" />
    21. <TextView
    22. android:layout_width="0dp"
    23. android:layout_height="wrap_content"
    24. android:layout_weight="1"
    25. android:text="06:00" />
    26. <TextView
    27. android:layout_width="0dp"
    28. android:layout_height="wrap_content"
    29. android:layout_weight="1"
    30. android:text="12:00" />
    31. <TextView
    32. android:layout_width="0dp"
    33. android:layout_height="wrap_content"
    34. android:layout_weight="1"
    35. android:text="18:00" />
    36. <Space
    37. android:layout_width="0dp"
    38. android:layout_height="wrap_content"
    39. android:layout_weight="1" />
    40. LinearLayout>
    41. <LinearLayout
    42. android:layout_width="wrap_content"
    43. android:layout_height="240dp"
    44. android:layout_marginStart="10dp"
    45. android:layout_toEndOf="@+id/chartView"
    46. android:orientation="vertical">
    47. <TextView
    48. android:layout_width="wrap_content"
    49. android:layout_height="0dp"
    50. android:layout_margin="-10dp"
    51. android:layout_weight="1"
    52. android:gravity="top"
    53. android:text="100%" />
    54. <TextView
    55. android:layout_width="wrap_content"
    56. android:layout_height="0dp"
    57. android:layout_weight="2"
    58. android:gravity="center_vertical"
    59. android:text="50%" />
    60. <TextView
    61. android:layout_width="wrap_content"
    62. android:layout_height="0dp"
    63. android:layout_weight="1"
    64. android:gravity="bottom"
    65. android:text="0%" />
    66. <TextView
    67. android:layout_width="wrap_content"
    68. android:layout_height="0dp"
    69. android:layout_weight="1"
    70. android:gravity="bottom"
    71. android:text="24:00" />
    72. LinearLayout>
    73. <RelativeLayout
    74. android:id="@+id/markView"
    75. android:layout_width="100dp"
    76. android:layout_height="40dp"
    77. android:background="@drawable/marker2"
    78. android:visibility="gone"
    79. tools:ignore="Overdraw">
    80. <TextView
    81. android:id="@+id/tvMarkView"
    82. android:layout_width="wrap_content"
    83. android:layout_height="wrap_content"
    84. android:layout_centerHorizontal="true"
    85. android:layout_marginLeft="5dp"
    86. android:layout_marginTop="7dp"
    87. android:layout_marginRight="5dp"
    88. android:ellipsize="end"
    89. android:gravity="center"
    90. android:singleLine="true"
    91. android:text="markView"
    92. android:textAppearance="?android:attr/textAppearanceSmall"
    93. android:textColor="@android:color/white"
    94. android:textSize="12sp" />
    95. RelativeLayout>
    96. RelativeLayout>

    2.3.7 父布局竖滑动冲突

    1. MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> {
    2. val x = event.x
    3. val y = event.y
    4. val isHorizontal = abs(lastX - x) > abs(lastY - y)
    5. Log.i("testchart", "isHorizontal $isHorizontal")
    6. // 解决滑动冲突 或指定明确冲突父+布局
    7. parent?.parent?.parent?.requestDisallowInterceptTouchEvent(isHorizontal)
    8. lastX = x
    9. lastY = y
    10. }

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

  • 相关阅读:
    如何报考PMP项目管理认证考试?
    Linux环境基础开发工具使用
    【VMware VCF】VMware Cloud Foundation Part 01:概述。
    day07 Elasticsearch搜索引擎3
    【咖啡品牌分析】Google Maps数据采集咖啡市场数据分析区域分析热度分布分析数据抓取瑞幸星巴克
    Windows核心编程 静态库与动态库
    4、项目整体管理
    rpc error: code = Unimplemented desc =
    《Seq2Path: Generating Sentiment Tuples as Paths of a Tree》论文阅读
    TypeScript学习笔记
  • 原文地址:https://blog.csdn.net/qq_28844947/article/details/127977828