
基于HarmonyOS的手写数学公式计算器实现 原创
基于HarmonyOS的手写数学公式计算器实现
一、项目概述
本项目基于HarmonyOS的手写识别能力(@ohos.ai.handwriting)和分布式技术,实现一个支持多设备协作的手写数学公式计算器。参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》的技术方案,我们将实现以下功能:
手写数学公式识别
实时计算结果展示
多设备协同计算
计算历史同步
手写笔迹动画共享
!https://example.com/handwriting-calculator-arch.png
图1:手写公式计算器系统架构(包含手写识别、分布式同步和计算引擎)
二、核心实现代码
主页面与手写识别(ArkTS)
// 手写公式计算器主页面
@Entry
@Component
struct HandwritingCalcPage {
@State recognizedText: string = ‘’
@State result: string = ‘’
@State history: CalcHistory[] = []
@State isWriting: boolean = false
@State strokes: Point[][] = []
@State currentDevice: string = deviceInfo.deviceName
private hwEngine: HandwritingEngine = HandwritingEngine.getInstance()
private calcEngine: MathCalcEngine = new MathCalcEngine()
private distManager: DistCalcManager = DistCalcManager.getInstance()
aboutToAppear() {
this.loadHistory()
this.setupListeners()
build() {
Column() {
// 设备切换与模式选择
this.buildToolbar()
// 识别结果与计算结果
this.buildResultDisplay()
// 手写画布
this.buildHandwritingCanvas()
// 历史记录
this.buildHistoryList()
.padding(15)
@Builder
buildToolbar() {
Row({ space: 10 }) {
Text(this.currentDevice)
.onClick(() => this.showDevicePicker())
Button(this.isWriting ? '计算' : '手写')
.onClick(() => {
if (this.isWriting) {
this.recognizeStrokes()
else {
this.startNewWriting()
})
.width(‘100%’)
.margin({ bottom: 10 })
@Builder
buildResultDisplay() {
Column() {
Text(this.recognizedText)
.fontSize(20)
.fontColor(‘#1890FF’)
Divider()
Text(this.result ? = ${this.result} : '')
.fontSize(24)
.fontColor('#FF4D4F')
.margin({ top: 5 })
.width(‘100%’)
.padding(10)
.borderRadius(5)
.backgroundColor('#F5F5F5')
.margin({ bottom: 15 })
@Builder
buildHandwritingCanvas() {
Stack() {
// 背景网格
ForEach(Array.from({length: 20}), (_, i) => {
Line()
.width(‘100%’)
.height(1)
.backgroundColor(‘#E8E8E8’)
.position({ y: i * 30 })
})
// 手写笔迹
ForEach(this.strokes, (stroke, strokeIndex) => {
Path()
.commands(this.getPathCommands(stroke))
.strokeWidth(3)
.stroke(Color.Black)
.fill(Color.Transparent)
})
// 触摸区域
Rect()
.width('100%')
.height(200)
.backgroundColor(Color.Transparent)
.onTouch((event: TouchEvent) => {
this.handleTouch(event)
})
.height(200)
.width('100%')
.border({ width: 1, color: '#D9D9D9' })
.margin({ bottom: 15 })
@Builder
buildHistoryList() {
Column() {
Text(‘历史记录’)
.fontSize(16)
.margin({ bottom: 5 })
List() {
ForEach(this.history, (item, index) => {
ListItem() {
Column() {
Row() {
Text(item.formula)
.fontColor('#1890FF')
Text(= ${item.result})
.fontColor('#FF4D4F')
.margin({ left: 10 })
Text({item.device} · {item.time})
.fontSize(12)
.fontColor('#8C8C8C')
.margin({ top: 5 })
.onClick(() => this.useHistoryItem(item))
.swipeAction({ end: this.buildHistorySwipeAction(item) })
})
.height(200)
}
@Builder
buildHistorySwipeAction(item: CalcHistory) {
Button(‘删除’)
.backgroundColor(‘#FF4D4F’)
.width(80)
.onClick(() => this.deleteHistory(item.id))
// 获取路径绘制命令
private getPathCommands(points: Point[]): string {
if (points.length === 0) return ‘’
let commands = M{points[0].x},{points[0].y}
for (let i = 1; i < points.length; i++) {
commands += L{points[i].x},{points[i].y}
return commands
// 处理触摸事件
private handleTouch(event: TouchEvent) {
if (!this.isWriting) return
switch (event.type) {
case TouchType.Down:
this.strokes.push([])
break
case TouchType.Move:
if (this.strokes.length > 0) {
const lastStroke = this.strokes[this.strokes.length - 1]
lastStroke.push({
x: event.touches[0].x,
y: event.touches[0].y
})
this.strokes = [...this.strokes] // 触发更新
break
case TouchType.Up:
this.distManager.syncStroke(this.strokes[this.strokes.length - 1])
break
}
// 开始新的书写
private startNewWriting() {
this.isWriting = true
this.strokes = []
this.recognizedText = ‘’
this.result = ‘’
// 识别手写笔迹
private async recognizeStrokes() {
this.isWriting = false
try {
// 转换为识别引擎需要的格式
const points = this.strokes.map(stroke =>
stroke.map(p => [p.x, p.y])
)
// 调用手写识别
const texts = await this.hwEngine.recognize(points)
this.recognizedText = texts.join(' ').replace(/\s+/g, ' ')
// 计算并同步结果
this.calculateResult()
catch (err) {
prompt.showToast({ message: '识别失败,请重写' })
}
// 计算结果
private calculateResult() {
try {
this.result = this.calcEngine.evaluate(this.recognizedText)
this.saveToHistory()
this.distManager.syncCalculation({
formula: this.recognizedText,
result: this.result
})
catch (err) {
this.result = '计算错误'
}
// 保存到历史记录
private saveToHistory() {
const item: CalcHistory = {
id: this.generateId(),
formula: this.recognizedText,
result: this.result,
device: deviceInfo.deviceName,
time: new Date().toLocaleTimeString(),
strokes: […this.strokes]
this.history.unshift(item)
if (this.history.length > 50) {
this.history.pop()
this.saveLocalHistory()
// 使用历史记录项
private useHistoryItem(item: CalcHistory) {
this.recognizedText = item.formula
this.result = item.result
this.strokes = item.strokes
// 删除历史记录
private deleteHistory(id: string) {
this.history = this.history.filter(item => item.id !== id)
this.saveLocalHistory()
this.distManager.deleteHistory(id)
// 加载历史记录
private loadHistory() {
const history = preferences.get(‘calcHistory’)
if (history) {
this.history = JSON.parse(history)
}
// 保存历史记录
private saveLocalHistory() {
preferences.put(‘calcHistory’, JSON.stringify(this.history))
// 显示设备选择器
private showDevicePicker() {
const devices = this.distManager.getAvailableDevices()
picker.show({
options: devices.map(d => ({ value: d.id, text: d.name })),
onAccept: (value: string) => {
this.distManager.switchTargetDevice(value)
this.currentDevice = devices.find(d => d.id === value)?.name || ‘’
})
// 设置监听器
private setupListeners() {
this.distManager.onNewCalculation((item: CalcHistory) => {
this.history.unshift(item)
this.saveLocalHistory()
})
this.distManager.onNewStroke((stroke: Point[]) => {
this.strokes = [...this.strokes, stroke]
})
this.distManager.onDeviceChange((device: string) => {
this.currentDevice = device
})
// 生成唯一ID
private generateId(): string {
return {Date.now()}-{Math.random().toString(36).substr(2, 6)}
}
// 数据类型定义
interface Point {
x: number
y: number
interface CalcHistory {
id: string
formula: string
result: string
device: string
time: string
strokes: Point[][]
手写识别引擎封装(ArkTS)
// 手写识别引擎封装
class HandwritingEngine {
private static instance: HandwritingEngine
private hwModel: handwriting.Model | null = null
static getInstance(): HandwritingEngine {
if (!HandwritingEngine.instance) {
HandwritingEngine.instance = new HandwritingEngine()
return HandwritingEngine.instance
constructor() {
this.initModel()
// 初始化模型
private async initModel() {
try {
// 创建数学公式专用模型
this.hwModel = await handwriting.createModel({
language: ‘zh-CN’,
mode: handwriting.RecognitionMode.MODE_TEXT,
type: handwriting.ModelType.TYPE_ONLINE,
customConfig: {
‘math_symbols’: ‘true’,
‘formula_recognition’: ‘true’
})
catch (err) {
console.error('手写模型初始化失败:', err)
}
// 识别手写笔迹
async recognize(strokes: number[][][]): Promise<string[]> {
if (!this.hwModel) {
throw new Error(‘手写模型未初始化’)
// 准备识别数据
const hwStrokeList: handwriting.Stroke[] = strokes.map(stroke => {
return {
points: stroke.map(([x, y]) => ({ x, y })),
// 数学公式需要更精细的点采样
pointCount: stroke.length
})
// 配置识别选项(优化数学公式识别)
const config: handwriting.RecognitionConfig = {
punctuation: false,
wordCount: 1,
language: 'zh-CN',
recognitionMode: handwriting.RecognitionMode.MODE_TEXT,
extraConfig: {
'math_mode': 'true',
'formula_timeout': '5000'
}
// 执行识别
const results = await this.hwModel.recognize(hwStrokeList, config)
// 后处理识别结果
return results.map(result => {
// 转换数学符号(如×→*, ÷→/)
return this.normalizeMathSymbols(result.text)
})
// 标准化数学符号
private normalizeMathSymbols(text: string): string {
const symbolMap: Record<string, string> = {
‘×’: ‘*’,
‘÷’: ‘/’,
‘−’: ‘-’,
‘=’: ‘=’,
‘(’: ‘(’,
‘)’: ‘)’
return text.split(‘’).map(char =>
symbolMap[char] || char
).join('')
// 销毁模型
async destroy() {
if (this.hwModel) {
await this.hwModel.release()
this.hwModel = null
}
分布式计算管理(ArkTS)
// 分布式计算管理
class DistCalcManager {
private static instance: DistCalcManager
private distObject: distributedDataObject.DataObject
private currentDeviceId: string = deviceInfo.deviceId
static getInstance(): DistCalcManager {
if (!DistCalcManager.instance) {
DistCalcManager.instance = new DistCalcManager()
return DistCalcManager.instance
constructor() {
this.initDistObject()
private initDistObject() {
this.distObject = distributedDataObject.create({
calculations: {},
strokes: {},
devices: {}
})
// 注册当前设备
this.distObject.devices[deviceInfo.deviceId] = {
name: deviceInfo.deviceName,
lastActive: Date.now()
// 监听数据变化
this.distObject.on('change', (fields: string[]) => {
if (fields.includes('calculations')) {
this.handleNewCalculation()
if (fields.includes(‘strokes’)) {
this.handleNewStroke()
if (fields.includes(‘devices’)) {
EventBus.emit('deviceListUpdated')
})
// 同步计算记录
syncCalculation(calc: { formula: string; result: string }): void {
const id = this.generateId()
this.distObject.calculations[id] = {
…calc,
deviceId: deviceInfo.deviceId,
timestamp: Date.now()
this.syncToDevice(this.currentDeviceId)
// 同步手写笔迹
syncStroke(stroke: Point[]): void {
const id = this.generateId()
this.distObject.strokes[id] = {
points: stroke,
deviceId: deviceInfo.deviceId,
timestamp: Date.now()
this.syncToDevice(this.currentDeviceId)
// 处理新计算记录
private handleNewCalculation(): void {
const remoteCalcs = Object.values(this.distObject.calculations)
.filter((calc: any) =>
calc.deviceId !== deviceInfo.deviceId &&
!this.isDuplicate(calc, ‘calculations’)
)
remoteCalcs.forEach((calc: any) => {
const historyItem: CalcHistory = {
id: calc.id || this.generateId(),
formula: calc.formula,
result: calc.result,
device: this.getDeviceName(calc.deviceId),
time: new Date(calc.timestamp).toLocaleTimeString(),
strokes: []
EventBus.emit(‘newCalculation’, historyItem)
})
// 处理新手写笔迹
private handleNewStroke(): void {
const remoteStrokes = Object.values(this.distObject.strokes)
.filter((stroke: any) =>
stroke.deviceId !== deviceInfo.deviceId &&
!this.isDuplicate(stroke, ‘strokes’)
)
remoteStrokes.forEach((stroke: any) => {
EventBus.emit('newStroke', stroke.points)
})
// 切换目标设备
switchTargetDevice(deviceId: string): void {
this.currentDeviceId = deviceId
EventBus.emit(‘deviceChanged’, this.getDeviceName(deviceId))
// 获取设备名称
private getDeviceName(deviceId: string): string {
return this.distObject.devices[deviceId]?.name || deviceId
// 获取可用设备
getAvailableDevices(): DeviceInfo[] {
return Object.entries(this.distObject.devices)
.map(([id, info]: [string, any]) => ({
id: id,
name: info.name,
isCurrent: id === deviceInfo.deviceId
}))
// 同步到指定设备
private syncToDevice(deviceId: string): void {
if (deviceId !== deviceInfo.deviceId) {
this.distObject.setDistributed([deviceId])
}
// 删除历史记录
deleteHistory(id: string): void {
delete this.distObject.calculations[id]
// 检查重复数据
private isDuplicate(item: any, type: string): boolean {
const existing = Object.values(this.distObject[type])
return existing.some((i: any) =>
i.deviceId === item.deviceId &&
Math.abs(i.timestamp - item.timestamp) < 1000
)
// 生成唯一ID
private generateId(): string {
return {Date.now()}-{Math.random().toString(36).substr(2, 6)}
// 事件监听
onNewCalculation(callback: (item: CalcHistory) => void): void {
EventBus.on(‘newCalculation’, callback)
onNewStroke(callback: (stroke: Point[]) => void): void {
EventBus.on('newStroke', callback)
onDeviceChange(callback: (name: string) => void): void {
EventBus.on('deviceChanged', callback)
}
// 设备信息类型
interface DeviceInfo {
id: string
name: string
isCurrent: boolean
数学计算引擎(ArkTS)
// 数学公式计算引擎
class MathCalcEngine {
private parser: MathParser = new MathParser()
// 计算表达式
evaluate(expr: string): string {
try {
// 预处理表达式
const normalized = this.normalizeExpression(expr)
// 解析并计算
const result = this.parser.evaluate(normalized)
// 格式化结果
return this.formatResult(result)
catch (err) {
console.error('计算错误:', err)
return 'Error'
}
// 标准化表达式
private normalizeExpression(expr: string): string {
// 移除空格
let normalized = expr.replace(/\s+/g, ‘’)
// 处理隐式乘法 (如 2π → 2*π)
normalized = normalized.replace(/(\d)([a-zA-Zπ])/g, '1*2')
// 处理科学计数法
normalized = normalized.replace(/(\d+)e([+-]?\d+)/g, '1*10^2')
return normalized
// 格式化结果
private formatResult(value: number): string {
// 处理整数
if (Number.isInteger(value)) {
return value.toString()
// 保留4位小数
const rounded = Math.round(value * 10000) / 10000
// 处理科学计数法
if (Math.abs(rounded) >= 1e6 || Math.abs(rounded) < 1e-4) {
return rounded.toExponential(4)
return rounded.toString()
}
// 简单数学解析器
class MathParser {
private operators: Record<string, (a: number, b: number) => number> = {
‘+’: (a, b) => a + b,
‘-’: (a, b) => a - b,
‘’: (a, b) => a b,
‘/’: (a, b) => a / b,
‘^’: (a, b) => Math.pow(a, b)
private constants: Record<string, number> = {
'π': Math.PI,
'e': Math.E
evaluate(expr: string): number {
// 处理括号
const bracketMatch = expr.match(/\(([^()]+)\)/)
if (bracketMatch) {
const innerValue = this.evaluate(bracketMatch[1])
return this.evaluate(expr.replace(bracketMatch[0], innerValue.toString()))
// 处理常量和函数
for (const [name, value] of Object.entries(this.constants)) {
if (expr.includes(name)) {
return this.evaluate(expr.replace(name, value.toString()))
}
// 按运算符优先级处理
const ops = ['^', '*/', '+-']
for (const opGroup of ops) {
const regex = new RegExp((-?\\d+\\.?\\d)([${opGroup}])(-?\\d+\\.?\\d))
const match = expr.match(regex)
if (match) {
const a = parseFloat(match[1])
const op = match[2]
const b = parseFloat(match[3])
const result = this.operatorsa, b
return this.evaluate(expr.replace(match[0], result.toString()))
}
// 最终数值结果
return parseFloat(expr)
}
三、关键功能说明
手写识别流程
graph TD
A[触摸输入] --> B[收集笔迹点]
–> C{抬起手指?}
–>否
B
–>是
D[发送识别请求]
–> E[标准化数学符号]
–> F[显示识别结果]
–> G[触发计算]
–> H[同步到其他设备]
分布式协作机制
同步类型 数据格式 同步策略
手写笔迹 点坐标数组 实时增量同步
公式识别 文本字符串 识别完成后同步
计算结果 文本+数值 计算完成后同步
历史记录 结构化数据 设备连接时全量同步
性能优化方案
笔迹采样优化:对密集点进行抽稀处理,减少数据量
差异同步:只同步新增或修改的内容
本地缓存:历史记录本地存储,减少网络传输
计算预处理:标准化表达式提升计算准确率
四、项目扩展与优化
功能扩展建议
高级数学支持:
// 扩展计算引擎
class AdvancedMathEngine extends MathCalcEngine {
private functions: Record<string, (…args: number[]) => number> = {
‘sqrt’: (x) => Math.sqrt(x),
‘sin’: (x) => Math.sin(x),
‘log’: (x, base=10) => Math.log(x) / Math.log(base)
evaluate(expr: string): string {
// 处理函数调用 (如 sqrt(4))
const fnMatch = expr.match(/([a-z]+)\(([^)]+)\)/i)
if (fnMatch) {
const args = fnMatch[2].split(',').map(Number)
const result = this.functions...args
return this.evaluate(expr.replace(fnMatch[0], result.toString()))
return super.evaluate(expr)
}
笔迹回放:
// 历史笔迹动画回放
function replayStrokes(strokes: Point[][], canvas: CanvasRenderingContext2D) {
let currentStroke = 0
let currentPoint = 0
const timer = setInterval(() => {
if (currentStroke >= strokes.length) {
clearInterval(timer)
return
const stroke = strokes[currentStroke]
if (currentPoint === 0) {
canvas.beginPath()
canvas.moveTo(stroke[0].x, stroke[0].y)
else {
canvas.lineTo(stroke[currentPoint].x, stroke[currentPoint].y)
canvas.stroke()
currentPoint++
if (currentPoint >= stroke.length) {
currentStroke++
currentPoint = 0
}, 16) // 60fps
云历史同步:
// 集成云服务同步历史记录
function syncHistoryToCloud() {
const history = preferences.get(‘calcHistory’)
if (history) {
cloud.upload({
key: ‘calcHistory’,
data: history,
success: () => console.log(‘同步成功’),
fail: (err) => console.error(‘同步失败:’, err)
})
}
高级交互特性
笔迹美化:
// 笔迹平滑处理
function smoothStroke(rawPoints: Point[]): Point[] {
if (rawPoints.length < 3) return rawPoints
const smoothed: Point[] = []
for (let i = 1; i < rawPoints.length - 1; i++) {
smoothed.push({
x: (rawPoints[i-1].x + rawPoints[i].x + rawPoints[i+1].x) / 3,
y: (rawPoints[i-1].y + rawPoints[i].y + rawPoints[i+1].y) / 3
})
return smoothed
多色标注:
// 多设备不同颜色笔迹
function getStrokeColor(deviceId: string): string {
const colors = [‘#1890FF’, ‘#FF4D4F’, ‘#52C41A’, ‘#FAAD14’]
const index = parseInt(deviceId.substr(-1), 16) % colors.length
return colors[index]
语音输入:
// 语音识别补充
import audio from ‘@ohos.multimedia.audio’
function startVoiceInput() {
const recorder = audio.createAudioRecorder()
recorder.start().then(() => {
setTimeout(() => {
recorder.stop().then((audioFile) => {
voiceRecognizer.recognize(audioFile, (text) => {
this.recognizedText += ’ ’ + this.normalizeMathSymbols(text)
})
})
}, 3000) // 录制3秒
})
五、总结
本手写数学公式计算器实现了以下核心价值:
自然交互:通过手写识别实现符合直觉的数学公式输入
精准计算:支持复杂数学表达式和科学计算
多设备协作:基于HarmonyOS分布式能力实现跨设备同步
历史追溯:完整记录计算过程和结果
通过参考《鸿蒙跨端U同步:同一局游戏中多设备玩家昵称/头像显示》的技术方案,我们验证了分布式数据对象在实时协作场景下的适用性。开发者可以基于此项目进一步探索:
与LaTeX语法互转实现学术论文支持
开发几何图形识别和计算功能
集成Wolfram Alpha等专业计算引擎
构建教育场景下的解题步骤演示系统
注意事项:
手写识别需要申请权限ohos.permission.READ_USER_STORAGE
分布式功能需要设备在同一局域网或已配对
复杂公式识别准确率依赖模型训练数据
生产环境需要添加更完善的错误处理
