package com.example.cmake_project_test import android.content.Context import android.graphics.* import android.util.AttributeSet import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ScaleGestureDetector import kotlin.math.max import kotlin.math.min /** * 可调节的通用生理信号图表视图 * 支持多种设备类型和通道数量的动态显示,具有缩放、平移、时间窗口调节等功能 */ class DynamicChartView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val paint = Paint().apply { color = Color.BLUE strokeWidth = 2f style = Paint.Style.STROKE isAntiAlias = false // 关闭抗锯齿,提高绘制性能 } private val gridPaint = Paint().apply { color = Color.LTGRAY strokeWidth = 1f style = Paint.Style.STROKE alpha = 80 } private val textPaint = Paint().apply { color = Color.BLACK textSize = 20f isAntiAlias = true } private val controlPaint = Paint().apply { color = Color.RED strokeWidth = 3f style = Paint.Style.STROKE isAntiAlias = true } private val path = Path() private var dataPoints = mutableListOf() private var maxDataPoints = 1000 // 默认数据点数量 private var minValue = Float.MAX_VALUE private var maxValue = Float.MIN_VALUE private var isDataAvailable = false // 图表配置 private var chartTitle = "通道" private var channelIndex = 0 private var deviceType = "未知设备" private var sampleRate = 250f // 默认采样率 private var timeWindow = 4f // 默认时间窗口(秒) // 性能优化:真正的逐点绘制 private val dataBuffer = mutableListOf() // 多通道数据支持(用于PPG等设备) private val multiChannelDataPoints = mutableListOf>() private val multiChannelDataBuffers = mutableListOf>() private var isMultiChannelMode = false private val channelColors = listOf(Color.RED, Color.BLUE) // 通道颜色 private var lastUpdateTime = 0L private val updateInterval = 2L // 2ms更新间隔,实现500Hz显示 private var lastDrawTime = 0L private val drawInterval = 1L // 1ms绘制间隔,实现1000Hz绘制频率 // 交互控制参数 private var scaleFactor = 1.0f private var translateX = 0f private var translateY = 0f private var viewportStart = 0f private var viewportEnd = 1.0f private var isDragging = false private var lastTouchX = 0f private var lastTouchY = 0f // 手势检测器 private val scaleGestureDetector = ScaleGestureDetector(context, ScaleListener()) // 控制按钮区域 private val controlButtonRect = RectF() private val fullscreenButtonRect = RectF() // 全屏模式相关 private var isFullscreenMode = false private var originalLayoutParams: android.view.ViewGroup.LayoutParams? = null /** * 设置图表配置 */ fun setChartConfig( title: String, channelIdx: Int, device: String, maxPoints: Int, sampleRateHz: Float, timeWindowSec: Float ) { chartTitle = title channelIndex = channelIdx deviceType = device maxDataPoints = maxPoints sampleRate = sampleRateHz timeWindow = timeWindowSec resetViewport() invalidate() } /** * 设置多通道模式(用于PPG等设备) */ fun setMultiChannelMode(channelCount: Int) { isMultiChannelMode = true multiChannelDataPoints.clear() multiChannelDataBuffers.clear() // 初始化每个通道的数据结构 for (i in 0 until channelCount) { multiChannelDataPoints.add(mutableListOf()) multiChannelDataBuffers.add(mutableListOf()) } } fun updateData(newData: List) { if (newData.isEmpty()) return isDataAvailable = true // 批量添加到缓冲区 dataBuffer.addAll(newData) val currentTime = System.currentTimeMillis() // 批量处理 - 每次处理更多点,提高数据累积速度 if (currentTime - lastUpdateTime >= updateInterval) { if (dataBuffer.isNotEmpty()) { // 每次处理多个点,提高效率 val batchSize = minOf(10, dataBuffer.size) // 每次最多处理10个点 val batch = dataBuffer.take(batchSize) dataBuffer.removeAll(batch) dataPoints.addAll(batch) // 限制数据点数量 if (dataPoints.size > maxDataPoints) { dataPoints = dataPoints.takeLast(maxDataPoints).toMutableList() } // 更新数据范围 - 使用批量数据 updateDataRange(batch) lastUpdateTime = currentTime } } // 超高频绘制 if (currentTime - lastDrawTime >= drawInterval) { invalidate() lastDrawTime = currentTime } } /** * 更新多通道数据(用于PPG等设备) */ fun updateMultiChannelData(channelData: List>) { if (channelData.isEmpty() || !isMultiChannelMode) { Log.d("DynamicChartView", "跳过多通道更新: channelData.isEmpty=${channelData.isEmpty()}, isMultiChannelMode=$isMultiChannelMode") return } Log.d("DynamicChartView", "多通道数据更新: 通道数=${channelData.size}, 每个通道数据点=${channelData.map { it.size }}") isDataAvailable = true val currentTime = System.currentTimeMillis() // 更新每个通道的数据 for (channelIndex in channelData.indices) { if (channelIndex >= multiChannelDataBuffers.size) { Log.w("DynamicChartView", "通道索引超出范围: $channelIndex >= ${multiChannelDataBuffers.size}") continue } val channelBuffer = multiChannelDataBuffers[channelIndex] channelBuffer.addAll(channelData[channelIndex]) Log.d("DynamicChartView", "通道$channelIndex 添加了${channelData[channelIndex].size}个数据点") } // 批量处理数据 if (currentTime - lastUpdateTime >= updateInterval) { var hasDataToProcess = false for (channelIndex in multiChannelDataBuffers.indices) { val buffer = multiChannelDataBuffers[channelIndex] if (buffer.isNotEmpty()) { // 每次处理多个点 val batchSize = minOf(10, buffer.size) val batch = buffer.take(batchSize) buffer.removeAll(batch) multiChannelDataPoints[channelIndex].addAll(batch) // 限制数据点数量 if (multiChannelDataPoints[channelIndex].size > maxDataPoints) { multiChannelDataPoints[channelIndex] = multiChannelDataPoints[channelIndex].takeLast(maxDataPoints).toMutableList() } hasDataToProcess = true } } if (hasDataToProcess) { // 更新数据范围 - 计算所有通道的数据范围 updateMultiChannelDataRange() lastUpdateTime = currentTime Log.d("DynamicChartView", "多通道数据处理完成: 通道数据点=${multiChannelDataPoints.map { it.size }}, 范围=${minValue}-${maxValue}") } } // 超高频绘制 if (currentTime - lastDrawTime >= drawInterval) { invalidate() lastDrawTime = currentTime } } private fun updateDataRange(batch: List) { // 使用全部数据点计算范围,提供更准确的范围 if (dataPoints.isEmpty()) return // 计算全部数据点的范围 minValue = dataPoints.minOrNull() ?: 0f maxValue = dataPoints.maxOrNull() ?: 0f // 确保有足够的显示范围 val range = maxValue - minValue if (range < 0.1f) { val center = (maxValue + minValue) / 2 minValue = center - 0.05f maxValue = center + 0.05f } else { val margin = range * 0.1f minValue -= margin maxValue += margin } } /** * 更新多通道数据范围 */ private fun updateMultiChannelDataRange() { if (multiChannelDataPoints.isEmpty()) { Log.w("DynamicChartView", "多通道数据点为空,无法计算范围") return } // 计算所有通道的最小值和最大值 var globalMin = Float.MAX_VALUE var globalMax = Float.MIN_VALUE var hasValidData = false for (channelIndex in multiChannelDataPoints.indices) { val channelData = multiChannelDataPoints[channelIndex] if (channelData.isNotEmpty()) { val channelMin = channelData.minOrNull() ?: 0f val channelMax = channelData.maxOrNull() ?: 0f globalMin = minOf(globalMin, channelMin) globalMax = maxOf(globalMax, channelMax) hasValidData = true Log.d("DynamicChartView", "通道$channelIndex: 范围=${channelMin}-${channelMax}, 数据点=${channelData.size}") } } if (!hasValidData) { Log.w("DynamicChartView", "没有有效的多通道数据") return } minValue = globalMin maxValue = globalMax Log.d("DynamicChartView", "多通道全局范围: ${minValue}-${maxValue}") // 确保有足够的显示范围 val range = maxValue - minValue if (range < 0.1f) { val center = (maxValue + minValue) / 2 minValue = center - 0.05f maxValue = center + 0.05f } else { val margin = range * 0.1f minValue -= margin maxValue += margin } Log.d("DynamicChartView", "多通道显示范围: ${minValue}-${maxValue}") } fun clearData() { dataPoints.clear() dataBuffer.clear() // 清除多通道数据 for (channelData in multiChannelDataPoints) { channelData.clear() } for (channelBuffer in multiChannelDataBuffers) { channelBuffer.clear() } isDataAvailable = false resetViewport() invalidate() } /** * 重置视口到默认状态 */ fun resetViewport() { scaleFactor = 1.0f translateX = 0f translateY = 0f viewportStart = 0f viewportEnd = 1.0f invalidate() } override fun onTouchEvent(event: MotionEvent): Boolean { scaleGestureDetector.onTouchEvent(event) when (event.action) { MotionEvent.ACTION_DOWN -> { val x = event.x val y = event.y // 开始拖拽 isDragging = true lastTouchX = x lastTouchY = y return true } MotionEvent.ACTION_MOVE -> { if (isDragging) { val deltaX = event.x - lastTouchX val deltaY = event.y - lastTouchY translateX += deltaX translateY += deltaY // 限制平移范围 val maxTranslate = width * 0.5f translateX = translateX.coerceIn(-maxTranslate, maxTranslate) translateY = translateY.coerceIn(-maxTranslate, maxTranslate) lastTouchX = event.x lastTouchY = event.y invalidate() return true } } MotionEvent.ACTION_UP -> { isDragging = false return true } } return super.onTouchEvent(event) } private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { scaleFactor *= detector.scaleFactor scaleFactor = scaleFactor.coerceIn(0.2f, 5.0f) invalidate() return true } } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val width = width.toFloat() val height = height.toFloat() // 应用变换 canvas.save() canvas.translate(translateX, translateY) canvas.scale(scaleFactor, scaleFactor, width / 2, height / 2) // 绘制网格 drawGrid(canvas, width, height) // 绘制标题和通道信息 drawTitle(canvas, width, height) // 绘制数据统计 drawStats(canvas, width, height) // 绘制全屏按钮 drawFullscreenButton(canvas, width, height) // 如果没有数据,显示默认内容 if (!isDataAvailable) { drawDefaultContent(canvas, width, height) } else { // 绘制曲线 - 检查单通道或多通道数据 val hasDataToDraw = if (isMultiChannelMode) { multiChannelDataPoints.any { it.size > 1 } } else { dataPoints.size > 1 } if (hasDataToDraw) { drawCurve(canvas, width, height) } } canvas.restore() } private fun drawGrid(canvas: Canvas, width: Float, height: Float) { val padding = 60f val drawWidth = width - 2 * padding val drawHeight = height - 2 * padding // 绘制水平网格线 val gridLines = 5 for (i in 0..gridLines) { val y = padding + (drawHeight * i / gridLines) canvas.drawLine(padding, y, width - padding, y, gridPaint) } // 绘制垂直网格线 for (i in 0..gridLines) { val x = padding + (drawWidth * i / gridLines) canvas.drawLine(x, padding, x, height - padding, gridPaint) } } private fun drawTitle(canvas: Canvas, width: Float, height: Float) { val title = "$chartTitle $channelIndex" canvas.drawText(title, 10f, 25f, textPaint) val deviceInfo = "$deviceType (${sampleRate.toInt()}Hz)" canvas.drawText(deviceInfo, 10f, 50f, textPaint) // 显示当前缩放和时间窗口信息 val scaleInfo = "缩放: ${String.format("%.1f", scaleFactor)}x" canvas.drawText(scaleInfo, width - 150f, 25f, textPaint) val timeInfo = "时间窗口: ${timeWindow.toInt()}秒" canvas.drawText(timeInfo, width - 150f, 50f, textPaint) } private fun drawStats(canvas: Canvas, width: Float, height: Float) { val totalDataPoints = if (isMultiChannelMode) { multiChannelDataPoints.sumOf { it.size } } else { dataPoints.size } val statsText = if (isMultiChannelMode) { "数据点: ${totalDataPoints} (${multiChannelDataPoints.size}通道)" } else { "数据点: ${dataPoints.size}" } canvas.drawText(statsText, 10f, height - 35f, textPaint) if (totalDataPoints > 0) { val rangeText = "范围: ${String.format("%.2f", minValue)} - ${String.format("%.2f", maxValue)}" canvas.drawText(rangeText, 10f, height - 15f, textPaint) } } private fun drawDefaultContent(canvas: Canvas, width: Float, height: Float) { val centerX = width / 2 val centerY = height / 2 val hintPaint = Paint().apply { color = Color.GRAY textSize = 16f textAlign = Paint.Align.CENTER isAntiAlias = true } canvas.drawText("等待数据...", centerX, centerY - 20f, hintPaint) canvas.drawText("请先连接蓝牙设备", centerX, centerY + 20f, hintPaint) canvas.drawText("然后点击'启动程序'显示数据", centerX, centerY + 60f, hintPaint) } private fun drawCurve(canvas: Canvas, width: Float, height: Float) { val padding = 60f val drawWidth = width - 2 * padding val drawHeight = height - 2 * padding if (isMultiChannelMode && multiChannelDataPoints.isNotEmpty()) { Log.d("DynamicChartView", "多通道绘制: 通道数=${multiChannelDataPoints.size}, 数据点=${multiChannelDataPoints.map { it.size }}") // 多通道绘制模式 for (channelIndex in multiChannelDataPoints.indices) { val channelData = multiChannelDataPoints[channelIndex] if (channelData.isEmpty()) { Log.d("DynamicChartView", "通道$channelIndex 数据为空,跳过绘制") continue } path.reset() // 设置通道颜色 val channelPaint = Paint(paint).apply { color = channelColors.getOrElse(channelIndex) { Color.GRAY } strokeWidth = 2f } val xStep = if (channelData.size > 1) drawWidth / (channelData.size - 1) else drawWidth for (i in channelData.indices) { val x = padding + i * xStep val normalizedValue = (channelData[i] - minValue) / (maxValue - minValue) val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight if (i == 0) { path.moveTo(x, y) } else { path.lineTo(x, y) } } canvas.drawPath(path, channelPaint) } } else { // 单通道绘制模式 if (dataPoints.isEmpty()) return path.reset() val xStep = if (dataPoints.size > 1) drawWidth / (dataPoints.size - 1) else drawWidth for (i in dataPoints.indices) { val x = padding + i * xStep val normalizedValue = (dataPoints[i] - minValue) / (maxValue - minValue) val y = padding + (0.1f + normalizedValue * 0.8f) * drawHeight if (i == 0) { path.moveTo(x, y) } else { path.lineTo(x, y) } } canvas.drawPath(path, paint) } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val desiredHeight = 200 // 每个图表的高度 val height = resolveSize(desiredHeight, heightMeasureSpec) setMeasuredDimension(widthMeasureSpec, height) } /** * 获取当前图表数据 */ fun getCurrentData(): List { return dataPoints.toList() } /** * 获取数据统计信息 */ fun getDataStats(): Map { val totalDataPoints = if (isMultiChannelMode) { multiChannelDataPoints.sumOf { it.size } } else { dataPoints.size } return mapOf( "dataPoints" to totalDataPoints, "multiChannelMode" to isMultiChannelMode, "channelCount" to if (isMultiChannelMode) multiChannelDataPoints.size else 1, "minValue" to minValue, "maxValue" to maxValue, "range" to (maxValue - minValue), "isDataAvailable" to isDataAvailable, "scaleFactor" to scaleFactor, "timeWindow" to timeWindow ) } }