/** * 自然写互动课堂电视端应用软件 V1.0 * 多学生同屏对比视图 - 选取学生笔迹并排大屏展示 * * 功能说明: * 1. 多学生笔迹同屏对比展示(2/4/6/9宫格布局) * 2. 学生选择器(从在线学生列表中选取展示对象) * 3. 实时笔迹同步更新(选中学生的笔迹实时追加) * 4. 笔迹回放对比(多学生同步回放书写过程) * 5. 学生信息叠加显示(姓名、座号、书写进度) * 6. 遥控器操作适配(D-Pad选择学生、切换布局) * 7. 范字参考叠加(可选显示标准字帖做对比参照) */ package com.writech.tv.renderer import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF import android.os.Handler import android.os.Looper import android.util.Log import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.ceil import kotlin.math.max import kotlin.math.min import kotlin.math.sqrt /** * 展示布局模式 */ enum class DisplayLayout(val columns: Int, val rows: Int) { SINGLE(1, 1), // 单人全屏 DUAL(2, 1), // 双人并排 QUAD(2, 2), // 四宫格 SIX(3, 2), // 六宫格 NINE(3, 3); // 九宫格 val cellCount: Int get() = columns * rows } /** * 学生展示信息 */ data class StudentDisplayInfo( val studentId: String, val studentName: String, val seatNumber: Int, val color: Int, // 分配的标识颜色 var strokeCount: Int = 0, // 已书写笔画数 var isWriting: Boolean = false, // 是否正在书写 var lastUpdateTime: Long = 0 // 最后更新时间 ) /** * 多学生同屏对比视图管理器 * 管理宫格布局中每个单元格的笔迹渲染 */ class MultiStudentView { companion object { private const val TAG = "MultiStudentView" /** 单元格间距(像素) */ private const val CELL_PADDING = 8 /** 标签栏高度(像素) */ private const val LABEL_HEIGHT = 48 /** 标签文字大小(像素) */ private const val LABEL_TEXT_SIZE = 24f /** 边框宽度(像素) */ private const val BORDER_WIDTH = 3f /** 正在书写的边框闪烁间隔(毫秒) */ private const val BLINK_INTERVAL = 500L } /** 当前布局模式 */ var layout: DisplayLayout = DisplayLayout.QUAD private set /** 展示的学生列表(按单元格位置排列) */ private val displayStudents = CopyOnWriteArrayList() /** 每个学生对应的笔迹数据 */ private val studentStrokes = ConcurrentHashMap>() /** 主线程Handler */ private val mainHandler = Handler(Looper.getMainLooper()) /** 绘制用Paint对象 */ private val borderPaint = Paint().apply { style = Paint.Style.STROKE strokeWidth = BORDER_WIDTH isAntiAlias = true } private val labelBgPaint = Paint().apply { style = Paint.Style.FILL color = Color.parseColor("#E0E0E0") } private val labelTextPaint = Paint().apply { color = Color.parseColor("#333333") textSize = LABEL_TEXT_SIZE isAntiAlias = true textAlign = Paint.Align.LEFT } private val writingIndicatorPaint = Paint().apply { color = Color.parseColor("#4CAF50") style = Paint.Style.FILL } private val strokePaint = Paint().apply { isAntiAlias = true style = Paint.Style.STROKE strokeCap = Paint.Cap.ROUND strokeJoin = Paint.Join.ROUND } /** 是否显示范字参考 */ var showReference: Boolean = false /** 范字图片路径 */ var referencePath: String = "" /** 当前选中的单元格索引(遥控器焦点) */ var selectedCellIndex: Int = -1 /** * 切换布局模式 */ fun setLayout(newLayout: DisplayLayout) { layout = newLayout // 如果学生数超过新布局的容量,截断显示 while (displayStudents.size > layout.cellCount) { val removed = displayStudents.removeAt(displayStudents.size - 1) studentStrokes.remove(removed.studentId) } Log.i(TAG, "布局切换为: ${newLayout.name} (${newLayout.columns}x${newLayout.rows})") } /** * 添加学生到展示区 * @return 分配的单元格索引,-1表示已满 */ fun addStudent(info: StudentDisplayInfo): Int { if (displayStudents.size >= layout.cellCount) { Log.w(TAG, "展示区已满 (${layout.cellCount}个)") return -1 } // 分配颜色 val coloredInfo = info.copy( color = StudentColorPalette.getColor(displayStudents.size) ) displayStudents.add(coloredInfo) studentStrokes[info.studentId] = mutableListOf() val index = displayStudents.size - 1 Log.i(TAG, "添加学生: ${info.studentName} -> 单元格$index") return index } /** * 移除学生 */ fun removeStudent(studentId: String) { displayStudents.removeAll { it.studentId == studentId } studentStrokes.remove(studentId) Log.i(TAG, "移除学生: $studentId") } /** * 添加笔迹数据到指定学生 */ fun addStroke(studentId: String, stroke: Stroke) { studentStrokes[studentId]?.add(stroke) displayStudents.find { it.studentId == studentId }?.let { it.strokeCount++ it.lastUpdateTime = System.currentTimeMillis() } } /** * 更新学生书写状态 */ fun updateWritingState(studentId: String, isWriting: Boolean) { displayStudents.find { it.studentId == studentId }?.isWriting = isWriting } /** * 在Canvas上绘制多学生对比视图 * @param canvas 目标画布 * @param width 画布总宽度 * @param height 画布总高度 */ fun draw(canvas: Canvas, width: Int, height: Int) { val cols = layout.columns val rows = layout.rows // 计算每个单元格的尺寸 val cellWidth = (width - CELL_PADDING * (cols + 1)) / cols val cellHeight = (height - CELL_PADDING * (rows + 1)) / rows for (index in 0 until min(displayStudents.size, layout.cellCount)) { val student = displayStudents[index] val col = index % cols val row = index / cols // 计算单元格位置 val left = CELL_PADDING + col * (cellWidth + CELL_PADDING) val top = CELL_PADDING + row * (cellHeight + CELL_PADDING) val cellRect = RectF( left.toFloat(), top.toFloat(), (left + cellWidth).toFloat(), (top + cellHeight).toFloat() ) // 绘制单元格内容 drawCell(canvas, cellRect, student, index) } } /** * 绘制单个单元格 */ private fun drawCell(canvas: Canvas, rect: RectF, student: StudentDisplayInfo, index: Int) { // 绘制单元格背景 val bgPaint = Paint().apply { color = Color.WHITE style = Paint.Style.FILL } canvas.drawRoundRect(rect, 8f, 8f, bgPaint) // 绘制边框(选中的单元格用高亮边框) borderPaint.color = if (index == selectedCellIndex) { Color.parseColor("#2196F3") // 选中态蓝色 } else if (student.isWriting) { student.color // 书写中用学生颜色 } else { Color.parseColor("#BDBDBD") // 默认灰色 } borderPaint.strokeWidth = if (index == selectedCellIndex) 5f else BORDER_WIDTH canvas.drawRoundRect(rect, 8f, 8f, borderPaint) // 绘制标签栏(学生姓名 + 座号 + 书写状态) val labelRect = RectF(rect.left, rect.top, rect.right, rect.top + LABEL_HEIGHT) labelBgPaint.color = Color.argb(230, Color.red(student.color), Color.green(student.color), Color.blue(student.color)) canvas.drawRoundRect( RectF(labelRect.left + 1, labelRect.top + 1, labelRect.right - 1, labelRect.bottom), 8f, 0f, labelBgPaint ) // 绘制学生姓名 labelTextPaint.color = Color.WHITE labelTextPaint.textSize = LABEL_TEXT_SIZE canvas.drawText( "${student.seatNumber}号 ${student.studentName}", rect.left + 12f, rect.top + LABEL_HEIGHT - 14f, labelTextPaint ) // 绘制书写状态指示点(绿色=正在书写) if (student.isWriting) { canvas.drawCircle( rect.right - 20f, rect.top + LABEL_HEIGHT / 2f, 6f, writingIndicatorPaint ) } // 绘制笔迹内容区域 val contentRect = RectF( rect.left + 4f, rect.top + LABEL_HEIGHT + 4f, rect.right - 4f, rect.bottom - 4f ) canvas.save() canvas.clipRect(contentRect) // 计算笔迹缩放(将点阵纸坐标映射到单元格内容区域) val scaleX = contentRect.width() / 200f // 假设点阵纸宽200mm val scaleY = contentRect.height() / 280f // 假设点阵纸高280mm val scale = min(scaleX, scaleY) canvas.translate(contentRect.left, contentRect.top) canvas.scale(scale, scale) // 绘制该学生的所有笔迹 val strokes = studentStrokes[student.studentId] ?: emptyList() for (stroke in strokes) { drawStroke(canvas, stroke, student.color) } canvas.restore() // 绘制笔画计数 val countText = "${student.strokeCount}笔" labelTextPaint.color = Color.GRAY labelTextPaint.textSize = 18f canvas.drawText(countText, rect.right - 60f, rect.bottom - 8f, labelTextPaint) } /** * 绘制单个笔画 */ private fun drawStroke(canvas: Canvas, stroke: Stroke, color: Int) { if (stroke.points.size < 2) return strokePaint.color = color strokePaint.strokeWidth = stroke.baseWidth for (i in 1 until stroke.points.size) { val prev = stroke.points[i - 1] val curr = stroke.points[i] canvas.drawLine(prev.x, prev.y, curr.x, curr.y, strokePaint) } } /** * 遥控器方向键导航(移动焦点到相邻单元格) */ fun navigateFocus(direction: Int): Boolean { val cols = layout.columns val totalCells = min(displayStudents.size, layout.cellCount) if (totalCells == 0) return false when (direction) { 0 -> selectedCellIndex = max(0, selectedCellIndex - cols) // 上 1 -> selectedCellIndex = min(totalCells - 1, selectedCellIndex + cols) // 下 2 -> selectedCellIndex = max(0, selectedCellIndex - 1) // 左 3 -> selectedCellIndex = min(totalCells - 1, selectedCellIndex + 1) // 右 } return true } /** 清除所有展示数据 */ fun clearAll() { displayStudents.clear() studentStrokes.clear() selectedCellIndex = -1 } /** 获取当前展示的学生数量 */ fun getDisplayCount(): Int = displayStudents.size /** 释放资源 */ fun release() { clearAll() Log.i(TAG, "多学生视图已释放") } }