自定义控件之——心电图控件的绘制

这是我在做项目的时候的一个需求,根据蓝牙设备采集到的数据画出心电图。 由于以前没有做过类似的东西,当我拿到这个需求的时候 整个人都不好了,没办法只能硬着头皮上呗。然后就开始在网上疯狂的找Demo,皇天不负有心人,我果然没有找到完全符合我们需求的demo快哭了,没有办法只能自己撸一个出来。先上一个效果图 (这只是demo想要好看点可以自己调)。


        数据只是使用的随机数,所以看起来有点不专业,当画完一屏幕的点后,会有类似擦除效果来更新我们的心电图,就和医院的类似。横坐标与纵坐标的值都可以自己修改,比如将横坐标修改成时间,都是行的,下面开始工作。




在开始定义以前,首先介绍一下android 的坐标系统如上图,android的坐标和我们数学中学习的坐标差不多,只是将坐标原点定在了左上角,某一点的坐标我们根据宽高来确定。比如右下角的坐标:Y坐标是当前位置距顶部的高度,X是当前位置距左边的距离,假设距顶部的距离为Height,距左边的距离为width那么该点的坐标为(width,height);

说了这么多咱们开始进入正题

1、定义ECGView继承自View,并实现重写它的三个构造函数(其实只需要重写两个参数就行,但是以备不时之需还是都写了)


2.定义declare-styleable标签,命名空间的时候使用

<declare-styleable name="elg">
	    <attr name="BackLineColor" format="color"/>
	    <attr name="TitleColor" format="color"/>
	    <attr name="PointerLineColor" format="color"/>
	    <attr name="TitleSize" format="dimension"/>
	    <attr name="XYTextSize" format="dimension"/>
	    
	</declare-styleable>    
BackLineColor为背景图中表格线的颜色,TitleColor标题颜色,PointerLineColor波形图线的颜色,TitleSize标题文字大小,XYTextSize XY轴文字的大小。

然后在构造方法中初始化

public ECGView(Context context, AttributeSet attrs) {
		super(context, attrs);
		_context = context;
		TypedArray typedArray = context.obtainStyledAttributes(attrs,
				R.styleable.elg);
		_backLineColor = typedArray.getColor(R.styleable.elg_BackLineColor,
				Color.GREEN);
		_titleColor = typedArray
				.getColor(R.styleable.elg_TitleColor, Color.RED);
		_pointerLineColor = typedArray.getColor(
				R.styleable.elg_PointerLineColor, Color.WHITE);
		_titleSize = typedArray.getDimensionPixelSize(
				R.styleable.elg_TitleSize, 30);
		_XYTextSize = typedArray.getDimensionPixelSize(
				R.styleable.elg_XYTextSize, 20);
		typedArray.recycle();
		initView();
	}
3.定义画笔类

private void initView() {
		_handler = new Handler(Looper.getMainLooper());
		_PaintLine = new Paint();
		_PaintLine.setStrokeWidth(2.5f);
		_PaintLine.setColor(_pointerLineColor);
		_PaintLine.setAntiAlias(true);
		_PaintDataLine = new Paint();
		_PaintDataLine.setColor(_backLineColor);
		_PaintDataLine.setAntiAlias(true);
		_PaintDataLine.setStrokeWidth(10);
		_XYTextPaint = new TextPaint();
		_XYTextPaint.setColor(_titleColor);
		_XYTextPaint.setTextSize(_XYTextSize);
		_TitleTextPaint = new TextPaint();
		_TitleTextPaint.setColor(_titleColor);
		_TitleTextPaint.setTextSize(_titleSize);
	}
定义这些就开始真正的开始我们的绘制了

4,绘制背景表格

怎么绘制背景网格呢?首先我们得考虑每个格子都应该有值,以便用来计算我们的点处于网格的哪个位置,比如我们垂直总共6000,总共有10个格子,那每个格子的值就应该是6000/10=600。反之我们可以通过设置格子的值和最大y值来计算格子数。这个时候我们就需要定义两个属性,一个为Y的最大值,和格子的值。还需要对外暴露设置方法。

private float _MaxYNumber;//Y最大值
private float _EveryOneValue = 1;// 每个格子的值
<span style="white-space:pre">	</span>// 设置Y轴最大值
<span style="white-space:pre">	</span>public void setMaxYNumber(float maxYNumber) {
<span style="white-space:pre">		</span>this._MaxYNumber = maxYNumber;
<span style="white-space:pre">	</span>}


<span style="white-space:pre">	</span>// 设置格子的值
<span style="white-space:pre">	</span>public void setEffticeValue(int value) {
<span style="white-space:pre">		</span>_EveryOneValue = value;
<span style="white-space:pre">	</span>}
通过_MaxYNumber 和_EveryOneValue我们可以知道y轴的格子数,格子应该都是正方形的,那我们怎么得到格子的高呢?我们可以通过网格的总高度除以格子数得到

那X轴的格子数怎么得到呢?可以通过控件的有效宽度除以格子的宽度得到。

上代码:

if (_isfristDrawBackGround) {//是否是第一次加載背景
			_YSize = (int) (_MaxYNumber / _EveryOneValue);// 垂直格子数量
			_LatticeWidth = (int) (_EffectiveHeight / _YSize);//每個格子的寬度
			_XSize = ((_Width - _RightIndent - _LeftIndent) / _LatticeWidth)+1;// 水平格子数量
			float curX = 0;
			if (_EveryNPointBold > _YSize || _EveryNPointBold > _XSize) {
				_EveryNPointBold = Math.min(_YSize, _XSize) / 2 + 1;
			}
			for (int i = 0; i < _XSize; i++) {//添加x坐标
				_ListVLine.add(curX);
				curX += _LatticeWidth;
			}
			float curY = 0;
			for (int j = 0; j < _YSize; j++) {//添加Y坐标
				_ListHLine.add(curY);
				curY += _LatticeWidth;
			}
			_isfristDrawBackGround = false;
		}

上面代码初始化每条线的XY坐标以及格子的数量

for (int i = 0; i < _ListVLine.size(); i++) {
			sText = 5 * i;
			canvas.drawText(sText + "", _ListVLine.get(i) + _TopIndent, _Height
					- _TopIndent + _XYTextSize, _XYTextPaint);
			if (i == 0) {
				_PaintDataLine.setStrokeWidth(8);
				canvas.drawLine(_ListVLine.get(i) + _LeftIndent,
						0 + _TopIndent, _ListVLine.get(i) + _LeftIndent,
						_Height - _BottomIndent, _PaintDataLine);
				_PaintDataLine.setStrokeWidth(1);
			} else {
				if (i % _EveryNPointBold == 0) {
					_PaintDataLine.setStrokeWidth(3);
					canvas.drawLine(_ListVLine.get(i) + _LeftIndent,
							0 + _TopIndent, _ListVLine.get(i) + _LeftIndent,
							_Height - _BottomIndent, _PaintDataLine);
					_PaintDataLine.setStrokeWidth(1);
				} else {
					canvas.drawLine(_ListVLine.get(i) + _LeftIndent,
							0 + _TopIndent, _ListVLine.get(i) + _LeftIndent,
							_Height - _BottomIndent, _PaintDataLine);
				}
			}
		}
上面代码主要化X坐标的文字和垂直的线,这里讲一下canva.drawLine(startx,startY,stopX,stopY)里面的几个参数,(startX,starY)为开始点,(stopX,stopY)为结束坐标,两点确定一条直线嘛!相信小伙伴们都知道。

// 分别为上下左右缩进
private int _LeftIndent = 100;
private int _RightIndent = 100;
private int _BottomIndent = 100;
private int _TopIndent = 100;

Y坐标的文字和水平的线,和上面类似这里就不仔细说了。

5.绘制波形图

在绘制波形图之前我们需要知道点和点之间的距离是多少,也就是我们应该怎么确定X坐标,我是这样做的

_XUnitLength = (_EffectiveWidth) / (_PointMaxAmount - 1);// 两个点之间的水平间距等于有效宽减出左右缩进除以点数,因为不需要太精确,这里不做误差分析。

public void setLinePoint(float curY) {
		Map<String, Float> temp = new HashMap<String, Float>();
		temp.put(X_KEY, _CurX);//放入x坐标
		_CurX += _XUnitLength;
		// 计算y真实坐标
		float number = curY / _EveryOneValue;// 这个数应该占的格子数
		if (_Height != 0) {
			_CurY = _Height - (_BottomIndent + number * _LatticeWidth);
		}
		if (_CurY < _TopIndent) {
			_CurY = _TopIndent + 10;
		}
		temp.put(Y_KEY, _CurY);
		// 判断当前点是否大于最大点
		if (_CurP < _PointMaxAmount) {
			try {
				if (_ListPoint.size() == _PointMaxAmount
						&& _ListPoint.get(_CurP) != null) {
					_ListPoint.remove(_CurP);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			_ListPoint.add(_CurP, temp);
			_CurP++;
		} else {
			_CurP = 0;
			_CurX = _RightIndent;
		}
		if (_CurP % _EveryNPointRefresh == 0) {
			invalidate();
		}
		if (isSetAlarmFlag) {
			if (!(curY > _minAlarmNumber && curY < _maxAlarmNumber)) {
				_handler.post(new alarmThread(
						curY < _minAlarmNumber ? LOW_ALARM : HIGH_ALARM));
			}

		}
	}

        这是第一个方法,具体是将外面的设置进来的点,比如4800计算成在设备上准确的坐标,并添加到点的集合中去。添加完后通知界面重绘,这里如果点数已经超过了我们设置的最大点数,则将前面的点替换,这就是我们看到的当一个屏幕满了,重头开始刷新,但是后面的波形没动效果的实现原理。

public void drawWave(Canvas canvas) {
		for (int index = 0; index < _ListPoint.size(); index++) {
			if (_ListPoint.size() == _PointMaxAmount
					&& (index >= _CurP && index < _CurP + _RemovedPointNum)) {//实现擦除效果
				continue;
			}
			if (index > 0) {
				if (_ListPoint.get(index).get(Y_KEY) < 0
						|| _ListPoint.get(index).get(Y_KEY) < _TopIndent) {
					continue;
				}
				canvas.drawLine(_ListPoint.get(index - 1).get(X_KEY),
						_ListPoint.get(index - 1).get(Y_KEY),
						_ListPoint.get(index).get(X_KEY), _ListPoint.get(index)
								.get(Y_KEY), _PaintLine);
				canvas.setDrawFilter(new PaintFlagsDrawFilter(0,
						Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));
			}
		}
	}
drawWave 在ondraw中,是真正的画波形图的核心代码,前面所做的一切都是为了这个方法做准备。然后我们看看使用方法

private static final int MSG_DATA_CHANGE = 0x11;
    private ECGView _ECG;
    private Handler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        _ECG = (ECGView) findViewById(R.id.electrocardiogram);
        _ECG.setMaxPointAmount(100);
        _ECG.setRemovedPointNum(2);
        _ECG.setEveryNPoint(5);
        _ECG.setEveryNPointRefresh(1);
        _ECG.setEffticeValue(400);
        _ECG.setMaxYNumber(6000f);
        _ECG.setAlarmMessage(4800, 1000, "低了", "高了");
        mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // TODO Auto-generated method stub
                switch (msg.what) {
                    case MSG_DATA_CHANGE:
                        _ECG.setLinePoint((int)(msg.arg2));
                        break;
                    default:
                        break;
                }
                super.handleMessage(msg);
            }
        };

        new Thread() {
            public void run() {
            	while(true){
            		Message message = new Message();
                    message.what = MSG_DATA_CHANGE;
                    try {
                        sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    message.arg2 =new Random().nextInt(5000);
                    mHandler.sendMessage(message);
                	
            	}
            }

            ;
        }.start();
    }

画波形图的核心代码都已经给出还有一些无关紧要代码大家可以下载我的demo参考,项目已经托管到github    ECGViewDemo.有什么不对或者需要优化的地方还望大牛指出。

终于写完了 ,弄这个该死的github 弄到凌晨两点。









  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
自定义控件Android开发中常见的任务之一。下面是一步一步教你如何自定义控件的简要指南: 第一步:创建一个新的Java类作为你的自定义控件。 首先,创建一个新的Java类,可以命名为你想要的控件名称。这个类应该继承自Android框架中的现有控件,例如View、TextView等。例如,如果你想要创建一个自定义按钮,可以创建一个名为CustomButton的类,并让它继承自Button类。 第二步:实现构造函数和属性。 在你的自定义控件类中,你可以实现构造函数和属性,以便对控件进行初始化和设置。你可以定义自己的属性,例如颜色、大小等,以及相应的getter和setter方法。 第三步:重写绘制方法。 要自定义控件的外观,你需要重写它的绘制方法。最常用的方法是重写`onDraw()`方法,在其中使用Canvas绘制你想要的形状、文本等。 第四步:处理用户交互。 如果你的自定义控件需要与用户进行交互,你可以重写相应的触摸事件(例如`onTouchEvent()`)或点击事件(例如`setOnClickListener()`)来处理用户操作。 第五步:在布局文件中使用自定义控件。 完成以上步骤后,你可以在布局文件中使用你的自定义控件了。只需在布局文件中添加一个与你的控件类名相对应的XML标签,并设置相应的属性。 这只是一个简要的指南,帮助你开始自定义控件的过程。在实际开发中,你可能需要更多的步骤和细节来完成你的自定义控件。你可以参考Android官方文档或其他教程来获取更多信息和示例代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值