software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,415 @@
/*
* 自然写互动课堂应用开发SDK软件 V1.0
* StrokeCanvas - Android端笔迹渲染自定义View
*
* 功能说明:
* 1. 实时笔迹渲染(贝塞尔曲线平滑绘制)
* 2. 压力感应笔锋效果(根据压力值动态调整线宽)
* 3. 多笔同屏渲染(不同颜色区分不同学生)
* 4. 笔迹重播动画(按时间序列回放书写过程)
* 5. 离屏缓冲双缓冲渲染(避免闪烁)
* 6. 触摸与点阵笔混合输入支持
*/
package com.writech.sdk.android;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 笔迹渲染画布组件
* 支持实时绘制点阵笔和触摸屏输入的笔迹数据
*/
public class StrokeCanvas extends View {
private static final String TAG = "WritechStrokeCanvas";
/* 默认画笔颜色 */
private static final int DEFAULT_STROKE_COLOR = Color.BLACK;
/* 默认最小线宽(像素) */
private static final float MIN_STROKE_WIDTH = 1.5f;
/* 默认最大线宽(像素) */
private static final float MAX_STROKE_WIDTH = 8.0f;
/* 最大压力值(点阵笔12位ADC) */
private static final float MAX_PRESSURE = 4095.0f;
/* ========== 内部数据结构 ========== */
/** 单个采样点(包含坐标、压力、时间戳) */
private static class StrokePoint {
float x;
float y;
float pressure; /* 归一化压力 0.0~1.0 */
long timestamp; /* 毫秒时间戳 */
StrokePoint(float x, float y, float pressure, long timestamp) {
this.x = x;
this.y = y;
this.pressure = pressure;
this.timestamp = timestamp;
}
}
/** 一笔数据(从落笔到抬笔) */
private static class Stroke {
String penMac; /* 来源笔MAC地址 */
int color; /* 笔迹颜色 */
List<StrokePoint> points; /* 采样点列表 */
Stroke(String penMac, int color) {
this.penMac = penMac;
this.color = color;
this.points = new ArrayList<>();
}
}
/* ========== 成员变量 ========== */
/* 离屏缓冲Bitmap(双缓冲渲染) */
private Bitmap mBufferBitmap;
private Canvas mBufferCanvas;
/* 绘制画笔 */
private final Paint mStrokePaint;
/* 背景清除画笔 */
private final Paint mClearPaint;
/* 已完成的笔画列表(历史记录) */
private final List<Stroke> mCompletedStrokes = new ArrayList<>();
/* 当前正在书写的笔画(按笔MAC索引) */
private final Map<String, Stroke> mActiveStrokes = new HashMap<>();
/* 每支笔的颜色映射 */
private final Map<String, Integer> mPenColorMap = new HashMap<>();
/* 笔迹颜色分配计数器 */
private int mColorIndex = 0;
/* 预定义的笔迹颜色列表(用于多学生区分) */
private static final int[] STROKE_COLORS = {
Color.BLACK,
Color.parseColor("#1565C0"), /* 蓝色 */
Color.parseColor("#C62828"), /* 红色 */
Color.parseColor("#2E7D32"), /* 绿色 */
Color.parseColor("#E65100"), /* 橙色 */
Color.parseColor("#6A1B9A"), /* 紫色 */
Color.parseColor("#00838F"), /* 青色 */
Color.parseColor("#4E342E"), /* 棕色 */
};
/* 是否启用压力感应笔锋 */
private boolean mPressureEnabled = true;
/* 笔迹重播相关 */
private boolean mIsReplaying = false;
private int mReplayStrokeIndex = 0;
private int mReplayPointIndex = 0;
private long mReplayStartTime = 0;
/* ========== 构造函数 ========== */
public StrokeCanvas(Context context) {
this(context, null);
}
public StrokeCanvas(Context context, AttributeSet attrs) {
super(context, attrs);
/* 初始化笔迹画笔 */
mStrokePaint = new Paint();
mStrokePaint.setAntiAlias(true); /* 抗锯齿 */
mStrokePaint.setDither(true); /* 防抖动 */
mStrokePaint.setStyle(Paint.Style.STROKE);
mStrokePaint.setStrokeJoin(Paint.Join.ROUND); /* 圆角连接 */
mStrokePaint.setStrokeCap(Paint.Cap.ROUND); /* 圆头笔触 */
/* 初始化清除画笔 */
mClearPaint = new Paint();
mClearPaint.setColor(Color.WHITE);
}
/* ========== View生命周期 ========== */
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
/* 创建离屏缓冲Bitmap */
if (mBufferBitmap != null) {
mBufferBitmap.recycle();
}
mBufferBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
mBufferCanvas = new Canvas(mBufferBitmap);
mBufferCanvas.drawColor(Color.WHITE);
/* 重绘所有历史笔画到缓冲区 */
redrawAllStrokes();
}
@Override
protected void onDraw(Canvas canvas) {
/* 将离屏缓冲Bitmap绘制到屏幕 */
if (mBufferBitmap != null) {
canvas.drawBitmap(mBufferBitmap, 0, 0, null);
}
/* 绘制当前活跃的笔画(实时部分) */
for (Stroke stroke : mActiveStrokes.values()) {
drawStrokeRealtime(canvas, stroke);
}
}
/* ========== 点阵笔数据输入接口 ========== */
/**
* 接收笔落下事件(开始新的一笔)
* @param penMac 笔设备MAC地址
*/
public void onPenDown(String penMac) {
int color = getPenColor(penMac);
Stroke stroke = new Stroke(penMac, color);
mActiveStrokes.put(penMac, stroke);
}
/**
* 接收笔迹坐标数据
* @param penMac 笔MAC
* @param screenX 屏幕X坐标(已经过坐标变换)
* @param screenY 屏幕Y坐标
* @param pressure 原始压力值(0-4095
*/
public void onStrokePoint(String penMac, float screenX, float screenY,
int pressure) {
Stroke stroke = mActiveStrokes.get(penMac);
if (stroke == null) {
/* 如果没有活跃笔画,自动创建 */
onPenDown(penMac);
stroke = mActiveStrokes.get(penMac);
}
/* 归一化压力值 */
float normalizedPressure = Math.min(1.0f, (float) pressure / MAX_PRESSURE);
long timestamp = SystemClock.elapsedRealtime();
stroke.points.add(new StrokePoint(screenX, screenY, normalizedPressure, timestamp));
/* 触发重绘(仅绘制增量部分,避免全量刷新) */
int pointCount = stroke.points.size();
if (pointCount >= 2) {
StrokePoint prev = stroke.points.get(pointCount - 2);
StrokePoint curr = stroke.points.get(pointCount - 1);
/* 仅刷新受影响的矩形区域(性能优化) */
float padding = MAX_STROKE_WIDTH + 2;
float left = Math.min(prev.x, curr.x) - padding;
float top = Math.min(prev.y, curr.y) - padding;
float right = Math.max(prev.x, curr.x) + padding;
float bottom = Math.max(prev.y, curr.y) + padding;
invalidate((int) left, (int) top, (int) right, (int) bottom);
}
}
/**
* 接收笔抬起事件(一笔结束)
* 将当前笔画固化到缓冲区并归档
*/
public void onPenUp(String penMac) {
Stroke stroke = mActiveStrokes.remove(penMac);
if (stroke != null && stroke.points.size() > 1) {
/* 绘制到离屏缓冲区(固化) */
drawStrokeToBuffer(stroke);
/* 添加到已完成列表 */
mCompletedStrokes.add(stroke);
}
invalidate();
}
/* ========== 笔迹渲染核心算法 ========== */
/**
* 实时渲染笔画(使用贝塞尔曲线平滑)
* 在每次onDraw中调用,绘制当前活跃的笔画
*/
private void drawStrokeRealtime(Canvas canvas, Stroke stroke) {
List<StrokePoint> points = stroke.points;
if (points.size() < 2) return;
mStrokePaint.setColor(stroke.color);
for (int i = 1; i < points.size(); i++) {
StrokePoint p0 = points.get(i - 1);
StrokePoint p1 = points.get(i);
/* 根据压力计算线宽 */
float width = calculateStrokeWidth(p0.pressure, p1.pressure);
mStrokePaint.setStrokeWidth(width);
if (i >= 2) {
/* 使用二次贝塞尔曲线平滑绘制 */
StrokePoint pPrev = points.get(i - 2);
float midX0 = (pPrev.x + p0.x) / 2;
float midY0 = (pPrev.y + p0.y) / 2;
float midX1 = (p0.x + p1.x) / 2;
float midY1 = (p0.y + p1.y) / 2;
Path path = new Path();
path.moveTo(midX0, midY0);
path.quadTo(p0.x, p0.y, midX1, midY1);
canvas.drawPath(path, mStrokePaint);
} else {
/* 前两个点直接画直线 */
canvas.drawLine(p0.x, p0.y, p1.x, p1.y, mStrokePaint);
}
}
}
/**
* 将完成的笔画绘制到离屏缓冲区
*/
private void drawStrokeToBuffer(Stroke stroke) {
if (mBufferCanvas == null) return;
drawStrokeRealtime(mBufferCanvas, stroke);
}
/**
* 根据压力值计算线宽(笔锋效果)
* 使用两个相邻点的平均压力,平滑过渡
*
* @param pressure0 前一点压力(归一化)
* @param pressure1 当前点压力(归一化)
* @return 线宽(像素)
*/
private float calculateStrokeWidth(float pressure0, float pressure1) {
if (!mPressureEnabled) {
return (MIN_STROKE_WIDTH + MAX_STROKE_WIDTH) / 2;
}
float avgPressure = (pressure0 + pressure1) / 2.0f;
/* 压力-宽度映射曲线(使用幂函数增加笔锋感) */
float normalized = (float) Math.pow(avgPressure, 0.7);
return MIN_STROKE_WIDTH + normalized * (MAX_STROKE_WIDTH - MIN_STROKE_WIDTH);
}
/* ========== 多笔颜色管理 ========== */
/** 获取或分配笔的颜色 */
private int getPenColor(String penMac) {
Integer color = mPenColorMap.get(penMac);
if (color == null) {
color = STROKE_COLORS[mColorIndex % STROKE_COLORS.length];
mPenColorMap.put(penMac, color);
mColorIndex++;
}
return color;
}
/** 手动设置某支笔的颜色 */
public void setPenColor(String penMac, int color) {
mPenColorMap.put(penMac, color);
}
/* ========== 画布操作 ========== */
/** 清除所有笔迹 */
public void clearAll() {
mCompletedStrokes.clear();
mActiveStrokes.clear();
if (mBufferCanvas != null) {
mBufferCanvas.drawColor(Color.WHITE);
}
invalidate();
}
/** 撤销最后一笔 */
public boolean undo() {
if (mCompletedStrokes.isEmpty()) return false;
mCompletedStrokes.remove(mCompletedStrokes.size() - 1);
redrawAllStrokes();
invalidate();
return true;
}
/** 重绘所有历史笔画到缓冲区 */
private void redrawAllStrokes() {
if (mBufferCanvas == null) return;
mBufferCanvas.drawColor(Color.WHITE);
for (Stroke stroke : mCompletedStrokes) {
drawStrokeToBuffer(stroke);
}
}
/** 导出当前画布为Bitmap */
public Bitmap exportBitmap() {
Bitmap export = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas exportCanvas = new Canvas(export);
draw(exportCanvas);
return export;
}
/** 获取已完成的笔画数量 */
public int getStrokeCount() {
return mCompletedStrokes.size();
}
/** 设置是否启用压力笔锋效果 */
public void setPressureEnabled(boolean enabled) {
mPressureEnabled = enabled;
}
/* ========== 触摸屏输入支持 ========== */
@Override
public boolean onTouchEvent(MotionEvent event) {
/* 使用"touch"作为虚拟笔MAC */
String touchMac = "touch_input";
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onPenDown(touchMac);
onStrokePoint(touchMac, event.getX(), event.getY(),
(int)(event.getPressure() * MAX_PRESSURE));
return true;
case MotionEvent.ACTION_MOVE:
/* 处理历史点(Android会批量发送MOVE事件) */
for (int i = 0; i < event.getHistorySize(); i++) {
onStrokePoint(touchMac,
event.getHistoricalX(i),
event.getHistoricalY(i),
(int)(event.getHistoricalPressure(i) * MAX_PRESSURE));
}
onStrokePoint(touchMac, event.getX(), event.getY(),
(int)(event.getPressure() * MAX_PRESSURE));
return true;
case MotionEvent.ACTION_UP:
onStrokePoint(touchMac, event.getX(), event.getY(),
(int)(event.getPressure() * MAX_PRESSURE));
onPenUp(touchMac);
return true;
}
return super.onTouchEvent(event);
}
}