这个是曲线不动,画笔动,有点复杂,想了很久

后一根线覆盖前一根线,循环

Android 自定义View 心电图

View的代码:

/**
 * 血氧仪心率View
 * @author cym
 */
class PlethView(mContext: Context, mAttrs: AttributeSet?) : View(mContext, mAttrs) {
    // 画笔
    private val mPaint = Paint()

    // 圆角画布Path
    private val mCanvasPath = Path()
    // 心率画布Path
    private val mNewCanvasPath = Path()
    private val mOldCanvasPath = Path()

    // 两条心率曲线
    private val mOldPath = Path()
    private val mNewPath = Path()

    // 颜色
    private val mColorBlack = ContextCompat.getColor(mContext, R.color.bleBloodOxygenColorBlack)
    private val mColorLine = ContextCompat.getColor(mContext, R.color.bleBloodOxygenColorShadowLine)
    private val mColorBlue = ContextCompat.getColor(mContext, R.color.bleBloodOxygenColorBlue)

    // 顶部和底部
    private var mTop = 0F
    private var mBottom = 0F

    // 背景线每格间隔
    private val mLineWeight = dp2px(10F)

    // 每隔多久刷新一次
    private val mRefreshStamp = 100L

    // 每个屏幕最多显示多少ms的数据
    private val mMaxStamp = 5000L

    // 心率数据类
    private data class HeartData(
        var heart: Float = 0F,// 心率百分比 0F ~ 1F ,我也不知道数据长什么样
        var stamp: Long = 0L// 心率的时间戳
    )

    // 心率数据列表
    private var mList = ArrayList<HeartData>()

    // 开始测量的时间
    private var mStartStamp = 0L

    init {
        mPaint.isAntiAlias = true
        mPaint.typeface = Typeface.DEFAULT_BOLD
        mPaint.textSize = dp2px(12F)
        mPaint.textAlign = Paint.Align.LEFT

        // 循环添加数据
        startTest(System.currentTimeMillis())
        Thread {
            var step = 0// 伪装成心跳。0,1:平;2:高;3:低
            while (true) {
                val heart: Float = when (step) {
                    0 -> {
                        step = 1
                        0.5F
                    }
                    1 -> {
                        step = 2
                        0.5F
                    }
                    2 -> {
                        step = 3
                        (80..100).random() / 100F
                    }
                    3 -> {
                        step = 0
                        (15..25).random() / 100F
                    }
                    else -> {
                        // 我觉得不应该
                        step = 0
                        0F
                    }
                }
                addHeart(heart, System.currentTimeMillis())
                if (step == 1) {
                    Thread.sleep(500)
                } else {
                    Thread.sleep(100)
                }
            }
        }.start()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        // 初始化圆角画布Path
        val roundRadius = dp2px(12.5F)
        mCanvasPath.reset()
        mCanvasPath.moveTo(roundRadius, 0F)
        mCanvasPath.lineTo(w - roundRadius, 0F)
        mCanvasPath.quadTo(w.toFloat(), 0F, w.toFloat(), roundRadius)
        mCanvasPath.lineTo(w.toFloat(), h - roundRadius)
        mCanvasPath.quadTo(w.toFloat(), h.toFloat(), w - roundRadius, h.toFloat())
        mCanvasPath.lineTo(roundRadius, h.toFloat())
        mCanvasPath.quadTo(0F, h.toFloat(), 0F, h - roundRadius)
        mCanvasPath.lineTo(0F, roundRadius)
        mCanvasPath.quadTo(0F, 0F, roundRadius, 0F)
        // 设置曲线可以绘制的区域
        mTop = h * 0.2F
        mBottom = h * 0.8F
    }

    override fun onDraw(canvas: Canvas) {
        // 因为背景是圆角的,所以裁剪画布,使绘制内容不超过圆角
        canvas.save()
        canvas.clipPath(mCanvasPath)

        // 画背景线
        mPaint.color = mColorLine
        mPaint.strokeWidth = 1F
        mPaint.style = Paint.Style.FILL
        // 画背景线竖线
        val yLineCount = (width / mLineWeight).toInt()
        for (i in 0..yLineCount) {
            canvas.drawLine(
                i * mLineWeight - mLineWeight * 0.2F,
                0F,
                i * mLineWeight - mLineWeight * 0.2F,
                height.toFloat(),
                mPaint
            )
        }
        // 画背景线横线
        val xLineCount = (height / mLineWeight).toInt()
        for (i in 0..xLineCount) {
            canvas.drawLine(
                0F,
                i * mLineWeight - mLineWeight * 0.5F,
                width.toFloat(),
                i * mLineWeight - mLineWeight * 0.5F,
                mPaint
            )
        }

        // 画左上角文本
        mPaint.color = mColorBlack
        canvas.drawText("Pleth", dp2px(8F), dp2px(14F), mPaint)

        canvas.restore()

        // 画曲线
        mPaint.color = mColorBlue
        mPaint.strokeWidth = dp2px(1.5F)
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.style = Paint.Style.STROKE
        // 从左向右画,如果画到顶了,之后的覆盖上一条
        // 线只画在中间,不会超出最大最小值
        if (mStartStamp > 0 && mList.size > 0) {
            // 最多显示两个屏幕长的数据
            var startIndex = 0
            for (i in mList.size - 1 downTo 0) {
                if (mList[i].stamp > System.currentTimeMillis() - mMaxStamp - 1000L) {
                    startIndex = i
                } else {
                    break
                }
            }
            // 当前时间向前推一些,不然画的线会不流畅。因为数据还没来(最好推数据列表时间来的间隔)
            val curStamp = System.currentTimeMillis() - 500L
            // 当前时间在中间走,左右两边时间也要跟着变
            val newLeftStamp = curStamp - (curStamp - mStartStamp) % mMaxStamp
            val newRightStamp = newLeftStamp + mMaxStamp
            val oldRightStamp = newLeftStamp
            val oldLeftStamp = oldRightStamp - mMaxStamp

            // 画数据曲线
            mNewPath.reset()
            mOldPath.reset()
            var mIsFirstNew = true
            var mIsFirstOld = true
            for (i in startIndex until mList.size) {
                val data = mList[i]
                val y = mBottom - (mBottom - mTop) * data.heart
                // 新曲线
                val xNew = (data.stamp - newLeftStamp) * 1.0F / (newRightStamp - newLeftStamp) * width
                if (mIsFirstNew) {
                    mNewPath.moveTo(xNew, y)
                    mIsFirstNew = false
                } else {
                    mNewPath.lineTo(xNew, y)
                }
                // 旧曲线
                if (data.stamp > mMaxStamp) {
                    val xOld = (data.stamp - oldLeftStamp) * 1.0F / (oldRightStamp - oldLeftStamp) * width
                    if (mIsFirstOld) {
                        mOldPath.moveTo(xOld, y)
                        mIsFirstOld = false
                    } else {
                        mOldPath.lineTo(xOld, y)
                    }
                }
            }

            // 裁剪画布
            val padding = dp2px(3F)
            val curX = (curStamp - newLeftStamp) * 1.0F / (newRightStamp - newLeftStamp) * (width + padding)
            // 新的只能画在当前时间左边
            mNewCanvasPath.reset()
            mNewCanvasPath.moveTo(curX - padding, 0F)
            mNewCanvasPath.lineTo(curX - padding, height.toFloat())
            mNewCanvasPath.lineTo(0F, height.toFloat())
            mNewCanvasPath.lineTo(0F, 0F)
            canvas.save()
            canvas.clipPath(mNewCanvasPath)
            canvas.drawPath(mNewPath, mPaint)
            canvas.restore()
            // 旧的只能画在当前时间右边
            // 并且第一个时间段不绘制旧的数据。不然一开始绘制,最右边会有一个小点
            if (curStamp - mStartStamp > mMaxStamp) {
                mOldCanvasPath.reset()
                mOldCanvasPath.moveTo(curX + padding, 0F)
                mOldCanvasPath.lineTo(curX + padding, height.toFloat())
                mOldCanvasPath.lineTo(width.toFloat(), height.toFloat())
                mOldCanvasPath.lineTo(width.toFloat(), 0F)
                canvas.save()
                canvas.clipPath(mOldCanvasPath)
                canvas.drawPath(mOldPath, mPaint)
                canvas.restore()
            }
        }

        // 不断重绘
        invalidate()
    }

    /**
     * 添加一条心率值
     */
    fun addHeart(heart: Float, stamp: Long) {
        mList.add(HeartData(heart, stamp))
    }

    /**
     * 开始测试
     */
    fun startTest(startStamp: Long) {
        mStartStamp = startStamp
    }

    /**
     * dp转px
     * @param dpValue dp
     */
    private fun dp2px(dpValue: Float): Float {
        val scale = resources.displayMetrics.density
        return dpValue * scale + 0.5f
    }
}