扫描设置是二维码应用中非常重要的功能模块,它让用户可以根据自己的使用习惯来定制扫描行为。有些用户喜欢扫描成功后有声音提示,有些用户则偏好静音模式;有些用户希望扫描到网址后自动打开浏览器,有些用户则更谨慎,想先看看链接再决定是否打开。这篇文章详细介绍扫描设置页面的实现,包括各种开关选项的设计和响应式状态管理。
请添加图片描述

扫描设置的功能规划

在开始写代码之前,我先梳理了一下扫描设置需要包含哪些选项。根据用户调研和竞品分析,我确定了以下四个核心设置项:

扫描提示音:扫描成功时是否播放提示音。这个功能在嘈杂环境中特别有用,用户可以通过声音确认扫描成功,不需要一直盯着屏幕看。但在安静的场合,比如图书馆或会议室,用户可能希望关闭提示音。

震动反馈:扫描成功时是否震动。和提示音类似,震动反馈也是一种确认机制。对于听力不便的用户,震动反馈尤其重要。有些用户同时开启声音和震动,双重确认更安心。

自动打开链接:扫描到网址时是否自动打开浏览器。这个选项有一定的安全风险,如果扫描到恶意链接自动打开可能会有问题,所以默认是关闭的。但对于信任的场景,比如扫描自己生成的二维码,自动打开能省去一步操作。

保存到历史:是否自动保存扫描记录。大多数用户需要这个功能,方便以后查看扫描过的内容。但有些注重隐私的用户可能不希望留下记录,所以提供关闭选项。

文件结构和依赖导入

扫描设置页面的代码位于 lib/app/modules/settings/scan_settings_view.dart,先来看文件开头的导入部分:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../data/services/storage_service.dart';

这四行导入语句各有用途。flutter/material.dart 是 Flutter 的核心 UI 库,提供了 Scaffold、AppBar、ListView、Card、SwitchListTile 等我们需要用到的组件。get 是 GetX 框架的核心包,我们用它来获取 StorageService 实例和实现响应式状态更新。

flutter_screenutil 是屏幕适配库,让我们可以使用 .w.h.sp.r 等单位,确保界面在不同尺寸的设备上都能正确显示。最后导入的 storage_service.dart 是我们自定义的存储服务,管理着所有的设置项状态。

ScanSettingsView 类的定义

接下来是页面类的定义:

class ScanSettingsView extends StatelessWidget {
  const ScanSettingsView({super.key});

  
  Widget build(BuildContext context) {
    final storage = Get.find<StorageService>();

ScanSettingsView 继承自 StatelessWidget,因为页面本身不需要维护状态,所有状态都存储在 StorageService 中。使用 StatelessWidget 而不是 StatefulWidget 可以让代码更简洁,也符合 GetX 的响应式编程理念。

构造函数使用 const 修饰,表示这个组件可以作为编译时常量。super.key 是 Dart 3.0 引入的简化写法,等价于之前的 Key? key 参数传递。

在 build 方法的开头,通过 Get.find<StorageService>() 获取已注册的 StorageService 实例。这个服务在应用启动时就已经通过 Get.put 注册了,所以这里可以直接获取。Get.find 是 GetX 的依赖注入机制,它会从容器中查找指定类型的实例并返回。

Scaffold 和 AppBar 的构建

页面的整体结构使用 Scaffold 组件:

    return Scaffold(
      appBar: AppBar(title: const Text('扫描设置')),
      body: ListView(
        padding: EdgeInsets.all(16.w),
        children: [
          Card(
            child: Column(
              children: [

Scaffold 是 Material Design 的基础页面结构,提供了 appBar、body、bottomNavigationBar 等常用布局位置。这里我们只用到了 appBar 和 body。

AppBar 的配置很简单,只设置了 title 为"扫描设置"。因为这个页面是从设置主页面跳转过来的,Flutter 会自动添加返回按钮,不需要我们手动处理。title 使用 const Text 可以获得编译时优化。

body 部分使用 ListView 作为容器,这样当内容超出屏幕时可以滚动。padding 设置为 16.w,也就是四周都有 16 个逻辑像素的内边距。使用 .w 单位可以让内边距根据屏幕宽度自适应。

ListView 的 children 中首先是一个 Card 组件,它会给内容添加圆角和阴影效果,让设置项看起来更有层次感。Card 内部使用 Column 垂直排列多个设置项。

扫描提示音开关

第一个设置项是扫描提示音:

                Obx(() => SwitchListTile(
                  title: const Text('扫描提示音'),
                  subtitle: const Text('扫描成功时播放提示音'),
                  value: storage.soundEnabled.value,
                  onChanged: (_) => storage.toggleSound(),
                )),

这段代码展示了 GetX 响应式编程的典型用法。Obx 是 GetX 提供的响应式组件,它会自动监听内部使用的响应式变量,当变量值变化时自动重建。这里监听的是 storage.soundEnabled,当用户切换开关时,soundEnabled 的值会变化,Obx 检测到变化后会重建 SwitchListTile,更新开关的显示状态。

SwitchListTile 是 Material Design 的标准开关列表项组件,它组合了 ListTile 和 Switch,非常适合用于设置页面。title 是主标题"扫描提示音",subtitle 是副标题说明"扫描成功时播放提示音",让用户清楚这个开关的作用。

value 属性绑定到 storage.soundEnabled.value,注意这里要用 .value 获取实际的布尔值。onChanged 回调在用户切换开关时触发,我们调用 storage.toggleSound() 方法来切换状态。参数 _ 表示我们不需要使用回调传入的新值,因为 toggleSound 方法内部会自己处理取反逻辑。

分隔线的添加

设置项之间需要分隔线来区分:

                const Divider(height: 1),

Divider 是 Material Design 的分隔线组件,height 设置为 1 表示分隔线只占用 1 像素的高度,看起来更精致。使用 const 修饰可以让 Flutter 复用这个组件实例,提高性能。

分隔线的作用是在视觉上区分不同的设置项,让用户更容易找到想要修改的选项。如果没有分隔线,多个设置项挤在一起会显得很拥挤,用户体验不好。

震动反馈开关

第二个设置项是震动反馈:

                Obx(() => SwitchListTile(
                  title: const Text('震动反馈'),
                  subtitle: const Text('扫描成功时震动提示'),
                  value: storage.vibrationEnabled.value,
                  onChanged: (_) => storage.toggleVibration(),
                )),
                const Divider(height: 1),

震动反馈开关的实现和提示音开关完全一样,只是绑定的状态变量不同。这里绑定的是 storage.vibrationEnabled,调用的方法是 storage.toggleVibration()

这种一致的实现方式让代码更容易维护。如果以后需要修改开关的样式或行为,只需要改一处就能影响所有开关。同时,用户在使用时也会感到熟悉,因为所有开关的交互方式都是一样的。

震动反馈在实际项目中需要调用系统 API 来实现。Flutter 中可以使用 HapticFeedback 类或者 vibration 插件来触发震动。在 OpenHarmony 平台上,需要确保震动功能的兼容性。

自动打开链接开关

第三个设置项是自动打开链接:

                Obx(() => SwitchListTile(
                  title: const Text('自动打开链接'),
                  subtitle: const Text('扫描到网址时自动打开浏览器'),
                  value: storage.autoOpenLinks.value,
                  onChanged: (_) => storage.toggleAutoOpenLinks(),
                )),
                const Divider(height: 1),

自动打开链接是一个需要谨慎处理的功能。虽然它能提升用户体验,但也存在安全风险。如果用户扫描到一个恶意网址,自动打开可能会导致钓鱼攻击或恶意软件下载。

因此,这个选项默认是关闭的,让用户自己决定是否开启。在 subtitle 中明确说明了功能的作用,让用户在开启前了解可能的风险。

在实际实现中,即使开启了自动打开,也应该做一些基本的安全检查,比如检查 URL 是否是 HTTPS 协议,是否在已知的恶意网站黑名单中等。

保存到历史开关

最后一个设置项是保存到历史:

                Obx(() => SwitchListTile(
                  title: const Text('保存到历史'),
                  subtitle: const Text('自动保存扫描记录'),
                  value: storage.saveToHistory.value,
                  onChanged: (_) => storage.toggleSaveToHistory(),
                )),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

保存到历史功能对大多数用户来说是必需的,所以默认是开启的。但有些用户可能出于隐私考虑不希望保留扫描记录,提供关闭选项可以满足这部分用户的需求。

这里的 Column 和 Card 在最后一个设置项后闭合,ListView 的 children 列表也在这里结束。整个页面的结构是:Scaffold > ListView > Card > Column > 多个 SwitchListTile。

这种嵌套结构在 Flutter 中很常见,虽然看起来层级较深,但每一层都有明确的职责:Scaffold 提供页面框架,ListView 提供滚动能力,Card 提供视觉分组,Column 提供垂直布局,SwitchListTile 提供具体的交互控件。

StorageService 中的状态定义

扫描设置的状态存储在 StorageService 中,来看一下相关的代码:

class StorageService extends GetxService {
  final RxBool soundEnabled = true.obs;
  final RxBool vibrationEnabled = true.obs;
  final RxBool autoOpenLinks = false.obs;
  final RxBool saveToHistory = true.obs;

StorageService 继承自 GetxService,这是 GetX 提供的服务基类。和普通的 GetxController 不同,GetxService 不会被自动销毁,适合用于全局服务。

四个设置项都定义为 RxBool 类型,这是 GetX 的响应式布尔类型。.obs 是 GetX 的扩展方法,可以把普通值转换为响应式对象。当这些值变化时,所有监听它们的 Obx 组件都会自动更新。

默认值的设置是经过考虑的:soundEnabled 和 vibrationEnabled 默认开启,因为大多数用户需要扫描反馈;autoOpenLinks 默认关闭,因为有安全风险;saveToHistory 默认开启,因为历史记录功能很常用。

状态切换方法的实现

StorageService 中还定义了切换状态的方法:

  void toggleSound() => soundEnabled.value = !soundEnabled.value;
  void toggleVibration() => vibrationEnabled.value = !vibrationEnabled.value;
  void toggleAutoOpenLinks() => autoOpenLinks.value = !autoOpenLinks.value;
  void toggleSaveToHistory() => saveToHistory.value = !saveToHistory.value;

这四个方法都使用箭头函数的简洁写法,每个方法只做一件事:把对应的布尔值取反。使用 .value 属性来修改响应式变量的值,这样才能触发 UI 更新。

如果直接写 soundEnabled = (!soundEnabled.value).obs,虽然语法上没问题,但不会触发响应式更新,因为这是在创建一个新的响应式对象,而不是修改现有对象的值。

这种设计遵循了单一职责原则,每个方法只负责一个设置项的切换。如果以后需要在切换时执行额外的逻辑,比如记录日志或同步到服务器,只需要修改对应的方法即可。

设置持久化的考虑

当前的实现把设置存储在内存中,应用关闭后设置会丢失。在实际项目中,需要把设置持久化到本地存储。可以使用 SharedPreferences 来实现:

import 'package:shared_preferences/shared_preferences.dart';

class StorageService extends GetxService {
  late SharedPreferences _prefs;
  
  final RxBool soundEnabled = true.obs;
  final RxBool vibrationEnabled = true.obs;
  final RxBool autoOpenLinks = false.obs;
  final RxBool saveToHistory = true.obs;

  Future<void> init() async {
    _prefs = await SharedPreferences.getInstance();
    soundEnabled.value = _prefs.getBool('soundEnabled') ?? true;
    vibrationEnabled.value = _prefs.getBool('vibrationEnabled') ?? true;
    autoOpenLinks.value = _prefs.getBool('autoOpenLinks') ?? false;
    saveToHistory.value = _prefs.getBool('saveToHistory') ?? true;
  }

SharedPreferences 是 Flutter 官方提供的本地存储插件,支持存储基本类型的数据。init 方法在应用启动时调用,从本地存储读取之前保存的设置值。?? 是空值合并运算符,如果读取的值为 null(比如第一次运行应用),就使用默认值。

保存设置到本地

切换设置时需要同时保存到本地:

  void toggleSound() {
    soundEnabled.value = !soundEnabled.value;
    _prefs.setBool('soundEnabled', soundEnabled.value);
  }
  
  void toggleVibration() {
    vibrationEnabled.value = !vibrationEnabled.value;
    _prefs.setBool('vibrationEnabled', vibrationEnabled.value);
  }
  
  void toggleAutoOpenLinks() {
    autoOpenLinks.value = !autoOpenLinks.value;
    _prefs.setBool('autoOpenLinks', autoOpenLinks.value);
  }
  
  void toggleSaveToHistory() {
    saveToHistory.value = !saveToHistory.value;
    _prefs.setBool('saveToHistory', saveToHistory.value);
  }

每个 toggle 方法在修改内存中的值后,还会调用 _prefs.setBool 把新值保存到本地。这样即使应用关闭重启,设置也会保持用户上次的选择。

SharedPreferences 的写入操作是异步的,但我们不需要等待它完成,因为内存中的值已经更新了,UI 会立即响应。即使写入失败,也只是下次启动时会恢复默认值,不会影响当前的使用。

扫描时使用设置

在扫描控制器中,需要根据设置来决定扫描成功后的行为:

class ScanController extends GetxController {
  final _storage = Get.find<StorageService>();
  final _historyService = Get.find<QrHistoryService>();

  void onCodeScanned(String code) {
    // 播放提示音
    if (_storage.soundEnabled.value) {
      _playBeepSound();
    }
    
    // 震动反馈
    if (_storage.vibrationEnabled.value) {
      HapticFeedback.mediumImpact();
    }
    
    // 保存到历史
    if (_storage.saveToHistory.value) {
      final record = QrRecord(
        content: code,
        type: QrRecord.detectType(code),
      );
      _historyService.addScanRecord(record);
    }

在 onCodeScanned 方法中,根据各个设置项的值来决定是否执行对应的操作。_storage.soundEnabled.value 获取当前的设置值,如果为 true 就播放提示音。

HapticFeedback.mediumImpact() 是 Flutter 提供的触觉反馈 API,会触发一次中等强度的震动。这个 API 在大多数平台上都能正常工作,包括 OpenHarmony。

保存到历史的逻辑也类似,只有当 saveToHistory 为 true 时才创建记录并保存。这样用户关闭保存功能后,扫描记录就不会被保留。

自动打开链接的处理

自动打开链接需要特殊处理:

    // 自动打开链接
    if (_storage.autoOpenLinks.value && 
        QrRecord.detectType(code) == QrType.url) {
      _openUrl(code);
    } else {
      // 跳转到结果页面
      Get.toNamed(Routes.SCAN_RESULT, arguments: record);
    }
  }

  Future<void> _openUrl(String url) async {
    final uri = Uri.parse(url);
    if (await canLaunchUrl(uri)) {
      await launchUrl(uri, mode: LaunchMode.externalApplication);
    }
  }
}

自动打开链接有两个条件:一是用户开启了 autoOpenLinks 设置,二是扫描到的内容确实是网址类型。只有同时满足这两个条件才会自动打开浏览器,否则跳转到结果页面让用户自己决定。

canLaunchUrllaunchUrl 是 url_launcher 插件提供的方法。canLaunchUrl 检查设备是否能打开这个 URL,launchUrl 执行实际的打开操作。LaunchMode.externalApplication 表示在外部应用(浏览器)中打开,而不是在应用内嵌的 WebView 中。

添加更多设置项

如果需要添加更多的扫描设置,可以按照相同的模式扩展。比如添加一个"连续扫描"设置:

// 在 StorageService 中添加
final RxBool continuousScan = false.obs;

void toggleContinuousScan() {
  continuousScan.value = !continuousScan.value;
  _prefs.setBool('continuousScan', continuousScan.value);
}

// 在 ScanSettingsView 中添加
Obx(() => SwitchListTile(
  title: const Text('连续扫描'),
  subtitle: const Text('扫描成功后继续扫描下一个'),
  value: storage.continuousScan.value,
  onChanged: (_) => storage.toggleContinuousScan(),
)),

连续扫描功能在批量扫描场景下很有用,用户不需要每次扫描后都点击"继续扫描"按钮。开启后,扫描成功会显示一个短暂的提示,然后自动继续扫描下一个二维码。

设置项的分组

当设置项较多时,可以按功能分组显示:

body: ListView(
  padding: EdgeInsets.all(16.w),
  children: [
    // 反馈设置组
    Text('扫描反馈', style: TextStyle(
      fontSize: 14.sp,
      fontWeight: FontWeight.w600,
      color: Colors.grey[600],
    )),
    SizedBox(height: 8.h),
    Card(
      child: Column(
        children: [
          // 提示音和震动开关
        ],
      ),
    ),
    SizedBox(height: 16.h),
    // 行为设置组
    Text('扫描行为', style: TextStyle(
      fontSize: 14.sp,
      fontWeight: FontWeight.w600,
      color: Colors.grey[600],
    )),
    SizedBox(height: 8.h),
    Card(
      child: Column(
        children: [
          // 自动打开链接和保存历史开关
        ],
      ),
    ),
  ],
),

分组显示让设置页面更有条理,用户可以快速找到想要修改的设置。每个分组有一个标题,使用较小的字号和灰色,不会太抢眼但能起到分隔作用。

设置说明的完善

对于一些可能引起困惑的设置,可以添加更详细的说明:

Obx(() => SwitchListTile(
  title: const Text('自动打开链接'),
  subtitle: const Text('扫描到网址时自动打开浏览器'),
  value: storage.autoOpenLinks.value,
  onChanged: (_) => storage.toggleAutoOpenLinks(),
)),
if (storage.autoOpenLinks.value)
  Padding(
    padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
    child: Row(
      children: [
        Icon(Icons.warning_amber, size: 16.sp, color: Colors.orange),
        SizedBox(width: 8.w),
        Expanded(
          child: Text(
            '注意:自动打开链接可能存在安全风险,请确保扫描的是可信来源的二维码。',
            style: TextStyle(fontSize: 12.sp, color: Colors.orange),
          ),
        ),
      ],
    ),
  ),

当用户开启自动打开链接时,显示一个警告提示,提醒用户注意安全风险。这种条件显示的提示可以帮助用户做出更明智的选择,同时不会在关闭状态下占用空间。

恢复默认设置

提供一个恢复默认设置的功能:

// 在页面底部添加
SizedBox(height: 24.h),
Center(
  child: TextButton(
    onPressed: () => _showResetDialog(context, storage),
    child: Text(
      '恢复默认设置',
      style: TextStyle(color: Colors.grey[600]),
    ),
  ),
),

void _showResetDialog(BuildContext context, StorageService storage) {
  Get.dialog(
    AlertDialog(
      title: const Text('恢复默认设置'),
      content: const Text('确定要将所有扫描设置恢复为默认值吗?'),
      actions: [
        TextButton(
          onPressed: () => Get.back(),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () {
            storage.resetScanSettings();
            Get.back();
            Get.snackbar('成功', '已恢复默认设置',
                snackPosition: SnackPosition.BOTTOM);
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

恢复默认设置功能让用户可以快速回到初始状态,不需要一个个手动调整。点击后会弹出确认对话框,避免误操作。确认后调用 storage.resetScanSettings() 方法重置所有设置。

扫描框样式设置

可以让用户自定义扫描框的样式:

Card(
  child: Column(
    children: [
      ListTile(
        leading: const Icon(Icons.crop_square),
        title: const Text('扫描框样式'),
        subtitle: const Text('选择扫描框的外观'),
      ),
      Padding(
        padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
        child: Obx(() => Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            _buildFrameStyleOption(storage, 'square', '方形'),
            _buildFrameStyleOption(storage, 'rounded', '圆角'),
            _buildFrameStyleOption(storage, 'corners', '四角'),
          ],
        )),
      ),
    ],
  ),
),

Widget _buildFrameStyleOption(StorageService storage, String style, String label) {
  final isSelected = storage.scanFrameStyle.value == style;
  return GestureDetector(
    onTap: () => storage.scanFrameStyle.value = style,
    child: Column(
      children: [
        Container(
          width: 50.w,
          height: 50.w,
          decoration: BoxDecoration(
            border: Border.all(
              color: isSelected ? Theme.of(Get.context!).primaryColor : Colors.grey,
              width: isSelected ? 2 : 1,
            ),
            borderRadius: style == 'rounded' ? BorderRadius.circular(8.r) : null,
          ),
          child: style == 'corners' ? _buildCornersDecoration() : null,
        ),
        SizedBox(height: 4.h),
        Text(
          label,
          style: TextStyle(
            color: isSelected ? Theme.of(Get.context!).primaryColor : Colors.grey,
            fontSize: 12.sp,
          ),
        ),
      ],
    ),
  );
}

提供三种扫描框样式:方形、圆角、四角。用户可以根据喜好选择。

扫描线动画设置

扫描线的动画效果也可以自定义:

Obx(() => SwitchListTile(
  title: const Text('扫描线动画'),
  subtitle: const Text('显示扫描线上下移动效果'),
  value: storage.showScanLine.value,
  onChanged: (value) => storage.showScanLine.value = value,
)),

Obx(() => storage.showScanLine.value
  ? ListTile(
      title: const Text('动画速度'),
      subtitle: Slider(
        value: storage.scanLineSpeed.value,
        min: 0.5,
        max: 2.0,
        divisions: 3,
        label: _getSpeedLabel(storage.scanLineSpeed.value),
        onChanged: (value) => storage.scanLineSpeed.value = value,
      ),
    )
  : const SizedBox.shrink(),
),

String _getSpeedLabel(double speed) {
  if (speed <= 0.75) return '慢';
  if (speed <= 1.25) return '正常';
  return '快';
}

用户可以选择是否显示扫描线,以及调整扫描线的移动速度。

扫描成功反馈设置

扫描成功后的反馈方式可以自定义:

Card(
  child: Column(
    children: [
      const ListTile(
        leading: Icon(Icons.feedback),
        title: Text('扫描成功反馈'),
      ),
      Obx(() => CheckboxListTile(
        title: const Text('震动反馈'),
        value: storage.vibrateOnScan.value,
        onChanged: (value) => storage.vibrateOnScan.value = value ?? true,
      )),
      Obx(() => CheckboxListTile(
        title: const Text('声音提示'),
        value: storage.soundOnScan.value,
        onChanged: (value) => storage.soundOnScan.value = value ?? true,
      )),
      Obx(() => CheckboxListTile(
        title: const Text('闪光提示'),
        value: storage.flashOnScan.value,
        onChanged: (value) => storage.flashOnScan.value = value ?? false,
      )),
    ],
  ),
),

提供三种反馈方式:震动、声音、闪光。用户可以根据使用场景选择合适的反馈方式。

扫描历史设置

关于扫描历史的一些设置:

Card(
  child: Column(
    children: [
      const ListTile(
        leading: Icon(Icons.history),
        title: Text('历史记录'),
      ),
      Obx(() => SwitchListTile(
        title: const Text('保存扫描历史'),
        subtitle: const Text('自动保存每次扫描结果'),
        value: storage.saveScanHistory.value,
        onChanged: (value) => storage.saveScanHistory.value = value,
      )),
      Obx(() => storage.saveScanHistory.value
        ? ListTile(
            title: const Text('历史记录上限'),
            subtitle: Text('最多保存 ${storage.maxHistoryCount.value} 条'),
            trailing: DropdownButton<int>(
              value: storage.maxHistoryCount.value,
              items: [50, 100, 200, 500].map((count) => DropdownMenuItem(
                value: count,
                child: Text('$count 条'),
              )).toList(),
              onChanged: (value) {
                if (value != null) storage.maxHistoryCount.value = value;
              },
            ),
          )
        : const SizedBox.shrink(),
      ),
    ],
  ),
),

用户可以选择是否保存扫描历史,以及设置历史记录的上限数量。

高级扫描设置

一些高级用户可能需要的设置:

ExpansionTile(
  title: const Text('高级设置'),
  leading: const Icon(Icons.settings_applications),
  children: [
    Obx(() => SwitchListTile(
      title: const Text('多码识别'),
      subtitle: const Text('同时识别画面中的多个二维码'),
      value: storage.multiCodeScan.value,
      onChanged: (value) => storage.multiCodeScan.value = value,
    )),
    Obx(() => SwitchListTile(
      title: const Text('反色识别'),
      subtitle: const Text('识别白底黑码的反色二维码'),
      value: storage.invertedScan.value,
      onChanged: (value) => storage.invertedScan.value = value,
    )),
    Obx(() => ListTile(
      title: const Text('扫描精度'),
      subtitle: Text(_getPrecisionLabel(storage.scanPrecision.value)),
      trailing: Slider(
        value: storage.scanPrecision.value,
        min: 1,
        max: 3,
        divisions: 2,
        onChanged: (value) => storage.scanPrecision.value = value,
      ),
    )),
  ],
),

String _getPrecisionLabel(double precision) {
  if (precision <= 1.5) return '快速(可能漏识别)';
  if (precision <= 2.5) return '平衡';
  return '精确(较慢)';
}

高级设置用ExpansionTile折叠起来,不影响普通用户的使用体验。

小结

这篇文章详细介绍了扫描设置页面的实现。从功能规划开始,介绍了四个核心设置项的设计思路,然后逐步实现了页面布局、响应式状态管理、设置持久化等功能。

扫描设置虽然看起来简单,但涉及到用户体验、安全性、数据持久化等多个方面。好的设置页面应该让用户一目了然,知道每个选项的作用,同时提供合理的默认值,让大多数用户不需要修改就能获得良好的体验。

通过 GetX 的响应式编程,我们实现了设置状态的自动同步,用户切换开关后 UI 会立即更新,扫描行为也会相应改变。这种声明式的编程方式让代码更简洁,也更容易维护。

扩展功能方面,扫描框样式、扫描线动画、成功反馈、历史记录设置、高级扫描选项都能进一步提升用户体验,满足不同用户的个性化需求。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐