software copyright
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user