手语识别是应用的核心功能之一,通过摄像头捕捉用户的手势动作并进行识别。本文介绍如何实现一个手语识别练习页面,包括摄像头预览、识别控制和词汇选择。
StatefulWidget与状态管理
识别页面需要管理多个状态:
dart
class RecognitionScreen extends StatefulWidget {
const RecognitionScreen({super.key});
@override
State<RecognitionScreen> createState() => _RecognitionScreenState();
}
class _RecognitionScreenState extends State<RecognitionScreen> {
String _currentWord = '你好';
bool _isRecording = false;
int _score = 0;
int _attempts = 0;
final List<String> _words = ['你好', '谢谢', '对不起', '再见', '我爱你', '帮助'];
使用StatefulWidget管理识别状态。_currentWord是当前练习的词汇,_isRecording表示是否正在识别,_score记录得分,_attempts记录尝试次数。_words列表包含可选择的练习词汇。这些状态变量共同构成了识别功能的核心数据。
AppBar与得分显示
构建顶部导航栏:
dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('手语识别练习'),
actions: [
Center(
child: Padding(
padding: EdgeInsets.only(right: 16.w),
child: Text('得分: $_score', style: TextStyle(fontSize: 16.sp)),
),
),
],
),
AppBar的actions区域显示当前得分,让用户随时了解练习成果。得分用Center和Padding包裹,确保垂直居中且与右边缘保持适当距离。这种实时反馈增强了练习的趣味性。
页面布局
上下分割的布局设计:
dart
body: Column(
children: [
Expanded(flex: 2, child: _buildCameraPreview()),
Expanded(flex: 3, child: _buildPracticeArea()),
],
),
);
}
用Column纵向排列摄像头预览和练习区域,Expanded的flex参数控制比例。摄像头占2份,练习区域占3份,形成2:3的黄金比例。这样既能看清摄像头画面,又有足够空间显示练习内容。
摄像头预览区域
构建摄像头的占位容器:
dart
Widget _buildCameraPreview() {
return Container(
margin: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(16.r),
),
child: Stack(
alignment: Alignment.center,
children: [
Icon(Icons.videocam, size: 80.sp, color: Colors.white24),
用深灰色背景和圆角模拟摄像头预览区域,中间放置半透明的摄像机图标作为占位符。实际项目中这里应该嵌入真实的摄像头预览 ,使用camera插件获取视频流。Stack用于层叠显示多个元素。
识别状态指示器
右上角显示识别状态:
dart
Positioned(
top: 16.h,
right: 16.w,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: _isRecording ? Colors.red : Colors.grey,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.circle, color: Colors.white, size: 8.sp),
SizedBox(width: 4.w),
Text(
_isRecording ? '识别中' : '待开始',
style: TextStyle(color: Colors.white, fontSize: 12.sp),
),
],
),
),
),
用Positioned将状态指示器固定在右上角。识别中显示红色背景和"识别中"文字,待机时显示灰色背景和"待开始"文字。小圆点图标模拟录制指示灯,红色表示正在工作。这种视觉反馈让用户清楚当前状态。
提示文字
未开始时的引导提示:
dart
if (!_isRecording)
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'点击下方按钮开始识别',
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
),
],
),
);
}
用if条件渲染,只在未识别时显示提示文字。半透明黑色背景让文字清晰可读,白色文字与深色背景形成对比。这种引导提示降低了用户的学习成本,知道下一步该做什么。
练习区域
构建下方的练习控制区:
dart
Widget _buildPracticeArea() {
return SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Card(
child: Padding(
padding: EdgeInsets.all(20.w),
child: Column(
children: [
Text('请用手语表达', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
SizedBox(height: 8.h),
用SingleChildScrollView包裹,内容过多时可以滚动。Column纵向排列各个功能区域。顶部卡片显示当前练习的词汇和控制按钮,用灰色小字提示"请用手语表达",说明任务目标。
当前词汇显示
大字号显示练习词汇:
dart
Text(
_currentWord,
style: TextStyle(fontSize: 36.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
词汇用36.sp的大字号和粗体显示,作为页面的视觉焦点。用户一眼就能看到要练习的内容。字号足够大,即使在一定距离外也能清晰看到,方便用户边看边做手势。
识别控制按钮
开始/停止识别的按钮:
dart
ElevatedButton.icon(
onPressed: () => setState(() => _isRecording = !_isRecording),
icon: Icon(_isRecording ? Icons.stop : Icons.play_arrow, color: Colors.white),
label: Text(_isRecording ? '停止识别' : '开始识别', style: const TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: _isRecording ? Colors.red : const Color(0xFF00897B),
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 12.h),
),
),
],
),
),
),
SizedBox(height: 16.h),
按钮根据识别状态动态变化:识别中显示红色背景和停止图标,待机时显示主题色背景和播放图标。点击按钮切换_isRecording状态,调用setState触发界面更新。这种状态切换的交互方式简单直观。
词汇选择器
构建词汇选择区域:
dart
_buildWordSelector(),
SizedBox(height: 16.h),
_buildTips(),
],
),
);
}
Widget _buildWordSelector() {
return Card(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('选择练习词汇', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 12.h),
词汇选择器用Card包裹,标题用粗体显示。crossAxisAlignment.start让内容左对齐,符合阅读习惯。这个区域让用户可以自由选择想要练习的词汇,提高了练习的灵活性。
Wrap布局
使用Wrap自动换行排列词汇:
dart
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: _words.map((word) {
final isSelected = word == _currentWord;
return ChoiceChip(
label: Text(word),
selected: isSelected,
onSelected: (selected) {
if (selected) setState(() => _currentWord = word);
},
selectedColor: const Color(0xFF00897B).withOpacity(0.2),
);
}).toList(),
),
],
),
),
);
}
Wrap类似Row但会自动换行,spacing控制水平间距,runSpacing控制垂直间距。ChoiceChip是单选芯片组件,选中时显示主题色半透明背景。点击芯片更新_currentWord并刷新界面。这种芯片选择的交互方式现代且易用。
提示信息
底部的使用提示:
dart
Widget _buildTips() {
return Card(
color: Colors.blue[50],
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.blue),
SizedBox(width: 12.w),
Expanded(
child: Text(
'确保光线充足,手势在摄像头范围内,动作清晰完整',
style: TextStyle(fontSize: 13.sp, color: Colors.blue[800]),
),
),
],
),
),
);
}
}
提示卡片用浅蓝色背景,左侧放置信息图标,右侧显示提示文字。Expanded让文字占据剩余空间,自动换行。蓝色配色表示这是信息提示而非警告或错误。提示内容告诉用户如何获得更好的识别效果。
Stack的层叠布局
摄像头预览区的层叠设计:
dart
Stack(
alignment: Alignment.center,
children: [
Icon(...), // 底层:占位图标
Positioned(...), // 顶层:状态指示器
if (!_isRecording) ..., // 条件层:提示文字
],
)
Stack让多个组件层叠显示,alignment设为center让未定位的子组件居中。Positioned精确控制状态指示器的位置,if条件渲染提示文字。这种层叠布局创造了丰富的视觉层次。
状态切换的逻辑
简洁的状态切换实现:
dart
onPressed: () => setState(() => _isRecording = !_isRecording),
使用!运算符取反,一行代码实现状态切换。setState包裹状态更新,触发界面重建。这种简洁的写法 比if-else判断更优雅,代码可读性高。
ChoiceChip的优势
使用ChoiceChip实现单选:
dart
ChoiceChip(
label: Text(word),
selected: isSelected,
onSelected: (selected) {
if (selected) setState(() => _currentWord = word);
},
selectedColor: const Color(0xFF00897B).withOpacity(0.2),
)
ChoiceChip是Flutter提供的选择芯片组件,自带选中状态管理和动画效果。selected属性控制是否选中,onSelected回调处理选择事件。相比自己实现单选逻辑,使用现成组件开发效率更高。
Wrap的自动换行
处理不同屏幕宽度:
dart
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: _words.map((word) => ChoiceChip(...)).toList(),
)
Wrap会根据可用宽度自动换行,不会像Row那样溢出屏幕。spacing控制同一行元素的间距,runSpacing控制不同行的间距。这种自适应布局在不同屏幕上都能正常显示。
颜色的语义化
不同颜色传达不同信息:
dart
Colors.red, // 识别中/停止
Color(0xFF00897B), // 主题色/开始
Colors.grey, // 待机状态
Colors.blue[50], // 信息提示背景
红色表示正在工作或停止操作,主题色表示可以开始,灰色表示待机,浅蓝色表示信息提示。这种颜色语义化符合用户认知习惯,无需文字说明就能理解含义。
Expanded的flex比例
控制区域大小比例:
dart
Expanded(flex: 2, child: _buildCameraPreview()),
Expanded(flex: 3, child: _buildPracticeArea()),
flex参数控制占用空间的比例,2:3的比例让摄像头预览占40%,练习区域占60%。这个比例经过视觉调试,既能看清摄像头画面,又有足够空间显示控制元素。
条件渲染的应用
根据状态显示不同内容:
dart
if (!_isRecording)
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(...),
child: Text('点击下方按钮开始识别', ...),
),
Dart的if语句可以直接用在集合中,条件为真时才添加元素。这比三元运算符!_isRecording ? Container(...) : SizedBox()更简洁。条件渲染让代码更声明式,逻辑清晰易懂。
动态图标和文字
根据状态变化UI:
dart
icon: Icon(_isRecording ? Icons.stop : Icons.play_arrow, color: Colors.white),
label: Text(_isRecording ? '停止识别' : '开始识别', ...),
backgroundColor: _isRecording ? Colors.red : const Color(0xFF00897B),
图标、文字、背景色都根据_isRecording状态动态变化。这种一致性变化让用户清楚按钮的当前功能,点击后会发生什么。UI与状态保持同步是良好用户体验的关键。
SingleChildScrollView的作用
处理内容溢出:
dart
SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
children: [...],
),
)
SingleChildScrollView让内容可以滚动,避免在小屏设备上溢出。padding设置整体内边距,Column纵向排列各个区域。这种防御性设计确保在各种屏幕尺寸上都能正常显示。
响应式布局
使用flutter_screenutil适配屏幕:
dart
fontSize: 36.sp,
padding: EdgeInsets.all(16.w),
spacing: 8.w,
runSpacing: 8.h,
.sp用于字号,.w和.h用于尺寸和间距。这些单位会根据屏幕尺寸自动缩放,确保在不同设备上比例一致。一套代码适配所有屏幕,无需针对不同设备单独调整。
小结
手语识别页面通过摄像头预览和练习控制实现识别功能,状态指示器实时反馈识别状态。词汇选择器让用户自由选择练习内容,提示信息指导用户获得更好的识别效果。整体设计注重状态管理和视觉反馈,打造流畅的识别练习体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net