359 lines
11 KiB
Kotlin
359 lines
11 KiB
Kotlin
/**
|
|
* 自然写互动课堂电视端应用软件 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<StudentDisplayInfo>()
|
|
|
|
/** 每个学生对应的笔迹数据 */
|
|
private val studentStrokes = ConcurrentHashMap<String, MutableList<Stroke>>()
|
|
|
|
/** 主线程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, "多学生视图已释放")
|
|
}
|
|
}
|