生成二维码的时候,很多用户不满足于黑白的默认样式,想要个性化定制。这篇文章我来讲解二维码样式设置页面的实现,包括颜色选择、大小调节、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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐