Flutter for OpenHarmony二维码扫描App实战 - 二维码样式设置实现
摘要 本文介绍了二维码样式设置页面的实现方案,采用Flutter框架开发。页面包含五个核心功能模块:顶部预览区、前景色选择、背景色选择、大小调节和Logo开关。通过GetX状态管理实现实时预览效果,用户操作会立即反映在预览区。颜色选择器提供多种预设颜色选项,大小调节采用Slider控件实现精确控制,Logo开关则使用SwitchListTile组件。页面布局采用响应式设计,适配不同屏幕尺寸,确保良
生成二维码的时候,很多用户不满足于黑白的默认样式,想要个性化定制。这篇文章我来讲解二维码样式设置页面的实现,包括颜色选择、大小调节、Logo添加等功能。
功能入口
样式设置页面的入口在生成二维码的流程中。用户填完内容后,可以点击"样式设置"按钮进入这个页面,调整二维码的外观,然后再生成。
页面设计思路
我把这个页面分成五个部分:顶部预览区、前景色选择、背景色选择、大小调节、Logo开关。用户调整任何设置都能在预览区实时看到效果。
先看导入和类定义:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'generate_controller.dart';
class QrStyleView extends StatelessWidget {
const QrStyleView({super.key});
这个页面用StatelessWidget,因为状态都在GenerateController里管理。通过GetX的响应式变量,设置变化时UI自动更新。
获取控制器
build方法开始先获取控制器实例:
Widget build(BuildContext context) {
final controller = Get.find<GenerateController>();
return Scaffold(
appBar: AppBar(title: const Text('二维码样式')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GenerateController是生成模块的控制器,里面定义了二维码颜色、背景色、大小、是否有Logo等响应式变量。Get.find获取之前注册的实例。
用SingleChildScrollView是因为内容比较多,小屏幕设备可能需要滚动。
预览区域
页面顶部放一个二维码预览,让用户实时看到效果:
Center(
child: Container(
width: 150.w,
height: 150.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey[300]!),
),
child: Obx(() => Icon(
Icons.qr_code_2,
size: 100.sp,
color: controller.qrColor.value,
)),
),
),
预览区域的设计考虑了多个方面:
- 150x150的尺寸 不会太大占空间,也够看清效果
- 白色背景加灰色边框 让预览区域有明确的边界
- Obx包裹Icon 监听qrColor变化,颜色改变时图标颜色跟着变
目前用Icon模拟,实际项目中应该用qr_flutter生成真正的二维码,并应用选中的颜色。
前景色选择器
二维码的前景色(就是那些黑色方块的颜色)选择:
SizedBox(height: 24.h),
Text('二维码颜色', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 12.h),
Wrap(
spacing: 12.w,
runSpacing: 12.h,
children: [
Colors.black,
Colors.blue,
Colors.red,
Colors.green,
Colors.purple,
Colors.orange,
Colors.teal,
Colors.indigo,
].map((color) => _ColorButton(
color: color,
isSelected: controller.qrColor.value == color,
onTap: () => controller.setQrColor(color),
)).toList(),
),
颜色选择器的实现有几个关键点:
- Wrap组件 让颜色按钮自动换行,适应不同屏幕宽度
- spacing和runSpacing 设置水平和垂直间距
- 预设8种颜色 覆盖常用色系,黑色放第一个作为默认
- map遍历生成按钮 每个颜色对应一个_ColorButton组件
_ColorButton是自定义组件,后面会讲实现。传入颜色值、是否选中、点击回调三个参数。
背景色选择器
背景色的选择和前景色类似,但颜色选项不同:
SizedBox(height: 24.h),
Text('背景颜色', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 12.h),
Wrap(
spacing: 12.w,
runSpacing: 12.h,
children: [
Colors.white,
Colors.grey[100]!,
Colors.yellow[50]!,
Colors.blue[50]!,
Colors.pink[50]!,
Colors.green[50]!,
].map((color) => _ColorButton(
color: color,
isSelected: controller.bgColor.value == color,
onTap: () => controller.setBgColor(color),
)).toList(),
),
背景色我选了一些浅色:
- 白色 最常用的背景色
- 浅灰 比纯白柔和一点
- 各种50色阶的浅色 淡淡的颜色,不会影响二维码识别
注意Colors.grey[100]后面要加感叹号,因为返回的是Color?类型,需要断言非空。
大小调节滑块
用Slider让用户调节二维码大小:
SizedBox(height: 24.h),
Text('二维码大小', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 12.h),
Obx(() => Slider(
value: controller.qrSize.value,
min: 100,
max: 300,
divisions: 4,
label: '${controller.qrSize.value.toInt()}',
onChanged: controller.setSize,
)),
Slider的配置需要仔细设置:
- value 当前值,绑定到控制器的qrSize
- min/max 范围100到300,单位是逻辑像素
- divisions: 4 分成4档,只能选100、150、200、250、300
- label 拖动时显示当前值,toInt去掉小数
- onChanged 值变化时调用控制器方法更新
Obx包裹整个Slider,qrSize变化时滑块位置自动更新。
Logo开关
用SwitchListTile实现Logo开关:
SizedBox(height: 24.h),
Text('中心Logo', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 12.h),
Obx(() => SwitchListTile(
title: const Text('添加Logo'),
subtitle: const Text('在二维码中心添加自定义Logo'),
value: controller.hasLogo.value,
onChanged: (_) => controller.toggleLogo(),
)),
SwitchListTile是Material Design的标准组件,集成了标题、副标题和开关。比自己用Row组合更规范。
onChanged的参数是新的开关状态,但我们用toggleLogo方法切换,所以用下划线忽略参数。
if (controller.hasLogo.value)
OutlinedButton.icon(
onPressed: () {
Get.snackbar('提示', '选择Logo图片', snackPosition: SnackPosition.BOTTOM);
},
icon: const Icon(Icons.add_photo_alternate),
label: const Text('选择Logo'),
),
],
),
),
开关打开后显示"选择Logo"按钮。用if条件渲染,开关关闭时按钮不显示。
实际项目中点击这个按钮应该打开图片选择器,让用户从相册选择Logo图片。
底部保存按钮
页面底部固定一个保存按钮:
bottomNavigationBar: SafeArea(
child: Padding(
padding: EdgeInsets.all(16.w),
child: ElevatedButton(
onPressed: () {
Get.back();
Get.snackbar('成功', '样式已保存', snackPosition: SnackPosition.BOTTOM);
},
style: ElevatedButton.styleFrom(
minimumSize: Size(double.infinity, 48.h),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
child: const Text('保存设置'),
),
),
),
);
}
}
点击保存后返回上一页,样式设置已经保存在控制器里了。因为用的是GetX的响应式变量,返回后生成页面会自动使用新的样式设置。
_ColorButton组件实现
颜色选择按钮是个独立组件:
class _ColorButton extends StatelessWidget {
final Color color;
final bool isSelected;
final VoidCallback onTap;
const _ColorButton({
required this.color,
required this.isSelected,
required this.onTap,
});
三个必需参数:颜色值、是否选中、点击回调。用下划线开头表示私有组件。
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? Theme.of(context).primaryColor : Colors.grey[300]!,
width: isSelected ? 3 : 1,
),
),
按钮设计成圆形,40x40的大小刚好适合手指点击。选中状态通过边框区分:
- 选中时 边框用主题色,宽度3
- 未选中时 边框用浅灰色,宽度1
child: isSelected
? Icon(Icons.check, size: 20.sp, color: color == Colors.white ? Colors.black : Colors.white)
: null,
),
);
}
}
选中的按钮中间显示一个勾,表示当前选中。勾的颜色要和背景形成对比:白色背景用黑色勾,其他颜色背景用白色勾。
控制器中的样式变量
GenerateController里定义了样式相关的响应式变量:
class GenerateController extends GetxController {
final qrColor = Colors.black.obs;
final bgColor = Colors.white.obs;
final qrSize = 200.0.obs;
final hasLogo = false.obs;
Rx<File?> logoFile = Rx<File?>(null);
void setQrColor(Color color) => qrColor.value = color;
void setBgColor(Color color) => bgColor.value = color;
void setSize(double size) => qrSize.value = size;
void toggleLogo() => hasLogo.value = !hasLogo.value;
void setLogoFile(File file) {
logoFile.value = file;
hasLogo.value = true;
}
void clearLogo() {
logoFile.value = null;
hasLogo.value = false;
}
}
每个变量都用.obs变成响应式的,修改时UI自动更新。方法都很简单,就是更新对应的值。
这些设置在生成二维码时会用到,传给qr_flutter的QrImage组件。
颜色选择的扩展
目前是预设颜色,还可以加自定义颜色功能:
void _showColorPicker(BuildContext context) {
Color selectedColor = controller.qrColor.value;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择颜色'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 色相选择
SizedBox(
height: 200,
child: ColorPicker(
pickerColor: selectedColor,
onColorChanged: (color) => selectedColor = color,
enableAlpha: false,
displayThumbColor: true,
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
controller.setQrColor(selectedColor);
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
可以用flutter_colorpicker包提供完整的颜色选择器,让用户选择任意颜色。
在颜色列表末尾加一个"自定义"按钮:
GestureDetector(
onTap: () => _showColorPicker(context),
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey[300]!),
gradient: const SweepGradient(
colors: [Colors.red, Colors.yellow, Colors.green, Colors.blue, Colors.red],
),
),
child: const Icon(Icons.add, color: Colors.white, size: 20),
),
),
用渐变色表示"自定义颜色",很直观。
样式预设功能
除了单独设置每个选项,还可以提供一些预设样式:
final presets = [
{'name': '经典黑白', 'qrColor': Colors.black, 'bgColor': Colors.white},
{'name': '科技蓝', 'qrColor': Colors.blue, 'bgColor': Colors.blue[50]},
{'name': '活力橙', 'qrColor': Colors.orange, 'bgColor': Colors.orange[50]},
{'name': '清新绿', 'qrColor': Colors.green, 'bgColor': Colors.green[50]},
{'name': '优雅紫', 'qrColor': Colors.purple, 'bgColor': Colors.purple[50]},
];
Widget _buildPresetSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('快速预设', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: presets.map((preset) => ActionChip(
label: Text(preset['name'] as String),
onPressed: () {
controller.setQrColor(preset['qrColor'] as Color);
controller.setBgColor(preset['bgColor'] as Color);
},
)).toList(),
),
],
);
}
用户点击预设就一键应用整套样式,比一个个选方便。
二维码识别率考虑
样式设置要注意不能影响二维码的识别率:
颜色对比度:前景色和背景色对比度要够,太接近的颜色会导致识别失败。可以加个检测,对比度不够时提示用户:
double _calculateContrast(Color fg, Color bg) {
final fgLuminance = fg.computeLuminance();
final bgLuminance = bg.computeLuminance();
final lighter = fgLuminance > bgLuminance ? fgLuminance : bgLuminance;
final darker = fgLuminance > bgLuminance ? bgLuminance : fgLuminance;
return (lighter + 0.05) / (darker + 0.05);
}
void _checkContrast() {
final contrast = _calculateContrast(controller.qrColor.value, controller.bgColor.value);
if (contrast < 4.5) {
Get.snackbar(
'警告',
'颜色对比度较低,可能影响扫描识别',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
}
}
Logo大小:Logo不能太大,一般不超过二维码面积的30%,否则会遮挡太多信息导致无法识别:
// 在生成二维码时检查Logo大小
void _validateLogoSize(File logoFile, double qrSize) async {
final bytes = await logoFile.readAsBytes();
final image = await decodeImageFromList(bytes);
final logoArea = image.width * image.height;
final qrArea = qrSize * qrSize;
if (logoArea > qrArea * 0.3) {
Get.snackbar(
'提示',
'Logo图片较大,建议使用更小的图片',
snackPosition: SnackPosition.BOTTOM,
);
}
}
纠错级别:加Logo时要用高纠错级别(H级),能容忍30%的数据损坏:
QrImage(
data: content,
version: QrVersions.auto,
errorCorrectionLevel: hasLogo ? QrErrorCorrectLevel.H : QrErrorCorrectLevel.M,
foregroundColor: qrColor,
backgroundColor: bgColor,
embeddedImage: hasLogo ? FileImage(logoFile!) : null,
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(qrSize * 0.2, qrSize * 0.2),
),
)
样式持久化
用户设置的样式可以保存到本地,下次打开应用时恢复:
Future<void> _saveStyleSettings() async {
final prefs = await SharedPreferences.getInstance();
prefs.setInt('qrColor', qrColor.value.value);
prefs.setInt('bgColor', bgColor.value.value);
prefs.setDouble('qrSize', qrSize.value);
prefs.setBool('hasLogo', hasLogo.value);
}
Future<void> _loadStyleSettings() async {
final prefs = await SharedPreferences.getInstance();
qrColor.value = Color(prefs.getInt('qrColor') ?? Colors.black.value);
bgColor.value = Color(prefs.getInt('bgColor') ?? Colors.white.value);
qrSize.value = prefs.getDouble('qrSize') ?? 200.0;
hasLogo.value = prefs.getBool('hasLogo') ?? false;
}
void onInit() {
super.onInit();
_loadStyleSettings();
}
在控制器的onInit里调用_loadStyleSettings,在样式变化时调用_saveStyleSettings。
Logo选择实现
选择Logo图片需要用image_picker包:
Future<void> _pickLogo() async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
maxWidth: 200,
maxHeight: 200,
);
if (image != null) {
controller.setLogoFile(File(image.path));
}
}
限制图片最大尺寸200x200,避免Logo太大。
选择后显示预览:
Obx(() => controller.logoFile.value != null
? Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: Image.file(
controller.logoFile.value!,
width: 60.w,
height: 60.w,
fit: BoxFit.cover,
),
),
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onTap: controller.clearLogo,
child: Container(
padding: EdgeInsets.all(2.w),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: Icon(Icons.close, size: 12.sp, color: Colors.white),
),
),
),
],
)
: OutlinedButton.icon(
onPressed: _pickLogo,
icon: const Icon(Icons.add_photo_alternate),
label: const Text('选择Logo'),
),
),
显示Logo缩略图,右上角有删除按钮。
小结
二维码样式设置页面让用户能个性化定制二维码外观。实现上用GetX的响应式变量管理状态,颜色选择器用Wrap加自定义按钮,大小调节用Slider,Logo开关用SwitchListTile。
设计上要注意颜色对比度和Logo大小,确保生成的二维码能正常识别。功能上可以扩展自定义颜色、预设样式、样式持久化等。
这是本系列文章的最后一篇样式相关内容,希望对你有帮助。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)