Flutter扫码功能完美实现

如何快速完美地实现Flutter扫码

效果预览

笔者非常执着于能使用直观的效果展示,但是因为CSDN的大小限制,笔者做了很强的压缩,大致还是能看出代码的最终结果
在这里插入图片描述

实现需求

实现需求是否实现
自定义扫码区域大小(可全屏)
连续扫码
从相册选择
支持二维码
支持条形码
扫描动画特效
Android 平台兼容
iOS 平台兼容

环境

Flutter版本 1.22.0.stable

pubspec.yaml中添加依赖

scan: 0.0.7
images_picker: 0.0.8

其中scan插件用于扫码,images_picker插件用于从相册选择二维码或者条形码图片

代码实现

前置条件

既然是扫码,肯定会访问到设备的摄像头,所以需要申请到相机的权限
至于权限申请的部分可以参考我的另一篇博文 Flutter Android权限问题

Dart代码部分

import 'package:flutter/material.dart';
import 'package:images_picker/images_picker.dart';
import 'package:scan/scan.dart';

/// 扫码页面
class QRScannerPage extends StatefulWidget {
  final QRScannerPageConfig config;

  const QRScannerPage({this.config});

  @override
  _QRScannerPageState createState() => _QRScannerPageState();
}

class _QRScannerPageState extends State<QRScannerPage> {
  StateSetter stateSetter;

  IconData lightIcon = Icons.flash_on;

  ScanController controller = ScanController();

  List<String> result = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('扫码'),
        ),
        body: _buildBody());
  }

  Widget _buildBody() {
    return Stack(children: [
      ScanView(
        controller: controller,
        scanAreaScale: widget.config.scanAreaSize,
        scanLineColor: widget.config.scanLineColor,
        onCapture: (String data) async {
          await showResult(content: '扫码结果\t$data');
          controller.resume();
        },
      ),
      Positioned(
        left: 50,
        bottom: 100,
        child: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            stateSetter = setState;
            return MaterialButton(
                child: Icon(lightIcon, size: 30, color: Colors.greenAccent),
                onPressed: () {
                  controller.toggleTorchMode();
                  if (lightIcon == Icons.flash_on) {
                    lightIcon = Icons.flash_off;
                  } else {
                    lightIcon = Icons.flash_on;
                  }
                  stateSetter(() {});
                });
          },
        ),
      ),
      Positioned(
        right: 50,
        bottom: 100,
        child: MaterialButton(
            child: Icon(Icons.image,
                size: 30, color: Color.fromRGBO(4, 184, 67, 1)),
            onPressed: () async {
              await pickImage();
              // DialogUtil.showCommonDialog(context, '$result');
            }),
      ),
    ]);
  }

  Future<void> showResult({String content}) async {
    return showGeneralDialog(
        context: context,
        pageBuilder: (context, anim1, anim2) {},
        barrierColor: Colors.black.withOpacity(.6),
        barrierDismissible: true,
        barrierLabel: "",
        transitionDuration: Duration(milliseconds: 150),
        transitionBuilder: (context, anim1, anim2, child) {
          return Transform.scale(
              scale: anim1.value,
              child: Opacity(
                  opacity: anim1.value,
                  child: Center(
                    child: Padding(
                        padding: const EdgeInsets.all(12.0),
                        child: new Material(
                          type: MaterialType.transparency,
                          child: new Container(
                              height: 450,
                              width: 300,
                              decoration: ShapeDecoration(
                                  color: Colors.white,
                                  shape: RoundedRectangleBorder(
                                      borderRadius: BorderRadius.all(
                                    Radius.circular(8.0),
                                  ))),
                              child: Column(
                                children: [
                                  Expanded(
                                    flex: 2,
                                    child: Align(
                                      alignment: Alignment.center,
                                      child: Text(
                                        content,
                                        style:
                                            TextStyle(height: 1, fontSize: 18),
                                      ),
                                    ),
                                  ),
                                  DividerHorizontal(),
                                  Expanded(
                                    flex: 1,
                                    child: Row(
                                      children: [
                                        Expanded(
                                            child: GestureDetector(
                                          behavior: HitTestBehavior.opaque,
                                          onTap: () {
                                            Navigator.pop(context);
                                          },
                                          child: Align(
                                            alignment: Alignment.center,
                                            child: Text(
                                              '确认',
                                              style: TextStyle(
                                                  color: Color(0xFFFF7B85),
                                                  fontSize: 18),
                                            ),
                                          ),
                                        ))
                                      ],
                                    ),
                                  )
                                ],
                              )),
                        )),
                  )));
        });
  }

  Future pickImage() async {
    List<Media> files = await ImagesPicker.pick(
      pickType: PickType.image,
      count: 9,
    );
    if (files != null && files.isNotEmpty) {
      for (int i = 0; i < files.length; i++) {
        String value = await Scan.parse(files[i].path);
        result.add(value);
      }
      showResult(content: result.toString());
    }
  }
}

class QRScannerPageConfig {
  double scanAreaSize;
  Color scanLineColor;

  QRScannerPageConfig(
      {this.scanAreaSize: 1.0,
      this.scanLineColor: const Color.fromRGBO(4, 184, 67, 1)});
}

class DividerHorizontal extends StatelessWidget {
  final double height;
  final Color color;

  DividerHorizontal({this.height: 1, this.color: const Color(0xFFF8F9F8)});

  @override
  Widget build(BuildContext context) {
    return Container(
      height: height,
      color: color,
    );
  }
}

这个代码都是可以直接copy使用的,如果有需要可以适当地更改

项目的配置

建议Android的最低兼容要在API 21(Android 5.0)以上,iOS的最低兼容要在iOS 10以上,这个是 images_picker这个插件所要求的
下图是Android build.gradle配置
Android的build.gradle配置

权限配置

Android权限配置

在AndroidManifest.xml文件配置相应权限,但是注意的一点:这个只是安装时的权限申请,在Android 7.0之后是需要运行时权限,所以这个问题的解决需要参考我的这篇文章 Flutter Android权限问题
其实说到底还是动态权限的问题。
下图是Android的权限配置
在这里插入图片描述

iOS权限配置

打开下图的配置文件(为了防止不熟悉iOS的同学找错地方,截了个图)

iOS权限配置文件位置
在 “dict” 这个标签里面添加以下代码

	<key>NSAppleMusicUsageDescription</key>
	<string>App需要您的同意,才能访问媒体资料库</string>
	<key>NSCameraUsageDescription</key>
	<string>App需要您的同意,才能访问相机</string>
	<key>NSPhotoLibraryUsageDescription</key>
	<string>App需要您的同意,才能访问相册</string>

这样就可以完成了对 iOS 平台的兼容,使用笔者的公司项目(代码其实差不多,只是UI稍微调整了一下)在 iPhone SE 2
iOS 13.4系统下完美运行,如下图

公司项目 iOS 扫码效果、

总结

笔者在此之前,试用了多个扫码的插件,效果要么就是无法全屏,要么就是无法出现扫码的效果,亦或者是无法连续扫码(每次扫码结束都会从扫码界面回退),总体来说这个scan插件算是比较好地满足了我的需求,后续我会专门写一篇文章来讲解:如何开发一个可供Dart调用的Android的扫码插件。

最近笔者在公司使用Flutter技术独自开发一款企业级的物联网应用,如果对笔者感兴趣,欢迎关注笔者。读者有好的建议,也欢迎在下面留言。码字不易,请给👍

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值