数据处理流程:

graph LR
心率数据-->心率仓库
心率仓库-->根据采样率获取心率数据
根据采样率获取心率数据--> 打印数据

思路篇:

  • 整个控件分成上下两层。上层画线条,下层画表格

    • 线条篇

      • 1.线条决定使用Path来画,而Path的数据,则使用一个Int数组来保存
      • 2.Int数组的大小,是依据采样频率 * 显示秒数 来决定的
      • 3.读取数据赋值到Path里,需要指定 x , y 的值
      • 4.X 依据采样频率,可以计算出每个点的 X 的值
      • 5.Y 的位置,则是依据值的大小,以及控件应该设置一个MAX最大值的大小的比例,计算出Y的绝对位置。
      • 6.线条走动,则是将数组内数据的移动 Int[n] = Int[n+1]
      • 7.在实际情况中,极有可能是先采集的数据,再对数据进行播放,所以控件内部需要维护一个数据仓库,数据添加不需要考虑其他问题,而速率问题则由控件内部维护。
      • 8.而速率,则是根据采样率,来进行控制速度从数据仓库定时取出。
      • 9.但是在实际情况中,有时候需要对速率进行慢速播放,实速播放,以及加速播放。所以需要一个控制播放速度。
    • 表格篇
      • 1.线条绘制由一个基准线标准,可以将线条的绘制维持在基准线上下,而不会导致线条偏移离谱
      • 2.由基准线衍生出来的表格,需要可以自定义表格的行数,线条宽度,以及颜色,等。

代码篇:

先看属性:

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="HeartView"><!--心电线的宽度--><attr name="heart_line_border" format="dimension" /><!--每个表格的行数(就是小格子数)--><attr name="heart_grid_row" format="integer" /><!--大表格的边框的宽度--><attr name="heart_grid_border" format="dimension" /><!--每个小格子的宽高--><attr name="heart_grid_row_height" format="dimension" /><!--每个小格子的线的宽度--><attr name="heart_grid_line_border" format="dimension" /><!--基准线--><attr name="heart_base_line" format="integer" /><!--最大值--><attr name="heart_max" format="integer" /><!--最小值--><attr name="heart_min" format="integer" /><!--数据采集频率--><attr name="heart_hz" format="integer" /><!--一个控件,可以显示的心率的时长--><attr name="heart_show_seconds" format="float" /><!--心率线条的颜色--><attr name="heart_color" format="color" /><!--表格线条的颜色--><attr name="heart_grid_line_color" format="color" /><!--表格边框的颜色--><attr name="heart_grid_border_color" format="color" /><!--控制播放速度的调整 值越小,播放速度越慢 --><attr name="heart_speed" format="float" /></declare-styleable>
</resources>

实现代码:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingDeque;/*** 显示心电的控件*/
public class HeartView extends View
{// 数据的最大值private int Max;// 数据的最小值private int Min;// 数据一秒钟采集频率,默认100个点一秒种private int hz;// 控件显示几秒钟的心跳,默认显示2秒钟的心跳private float showSeconds;// 要画的基准线private int baseLine;// 每个方格的行数private int grid_row;// 每个方格的高度private int grid_row_height;// 心率线条的颜色 默认红色private int heartColor;// 表格线条的颜色 默认灰色private int heart_grid_line_color;// 表格边框的颜色 默认灰色private int heart_grid_border_color;// 心电线的宽度private int heart_line_border;// 大表格的边框的宽度private int heart_grid_border;// 每个小格子的线的宽度private int heart_grid_line_border;// 速度控制private float heart_speed;private int viewHeight = 0;private int viewWidth = 0;// 画笔private Paint paint;// 需要画心电的路径private Path path = new Path();// 根据显示秒数,以及采样频率算出总共需要申请多少个内存的数据private int[] showTimeDatas;// 待显示的数据队列private LinkedBlockingDeque<Integer> dataQueue = new LinkedBlockingDeque<>();// 定时运行栈private HeartTask heartTask = null;// 精准定时器private Timer timer = new Timer();public HeartView(Context context){this(context, null);}public HeartView(Context context, AttributeSet attrs){this(context, attrs, 0);}public HeartView(Context context, AttributeSet attrs, int defStyleAttr){super(context, attrs, defStyleAttr);paint = new Paint();paint.setStyle(Paint.Style.STROKE);paint.setAntiAlias(true);paint.setColor(Color.RED);// 线条交界处,钝化处理,看起来是圆点paint.setStrokeJoin(Paint.Join.ROUND);TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.HeartView);// 心电线的宽度heart_line_border = typedArray.getDimensionPixelSize(R.styleable.HeartView_heart_line_border, (int) dip2px(context, 1f));// 每个表格的行数(就是小格子数,默认5格grid_row = typedArray.getInt(R.styleable.HeartView_heart_grid_row, 5);// 大表格的边框的宽度heart_grid_border = typedArray.getDimensionPixelSize(R.styleable.HeartView_heart_grid_border, (int) dip2px(context, 2f));// 每个小格子的宽高grid_row_height = typedArray.getDimensionPixelSize(R.styleable.HeartView_heart_grid_row_height, (int) dip2px(context, 10f));// 每个小格子的线的宽度heart_grid_line_border = typedArray.getDimensionPixelSize(R.styleable.HeartView_heart_grid_line_border, (int) dip2px(context, 1f));// 基准线,默认2000baseLine = typedArray.getInteger(R.styleable.HeartView_heart_base_line, 2000);// 最大值,默认4000Max = typedArray.getInteger(R.styleable.HeartView_heart_max, 4096);// 最小值,默认0Min = typedArray.getInteger(R.styleable.HeartView_heart_min, 0);// 数据采集频率,默认100个点一秒钟hz = typedArray.getInteger(R.styleable.HeartView_heart_hz, 100);// 一个控件,可以显示的心率的时长 ,默认为2秒钟showSeconds = typedArray.getFloat(R.styleable.HeartView_heart_show_seconds, 2f);// 心率线条的颜色 默认红色heartColor = typedArray.getColor(R.styleable.HeartView_heart_color, Color.RED);// 表格线条的颜色 默认绿色heart_grid_line_color = typedArray.getColor(R.styleable.HeartView_heart_grid_line_color, Color.parseColor("#DBDBDB"));// 表格边框的颜色 默认绿色heart_grid_border_color = typedArray.getColor(R.styleable.HeartView_heart_grid_border_color, Color.parseColor("#DBDBDB"));// 播放速度的控制heart_speed = typedArray.getFloat(R.styleable.HeartView_heart_speed, 1.0f);typedArray.recycle();// 速度怎么可以小于0if (heart_speed < 0){throw new RuntimeException("Attributes heart_speed Can Not < 0 ");}// 最小值怎么可以大于或等于最大值if (Min >= Max){throw new RuntimeException("Attributes heart_min Can Not >= heart_max ");}showTimeDatas = new int[(int) (showSeconds * hz)];}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){super.onMeasure(widthMeasureSpec, heightMeasureSpec);viewHeight = measureHeight(heightMeasureSpec);viewWidth = measureWidth(widthMeasureSpec);path.moveTo(0, viewHeight);}/*** 重新部署发点任务*/private synchronized void publishJob(){// 根据采集的频率,自动算出每一个点之间暂停的时间long yield = (int) (1000 / (hz * heart_speed));if (heartTask != null){heartTask.cancel();heartTask = null;}heartTask = new HeartTask();timer.scheduleAtFixedRate(heartTask, 0, yield);}/*** 设置表格的行数** @param grid_row*/public void setGrid_row(int grid_row){this.grid_row = grid_row;}/*** 设置每个小方格的高度** @param height*/public void setGrid_row_height(int height){this.grid_row_height = height;}/*** 设置线条颜色** @param color*/public void setHeartColor(@ColorInt int color){this.heartColor = color;}/*** 设置画小表格的颜色** @param color*/public void setHeartGridLineColor(@ColorInt int color){this.heart_grid_line_color = color;}/*** 设置大表格边框颜色** @param color*/public void setHeartGridBorderColor(@ColorInt int color){this.heart_grid_border_color = color;}/*** 设置线条宽度** @param border*/public void setHeartLineBorder(@ColorInt int border){this.heart_line_border = border;}/*** 设置大格边框线宽** @param border*/public void setHeartGridBorder(int border){this.heart_grid_border = border;}/*** 设置小格线宽** @param border*/public void setHeartGridLineBorder(int border){this.heart_grid_line_border = border;}/*** 设置倍速** @param speed*/public void setHeartSpeed(@FloatRange(from = 0.0, to = Float.MAX_VALUE) float speed){this.heart_speed = speed;// 速度怎么可以小于0if (heart_speed < 0){throw new RuntimeException("Attributes heart_speed Can Not < 0 ");}publishJob();}/*** 添加一个点,会自动依据频率来动态显示** @param point*/public synchronized void offer(int point){dataQueue.offer(point);if (heartTask == null){publishJob();}}/*** 添加一组点,自动依据频率来动态显示** @param points*/public void offer(int[] points){for (int i = 0; i < points.length; i++)offer(points[i]);}/*** 设置显示死数据,没有动态走动效果*/public synchronized void setData(int[] points){// 如果传过来的数据 比要显示的短,那么先根据数据长度替换,再将尾巴数据清空// 传递数据:[5,6]// 显示数据:[1,1,1]// 替换数据:[5,6,1]// 尾巴清空:[5,6,0]if (points.length <= showTimeDatas.length){System.arraycopy(points, 0, showTimeDatas, 0, points.length);for (int i = points.length; i < showTimeDatas.length; i++){showTimeDatas[i] = 0;}} else{// 如果传过来的数据,比显示的要长,那么以显示的长度为依据进行数据替换// 传递数据:[5,6,7]// 显示数据:[1,2]// 替换数据:[5,6]System.arraycopy(points, 0, showTimeDatas, 0, showTimeDatas.length);}postInvalidate();}/*** 设置每秒的采集频率** @param hz*/public synchronized void setHz(int hz){this.hz = hz;this.showTimeDatas = new int[(int) (showSeconds * hz)];publishJob();}/*** 设置最大值** @param max*/public synchronized void setMax(int max){this.Max = max;// 最小值怎么可以大于或等于最大值if (Min >= Max){throw new RuntimeException("Attributes heart_min Can Not >= heart_max ");}}/*** 设置最小值** @param min*/public void setMin(int min){Min = min;// 最小值怎么可以大于或等于最大值if (Min >= Max){throw new RuntimeException("Attributes heart_min Can Not >= heart_max ");}}/*** 设置控件显示几秒钟的数据** @param showSeconds*/public synchronized void setShowSeconds(float showSeconds){this.showSeconds = showSeconds;this.showTimeDatas = new int[(int) (showSeconds * hz)];}/*** 清空图案*/public synchronized void clear(){for (int i = 0; i < showTimeDatas.length; i++)showTimeDatas[i] = 0;postInvalidate();}/*** 设置基准线** @param baseLine*/public void setBaseLine(int baseLine){this.baseLine = baseLine;}@Overrideprotected void onDraw(Canvas canvas){super.onDraw(canvas);int[] showDatas = showTimeDatas;// 画表格int baseY = calculateY(baseLine - Min, Max - Min, viewHeight);// 基准线以上for (int y = baseY; y > 0; y -= grid_row_height){if ((baseY - y) / grid_row_height % grid_row == 0){paint.setStrokeWidth(heart_grid_border);paint.setColor(heart_grid_border_color);} else{paint.setStrokeWidth(heart_grid_line_border);paint.setColor(heart_grid_line_color);}canvas.drawLine(0, y, viewWidth, y, paint);}// 基准线以下for (int y = baseY; y < viewHeight; y += grid_row_height){if ((y - baseY) / grid_row_height % grid_row == 0){paint.setStrokeWidth(heart_grid_border);paint.setColor(heart_grid_border_color);} else{paint.setStrokeWidth(heart_grid_line_border);paint.setColor(heart_grid_line_color);}canvas.drawLine(0, y, viewWidth, y, paint);}// 中心线以右int centerX = viewWidth / 2;for (int x = centerX; x < viewWidth; x += grid_row_height){if ((x - centerX) / grid_row_height % grid_row == 0){paint.setStrokeWidth(heart_grid_border);paint.setColor(heart_grid_border_color);} else{paint.setStrokeWidth(heart_grid_line_border);paint.setColor(heart_grid_line_color);}canvas.drawLine(x, 0, x, viewHeight, paint);}// 中心线以左for (int x = centerX; x > 0; x -= grid_row_height){if ((centerX - x) / grid_row_height % grid_row == 0){paint.setStrokeWidth(heart_grid_border);paint.setColor(heart_grid_border_color);} else{paint.setStrokeWidth(heart_grid_line_border);paint.setColor(heart_grid_line_color);}canvas.drawLine(x, 0, x, viewHeight, paint);}// 画心电paint.setColor(heartColor);paint.setStrokeWidth(heart_line_border);int firstData = showDatas[0];int firstY = calculateY(firstData - Min, Max - Min, viewHeight);path.reset();path.moveTo(0, firstY);for (int i = 0; i < showDatas.length; i++){int value = showDatas[i];int x = (int) (((float) i / showDatas.length) * viewWidth);int y = calculateY(value - Min, Max - Min, viewHeight);path.lineTo(x, y);}canvas.drawPath(path, paint);}/*** 根据最大值,控件高度,计算出当前值对应的控件的 Y 坐标** @param value      参与计算的值* @param Region     最大值 - 最小值的区域* @param viewHeight 控件高度* @return*/private static int calculateY(int value, int Region, int viewHeight){return viewHeight - ((int) (((float) value / Region) * viewHeight));}/*** 测量自定义View的高度*/private int measureHeight(int heightMeasureSpec){int heightResult = 0;int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);int heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec);switch (heightSpecMode){case View.MeasureSpec.UNSPECIFIED:{heightResult = heightSpecSize;}break;case View.MeasureSpec.AT_MOST:{heightResult = View.MeasureSpec.getSize(heightMeasureSpec);}break;case View.MeasureSpec.EXACTLY:{heightResult = View.MeasureSpec.getSize(heightMeasureSpec);}}return heightResult;}/*** 测量自定义View的宽度*/private int measureWidth(int widthMeasureSpec){int widthResult = 0;int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec);switch (widthSpecMode){case View.MeasureSpec.UNSPECIFIED:{widthResult = widthSpecSize;}break;case View.MeasureSpec.AT_MOST:{widthResult = View.MeasureSpec.getSize(widthMeasureSpec);}break;case View.MeasureSpec.EXACTLY:{widthResult = View.MeasureSpec.getSize(widthMeasureSpec);}}return widthResult;}/*** dp 转 px** @param context  上下文* @param dipValue dp值* @return*/private float dip2px(Context context, float dipValue){float scale = context.getResources().getDisplayMetrics().density;return dipValue * scale + 0.5f;}/*** 释放资源*/public synchronized void recycle(){if (heartTask != null){heartTask.cancel();}timer.cancel();}/*** 发点的任务*/private class HeartTask extends TimerTask{@Overridepublic void run(){try{Integer point = dataQueue.poll();if (point != null){for (int i = 0; i < showTimeDatas.length; i++){if (i + 1 < showTimeDatas.length){showTimeDatas[i] = showTimeDatas[i + 1];} else{showTimeDatas[i] = point;}}postInvalidate();} else{cancel();heartTask = null;}} catch (Exception e){e.printStackTrace();}}}
}

示意图:

更新时间:2019-08-04 使用timer进行速度控制,可精确到毫秒级实时,代替 Thread.sleep() 函数对毫秒级控制的不准确性。因为 Thread.sleep 会将线程转为等待状态,而没有在就绪状态,状态恢复对比毫秒级比较耗时。倘若次数较多累加,则时间延误明显。

转载于:https://my.oschina.net/xiaolei123/blog/3081580

Android开发 - 实时心率控件图相关推荐

  1. android include 控件详解,Android开发中include控件用法分析

    本文实例讲述了Android开发中include控件用法.分享给大家供大家参考,具体如下: 我们知道,基于Android系统的应用程序的开发,界面设计是非常重要的,它关系着用户体验的好坏.一个好的界面 ...

  2. android什么控件能够输入多行文字,Android开发:文本控件详解——EditText(一)基本属性...

    一.简单实例: EditText输入的文字样式部分的属性,基本都是和TextView中的属性一样. 除此之外,EditText还有自己独有的属性. 二.基本属性: hint 输入框显示的提示文本 te ...

  3. android图表控件 坐标,android-charts 基于Java和Android开发的图形图表控件 | 李大仁博客...

    王晓龙 April 8th, 2014 at 09:44 | #1 我再github上看到了你写的控件,给了我很大启发,我现在在做K线图一类的图表,正好用到了您开发的这款控件,但是有些地方使用的不太舒 ...

  4. 【android开发】 修改控件Spinner内容的字体大小 颜色等属性

    在anadroid开发中遇到此问题: 直接定义的Spinner控件,不能调整内容字体的大小,所以常会 因为控件太小,内容太长而使得不能完全显示,各种搜索,找到如下解决方法!希望对遇到同样问题 的朋友们 ...

  5. android开发中WebView控件的实例与注意要点——个人主页浏览器简易实现

    转自:http://itindex.net/detail/53169- ... 80%E5%8F%91-webview 在Android开发中往往需要实现网页的浏览,webview就是android开 ...

  6. Android开发基础之控件WebView

    WebView可以实现如下功能: 加载本地assets文件夹下的html文件 wedview.loadUrl("file:///android_asset/test.html"); ...

  7. Android开发基础之控件CheckBox

    目录 一.基础属性 二.自定义样式 1.去掉CheckBox的勾选框 2.自定义背景颜色 3.自定义勾选框的背景图片 三.监听事件 一.基础属性 1.layout_width 宽度 2.layout_ ...

  8. Android开发基础之控件EditText

    目录 一.基础属性 二.在提示文本旁添加图片 三.获取EditText内容 四.监听事件 五.制作登录界面 一.基础属性 1.id 设置组件id 2.layout_width 宽度 3.layout_ ...

  9. Android开发中的控件--ProgressBar和ListView

    ProgressBar即进度条,相信对于用户来说并不陌生,当用户等待某一动作的执行时就会出现这个控件提醒用户执行的进度,一种是水平方向的,另一种是圆形的. ListView即列表控件,一般Activi ...

最新文章

  1. pytorch学习笔记(九):PyTorch结构介绍
  2. Keil错误Unable to automatically place AT section malloc.o(.ARM.__AT_0x20000000) with required base add
  3. RBF网络——核心思想:把向量从低维m映射到高维P,低维线性不可分的情况到高维就线性可分了...
  4. spark.mllib:GradientBoostedTrees
  5. 【前端就业课 第一阶段】HTML5 零基础到实战(十一)canvas 基础
  6. ssl1197-质数和分解【dp练习】
  7. html5 拍照 清晰度,html5强大的功能(一)
  8. JAVA maven Spring 开发 webservice 步骤
  9. QLayout的属性介绍
  10. eclipse配置JDK9.0.4
  11. Caffe傻瓜系列(6):solver及其配置
  12. 郝斌C语言 流程控制
  13. Windows沙拉:开机时自动打开NumLock键背后的故事
  14. jupyter notebook的安装与启动
  15. 登录学习通报错:浏览器没有开启cookie功能
  16. jquery中的for循环
  17. 计算机研究生论文写作 问题记录
  18. 话生态 | F-One联手思凯普,靠谱!
  19. 6_计算机网络_应用层-HTTP-DNS-跳板机
  20. JAVA网络传输乱码问题

热门文章

  1. 如何用计算机解自控分离点,自动控制原理 答案 黄坚习题详解
  2. TextTiling: Segmenting Text into Multi-paragraph Subtopic Passages阅读笔记
  3. 奶粉的秘密(每个关爱孩子的家长都抽时间来看看)-转载
  4. android智能家居ppt,U-home智能家居整体解决方案(PPT 40页)
  5. iOS开发之第三方支付微信支付教程,史上最新最全第三方微信支付方式实现、微信集成教程,微信实现流程
  6. 【C++实验】模拟实现一个ATM自助存取款机
  7. java在进行流操作的close()方法
  8. 如何在PDF文件中编辑文本的大小?
  9. 设计图纸管理系统哪个更有优势?
  10. 华为交换机 查ip冲突_巧用交换机控制IP地址冲突故障的技巧