SDK_APP/DynamicChartView_backup.kt

602 lines
20 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
)
}
}