Flutter实战:打造表情包制作器应用

前言

表情包制作器是一款实用的图片编辑工具,让用户可以轻松制作个性化的表情包。本文将带你从零开始,使用Flutter开发一个功能完整的表情包制作器,支持添加文字、贴纸、滤镜等功能。

应用特色

  • ✏️ 文字编辑:添加、编辑、移动文字
  • 🎨 文字样式:字号、颜色、粗体、旋转
  • 😊 贴纸系统:20种表情和图标贴纸
  • 🎭 滤镜效果:黑白、怀旧、反色、模糊等7种滤镜
  • 🎨 背景颜色:18种预设背景颜色
  • 📱 拖拽操作:直观的拖拽移动元素
  • 💾 导出分享:导出PNG图片并分享
  • 🖼️ 实时预览:所见即所得的编辑体验
  • 🎯 选中高亮:清晰的选中状态提示
  • 🔄 旋转功能:文字和贴纸支持旋转

效果展示

在这里插入图片描述
在这里插入图片描述

表情包制作器

文字功能

添加文字

编辑内容

调整字号

更改颜色

粗体样式

旋转角度

贴纸功能

表情贴纸

图标贴纸

拖拽移动

调整大小

旋转角度

滤镜效果

黑白滤镜

怀旧滤镜

反色滤镜

模糊效果

增亮效果

对比度

导出分享

PNG格式

高清导出

分享功能

数据模型设计

1. 文字元素

class TextElement {
  String text;
  Offset position;
  double fontSize;
  Color color;
  FontWeight fontWeight;
  String fontFamily;
  double rotation;
  TextAlign textAlign;

  TextElement({
    required this.text,
    required this.position,
    this.fontSize = 32,
    this.color = Colors.white,
    this.fontWeight = FontWeight.bold,
    this.fontFamily = 'default',
    this.rotation = 0,
    this.textAlign = TextAlign.center,
  });
}

2. 贴纸元素

class StickerElement {
  IconData icon;
  Offset position;
  double size;
  Color color;
  double rotation;

  StickerElement({
    required this.icon,
    required this.position,
    this.size = 60,
    this.color = Colors.white,
    this.rotation = 0,
  });
}

3. 滤镜类型

enum FilterType {
  none('无滤镜'),
  grayscale('黑白'),
  sepia('怀旧'),
  invert('反色'),
  blur('模糊'),
  brightness('增亮'),
  contrast('对比度');

  final String label;
  const FilterType(this.label);
}

核心功能实现

1. 添加文字

void _addText() {
  setState(() {
    _textElements.add(TextElement(
      text: '双击编辑',
      position: const Offset(150, 200),
    ));
    _selectedTextIndex = _textElements.length - 1;
    _selectedStickerIndex = null;
  });
}

2. 编辑文字

void _editText(int index) {
  final controller = TextEditingController(text: _textElements[index].text);

  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('编辑文字'),
      content: TextField(
        controller: controller,
        decoration: const InputDecoration(
          hintText: '输入文字',
          border: OutlineInputBorder(),
        ),
        maxLines: 3,
        autofocus: true,
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        FilledButton(
          onPressed: () {
            setState(() {
              _textElements[index].text = controller.text;
            });
            Navigator.pop(context);
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

3. 拖拽移动元素

GestureDetector(
  onPanUpdate: (details) {
    setState(() {
      element.position += details.delta;
    });
  },
  onTap: () {
    setState(() {
      _selectedTextIndex = index;
      _selectedStickerIndex = null;
    });
  },
  onDoubleTap: () => _editText(index),
  child: // 文字或贴纸
)

手势说明

  • onPanUpdate:拖拽移动
  • onTap:选中元素
  • onDoubleTap:编辑文字

4. 滤镜实现

使用ColorFilter矩阵实现各种滤镜效果:

ColorFilter _getColorFilter() {
  switch (_currentFilter) {
    case FilterType.grayscale:
      // 黑白滤镜
      return const ColorFilter.matrix([
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0.2126, 0.7152, 0.0722, 0, 0,
        0, 0, 0, 1, 0,
      ]);
    case FilterType.sepia:
      // 怀旧滤镜
      return const ColorFilter.matrix([
        0.393, 0.769, 0.189, 0, 0,
        0.349, 0.686, 0.168, 0, 0,
        0.272, 0.534, 0.131, 0, 0,
        0, 0, 0, 1, 0,
      ]);
    case FilterType.invert:
      // 反色滤镜
      return const ColorFilter.matrix([
        -1, 0, 0, 0, 255,
        0, -1, 0, 0, 255,
        0, 0, -1, 0, 255,
        0, 0, 0, 1, 0,
      ]);
    case FilterType.brightness:
      // 增亮滤镜
      return const ColorFilter.matrix([
        1.2, 0, 0, 0, 0,
        0, 1.2, 0, 0, 0,
        0, 0, 1.2, 0, 0,
        0, 0, 0, 1, 0,
      ]);
    case FilterType.contrast:
      // 对比度滤镜
      return const ColorFilter.matrix([
        1.5, 0, 0, 0, -0.25 * 255,
        0, 1.5, 0, 0, -0.25 * 255,
        0, 0, 1.5, 0, -0.25 * 255,
        0, 0, 0, 1, 0,
      ]);
    default:
      return const ColorFilter.mode(Colors.transparent, BlendMode.dst);
  }
}

ColorFilter矩阵说明

5×4矩阵,每行代表一个颜色通道的变换:

[R', G', B', A', offset]

R' = R×m[0] + G×m[1] + B×m[2] + A×m[3] + m[4]
G' = R×m[5] + G×m[6] + B×m[7] + A×m[8] + m[9]
B' = R×m[10] + G×m[11] + B×m[12] + A×m[13] + m[14]
A' = R×m[15] + G×m[16] + B×m[17] + A×m[18] + m[19]

黑白滤镜原理

灰度值 = 0.2126×R + 0.7152×G + 0.0722×B
将RGB三个通道都设置为相同的灰度值

5. 导出图片

使用RepaintBoundary捕获Widget为图片:

Future<void> _exportImage() async {
  try {
    final boundary = _canvasKey.currentContext!.findRenderObject()
        as RenderRepaintBoundary;
    final image = await boundary.toImage(pixelRatio: 3.0);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    final pngBytes = byteData!.buffer.asUint8List();

    // 分享图片
    await Share.shareXFiles(
      [XFile.fromData(pngBytes, mimeType: 'image/png', name: 'meme.png')],
      text: '我的表情包',
    );
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('导出失败: $e')),
      );
    }
  }
}

导出步骤

  1. 获取RepaintBoundary的RenderObject
  2. 调用toImage()转换为ui.Image
  3. 转换为PNG字节数据
  4. 使用share_plus分享

UI组件设计

1. 画布渲染

Widget _buildCanvas() {
  return Container(
    width: 400,
    height: 400,
    decoration: BoxDecoration(
      color: _backgroundColor,
      border: Border.all(color: Colors.grey),
    ),
    child: ColorFiltered(
      colorFilter: _getColorFilter(),
      child: Stack(
        children: [
          // 文字元素
          ..._textElements.asMap().entries.map((entry) {
            // 渲染文字
          }),
          // 贴纸元素
          ..._stickerElements.asMap().entries.map((entry) {
            // 渲染贴纸
          }),
        ],
      ),
    ),
  );
}

2. 文字渲染

Transform.rotate(
  angle: element.rotation,
  child: Container(
    padding: const EdgeInsets.all(8),
    decoration: BoxDecoration(
      border: _selectedTextIndex == index
          ? Border.all(color: Colors.blue, width: 2)
          : null,
      color: _selectedTextIndex == index
          ? Colors.blue.withOpacity(0.1)
          : null,
    ),
    child: Text(
      element.text,
      style: TextStyle(
        fontSize: element.fontSize,
        color: element.color,
        fontWeight: element.fontWeight,
        shadows: [
          Shadow(
            color: Colors.black.withOpacity(0.5),
            blurRadius: 2,
            offset: const Offset(1, 1),
          ),
        ],
      ),
      textAlign: element.textAlign,
    ),
  ),
)

文字效果

  • Transform.rotate:旋转
  • Shadow:阴影描边
  • 选中状态:蓝色边框和背景

3. 贴纸选择器

void _showStickerPicker() {
  showModalBottomSheet(
    context: context,
    builder: (context) => Container(
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text(
            '选择贴纸',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 5,
                mainAxisSpacing: 8,
                crossAxisSpacing: 8,
              ),
              itemCount: _stickers.length,
              itemBuilder: (context, index) {
                return InkWell(
                  onTap: () => _addSticker(_stickers[index]),
                  child: Container(
                    decoration: BoxDecoration(
                      color: Colors.grey.shade200,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Icon(
                      _stickers[index],
                      size: 40,
                      color: Colors.black87,
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

4. 文字样式编辑器

void _showTextStyleEditor() {
  if (_selectedTextIndex == null) return;

  final element = _textElements[_selectedTextIndex!];

  showModalBottomSheet(
    context: context,
    builder: (context) => StatefulBuilder(
      builder: (context, setModalState) {
        return Container(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              // 字号滑块
              Row(
                children: [
                  const Text('字号:'),
                  Expanded(
                    child: Slider(
                      value: element.fontSize,
                      min: 16,
                      max: 72,
                      divisions: 28,
                      label: element.fontSize.round().toString(),
                      onChanged: (value) {
                        setModalState(() {
                          element.fontSize = value;
                        });
                        setState(() {});
                      },
                    ),
                  ),
                ],
              ),
              // 颜色选择
              // 粗体和旋转按钮
            ],
          ),
        );
      },
    ),
  );
}

5. 工具栏

Widget _buildToolbar() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            _buildToolButton(
              icon: Icons.text_fields,
              label: '文字',
              onPressed: _addText,
            ),
            _buildToolButton(
              icon: Icons.emoji_emotions,
              label: '贴纸',
              onPressed: _showStickerPicker,
            ),
            _buildToolButton(
              icon: Icons.filter,
              label: '滤镜',
              onPressed: _showFilterPicker,
            ),
            _buildToolButton(
              icon: Icons.palette,
              label: '背景',
              onPressed: _showBackgroundPicker,
            ),
          ],
        ),
      ],
    ),
  );
}

技术要点详解

1. RepaintBoundary

用于捕获Widget为图片:

RepaintBoundary(
  key: _canvasKey,
  child: _buildCanvas(),
)

作用

  • 创建独立的渲染层
  • 可以转换为图片
  • 优化重绘性能

2. ColorFiltered

应用颜色滤镜:

ColorFiltered(
  colorFilter: _getColorFilter(),
  child: // 内容
)

3. Transform.rotate

旋转Widget:

Transform.rotate(
  angle: element.rotation, // 弧度
  child: // 内容
)

角度转换

  • 弧度 = 角度 × π / 180
  • 90° = π/2
  • 180° = π
  • 360° = 2π

4. GestureDetector手势

GestureDetector(
  onPanUpdate: (details) {
    // 拖拽移动
    element.position += details.delta;
  },
  onTap: () {
    // 单击选中
  },
  onDoubleTap: () {
    // 双击编辑
  },
  child: // 内容
)

5. StatefulBuilder

在BottomSheet中使用局部状态:

showModalBottomSheet(
  context: context,
  builder: (context) => StatefulBuilder(
    builder: (context, setModalState) {
      return // 内容
    },
  ),
)

滤镜效果详解

1. 黑白滤镜

灰度值 = 0.2126×R + 0.7152×G + 0.0722×B

这个公式基于人眼对不同颜色的敏感度。

2. 怀旧滤镜

R' = 0.393×R + 0.769×G + 0.189×B
G' = 0.349×R + 0.686×G + 0.168×B
B' = 0.272×R + 0.534×G + 0.131×B

产生偏黄褐色的复古效果。

3. 反色滤镜

R' = 255 - R
G' = 255 - G
B' = 255 - B

颜色取反,产生负片效果。

4. 增亮滤镜

R' = R × 1.2
G' = G × 1.2
B' = B × 1.2

所有颜色通道乘以1.2,整体变亮。

5. 对比度滤镜

R' = (R - 128) × 1.5 + 128
G' = (G - 128) × 1.5 + 128
B' = (B - 128) × 1.5 + 128

增强明暗对比。

功能扩展建议

1. 图片导入

import 'package:image_picker/image_picker.dart';

Future<void> _pickImage() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  
  if (image != null) {
    setState(() {
      _backgroundImage = File(image.path);
    });
  }
}

2. 更多字体

class TextElement {
  String fontFamily;
  
  static const List<String> fonts = [
    'Roboto',
    'Arial',
    'Times New Roman',
    'Courier New',
    'Comic Sans MS',
  ];
}

3. 图层管理

class Layer {
  String id;
  LayerType type;
  bool visible;
  double opacity;
  int zIndex;
  
  Layer({
    required this.id,
    required this.type,
    this.visible = true,
    this.opacity = 1.0,
    this.zIndex = 0,
  });
}

enum LayerType {
  text,
  sticker,
  image,
}

4. 撤销/重做

class HistoryManager {
  List<EditorState> _history = [];
  int _currentIndex = -1;
  
  void push(EditorState state) {
    _history = _history.sublist(0, _currentIndex + 1);
    _history.add(state);
    _currentIndex++;
  }
  
  EditorState? undo() {
    if (_currentIndex > 0) {
      _currentIndex--;
      return _history[_currentIndex];
    }
    return null;
  }
  
  EditorState? redo() {
    if (_currentIndex < _history.length - 1) {
      _currentIndex++;
      return _history[_currentIndex];
    }
    return null;
  }
}

5. 模板系统

class MemeTemplate {
  String id;
  String name;
  String thumbnail;
  List<TextElement> textElements;
  List<StickerElement> stickerElements;
  Color backgroundColor;
  
  MemeTemplate({
    required this.id,
    required this.name,
    required this.thumbnail,
    required this.textElements,
    required this.stickerElements,
    required this.backgroundColor,
  });
}

List<MemeTemplate> templates = [
  MemeTemplate(
    id: 'template1',
    name: '经典上下文字',
    thumbnail: 'assets/templates/template1.png',
    textElements: [
      TextElement(text: '上方文字', position: Offset(200, 50)),
      TextElement(text: '下方文字', position: Offset(200, 350)),
    ],
    stickerElements: [],
    backgroundColor: Colors.white,
  ),
];

6. 自定义贴纸

import 'dart:io';

class CustomSticker {
  File imageFile;
  Offset position;
  double size;
  double rotation;
  
  CustomSticker({
    required this.imageFile,
    required this.position,
    this.size = 100,
    this.rotation = 0,
  });
}

Future<void> _addCustomSticker() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  
  if (image != null) {
    setState(() {
      _customStickers.add(CustomSticker(
        imageFile: File(image.path),
        position: Offset(150, 200),
      ));
    });
  }
}

7. 保存到相册

import 'package:image_gallery_saver/image_gallery_saver.dart';

Future<void> _saveToGallery() async {
  try {
    final boundary = _canvasKey.currentContext!.findRenderObject()
        as RenderRepaintBoundary;
    final image = await boundary.toImage(pixelRatio: 3.0);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    final pngBytes = byteData!.buffer.asUint8List();
    
    final result = await ImageGallerySaver.saveImage(
      pngBytes,
      quality: 100,
      name: 'meme_${DateTime.now().millisecondsSinceEpoch}',
    );
    
    if (result['isSuccess']) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('已保存到相册')),
      );
    }
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('保存失败: $e')),
    );
  }
}

性能优化

1. 限制元素数量

void _addText() {
  if (_textElements.length >= 10) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('文字元素不能超过10个')),
    );
    return;
  }
  // 添加文字
}

2. 使用const

const Text('文字')
const SizedBox(height: 16)
const Icon(Icons.text_fields)

3. 避免不必要的重建

// 使用RepaintBoundary隔离重绘区域
RepaintBoundary(
  child: _buildCanvas(),
)

常见问题解答

Q1: 如何添加自定义字体?

A: 在pubspec.yaml中添加字体文件,然后在TextStyle中使用fontFamily属性。

Q2: 导出的图片质量如何控制?

A: 通过toImage()的pixelRatio参数控制,值越大质量越高。

Q3: 如何实现更多滤镜效果?

A: 调整ColorFilter矩阵的参数,或使用ImageFilter实现模糊等效果。

项目结构

lib/
├── main.dart                    # 主程序入口
├── models/
│   ├── text_element.dart       # 文字元素模型
│   ├── sticker_element.dart    # 贴纸元素模型
│   └── filter_type.dart        # 滤镜类型
├── screens/
│   ├── editor_page.dart        # 编辑器页面
│   └── template_page.dart      # 模板页面
├── widgets/
│   ├── canvas_widget.dart      # 画布组件
│   ├── toolbar_widget.dart     # 工具栏组件
│   └── style_editor.dart       # 样式编辑器
└── utils/
    ├── image_exporter.dart     # 图片导出工具
    └── filter_helper.dart      # 滤镜辅助工具

总结

本文实现了一个功能完整的表情包制作器应用,涵盖了以下核心技术:

  1. 文字编辑:添加、编辑、样式调整
  2. 贴纸系统:图标贴纸的添加和管理
  3. 滤镜效果:ColorFilter矩阵实现多种滤镜
  4. 拖拽操作:GestureDetector实现元素移动
  5. 图片导出:RepaintBoundary捕获Widget为图片
  6. 分享功能:share_plus实现图片分享
  7. 旋转变换:Transform.rotate实现元素旋转

通过本项目,你不仅学会了如何实现表情包制作器,还掌握了Flutter中图片处理、手势识别、自定义绘制的核心技术。这些知识可以应用到更多图片编辑和创意工具的开发。

释放你的创意,制作独特的表情包!
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐