Android開發 - 實時心率控件圖

數據處理流程:

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容