數據處理流程:
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));
// 基準線,默認2000
baseLine = typedArray.getInteger(R.styleable.HeartView_heart_base_line, 2000);
// 最大值,默認4000
Max = typedArray.getInteger(R.styleable.HeartView_heart_max, 4096);
// 最小值,默認0
Min = 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();
// 速度怎么可以小于0
if (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)];
}
@Override
protected 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;
// 速度怎么可以小于0
if (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;
}
@Override
protected 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
{
@Override
public 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();
}
}
}
}
示意圖:
image
更新時間:2019-08-04 使用timer進行速度控制,可精確到毫秒級實時,代替 Thread.sleep() 函數對毫秒級控制的不準確性。因為 Thread.sleep 會將線程轉為等待狀態,而沒有在就緒狀態,狀態恢復對比毫秒級比較耗時。倘若次數較多累加,則時間延誤明顯。
老群被封,+新Q群709287944