Flutter表情包本地管理器应用开发教程
项目简介
表情包本地管理器是一款专为表情包爱好者设计的移动应用,帮助用户高效管理和使用本地存储的表情包。应用采用Flutter框架开发,提供直观的界面和丰富的管理功能,让表情包的收集、分类、使用变得更加便捷。
运行效果图




核心功能
- 表情包管理:创建、编辑、删除表情包,支持分类和标签
- 表情浏览:网格和列表两种视图模式,支持搜索和排序
- 收藏功能:收藏常用表情,快速访问喜爱的内容
- 使用统计:记录表情使用次数,展示最近使用历史
- 一键复制:点击表情即可复制到剪贴板,方便分享
- 多种格式:支持PNG、JPG、GIF、WebP等常见图片格式
技术特点
- 单文件架构,代码结构清晰简洁
- Material Design 3设计风格
- 响应式布局,适配不同屏幕尺寸
- 丰富的交互动画和用户体验
- 完整的数据模型和状态管理
项目架构设计
整体架构
EmojiManagerApp
EmojiManagerHomePage
表情包页面
浏览页面
收藏页面
设置页面
数据模型
EmojiPack
EmojiItem
对话框组件
AddPackDialog
PackDetailsDialog
EmojiDetailsDialog
页面结构
应用采用底部导航栏设计,包含四个主要页面:
- 表情包页面:管理表情包,支持创建、编辑、删除操作
- 浏览页面:浏览所有表情,显示最近使用和全部表情
- 收藏页面:管理收藏的表情,快速访问喜爱内容
- 设置页面:应用设置和存储管理功能
数据模型设计
EmojiPack 表情包模型
dart
class EmojiPack {
final String id; // 唯一标识
final String name; // 表情包名称
final String description; // 描述
final String coverPath; // 封面图片路径
final List<EmojiItem> emojis; // 表情列表
final DateTime createdAt; // 创建时间
final DateTime updatedAt; // 更新时间
final EmojiPackType type; // 表情包类型
final List<String> tags; // 标签列表
}
EmojiItem 表情项目模型
dart
class EmojiItem {
final String id; // 唯一标识
final String name; // 表情名称
final String filePath; // 文件路径
final String fileName; // 文件名
final EmojiFormat format; // 文件格式
final double fileSize; // 文件大小(KB)
final int width; // 图片宽度
final int height; // 图片高度
final DateTime addedAt; // 添加时间
final List<String> tags; // 标签列表
final int usageCount; // 使用次数
}
枚举定义
dart
// 表情包类型
enum EmojiPackType {
custom, // 自定义
imported, // 导入的
favorite, // 收藏
}
// 表情格式
enum EmojiFormat {
png, // PNG格式
jpg, // JPG格式
gif, // GIF格式
webp, // WebP格式
}
// 排序类型
enum SortType {
name, // 按名称
date, // 按日期
size, // 按大小
usage, // 按使用次数
}
核心功能实现
1. 主页面结构
主页面使用StatefulWidget实现,包含底部导航栏和对应的页面内容:
dart
class _EmojiManagerHomePageState extends State<EmojiManagerHomePage>
with TickerProviderStateMixin {
int _selectedIndex = 0;
late TabController _packsTabController;
// 数据列表
final List<EmojiPack> _emojiPacks = [];
final List<EmojiItem> _favoriteEmojis = [];
final List<EmojiItem> _recentEmojis = [];
@override
Widget build(BuildContext context) {
return Scaffold(
body: [
_buildPacksPage(), // 表情包页面
_buildBrowsePage(), // 浏览页面
_buildFavoritePage(), // 收藏页面
_buildSettingsPage(), // 设置页面
][_selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() {
_selectedIndex = index;
});
},
destinations: const [
NavigationDestination(icon: Icon(Icons.folder_outlined), label: '表情包'),
NavigationDestination(icon: Icon(Icons.explore_outlined), label: '浏览'),
NavigationDestination(icon: Icon(Icons.favorite_outline), label: '收藏'),
NavigationDestination(icon: Icon(Icons.settings_outlined), label: '设置'),
],
),
);
}
}
2. 表情包管理功能
表情包列表展示
支持网格视图和列表视图两种模式:
dart
Widget _buildPacksList(bool onlyFavorites) {
var filteredPacks = _emojiPacks.where((pack) {
// 搜索过滤
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
if (!pack.name.toLowerCase().contains(query) &&
!pack.description.toLowerCase().contains(query) &&
!pack.tags.any((tag) => tag.toLowerCase().contains(query))) {
return false;
}
}
return true;
}).toList();
if (_isGridView) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.8,
),
itemCount: filteredPacks.length,
itemBuilder: (context, index) => _buildPackGridCard(filteredPacks[index]),
);
} else {
return ListView.builder(
itemCount: filteredPacks.length,
itemBuilder: (context, index) => _buildPackListCard(filteredPacks[index]),
);
}
}
表情包卡片设计
dart
Widget _buildPackGridCard(EmojiPack pack) {
final typeConfig = _typeConfigs[pack.type]!;
return Card(
child: InkWell(
onTap: () => _showPackDetails(pack),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 封面图片
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: pack.emojis.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _buildEmojiPreview(pack.emojis.first),
)
: Icon(Icons.emoji_emotions, size: 48, color: Colors.grey.shade400),
),
),
// 标题和类型标签
Row(
children: [
Expanded(
child: Text(pack.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: typeConfig.color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(typeConfig.icon, size: 14, color: typeConfig.color),
),
],
),
// 统计信息
Row(
children: [
Icon(Icons.emoji_emotions, size: 14, color: Colors.grey.shade600),
Text('${pack.emojiCount}个'),
const SizedBox(width: 12),
Icon(Icons.storage, size: 14, color: Colors.grey.shade600),
Text('${(pack.totalSize / 1024).toStringAsFixed(1)}MB'),
],
),
],
),
),
),
);
}
3. 表情使用功能
表情点击复制
dart
void _useEmoji(EmojiItem emoji) {
// 复制到剪贴板
Clipboard.setData(ClipboardData(text: emoji.name));
// 更新使用次数
final packIndex = _emojiPacks.indexWhere((pack) =>
pack.emojis.any((e) => e.id == emoji.id));
if (packIndex != -1) {
final pack = _emojiPacks[packIndex];
final emojiIndex = pack.emojis.indexWhere((e) => e.id == emoji.id);
if (emojiIndex != -1) {
final updatedEmoji = pack.emojis[emojiIndex].copyWith(
usageCount: pack.emojis[emojiIndex].usageCount + 1,
);
setState(() {
// 更新表情包中的表情
final updatedEmojis = List<EmojiItem>.from(pack.emojis);
updatedEmojis[emojiIndex] = updatedEmoji;
_emojiPacks[packIndex] = pack.copyWith(emojis: updatedEmojis);
// 更新最近使用列表
_recentEmojis.removeWhere((e) => e.id == emoji.id);
_recentEmojis.insert(0, updatedEmoji);
if (_recentEmojis.length > 20) {
_recentEmojis.removeLast();
}
});
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已复制"${emoji.name}"到剪贴板')),
);
}
表情长按选项
dart
void _showEmojiOptions(EmojiItem emoji) {
final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);
showModalBottomSheet(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.copy),
title: const Text('复制'),
onTap: () {
Navigator.pop(context);
_useEmoji(emoji);
},
),
ListTile(
leading: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
title: Text(isFavorite ? '取消收藏' : '添加收藏'),
onTap: () {
Navigator.pop(context);
_toggleFavorite(emoji);
},
),
ListTile(
leading: const Icon(Icons.info),
title: const Text('详细信息'),
onTap: () {
Navigator.pop(context);
_showEmojiDetails(emoji);
},
),
ListTile(
leading: const Icon(Icons.share),
title: const Text('分享'),
onTap: () {
Navigator.pop(context);
_shareEmoji(emoji);
},
),
],
),
),
);
}
4. 收藏管理功能
dart
void _toggleFavorite(EmojiItem emoji) {
setState(() {
final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);
if (isFavorite) {
_favoriteEmojis.removeWhere((e) => e.id == emoji.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已取消收藏"${emoji.name}"')),
);
} else {
_favoriteEmojis.add(emoji);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已收藏"${emoji.name}"')),
);
}
});
}
5. 搜索和排序功能
搜索实现
dart
// 搜索框UI
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索表情包名称或标签...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
setState(() {
_searchQuery = '';
});
},
)
: null,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
)
排序功能
dart
void _sortPacks(List<EmojiPack> packs) {
switch (_sortType) {
case SortType.name:
packs.sort((a, b) => a.name.compareTo(b.name));
break;
case SortType.date:
packs.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
break;
case SortType.size:
packs.sort((a, b) => b.totalSize.compareTo(a.totalSize));
break;
case SortType.usage:
packs.sort((a, b) {
final aUsage = a.emojis.fold(0, (sum, emoji) => sum + emoji.usageCount);
final bUsage = b.emojis.fold(0, (sum, emoji) => sum + emoji.usageCount);
return bUsage.compareTo(aUsage);
});
break;
}
}
UI组件设计
1. 渐变头部设计
每个页面都使用渐变色头部提升视觉效果:
dart
Container(
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.amber.shade600, Colors.amber.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: [
// 标题行
Row(
children: [
const Icon(Icons.emoji_emotions, color: Colors.white, size: 32),
const SizedBox(width: 12),
const Text('表情包管理器', style: TextStyle(fontSize: 24, color: Colors.white)),
const Spacer(),
IconButton(
icon: Icon(_isGridView ? Icons.list : Icons.grid_view, color: Colors.white),
onPressed: () => setState(() => _isGridView = !_isGridView),
),
],
),
// 搜索框和统计卡片
],
),
)
2. 统计卡片组件
dart
Widget _buildSummaryCard(String title, String value, String unit, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon, color: Colors.white, size: 20),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
Text(unit, style: const TextStyle(fontSize: 10, color: Colors.white70)),
Text(title, style: const TextStyle(fontSize: 12, color: Colors.white70)),
],
),
);
}
3. 表情卡片组件
dart
Widget _buildEmojiCard(EmojiItem emoji, {bool isCompact = false, bool showFavorite = false}) {
final formatConfig = _formatConfigs[emoji.format]!;
final isFavorite = _favoriteEmojis.any((e) => e.id == emoji.id);
return Card(
child: InkWell(
onTap: () => _useEmoji(emoji),
onLongPress: () => _showEmojiOptions(emoji),
child: Padding(
padding: EdgeInsets.all(isCompact ? 8 : 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 表情图片
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Center(child: _buildEmojiPreview(emoji)),
// 格式标签
Positioned(
top: 4, right: 4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
decoration: BoxDecoration(
color: formatConfig.color.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(4),
),
child: Text(formatConfig.name, style: const TextStyle(fontSize: 8, color: Colors.white)),
),
),
// 收藏标记
if (isFavorite)
const Positioned(
top: 4, left: 4,
child: Icon(Icons.favorite, size: 16, color: Colors.red),
),
],
),
),
),
// 表情信息
Text(emoji.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
Row(
children: [
Icon(Icons.storage, size: 12, color: Colors.grey.shade600),
Text(emoji.fileSizeText, style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
if (emoji.usageCount > 0) ...[
Icon(Icons.trending_up, size: 12, color: Colors.grey.shade600),
Text('${emoji.usageCount}', style: TextStyle(fontSize: 10, color: Colors.grey.shade600)),
],
],
),
],
),
),
),
);
}
4. 表情预览组件
dart
Widget _buildEmojiPreview(EmojiItem emoji) {
// 这里应该根据实际文件路径加载图片
// 由于是示例,使用占位符
return Container(
width: 48, height: 48,
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
emoji.name.isNotEmpty ? emoji.name[0] : '😀',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
);
}
对话框组件实现
1. 添加表情包对话框
dart
class _AddPackDialog extends StatefulWidget {
final Function(EmojiPack) onSave;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('添加表情包'),
content: SizedBox(
width: 400, height: 300,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
children: [
// 名称输入框
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '表情包名称 *',
prefixIcon: Icon(Icons.title),
),
validator: (value) => value?.trim().isEmpty == true ? '请输入表情包名称' : null,
),
// 描述输入框
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: '描述(可选)',
prefixIcon: Icon(Icons.description),
),
maxLines: 2,
),
// 类型选择
DropdownButtonFormField<EmojiPackType>(
value: _selectedType,
decoration: const InputDecoration(
labelText: '类型',
prefixIcon: Icon(Icons.category),
),
items: EmojiPackType.values.map((type) =>
DropdownMenuItem(value: type, child: Text(_getTypeText(type)))).toList(),
onChanged: (value) => setState(() => _selectedType = value!),
),
// 标签输入框
TextFormField(
controller: _tagsController,
decoration: const InputDecoration(
labelText: '标签(用逗号分隔)',
prefixIcon: Icon(Icons.tag),
),
),
],
),
),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')),
ElevatedButton(onPressed: _savePack, child: const Text('保存')),
],
);
}
}
2. 表情包详情对话框
dart
class _PackDetailsDialog extends StatelessWidget {
final EmojiPack pack;
final Function(EmojiPack) onEdit;
final VoidCallback onDelete;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(pack.name),
content: SizedBox(
width: 400, height: 500,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('名称', pack.name),
_buildDetailRow('描述', pack.description),
_buildDetailRow('类型', _getTypeText(pack.type)),
_buildDetailRow('表情数量', '${pack.emojiCount}个'),
_buildDetailRow('总大小', '${(pack.totalSize / 1024).toStringAsFixed(1)}MB'),
_buildDetailRow('创建时间', _formatDate(pack.createdAt)),
_buildDetailRow('更新时间', _formatDate(pack.updatedAt)),
// 标签展示
if (pack.tags.isNotEmpty) ...[
const Text('标签:', style: TextStyle(fontWeight: FontWeight.w500)),
Wrap(
spacing: 8,
children: pack.tags.map((tag) => Chip(label: Text(tag))).toList(),
),
],
// 表情预览
if (pack.emojis.isNotEmpty) ...[
const Text('表情预览:', style: TextStyle(fontWeight: FontWeight.w500)),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: pack.emojis.length > 8 ? 8 : pack.emojis.length,
itemBuilder: (context, index) => _buildEmojiPreview(pack.emojis[index]),
),
],
],
),
),
),
actions: [
TextButton(onPressed: () { Navigator.pop(context); onDelete(); },
child: const Text('删除', style: TextStyle(color: Colors.red))),
TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
],
);
}
}
3. 表情详情对话框
dart
class _EmojiDetailsDialog extends StatelessWidget {
final EmojiItem emoji;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(emoji.name),
content: SizedBox(
width: 300, height: 400,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 表情预览
Center(
child: Container(
width: 120, height: 120,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Center(child: _buildEmojiPreview(emoji)),
),
),
// 详细信息
_buildDetailRow('名称', emoji.name),
_buildDetailRow('文件名', emoji.fileName),
_buildDetailRow('格式', _getFormatText(emoji.format)),
_buildDetailRow('大小', emoji.fileSizeText),
_buildDetailRow('分辨率', emoji.resolutionText),
_buildDetailRow('使用次数', '${emoji.usageCount}次'),
_buildDetailRow('添加时间', _formatDate(emoji.addedAt)),
// 标签展示
if (emoji.tags.isNotEmpty) ...[
const Text('标签:', style: TextStyle(fontWeight: FontWeight.w500)),
Wrap(
spacing: 8,
children: emoji.tags.map((tag) => Chip(label: Text(tag))).toList(),
),
],
],
),
),
),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: const Text('关闭')),
],
);
}
}
状态管理
1. 本地状态管理
应用使用setState进行简单的本地状态管理:
dart
class _EmojiManagerHomePageState extends State<EmojiManagerHomePage> {
// 数据列表
final List<EmojiPack> _emojiPacks = [];
final List<EmojiItem> _favoriteEmojis = [];
final List<EmojiItem> _recentEmojis = [];
// 搜索和筛选状态
String _searchQuery = '';
SortType _sortType = SortType.date;
bool _isGridView = true;
// 添加表情包
void _addPack(EmojiPack pack) {
setState(() {
_emojiPacks.add(pack);
});
}
// 删除表情包
void _deletePack(String packId) {
setState(() {
_emojiPacks.removeWhere((pack) => pack.id == packId);
});
}
// 更新表情包
void _updatePack(EmojiPack updatedPack) {
setState(() {
final index = _emojiPacks.indexWhere((p) => p.id == updatedPack.id);
if (index != -1) {
_emojiPacks[index] = updatedPack;
}
});
}
}
2. 数据持久化扩展
虽然当前版本使用内存存储,但可以轻松扩展为持久化存储:
dart
// 保存数据到本地存储
Future<void> _saveData() async {
final prefs = await SharedPreferences.getInstance();
// 保存表情包数据
final packsJson = _emojiPacks.map((pack) => pack.toJson()).toList();
await prefs.setString('emoji_packs', jsonEncode(packsJson));
// 保存收藏数据
final favoritesJson = _favoriteEmojis.map((emoji) => emoji.toJson()).toList();
await prefs.setString('favorite_emojis', jsonEncode(favoritesJson));
// 保存最近使用数据
final recentJson = _recentEmojis.map((emoji) => emoji.toJson()).toList();
await prefs.setString('recent_emojis', jsonEncode(recentJson));
}
// 从本地存储加载数据
Future<void> _loadData() async {
final prefs = await SharedPreferences.getInstance();
// 加载表情包数据
final packsString = prefs.getString('emoji_packs');
if (packsString != null) {
final packsList = jsonDecode(packsString) as List;
_emojiPacks.addAll(packsList.map((json) => EmojiPack.fromJson(json)));
}
// 加载收藏数据
final favoritesString = prefs.getString('favorite_emojis');
if (favoritesString != null) {
final favoritesList = jsonDecode(favoritesString) as List;
_favoriteEmojis.addAll(favoritesList.map((json) => EmojiItem.fromJson(json)));
}
// 加载最近使用数据
final recentString = prefs.getString('recent_emojis');
if (recentString != null) {
final recentList = jsonDecode(recentString) as List;
_recentEmojis.addAll(recentList.map((json) => EmojiItem.fromJson(json)));
}
}
工具方法实现
1. 日期格式化
dart
String _formatDate(DateTime date) {
return '${date.month}/${date.day}'; // 简短格式:月/日
}
String _formatFullDate(DateTime date) {
return '${date.year}年${date.month}月${date.day}日'; // 完整格式
}
2. 文件大小格式化
dart
String get fileSizeText {
if (fileSize < 1024) {
return '${fileSize.toStringAsFixed(1)} KB';
} else {
return '${(fileSize / 1024).toStringAsFixed(1)} MB';
}
}
3. 类型和格式转换
dart
String _getTypeText(EmojiPackType type) {
switch (type) {
case EmojiPackType.custom:
return '自定义';
case EmojiPackType.imported:
return '导入';
case EmojiPackType.favorite:
return '收藏';
}
}
String _getFormatText(EmojiFormat format) {
switch (format) {
case EmojiFormat.png:
return 'PNG';
case EmojiFormat.jpg:
return 'JPG';
case EmojiFormat.gif:
return 'GIF';
case EmojiFormat.webp:
return 'WebP';
}
}
String _getSortTypeName(SortType type) {
switch (type) {
case SortType.name:
return '按名称';
case SortType.date:
return '按日期';
case SortType.size:
return '按大小';
case SortType.usage:
return '按使用次数';
}
}
4. 搜索和筛选方法
dart
// 搜索表情包
List<EmojiPack> _searchPacks(String query) {
if (query.isEmpty) return _emojiPacks;
final lowerQuery = query.toLowerCase();
return _emojiPacks.where((pack) {
return pack.name.toLowerCase().contains(lowerQuery) ||
pack.description.toLowerCase().contains(lowerQuery) ||
pack.tags.any((tag) => tag.toLowerCase().contains(lowerQuery));
}).toList();
}
// 搜索表情
List<EmojiItem> _searchEmojis(String query) {
final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
if (query.isEmpty) return allEmojis;
final lowerQuery = query.toLowerCase();
return allEmojis.where((emoji) {
return emoji.name.toLowerCase().contains(lowerQuery) ||
emoji.tags.any((tag) => tag.toLowerCase().contains(lowerQuery));
}).toList();
}
// 按类型筛选表情包
List<EmojiPack> _filterPacksByType(EmojiPackType type) {
return _emojiPacks.where((pack) => pack.type == type).toList();
}
// 按格式筛选表情
List<EmojiItem> _filterEmojisByFormat(EmojiFormat format) {
final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
return allEmojis.where((emoji) => emoji.format == format).toList();
}
功能扩展建议
1. 文件导入功能
dart
// 添加文件选择依赖
dependencies:
file_picker: ^8.1.2
path_provider: ^2.1.4
// 实现文件导入
Future<void> _importEmojis() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
);
if (result != null) {
final directory = await getApplicationDocumentsDirectory();
final emojiDir = Directory('${directory.path}/emojis');
if (!await emojiDir.exists()) {
await emojiDir.create(recursive: true);
}
for (final file in result.files) {
if (file.path != null) {
final originalFile = File(file.path!);
final fileName = file.name;
final newPath = '${emojiDir.path}/$fileName';
// 复制文件到应用目录
await originalFile.copy(newPath);
// 创建表情项目
final emoji = EmojiItem(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: fileName.split('.').first,
filePath: newPath,
fileName: fileName,
format: _getFormatFromExtension(fileName.split('.').last),
fileSize: await originalFile.length() / 1024,
addedAt: DateTime.now(),
);
// 添加到默认表情包
_addEmojiToDefaultPack(emoji);
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('成功导入${result.files.length}个表情')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('导入失败:$e')),
);
}
}
2. 表情包导出功能
dart
// 添加压缩依赖
dependencies:
archive: ^3.6.1
// 实现表情包导出
Future<void> _exportEmojiPack(EmojiPack pack) async {
try {
final directory = await getApplicationDocumentsDirectory();
final exportDir = Directory('${directory.path}/exports');
if (!await exportDir.exists()) {
await exportDir.create(recursive: true);
}
// 创建ZIP压缩包
final archive = Archive();
// 添加表情包信息文件
final packInfo = {
'name': pack.name,
'description': pack.description,
'type': pack.type.toString(),
'tags': pack.tags,
'createdAt': pack.createdAt.toIso8601String(),
};
final infoJson = jsonEncode(packInfo);
archive.addFile(ArchiveFile('pack_info.json', infoJson.length, infoJson.codeUnits));
// 添加表情文件
for (final emoji in pack.emojis) {
final file = File(emoji.filePath);
if (await file.exists()) {
final bytes = await file.readAsBytes();
archive.addFile(ArchiveFile(emoji.fileName, bytes.length, bytes));
}
}
// 保存ZIP文件
final zipEncoder = ZipEncoder();
final zipBytes = zipEncoder.encode(archive);
final zipFile = File('${exportDir.path}/${pack.name}.zip');
await zipFile.writeAsBytes(zipBytes!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('表情包已导出到:${zipFile.path}')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('导出失败:$e')),
);
}
}
3. 表情分享功能
dart
// 添加分享依赖
dependencies:
share_plus: ^10.1.2
// 实现表情分享
Future<void> _shareEmoji(EmojiItem emoji) async {
try {
final file = File(emoji.filePath);
if (await file.exists()) {
await Share.shareXFiles(
[XFile(emoji.filePath)],
text: emoji.name,
subject: '分享表情:${emoji.name}',
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('表情文件不存在')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('分享失败:$e')),
);
}
}
// 分享表情包
Future<void> _shareEmojiPack(EmojiPack pack) async {
final packInfo = '''
表情包:${pack.name}
描述:${pack.description}
表情数量:${pack.emojiCount}个
总大小:${(pack.totalSize / 1024).toStringAsFixed(1)}MB
标签:${pack.tags.join(', ')}
''';
await Share.share(
packInfo,
subject: '分享表情包:${pack.name}',
);
}
4. 表情搜索优化
dart
// 实现模糊搜索
class FuzzySearch {
static double similarity(String a, String b) {
if (a == b) return 1.0;
if (a.isEmpty || b.isEmpty) return 0.0;
final longer = a.length > b.length ? a : b;
final shorter = a.length > b.length ? b : a;
if (longer.length == 0) return 1.0;
return (longer.length - editDistance(longer, shorter)) / longer.length;
}
static int editDistance(String a, String b) {
final matrix = List.generate(
a.length + 1,
(i) => List.generate(b.length + 1, (j) => 0),
);
for (int i = 0; i <= a.length; i++) {
matrix[i][0] = i;
}
for (int j = 0; j <= b.length; j++) {
matrix[0][j] = j;
}
for (int i = 1; i <= a.length; i++) {
for (int j = 1; j <= b.length; j++) {
final cost = a[i - 1] == b[j - 1] ? 0 : 1;
matrix[i][j] = [
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost,
].reduce((a, b) => a < b ? a : b);
}
}
return matrix[a.length][b.length];
}
}
// 使用模糊搜索
List<EmojiItem> _fuzzySearchEmojis(String query) {
final allEmojis = _emojiPacks.expand((pack) => pack.emojis).toList();
if (query.isEmpty) return allEmojis;
final results = allEmojis.map((emoji) {
final similarity = FuzzySearch.similarity(
emoji.name.toLowerCase(),
query.toLowerCase(),
);
return MapEntry(emoji, similarity);
}).where((entry) => entry.value > 0.3).toList();
results.sort((a, b) => b.value.compareTo(a.value));
return results.map((entry) => entry.key).toList();
}
5. 表情标签管理
dart
// 标签管理器
class TagManager {
static final Set<String> _allTags = {};
static void addTag(String tag) {
_allTags.add(tag.trim().toLowerCase());
}
static void addTags(List<String> tags) {
_allTags.addAll(tags.map((tag) => tag.trim().toLowerCase()));
}
static List<String> getAllTags() {
return _allTags.toList()..sort();
}
static List<String> searchTags(String query) {
if (query.isEmpty) return getAllTags();
final lowerQuery = query.toLowerCase();
return _allTags
.where((tag) => tag.contains(lowerQuery))
.toList()
..sort();
}
static List<String> getPopularTags(List<EmojiPack> packs) {
final tagCounts = <String, int>{};
for (final pack in packs) {
for (final tag in pack.tags) {
tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
}
for (final emoji in pack.emojis) {
for (final tag in emoji.tags) {
tagCounts[tag] = (tagCounts[tag] ?? 0) + 1;
}
}
}
final sortedTags = tagCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
return sortedTags.take(10).map((entry) => entry.key).toList();
}
}
// 标签输入组件
class TagInputWidget extends StatefulWidget {
final List<String> initialTags;
final Function(List<String>) onTagsChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标签输入框
TextField(
controller: _tagController,
decoration: InputDecoration(
labelText: '添加标签',
suffixIcon: IconButton(
icon: const Icon(Icons.add),
onPressed: _addTag,
),
),
onSubmitted: (_) => _addTag(),
),
// 已添加的标签
if (_tags.isNotEmpty) ...[
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _tags.map((tag) {
return Chip(
label: Text(tag),
onDeleted: () => _removeTag(tag),
);
}).toList(),
),
],
// 推荐标签
const SizedBox(height: 8),
const Text('推荐标签:', style: TextStyle(fontWeight: FontWeight.w500)),
Wrap(
spacing: 8,
children: TagManager.getPopularTags(_emojiPacks).map((tag) {
return ActionChip(
label: Text(tag),
onPressed: () => _addRecommendedTag(tag),
);
}).toList(),
),
],
);
}
}
性能优化策略
1. 图片加载优化
dart
// 使用缓存图片加载
class CachedImageWidget extends StatelessWidget {
final String imagePath;
final double? width;
final double? height;
const CachedImageWidget({
super.key,
required this.imagePath,
this.width,
this.height,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<Uint8List?>(
future: _loadImageBytes(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.memory(
snapshot.data!,
width: width,
height: height,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholder();
},
);
} else if (snapshot.hasError) {
return _buildPlaceholder();
} else {
return _buildLoadingPlaceholder();
}
},
);
}
Future<Uint8List?> _loadImageBytes() async {
try {
final file = File(imagePath);
if (await file.exists()) {
return await file.readAsBytes();
}
} catch (e) {
print('Error loading image: $e');
}
return null;
}
Widget _buildPlaceholder() {
return Container(
width: width ?? 48,
height: height ?? 48,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.image, color: Colors.grey),
);
}
Widget _buildLoadingPlaceholder() {
return Container(
width: width ?? 48,
height: height ?? 48,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
}
2. 列表性能优化
dart
// 使用ListView.builder进行懒加载
ListView.builder(
itemCount: filteredEmojis.length,
itemBuilder: (context, index) {
final emoji = filteredEmojis[index];
return _buildEmojiCard(emoji);
},
// 添加缓存范围
cacheExtent: 1000,
)
// 使用AutomaticKeepAliveClientMixin保持页面状态
class _BrowsePageState extends State<BrowsePage>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return /* 页面内容 */;
}
}
3. 搜索性能优化
dart
// 使用防抖动搜索
Timer? _searchTimer;
void _onSearchChanged(String query) {
_searchTimer?.cancel();
_searchTimer = Timer(const Duration(milliseconds: 300), () {
setState(() {
_searchQuery = query;
});
});
}
// 建立搜索索引
class SearchIndex {
final Map<String, List<EmojiItem>> _nameIndex = {};
final Map<String, List<EmojiItem>> _tagIndex = {};
void buildIndex(List<EmojiItem> emojis) {
_nameIndex.clear();
_tagIndex.clear();
for (final emoji in emojis) {
// 按名称建立索引
final nameWords = emoji.name.toLowerCase().split(' ');
for (final word in nameWords) {
_nameIndex.putIfAbsent(word, () => []).add(emoji);
}
// 按标签建立索引
for (final tag in emoji.tags) {
_tagIndex.putIfAbsent(tag.toLowerCase(), () => []).add(emoji);
}
}
}
List<EmojiItem> search(String query) {
final queryWords = query.toLowerCase().split(' ');
final results = <EmojiItem>{};
for (final word in queryWords) {
results.addAll(_nameIndex[word] ?? []);
results.addAll(_tagIndex[word] ?? []);
}
return results.toList();
}
}
4. 内存管理
dart
// 及时释放资源
@override
void dispose() {
_searchController.dispose();
_packsTabController.dispose();
_searchTimer?.cancel();
super.dispose();
}
// 使用弱引用管理大量对象
class EmojiCache {
final Map<String, WeakReference<Uint8List>> _cache = {};
Future<Uint8List?> getImageBytes(String path) async {
final weakRef = _cache[path];
final cached = weakRef?.target;
if (cached != null) {
return cached;
}
try {
final file = File(path);
if (await file.exists()) {
final bytes = await file.readAsBytes();
_cache[path] = WeakReference(bytes);
return bytes;
}
} catch (e) {
print('Error loading image: $e');
}
return null;
}
void clearCache() {
_cache.clear();
}
}
测试指南
1. 单元测试
dart
// test/models/emoji_pack_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:emoji_manager/models/emoji_pack.dart';
void main() {
group('EmojiPack Tests', () {
test('should calculate total size correctly', () {
final emojis = [
EmojiItem(id: '1', name: 'emoji1', filePath: '', fileName: '',
addedAt: DateTime.now(), fileSize: 100),
EmojiItem(id: '2', name: 'emoji2', filePath: '', fileName: '',
addedAt: DateTime.now(), fileSize: 200),
];
final pack = EmojiPack(
id: '1',
name: 'Test Pack',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
emojis: emojis,
);
expect(pack.totalSize, equals(300.0));
expect(pack.emojiCount, equals(2));
});
test('should create copy with updated values', () {
final original = EmojiPack(
id: '1',
name: 'Original',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final updated = original.copyWith(name: 'Updated');
expect(updated.name, equals('Updated'));
expect(updated.id, equals('1'));
});
});
}
2. Widget测试
dart
// test/widgets/emoji_card_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:emoji_manager/main.dart';
void main() {
testWidgets('EmojiCard displays correct information', (tester) async {
final emoji = EmojiItem(
id: '1',
name: 'Test Emoji',
filePath: 'test.png',
fileName: 'test.png',
addedAt: DateTime.now(),
fileSize: 50.0,
usageCount: 5,
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: EmojiCard(emoji: emoji),
),
),
);
expect(find.text('Test Emoji'), findsOneWidget);
expect(find.text('50.0 KB'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
});
}
3. 集成测试
dart
// integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:emoji_manager/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Emoji Manager App Tests', () {
testWidgets('should add new emoji pack', (tester) async {
app.main();
await tester.pumpAndSettle();
// 点击添加按钮
await tester.tap(find.byType(FloatingActionButton));
await tester.pumpAndSettle();
// 填写表单
await tester.enterText(find.byKey(const Key('name_field')), 'Test Pack');
await tester.enterText(find.byKey(const Key('description_field')), 'Test Description');
// 保存表情包
await tester.tap(find.text('保存'));
await tester.pumpAndSettle();
// 验证表情包已添加
expect(find.text('Test Pack'), findsOneWidget);
});
testWidgets('should navigate between tabs', (tester) async {
app.main();
await tester.pumpAndSettle();
// 点击浏览标签
await tester.tap(find.text('浏览'));
await tester.pumpAndSettle();
expect(find.text('表情浏览'), findsOneWidget);
// 点击收藏标签
await tester.tap(find.text('收藏'));
await tester.pumpAndSettle();
expect(find.text('我的收藏'), findsOneWidget);
});
});
}
部署指南
1. Android部署
bash
# 构建APK
flutter build apk --release
# 构建App Bundle(推荐用于Google Play)
flutter build appbundle --release
# 安装到设备
flutter install
2. iOS部署
bash
# 构建iOS应用
flutter build ios --release
# 使用Xcode打开项目进行签名和发布
open ios/Runner.xcworkspace
3. Web部署
bash
# 构建Web版本
flutter build web --release
# 部署到服务器
# 将build/web目录下的文件上传到Web服务器
4. 桌面应用部署
bash
# Windows
flutter build windows --release
# macOS
flutter build macos --release
# Linux
flutter build linux --release
项目总结
技术亮点
- 简洁实用的设计:专注核心功能,界面简洁直观
- 完整的管理功能:表情包创建、分类、搜索、收藏一应俱全
- 优秀的用户体验:一键复制、长按选项、使用统计等贴心功能
- 灵活的视图模式:支持网格和列表两种浏览方式
- 强大的搜索功能:支持名称、标签多维度搜索
- 扩展性强:提供了丰富的功能扩展建议
学习价值
- Flutter基础:Widget组合、状态管理、导航等核心概念
- UI设计:Material Design 3组件使用和自定义样式
- 数据管理:模型设计、列表操作、搜索筛选
- 文件操作:图片加载、文件管理、数据持久化
- 用户体验:交互设计、动画效果、性能优化
这个表情包本地管理器应用展示了Flutter在工具类应用开发中的实用性,从简单的需求出发,构建了一个功能完整、体验良好的应用。通过这个项目,开发者可以学习到如何用Flutter构建实用的工具应用,掌握文件管理、数据处理、用户交互等关键技能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net