美文网首页Androidflutter
Flutter插件开发之HmsScanKit

Flutter插件开发之HmsScanKit

作者: Fitem | 来源:发表于2022-04-10 16:37 被阅读0次

    前沿

    从事Flutter开发以来,一直都是使用已有的插件,没有自己开发过。最近同事推荐让我使用华为的扫码SDK(hms_scan_kit),正好借此机会来开发一个Flutter的原生插件。算是对最近的插件学习做一个简单的总结。

    效果图

    我们先看一下实现的扫码效果:点击LoadScanKit按钮调起插件的扫码功能,扫码成功后在界面显示扫码结果。


    效果图.gif

    相关知识点

    1. Flutter Packages

    通过使用 package(的模式)可以创建易于共享的模块化代码。一个最基本的 package 由以下内容构成:

    - pubspec.yaml 文件
    用于定义 package 名称、版本号、作者等其他信息的元数据文件。
    
    - lib 目录
    包含共享代码的 lib 目录,其中至少包含一个 <package-name>.dart 文件。
    

    2. Package类别

    Package包分为二种:

    1. 纯Dart库(Dart packages)
    • 只用Dart编写的传统package,比如 path。
    1. 原生插件(Plugin packages)
    • 使用Dart编写的,按需使用Java或 Kotlin、Objective-C或Swift 分别在Android或iOS平台实现的package。

    3. 原生插件开发步骤

    1. 创建package
    • 想要创建原生插件 package,请使用带有 --template=plugin 标志的 flutter create 命令
    flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello
    
    1. 实现package
      a. 定义 package API(.dart)
      b. 添加 Android/iOS 平台代码(.kt/.swift)
      C. 关联 API 和平台代码

    2. 指定插件支持的平台,比如hms_scan插件就如下定义:

    name: flutter_hms_scan
    description: A new Flutter project.
    version: 0.0.1
    homepage:
    
    environment:
      sdk: ">=2.15.1 <3.0.0"
      flutter: ">=2.5.0"
    flutter:
      plugin:
        platforms:
          android:
            package: com.fitem.flutter_hms_scan
            pluginClass: HmsScanPlugin
          ios:
            pluginClass: HmsScanPlugin
    

    备注:如果使用IDE(比如Android Studio)直接在创建Flutter项目处选择Plugin类型即可,IDE会创建插件模板并实现获取平台系统版本的example,无需上面的步骤

    1. Dart对应原生类型:
    Dart kotlin Swift
    null null nil
    bool Boolean NSNumber(value: Bool)
    int Int NSNumber(value: Int32)
    int Long NSNumber(value: Int)
    double Double NSNumber(value: Double)
    String String String
    Uint8List ByteArray FlutterStandardTypedData(bytes: Data)
    Int32List IntArray FlutterStandardTypedData(int32: Data)
    Int64List LongArray FlutterStandardTypedData(int64: Data)
    Float32List FloatArray FlutterStandardTypedData(float32: Data)
    Float64List DoubleArray FlutterStandardTypedData(float64: Data)
    List List Array
    Map HashMap Dictionary
    1. Flutter的plugin通信流程如下:


      PlatformChannels.png

    HmsScan插件的实现

    前面说了这么多,终于进入正题,下面我们开始HmsScan插件的开发吧。

    1. 定义 package API:
    class FlutterHmsScan {
      // 创建插件
      static const MethodChannel _channel = MethodChannel('hms_scan');
      // 定义调用方法
      static Future<ScanBean> loadScanKit() async {
        return await _channel
            .invokeMethod("loadScanKit")
            .then((value) => scanBeanFromJson(json.encode(value)));
      }
    }
    
    2. Android代码实现:

    a. 使用IDE打开Android目录,根据官方SDK导入库

     // scankitSDK
        implementation 'com.huawei.hms:scanplus:2.4.0.301'
    
     // 需要在repositories中导入url
       maven {url 'https://developer.huawei.com/repo/'}
    

    b. 继承FlutterPlugin类,接入Flutter管道。由于sdk用到权限请求和onActivityResult的回调,因此我们需要继承ActivityAware对Activity添加监听。其中registerWith()方法是为了适配老版本Flutter的兼容。

    class HmsScanPlugin : FlutterPlugin, ActivityAware {
        /// The MethodChannel that will the communication between Flutter and native Android
        ///
        /// This local reference serves to register the plugin with the Flutter Engine and unregister it
        /// when the Flutter Engine is detached from the Activity
        private lateinit var mScanLauncher: ScanLauncher
        private lateinit var mHandler: MethodCallHandlerImpl
    
        /**
         * 老版本Flutter兼容
         */
        fun registerWith(registrar: Registrar) {
            mScanLauncher = ScanLauncher(registrar.context(), registrar.activity())
            mHandler = MethodCallHandlerImpl(mScanLauncher)
            mHandler.startService(registrar.messenger())
            registrar.addActivityResultListener(mHandler)
            registrar.addRequestPermissionsResultListener(mHandler)
        }
    
        override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
            mScanLauncher = ScanLauncher(flutterPluginBinding.applicationContext, null)
            mHandler = MethodCallHandlerImpl(mScanLauncher)
            mHandler.startService(flutterPluginBinding.binaryMessenger)
        }
    
        override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
            mHandler.stopService()
        }
    
        override fun onAttachedToActivity(binding: ActivityPluginBinding) {
            mScanLauncher.activity = binding.activity
            binding.addActivityResultListener(mHandler)
            binding.addRequestPermissionsResultListener(mHandler)
        }
    
        override fun onDetachedFromActivity() {
            mScanLauncher.activity = null
        }
    
        override fun onDetachedFromActivityForConfigChanges() {
            onDetachedFromActivity()
        }
    
        override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
            onAttachedToActivity(binding)
        }
    }
    

    c. 考虑到HmsScanPlugin职责过多,这里使用MethodCallHandlerImpl进行分离解耦,专门处理Flutter管道的通信。

    /**
     * 插件方法监听
     * Created by Fitem on 2022/3/2.
     */
    class MethodCallHandlerImpl(var scanLauncher: ScanLauncher) : MethodChannel.MethodCallHandler,
        MethodCallHandlerListener, PluginRegistry.ActivityResultListener,
        PluginRegistry.RequestPermissionsResultListener {
    
        private lateinit var channel: MethodChannel
    
        override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
            when (call.method) {
                "getPlatformVersion" -> {
                    result.success("Android ${android.os.Build.VERSION.RELEASE}")
                }
                "loadScanKit" -> {
                    scanLauncher.loadScanKit(call, result)
                }
                else -> {
                    result.notImplemented()
                }
            }
        }
    
        override fun startService(binaryMessenger: BinaryMessenger) {
            channel = MethodChannel(binaryMessenger, "hms_scan")
            channel.setMethodCallHandler(this)
        }
    
    
        override fun stopService() {
            channel.setMethodCallHandler(null)
        }
    
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
            if (resultCode != Activity.RESULT_OK || data == null) {
                return false
            }
            return scanLauncher.onActivityResult(requestCode, resultCode, data)
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>?,
            grantResults: IntArray?
        ): Boolean {
            if (permissions == null || grantResults == null) {
                return false
            }
            return  scanLauncher.onRequestPermissionResult(requestCode, permissions, grantResults)
        }
    }
    
    // 管道通信生命周期的绑定
    interface MethodCallHandlerListener {
    
        fun startService(binaryMessenger: BinaryMessenger)
    
        fun stopService()
    }
    

    d. 最后通过ScanLauncher来专门处理扫码功能的相关实现

    class ScanLauncher(var applicationContext: Context, var activity: Activity?) {
    
        companion object {
            const val CAMERA_REQ_CODE = 111
            const val DEFINED_CODE = 222
            const val BITMAP_CODE = 333
            const val MULTIPROCESSOR_SYN_CODE = 444
            const val MULTIPROCESSOR_ASYN_CODE = 555
            const val GENERATE_CODE = 666
            const val DECODE = 1
            const val GENERATE = 2
            const val REQUEST_CODE_SCAN_ONE = 0X01
            const val REQUEST_CODE_DEFINE = 0X0111
            const val REQUEST_CODE_SCAN_MULTI = 0X011
            const val DECODE_MODE = "decode_mode"
            const val RESULT = "SCAN_RESULT"
            const val SCAN_STATUS = "scanStatus"
            const val CODE_FORMAT = "codeFormat"
            const val RESULT_TYPE = "resultType"
            const val CODE_RESULT = "codeResult"
        }
    
        private var result: MethodChannel.Result? = null
    
        /**
         * 扫码
         */
        fun loadScanKit(call: MethodCall, result: MethodChannel.Result) {
            this.result = result
            requestPermission(CAMERA_REQ_CODE, DECODE)
        }
    
        /**
         * Apply for permissions.
         */
        private fun requestPermission(requestCode: Int, mode: Int) {
            if (activity == null) {
                result?.success(mapOf(SCAN_STATUS to false))
                return
            }
            if (mode == DECODE) {
                decodePermission(requestCode)
            } else if (mode == GENERATE) {
                generatePermission(requestCode)
            }
        }
    
        /**
         * Apply for permissions.
         */
        private fun decodePermission(requestCode: Int) {
            ActivityCompat.requestPermissions(
                activity!!,
                arrayOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE),
                requestCode
            )
        }
    
        /**
         * Apply for permissions.
         */
        private fun generatePermission(requestCode: Int) {
            ActivityCompat.requestPermissions(
                activity!!, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                requestCode
            )
        }
    
        fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Boolean {
            //Default View
            if (requestCode == REQUEST_CODE_SCAN_ONE) {
                val obj: HmsScan? = data.getParcelableExtra(ScanUtil.RESULT)
                if (obj != null) {
                    result?.success(
                        mapOf(
                            SCAN_STATUS to true,
                            CODE_FORMAT to getCodeFormat(obj.scanType),
                            RESULT_TYPE to getResultType(obj),
                            CODE_RESULT to obj.originalValue
                        )
                    )
                    return true
                }
                //MultiProcessor & Bitmap
            }
            return false
        }
    
        fun onRequestPermissionResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
        ): Boolean {
    
            if (grantResults.size < 2 || grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) {
                return false
            }
            //Default View Mode
            if (requestCode == CAMERA_REQ_CODE) {
                ScanUtil.startScan(
                    activity,
                    REQUEST_CODE_SCAN_ONE,
                    HmsScanAnalyzerOptions.Creator().create()
                )
                return true
            }
            return false
        }
    
        /**
         * 获取CodeFormat
         */
        private fun getCodeFormat(codeFormat: Int): String {
            return when (codeFormat) {
                HmsScan.QRCODE_SCAN_TYPE -> "QR code"
                HmsScan.AZTEC_SCAN_TYPE -> "AZTEC code"
                HmsScan.DATAMATRIX_SCAN_TYPE -> "DATAMATRIX code"
                HmsScan.PDF417_SCAN_TYPE -> "PDF417 code"
                HmsScan.CODE93_SCAN_TYPE -> "CODE93"
                HmsScan.CODE39_SCAN_TYPE -> "CODE39"
                HmsScan.CODE128_SCAN_TYPE -> "CODE128"
                HmsScan.EAN13_SCAN_TYPE -> "EAN13 code"
                HmsScan.EAN8_SCAN_TYPE -> "EAN8 code"
                HmsScan.ITF14_SCAN_TYPE -> "ITF14 code"
                HmsScan.UPCCODE_A_SCAN_TYPE -> "UPCCODE_A"
                HmsScan.UPCCODE_E_SCAN_TYPE -> "UPCCODE_E"
                HmsScan.CODABAR_SCAN_TYPE -> "CODABAR"
                else -> "OTHER"
            }
        }
    
        /**
         * 获取ResultType
         */
        private fun getResultType(hmsScan: HmsScan): String {
            return when (hmsScan.scanType) {
                HmsScan.QRCODE_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                    HmsScan.QRCODE_SCAN_TYPE -> "Text"
                    HmsScan.EVENT_INFO_FORM -> "Event"
                    HmsScan.CONTACT_DETAIL_FORM -> "Contact"
                    HmsScan.DRIVER_INFO_FORM -> "License"
                    HmsScan.EMAIL_CONTENT_FORM -> "Email"
                    HmsScan.LOCATION_COORDINATE_FORM -> "Location"
                    HmsScan.TEL_PHONE_NUMBER_FORM -> "Tel"
                    HmsScan.SMS_FORM -> "SMS"
                    HmsScan.WIFI_CONNECT_INFO_FORM -> "Wi-Fi"
                    HmsScan.URL_FORM -> "WebSite"
                    HmsScan.URL_FORM -> "WebSite"
                    else -> "Text"
                }
                HmsScan.EAN13_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                    HmsScan.ISBN_NUMBER_FORM -> "ISBN"
                    HmsScan.ARTICLE_NUMBER_FORM -> "Product"
                    else -> "Text"
                }
                HmsScan.EAN8_SCAN_TYPE,
                HmsScan.UPCCODE_A_SCAN_TYPE,
                HmsScan.UPCCODE_E_SCAN_TYPE -> when (hmsScan.scanTypeForm) {
                    HmsScan.ARTICLE_NUMBER_FORM -> "Product"
                    else -> "Text"
    
                }
                else -> "Text"
            }
        }
    }
    

    最后在AndroidManifest.xml中添加需要的权限:

     <!--相机权限-->
        <uses-permission android:name="android.permission.CAMERA" />
        <!--文件读取权限-->
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    
    3. ios部分的实现

    ios原本也是打算使用hms的,但是官方居然2年没有更新了,并且不支持bitcode版本、不支持cocopod,demo也无法正常运行。经过一番尝试后,决定放弃使用该库,换成了MTBBarcodeScanner库。(ios新人一个,如果有精通IOS的同学们欢迎指教!)

    a. 通过SwiftHmsScanPlugin创建Flutter管道

    public class SwiftHmsScanPlugin: NSObject, FlutterPlugin, BarcodeScannerViewControllerDelegate {
        
        private var result: FlutterResult?
        private var hostViewController: UIViewController?
        
        public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "hms_scan", binaryMessenger: registrar.messenger())
        let instance = SwiftHmsScanPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
      }
    
      public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
          self.result = result
          if ("loadScanKit" == call.method) {
              loadScanKit()
          } else {
              result("iOS " + UIDevice.current.systemVersion)
          }
      }
        
        public func loadScanKit() {
            
            if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
                        hostViewController = topViewController(base:rootVC)
                    } else if let window = UIApplication.shared.delegate?.window,let rootVC = window?.rootViewController {
                        hostViewController = topViewController(base:rootVC)
                    }
            
            let scannerViewController = BarcodeScannerViewController()
    
            let navigationController = UINavigationController(rootViewController: scannerViewController)
            
            if #available(iOS 13.0, *) {
                  navigationController.modalPresentationStyle = .fullScreen
              }
              
            scannerViewController.delegate = self
            hostViewController?.present(navigationController, animated: false)
        }
        
        private func topViewController(base: UIViewController?) -> UIViewController? {
            if let nav = base as? UINavigationController {
                return topViewController(base: nav.visibleViewController)
    
            } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
                return topViewController(base: selected)
    
            } else if let presented = base?.presentedViewController {
                return topViewController(base: presented)
            }
            return base
        }
        
        func didScanBarcodeWithResult(_ controller: BarcodeScannerViewController?, scanResult: ScanResult) {
            result?(["codeResult":scanResult.rawContent, "scanStatus" : String(true), "resultType": String(scanResult.format.rawValue)])
        }
        
        func didFailWithErrorCode(_ controller: BarcodeScannerViewController?, errorCode: String) {
            result?(["scanStatus" : String(false)])
        }
        
    }
    

    b. BarcodeScannerViewController实现扫码功能

    class BarcodeScannerViewController: UIViewController {
      private var previewView: UIView?
      private var scanRect: ScannerOverlay?
      private var scanner: MTBBarcodeScanner?
      
      private let formatMap = [
        BarcodeFormat.aztec : AVMetadataObject.ObjectType.aztec,
        BarcodeFormat.code39 : AVMetadataObject.ObjectType.code39,
        BarcodeFormat.code93 : AVMetadataObject.ObjectType.code93,
        BarcodeFormat.code128 : AVMetadataObject.ObjectType.code128,
        BarcodeFormat.dataMatrix : AVMetadataObject.ObjectType.dataMatrix,
        BarcodeFormat.ean8 : AVMetadataObject.ObjectType.ean8,
        BarcodeFormat.ean13 : AVMetadataObject.ObjectType.ean13,
        BarcodeFormat.interleaved2Of5 : AVMetadataObject.ObjectType.interleaved2of5,
        BarcodeFormat.pdf417 : AVMetadataObject.ObjectType.pdf417,
        BarcodeFormat.qr : AVMetadataObject.ObjectType.qr,
        BarcodeFormat.upce : AVMetadataObject.ObjectType.upce,
      ]
      
      var delegate: BarcodeScannerViewControllerDelegate?
      
      private var device: AVCaptureDevice? {
        return AVCaptureDevice.default(for: .video)
      }
      
      private var isFlashOn: Bool {
        return device != nil && (device?.flashMode == AVCaptureDevice.FlashMode.on || device?.torchMode == .on)
      }
      
      private var hasTorch: Bool {
        return device?.hasTorch ?? false
      }
      
      override func viewDidLoad() {
        super.viewDidLoad()
    
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
        
        #if targetEnvironment(simulator)
        view.backgroundColor = .lightGray
        #endif
        
        previewView = UIView(frame: view.bounds)
        if let previewView = previewView {
          previewView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
          view.addSubview(previewView)
        }
        setupScanRect(view.bounds)
        
        scanner = MTBBarcodeScanner(previewView: previewView)
                                      
        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "cancel",
                                                            style: .plain,
                                                            target: self,
                                                            action: #selector(cancel)
        )
        updateToggleFlashButton()
      }
      
      override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if scanner!.isScanning() {
          scanner!.stopScanning()
        }
        
        UIDevice.current.endGeneratingDeviceOrientationNotifications()
        
        scanRect?.startAnimating()
        MTBBarcodeScanner.requestCameraPermission(success: { success in
          if success {
            self.startScan()
          } else {
            #if !targetEnvironment(simulator)
            self.errorResult(errorCode: "PERMISSION_NOT_GRANTED")
            #endif
          }
        })
      }
      
      override func viewWillDisappear(_ animated: Bool) {
        scanner?.stopScanning()
        scanRect?.stopAnimating()
        
        UIDevice.current.beginGeneratingDeviceOrientationNotifications()
        
        if isFlashOn {
          setFlashState(false)
        }
        
        super.viewWillDisappear(animated)
      }
      
      override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        setupScanRect(CGRect(origin: CGPoint(x: 0, y:0),
                             size: size
        ))
      }
      
      private func setupScanRect(_ bounds: CGRect) {
        if scanRect != nil {
          scanRect?.stopAnimating()
          scanRect?.removeFromSuperview()
        }
        scanRect = ScannerOverlay(frame: bounds)
        if let scanRect = scanRect {
          scanRect.translatesAutoresizingMaskIntoConstraints = false
          scanRect.backgroundColor = UIColor.clear
          view.addSubview(scanRect)
          scanRect.startAnimating()
        }
      }
      
      private func startScan() {
        do {
          try scanner!.startScanning(with: cameraFromConfig, resultBlock: { codes in
            if let code = codes?.first {
              let codeType = self.formatMap.first(where: { $0.value == code.type });
              let scanResult = ScanResult.with {
                $0.type = .barcode
                $0.rawContent = code.stringValue ?? ""
                $0.format = codeType?.key ?? .unknown
                $0.formatNote = codeType == nil ? code.type.rawValue : ""
              }
              self.scanner!.stopScanning()
              self.scanResult(scanResult)
            }
          })
        } catch {
          self.scanResult(ScanResult.with {
            $0.type = .error
            $0.rawContent = "\(error)"
            $0.format = .unknown
          })
        }
      }
      
      @objc private func cancel() {
        scanResult( ScanResult.with {
          $0.type = .cancelled
          $0.format = .unknown
        });
      }
      
      @objc private func onToggleFlash() {
        setFlashState(!isFlashOn)
      }
      
      private func updateToggleFlashButton() {
        if !hasTorch {
          return
        }
        
        let buttonText = isFlashOn ? "flash_off" : "flash_on"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: buttonText,
                                                            style: .plain,
                                                            target: self,
                                                            action: #selector(onToggleFlash)
        )
      }
      
      private func setFlashState(_ on: Bool) {
        if let device = device {
          guard device.hasFlash && device.hasTorch else {
            return
          }
          
          do {
            try device.lockForConfiguration()
          } catch {
            return
          }
          
          device.flashMode = on ? .on : .off
          device.torchMode = on ? .on : .off
          
          device.unlockForConfiguration()
          updateToggleFlashButton()
        }
      }
      
      private func errorResult(errorCode: String){
        delegate?.didFailWithErrorCode(self, errorCode: errorCode)
        dismiss(animated: false)
      }
      
      private func scanResult(_ scanResult: ScanResult){
        self.delegate?.didScanBarcodeWithResult(self, scanResult: scanResult)
        dismiss(animated: false)
      }
      
      private var cameraFromConfig: MTBCamera {
        return .back
      }
    }
    
    

    c. 最后需要在example的ios目录Info.plist文件中添加相机权限:

    // example/ios/Runner/Info.plist
    <key>NSCameraUsageDescription</key>
    <string>Camera permission is required for barcode scanning.</string>
    

    至此,一个简单的应用于Android、iOS的plugin插件已完成。

    4. 需要注意的点
    1. 使用Android Studio右键选择Flutter即可通过Android Studio和Xcode打开项目,如图:


      WX20220408-114305@2x.png
    2. Android目录打开后,若看不到插件module,可以选择Project Files模式下查看,如图:


    3. ios目录打开前,需要进入example目录输入命令 flutter build ios,待编译完成后再通过Xcode打开。

    总结

    Plugin原生插件其实就是基于Flutter提供的管道进行通信,和原生开发的使用并无太大区别。但需要我们对原生代码的调用有一个基本的了解,然后引入其他原生开发库进行调用。最后附上项目地址:flutter_hms_scan

    相关文章

      网友评论

        本文标题:Flutter插件开发之HmsScanKit

        本文链接:https://www.haomeiwen.com/subject/srbvsrtx.html