Android自定义View 您所在的位置:网站首页 心电图横坐标每一横格为多少 Android自定义View

Android自定义View

#Android自定义View| 来源: 网络整理| 查看: 265

概述

这次来讲讲心电图的绘制,这也是项目当中用到过的。心电图继承自View,概括一下主要有以下内容要实现:**实时显示动态心电测量数据、心电波形左右滑动、惯性滑动及波形 X轴和 Y轴方向双指滑动缩放。**下面我们来看看效果图,图片上传大小有限制,所以分两张:

Screenrecorder-2021-08-09-18-44-54-1282021891847387.gif

ECG_2.gif

下面我们将功能拆解,分步实现:

画背景绿色网格线 绘制实时动态心电曲线 实现单指曲线左右平移 实现曲线惯性滑动 实现 X轴及 Y轴方向上曲线的双指滑动缩放(多点触控改变曲线增益) 左上角显示当前增益 1、画网格线

这个就比较简单了。首先确定每一小格的边长,然后获取控件宽高。这样就能分别计算出水平方向及竖直方向有多少小格,也就是可以确定横线和竖线一共要画多少条。然后就可以用循环画出所有的线条,其中每隔5条进行线条加粗,而且画实线,这样就形成了实线大格。下面先看实现:

// 画 Bitmap protected Bitmap gridBitmap; // 画 Canvas protected Canvas bitmapCanvas; // 控件宽高 protected int viewWidth, viewHeight; @Override protected void onSizeChange() { // 获取控件宽高 viewWidth = mBaseChart.getWidth(); viewHeight = mBaseChart.getHeight(); // 初始化网格 Bitmap gridBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); bitmapCanvas = new Canvas(gridBitmap); Log.d(TAG, "onSizeChange - " + "-- width = " + mBaseChart.getWidth() + "-- height = " + mBaseChart.getHeight()); } /** * 准备好画网格的 Bitmap */ private void initBitmap(){ // 计算横线和竖线条数 hLineCount = (int) (viewHeight / gridSpace) + 2; vLineCount = (int) (viewWidth / gridSpace) + 2; // 画横线 for (int h = 0; h < hLineCount; h ++){ float startX = 0f; float startY = gridSpace * h; float stopX = viewWidth; float stopY = gridSpace * h; // 每个 5根画一条粗实线 if (h % 5 != 0){ linePaint.setPathEffect(pathEffect); linePaint.setStrokeWidth(1.5f); }else { linePaint.setPathEffect(null); linePaint.setStrokeWidth(3f); } // 画线 bitmapCanvas.drawLine(startX, startY, stopX,stopY, linePaint); } // 画竖线 for (int v = 0; v < vLineCount; v ++){ float startX = gridSpace * v; float startY = 0f; float stopX = gridSpace * v; float stopY = viewHeight; // 每隔 5根画一条粗实线 if (v % 5 != 0){ linePaint.setPathEffect(pathEffect); linePaint.setStrokeWidth(1.5f); }else { linePaint.setPathEffect(null); linePaint.setStrokeWidth(3f); Log.d(TAG, "v = " + v); } // 画线 bitmapCanvas.drawLine(startX, startY, stopX,stopY, linePaint); } } @Override protected void onDraw(Canvas canvas) { // 注释 1,Bitmap左边缘位置为getScrollX(),防止网格滑动 canvas.drawBitmap(gridBitmap, mBaseChart.getScrollX(), 0, null); }

这里想提一下的是,这里网格线并不是直接画在控件 onDraw方法的 Canvas上的。而是在控件初始化时,事先将网格所有线条画在一张 Bitmap上,然后绘制时直接绘制 Bitmap。这样搞就不用每次绘制时都计算一遍线条的位置了。

还有就是上面注释 1处,绘制网格 Bitmap的左边缘的位置是 getScrollX()。因为后面要实现曲线左右滑动,但网格要固定不动。

2、绘制动态实时心电曲线

这就是心电图最主要的实现了。心电在测量的时候会实时传递电压值,我们需要把电压值实时存进数组里。然后把电压值换算成 Y坐标值,再根据事先确定好的 X轴方向两个数据点的距离来确定每个电压值在 X轴方向的坐标。然后从左到右确定曲线的路径Path,再将Path绘制到Canvas上就可以了。

我们观察上面效果图会发现,这里的实现是最后一个到达的数据的显示不会超过控件右边缘。也就是当曲线 X方向的长度不超过控件宽度时,曲线第一个点的横坐标 x = 0。当曲线 X方向长度大于控件宽度时,曲线 Path的第一个点的横坐标就向左移,也就是 x为负的了。这样就实现上面效果中,测量实时心电时,曲线会向左移。这样新来的数据就显示在控件可见范围内,早来的数据逐步向左移出控件可见范围。下面画个草图吧,草图大概就这么个意思:

心电.png

下面看一下实现:

/** * 创建曲线 */ private boolean createPath() { // 曲线长度超过控件宽度,曲线起点往左移 // 根据控件宽度和数组长度以及 X增益算出数组第一个数的 X坐标 float startX = (this.data.size() * dataSpaceX > viewWidth) ? (viewWidth - (this.data.size() * dataSpaceX)) : 0f; // 曲线复位 dataPath.reset(); for (int i = 0; i < this.data.size(); i++) { // 确定 X轴坐标 float x = startX + i * this.dataSpaceX; // 确定 Y轴坐标 float y = getVisibleY(this.data.get(i)); // 绘制曲线 if (i == 0) { dataPath.moveTo(x, y); } else { dataPath.lineTo(x, y); } } return true; } /** * 电压 mv(毫伏)在 Y轴方向的换算 * 屏幕向上往下是 Y 轴正方向,所以电压值要乘以 -1进行翻转 * 目前默认每一大格代表 1000 mv,而真正一大格的宽度只有 150,所以 data要以两数换算 * Y == 0,是在 View的上边缘,所以要向下偏移将波形显示在中间 * * @param data * @return */ // 注释 2 private float getVisibleY(int data) { // 电压值换算成 Y值 float visibleY = -smallGridSpace * 5 / mvPerLargeGrid * data; // 向下偏移 visibleY = visibleY + smallGridSpace * 5 * offset; return visibleY; } @Override protected void onDraw(Canvas canvas) { // 绘制心电曲线 canvas.drawPath(dataPath, linePaint); }

上面有一点需要注意的,就是我们的 Y值的换算。我们知道Android屏幕自上而下是 Y轴正方向,所以我们如果直接把电压值画在屏幕上它是倒挂的。另外,这里默认的一大格代表1000mv电压值(可设),而真正一大格的边长是150。所以我们需要将电压值换算成屏幕像素。具体看上面注释 2的getVisibleY方法上面注释。

3、实现曲线左右平移

当心电测量完之后,我们需要实现曲线随手指滑动平移。这样才能看到心电图的全部内容。这个实现原理也简单,也就是监听onTouch事件,根据手指位移使用View的scrollBy方法来实现内容平移就可以了:

/** * @param event 单指事件 */ private void singlePoint(MotionEvent event) { mVelocityTracker.addMovement(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = event.getX(); break; case MotionEvent.ACTION_MOVE: float deltaX = event.getX() - lastX; delWithActionMove(deltaX); lastX = event.getX(); break; case MotionEvent.ACTION_UP: // 计算滑动速度 computeVelocity(); break; } } /** * @param deltaX 处理 MOVE事件 */ private void delWithActionMove(float deltaX) { if (this.data.size() * dataSpaceX = rightBorder) && (deltaX < 0)) { mBaseChart.scrollTo(0, 0); } else { // 内容平移 mBaseChart.scrollBy((int) -deltaX, 0); } }

注意上面左右边界的设定,别让曲线划出屏幕了。

4、惯性滑动

惯性滑动的实现,这里使用的套路是 VelocityTracker。先追踪手指滑动速度,然后使用 Scroller并结合 View的 computeScroll()方法和 scrollTo方法,实现手指离开屏幕后的惯性滑动。这部分内容在我上一篇文章画一个FM调频收音机刻度表 有讲,这里不再重复。

5、实现双指滑动,在横纵坐标方向缩放曲线

在实现双指滑动曲线缩放功能之前,我们先讲讲一小部分 MotionEvent的基础知识。为什么说只讲一小部分呢?因为 MotionEvent这个事件体系还蛮大。我们只讲一下这次用到的部分。

onTou.png

onTouch2.png

好吧,还是直接画表格吧。这样也直观一点,不用解释那么多。上面红色圈圈圈出来的几个哥们是我们这次要用到的。

event.getActionMasked() :上面也有解释,这个方法和 getAction()类似。只不过我们这次要处理多点触控,所以一定要用 getActionMasked() 来获取事件类型。

event.getPointerCount() :上面也有解释,获取屏幕上手指个数。因为我们这次要处理双指滑动,所以要用 (getPointerCount() == 2)进行判断。两根手指以外的事件我们不做缩放处理。

ACTION_POINTER_DOWN :上面又有解释,第一根手指之后,按下的其他手指。如果结合 (getPointerCount() == 2)这个前提条件,那么我们可以认为这次ACTION_POINTER_DOWN 就是第二根手指按下所触发的事件。

event.getX(int pointerIndex):上面也有介绍,获取某个手指当前的 X坐标。我们在获取到两个手指当前的 X坐标之后,就可以算出两指当前在 X轴方向的距离。然后再结合 ACTION_POINTER_DOWN 时所记录的坐标值,就可以计算出两个手指在 X方向上是靠近了还是疏远了(收缩了还是放大了)。getY(int pointerIndex) 方法同理,不做解释了。

ACTION_MOVE :两指滑动当然也要用到 MOVE事件,只不过这里 ACTION_MOVE 和单指的使用方法一样,就不做解释了。

好了,我们再看看 X轴方向缩放具体实现吧:

/** * 处理onTouch事件 * * @param event 事件 * @return 拦截 */ @Override protected boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "pointerCount = " + event.getPointerCount()); if (event.getPointerCount() == 1) { // 单指平滑 singlePoint(event); } if (event.getPointerCount() == 2) { // 双指缩放 doublePoint(event); } return true; } /** * @param event 双指事件 */ private void doublePoint(MotionEvent event) { if (pointOne == null) pointOne = new PointF(); if (pointTwo == null) pointTwo = new PointF(); switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: // 第二根手指按下 Log.d(TAG, "ACTION_POINTER_DOWN"); // 记录第二根手指按下时,两指的坐标点 saveLastPoint(event); numbersPerLargeGridOnThisTime = getDataNumbersPerGrid(); mvPerLargeGridOnThisTime = getMvPerLargeGrid(); break; case MotionEvent.ACTION_MOVE: // 双指拉伸 Log.d(TAG, "ACTION_MOVE"); // 计算 X方向缩放量 getScaleX(event); // 计算 Y轴方向所放量 getScaleY(event); break; case MotionEvent.ACTION_POINTER_UP: // 先离开的手指 Log.d(TAG, "ACTION_POINTER_UP"); break; } } /** * 处理 X方向的缩放 * * @param event 事件 * @return 拉伸量 */ private float getScaleX(MotionEvent event) { float pointOneX = event.getX(0); float pointTwoX = event.getX(1); // 算出 X轴方向的拉伸量 float deltaScaleX = Math.abs(pointOneX - pointTwoX) - Math.abs(pointOne.x - pointTwo.x); // 设置拉伸敏感度 int inDevi = mBaseChart.getWidth() / 54; // 计算拉伸时增益偏移量 int inDe = (int) deltaScaleX / inDevi; // 算出最终增益 int perNumber = numbersPerLargeGridOnThisTime - inDe; // 设置增益 setDataNumbersPerGrid(perNumber); return deltaScaleX; }

好了,该解释的原理上面都做了解释。上面代码要解释的无非就是缩放敏感度调节的问题,代码里做了解释。缩放量计算出来之后,我们就可以改变心电曲线的增益了。比如说 X方向两点数据之间的距离做了调整、Y方向心电数值计算因子做了调整,然后重新算出曲线 Path再重绘,也就可以了。

6、左上角显示当前增益

最后我们要把当前增益显示出来,比如说 X轴方向一大格绘制了多少点数据、Y轴方向一大格代表多少毫伏。这两个参数都是在上一步双指缩放时动态改变的,所以要留一个对外接口让外界获取到这两个参数。

/** * 获取每大格显示的数据个数,再结合医疗版的采样率,就可以算出一格显示了多长时间的数据 * * @return */ public int getDataNumbersPerGrid() { return this.dataNumbersPerGrid; } /** * @return 获取每大格代表多少毫伏 */ public float getMvPerLargeGrid() { return this.mvPerLargeGrid; }

因为这次心电图的绘制比以往的文章都涉及到更多的细节,所以之前文章里讲过的一些实现细节这里就没重复讲。另外,这次自定义 View使用了 Base模板设计模式,用好几个类来实现了这幅心电图,所以没把完整代码贴在这里。代码还是直接放Github吧 :心电图



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有