602 lines
20 KiB
Kotlin
602 lines
20 KiB
Kotlin
|
|
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<Float>()
|
|||
|
|
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<Float>()
|
|||
|
|
|
|||
|
|
// 多通道数据支持(用于PPG等设备)
|
|||
|
|
private val multiChannelDataPoints = mutableListOf<MutableList<Float>>()
|
|||
|
|
private val multiChannelDataBuffers = mutableListOf<MutableList<Float>>()
|
|||
|
|
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<Float>) {
|
|||
|
|
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<List<Float>>) {
|
|||
|
|
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<Float>) {
|
|||
|
|
// 使用全部数据点计算范围,提供更准确的范围
|
|||
|
|
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<Float> {
|
|||
|
|
return dataPoints.toList()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取数据统计信息
|
|||
|
|
*/
|
|||
|
|
fun getDataStats(): Map<String, Any> {
|
|||
|
|
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
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|