Android Ble心电图自定义绘制(支持左右自动滚动)

一,最近在做蓝牙开发项目,涉及到一个自定义心电图功能,效果图如下(这里只展示一张静态图)。
心电图
二,分析:首先我们看到这个效果图,自己动手画要怎么实现呢?网格需要怎么绘制,横向和纵向,还有波形需要怎么绘制?
需要定义几个Paint,比如硬件返回的值范围 0-255,我们波形数据显示的位置应该在中间,比例多大呢等等,带着这些疑问,那我直接贴代码了哈:

/**
 * Created by xiaoyi
 * Email 2585760090@qq.com
 */
public class ECGChart extends View {
    private float mGraphMax = 256.f;  //心电波动区间256.f
    private int mRedrawInterval = 1; //屏幕刷新间隔 ms
    private int mRedrawPoints;

    public static final int SWEEP_MODE = 0;
    public static final int FLOW_MODE = 1;
    private int mLineColor;
    private int mGridColor;
    private int mArrowColor;

    private int mWindowSize;
    private int mWindowCount = 2;
    private int ONEWINDOW = 240;//240
    private LinkedBlockingDeque<Integer> mInputBuf; //波形数据纵坐标
    private Vector<Integer> mDrawingBuf;            //下标标识横坐标 数值标识纵坐标

    private Paint mPaint;

    private Paint mPaintGrid;
    private Paint mPaintRuler;
    private Paint mPaintArrow;
    private Paint mPaintSmallGrid;

    private Paint mMaskBarPaint;
    private int mDrawPosition;

    private Activity mActivity;

    private int mGraphMode = 1;
    private boolean mGrid = true;
    private boolean mArrow = false;
    private boolean mFullscreen = false;
    private boolean isConnected = false;
    TimerTask mDrawEmitter;
    Timer mTimer;

    //画网格线第二种方法
    private Paint paint;
    int horizontalBigGirdNum = 6;// 横向的线,即纵向大格子的数量,每个大格子里面包含5个小格子
    int verticalBigGirdNum = 8;
    private int width;
    private int height;
    private int widthOfSmallGird;

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        width = w;
        height = h;
        widthOfSmallGird = width / (verticalBigGirdNum * 5); // 小网格的宽度,每个大网格有 5 个小网格
    }

    public ECGChart(Context context, AttributeSet attrs) {
        super(context, attrs);
        mActivity = (Activity) context;
        TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                com.nanochap.ncsdk.R.styleable.ECGChart,
                0, 0);

        try {
            //线颜色
            mLineColor = a.getColor(com.nanochap.ncsdk.R.styleable.ECGChart_lineColor, Color.RED);
            //网格颜色
            mGridColor = a.getColor(com.nanochap.ncsdk.R.styleable.ECGChart_gridColor, Color.argb(0x33, 0x00, 0xFF, 0x00));
            //箭头颜色
            mArrowColor = a.getColor(com.nanochap.ncsdk.R.styleable.ECGChart_arrowColor, Color.rgb(255, 255, 255));

            mGraphMode = a.getInt(com.nanochap.ncsdk.R.styleable.ECGChart_graphMode, SWEEP_MODE);
            mGrid = a.getBoolean(com.nanochap.ncsdk.R.styleable.ECGChart_grid, true);
            mArrow = a.getBoolean(com.nanochap.ncsdk.R.styleable.ECGChart_arrow, false);

            //窗口大小
            mWindowSize = a.getInt(com.nanochap.ncsdk.R.styleable.ECGChart_windowSize, ONEWINDOW * mWindowCount);

            //数据源
            mInputBuf = new LinkedBlockingDeque<>();
            mDrawingBuf = new Vector<>();

            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(1f);
            mPaint.setColor(mLineColor);

            //画大格
            mPaintGrid = new Paint();
            mPaintGrid.setColor(mGridColor);
            mPaintGrid.setStrokeWidth(2f);
            //画小格
            mPaintSmallGrid = new Paint();
            mPaintSmallGrid.setColor(mGridColor);
            mPaintSmallGrid.setStrokeWidth(1f);

            //画箭头
            mPaintArrow = new Paint();
            mPaintArrow.setColor(mArrowColor);
            mPaintArrow.setStrokeWidth(1);
            mPaintArrow.setStyle(Paint.Style.STROKE);

            //尺寸
            mPaintRuler = new Paint();
            mPaintRuler.setColor(Color.argb(0xAA, 0xFF, 0xFF, 0xFF));


            mMaskBarPaint = new Paint();
            mMaskBarPaint.setColor(Color.rgb(0x33, 0x33, 0x33));
            mMaskBarPaint.setStyle(Paint.Style.STROKE);

            // TODO: 2017/10/25 0025
            mGraphMode = FLOW_MODE;
        } finally {
            a.recycle();
        }
        init();
        //画网格线
        paint = new Paint();                            // 画网格的 Paint
        paint.setStyle(Paint.Style.STROKE);
    }

    private void init() {
        mRedrawPoints = ONEWINDOW * mRedrawInterval / 100;
        for (int i = 0; i < mWindowSize; i++)
            mDrawingBuf.add(128);

        mDrawEmitter = new TimerTask() {
            @Override
            public void run() {
                mActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        refreshData();
                        //L.d("inputBufSizeinput", mInputBuf.size() + "");
                        //L.d("inputBufSizedraw", mDrawingBuf.size() + "");
                    }
                });
            }
        };
        mTimer = new Timer();
        mTimer.schedule(mDrawEmitter, 0, mRedrawInterval);
    }

    private void refreshData() {
        if (!isConnected) {
            //L.d(" isConnected  return");
            return;
        }
        if (mInputBuf.size() < mRedrawPoints) {
            return;
        } 
        if (mGraphMode == SWEEP_MODE) {
            for (int i = 0; i < mRedrawPoints; i++) {
                int val = mInputBuf.pollFirst();
                mDrawingBuf.remove(mDrawPosition);
                mDrawingBuf.add(mDrawPosition++, val);
                if (mDrawPosition >= mWindowSize)
                    mDrawPosition = 0;
            }
        } else {
            for (int i = 0; i < mRedrawPoints; i++) {
                int val = mInputBuf.pollFirst();
                mDrawingBuf.remove(0);
                mDrawingBuf.add(val);
            }
        }
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float width = this.getWidth();           //控件宽度
        float height = this.getHeight();         //控件高度

        //L.e("ECGCHAR width = "+width + "  height ="+height);
        /*if (mDrawingBuf.size() < mWindowSize) // return?
            return;*/

        float mapRatio = width / mWindowSize;   //
        int start = mDrawingBuf.get(0);
        if (mGraphMode == FLOW_MODE) {
            //L.e("FLOW_MODE");
            for (int i = 1; i < mWindowSize; i++) {
                //画波形
                if (i >= mDrawingBuf.size())
                    break;
                int end = mDrawingBuf.get(i);
                //start / mGraphMax 当前点占控件相对位置
                /*canvas.drawLine(i , start / mGraphMax * height,
                        i+1 ,end / mGraphMax * height, mPaint);*/
                canvas.drawLine(i * mapRatio, (1 - start / mGraphMax) * height,
                        (i + 1) * mapRatio, (1 - end / mGraphMax) * height, mPaint);
                /*drawAnimLine(canvas, i * mapRatio, (1 - start / mGraphMax) * height,
                        (i + 1) * mapRatio, (1 - end / mGraphMax) * height);*/
                start = end;
            }
        }

        if (mGraphMode == SWEEP_MODE) {
            L.e("SWEEP_MODE");
            canvas.drawRect((mDrawPosition - 10) * mapRatio,
                    0,
                    (mDrawPosition + 10) * mapRatio,
                    height,
                    mMaskBarPaint);
        }

        if (mGrid) {
            //画背景格
            drawGridLine(canvas);
        }
    }

    /*private ValueAnimator valueAnimator;
    private float tempX;
    private float tempY;

    private void drawAnimLine(final Canvas canvas, final float startX, final float startY, final float endX, final float endY) {
        valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                //canvas.drawLine(startX + DoubleUtil.mul());
                tempX = FloatUtil.add(startX, FloatUtil.mul(fraction, FloatUtil.sub(endX, startX)));
                tempY = FloatUtil.add(startY, FloatUtil.mul(fraction, FloatUtil.sub(endY, startY)));
                canvas.drawLine(startX, startY, tempX, tempY, mPaint);
            }
        });
        valueAnimator.start();
    }*/

    public void setMode(int type) {
        mInputBuf.clear();
        mGraphMode = type;
    }

    public void setConnection(boolean isConnect) {
        if (!isConnect)
            mInputBuf.clear();
        isConnected = isConnect;
    }

    /**
     * 画网格 第一种方式
     */
    private void drawGird(Canvas canvas) {
        paint.setColor(Color.GREEN);                   // 网格浅绿
        // 横向的网格
        for (int i = 0; i <= verticalBigGirdNum * 5; i++) {
            if (i % 5 == 0) {       // 每隔 5 个小格,线变粗
                paint.setStrokeWidth(3);
            } else {
                paint.setStrokeWidth(1);
            }
            canvas.drawLine(i * widthOfSmallGird, 0, i * widthOfSmallGird, height, paint);     // 画线
        }

        //  纵向的网格
        for (int i = 0; i <= horizontalBigGirdNum * 5; i++) {
            if (i % 5 == 0) {    // 每隔 5 个小格,线变粗
                paint.setStrokeWidth(3);
            } else {
                paint.setStrokeWidth(1);
            }
            canvas.drawLine(0, i * widthOfSmallGird, width, i * widthOfSmallGird, paint);
        }
    }

    /**
     * 画背景线 第二种方式
     *
     * @param canvas
     */
    public void drawGridLine(Canvas canvas) {
        float width = this.getWidth();
        float height = this.getHeight();
        int gridXNumber = 8;
        int gridYNumber = 4; //最大值4,0mV
        for (int i = 0; i < gridXNumber; i++) {
            canvas.drawLine(i * width / gridXNumber, 0,
                    i * width / gridXNumber, height, mPaintGrid)
        }

        for (int i = 0; i < gridXNumber; i++) {
            for (int j = 0; j < 10; j++)
                canvas.drawLine(i * width / gridXNumber + j * width / gridXNumber / 10f, 0,
                        i * width / gridXNumber + j * width / gridXNumber / 10f, height, mPaintSmallGrid);
        }

        for (int i = 0; i < gridYNumber; i++) {
            canvas.drawLine(0, i * height / gridYNumber,
                    width, i * height / gridYNumber, mPaintGrid);
        }

        for (int i = 0; i < gridYNumber; i++) {
            for (int j = 0; j < 10; j++)
                canvas.drawLine(0, i * height / gridYNumber + j * height / gridYNumber / 10f,
                        width, i * height / gridYNumber + j * height / gridYNumber / 10f, mPaintSmallGrid);
        }

        //画刻度
        float pX = 10;
        float pY = height - 10;
        float dx = width / gridXNumber;
        float dy = height / gridYNumber;
        canvas.drawLine(pX, pY, pX, pY - dy + 5, mPaintArrow);
        canvas.drawLine(pX, pY, pX + dx - 5, pY, mPaintArrow);

        Path path = new Path();
        path.moveTo(0, -10);
        path.lineTo(5, 0);
        path.lineTo(-5, 0);
        path.close();

        Path path2 = new Path();
        path2.moveTo(10, 0);
        path2.lineTo(0, 5);
        path2.lineTo(0, -5);
        path2.close();

        path.offset(pX, pY - dy + 5);
        path2.offset(pX + dx - 5, pY);
        canvas.drawPath(path, mPaintArrow);
        canvas.drawPath(path2, mPaintArrow);
        mPaintArrow.setStrokeWidth(1);
        mPaintArrow.setAntiAlias(true);
        if (mArrow) {//写文字
            canvas.drawText("250 ms", pX + 28, pY - 10, mPaintArrow);
            canvas.drawText("1.0 mV", pX + 8, pY - dy + 15, mPaintArrow);
        }
    }

    public void addEcgData(final int data) {
        if (data != 0) {
            mInputBuf.addLast(data);
        }
        //L.i("size", "" + mInputBuf.size()+" data "+data);
    }
    private void checkBufOverflow() {
        L.i("size", mInputBuf.size() + "");
        if (mInputBuf.size() > 2000)
            mInputBuf.clear();
    }

    /**
     * 定时器关闭-释放资源
     */
    public void clearTimeData() {
        mDrawEmitter.cancel();
        mTimer.cancel();
        mTimer.purge();
    }
}

三.attrs资源文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ECGChart">
        <attr name="lineColor" format="color" />
        <attr name="windowSize" format="integer" />
        <attr name="graphMode" format="integer" />
        <attr name="grid" format="boolean" />
        <attr name="gridColor" format="color" />
        <attr name="arrow" format="boolean" />
        <attr name="arrowColor" format="color" />
    </declare-styleable>
</resources>

四,DeviceActivity类调用地方,布局引用我这里省略了:

   @BindView(R.id.ecg_chart)
    ECGChart mEcgChart;
      /**
     * 心电图测量反馈回调
     */
    EMDataCallBack mEMDataCallBack = new EMDataCallBack() {
        @Override
        public void onSuccess(final EMData data, final String msg) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                if(data!=null){
                       int[] wave = data.getWave();
                       addEcgData(wave);
                    }
                  }
                }
            });
        }
    };
     /**
     * 绘制ecg心电图
     *
     * @param wave
     */
    private void addEcgData(final int[] wave) {
        mEcgChart.setConnection(true);
        for (final int data : wave) {
            if (mEcgChart != null) {
                mEcgChart.addEcgData(data);
            }
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //这个方法很重要切记,切记,切记。
        mEcgChart.clearTimeData();
    }

五,总结:以上就是自定义心电图的全部代码,代码不难理解,demo暂时没放到github上,有需求的同学可自行对着代码写一个demo,这里稍微解释下,由于波形是自动左右滚动的,所以我自定义view时候 开了一个定时器,调用的地方切记要在onDestroy()方法里调用 mEcgChart.clearTimeData(); 不然定时器开了 不关掉,肯定会出问题的,波形区间范围,根据自己需求修改,包括波形 和表格线条颜色等。

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值