Flutter插件开发之HmsScanKit实现示例详解

目录
  • 前沿
  • 效果图
  • 相关知识点
    • 1. Flutter Packages
    • 2. Package类别
    • 3. 原生插件开发步骤
  • HmsScan插件的实现
    • 1. 定义 package API:
    • 2. Android代码实现:
    • 3. ios部分的实现
    • 4. 需要注意的点
  • 总结

前沿

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

效果图

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

相关知识点

1. Flutter Packages

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

- pubspec.yaml 文件
用于定义 package 名称、版本号、作者等其他信息的元数据文件。

- lib 目录
包含共享代码的 lib 目录,其中至少包含一个 <package-name>.dart 文件。

2. Package类别

Package包分为二种:

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

3. 原生插件开发步骤

  • 创建package
  • 想要创建原生插件 package,请使用带有 --template=plugin 标志的 flutter create 命令
flutter create --org com.example --template=plugin --platforms=android,ios -a kotlin hello
  • 实现package a. 定义 package API(.dart) b. 添加 Android/iOS 平台代码(.kt/.swift) C. 关联 API 和平台代码
  • 指定插件支持的平台,比如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,无需上面的步骤

  • 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
  • Flutter的plugin通信流程如下:

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. 需要注意的点

  • 使用Android Studio右键选择Flutter即可通过Android Studio和Xcode打开项目,如图:

  • Android目录打开后,若看不到插件module,可以选择Project Files模式下查看,如图:

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

总结

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

以上就是Flutter插件开发之HmsScanKit实现示例详解的详细内容,更多关于Flutter插件开发HmsScanKit的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter自动路由插件auto_route使用详解

    目录 一.简介 二.基本使用 2.1 安装插件 2.2 定义路由表 2.3 生成路由 2.4 路由跳转 2.5 处理返回结果 三.路由导航 3.1 嵌套导航 3.2 Tab 导航 3.3 PageView 3.4 声明式导航 四.高级用法 4.1 路由控制器 4.2 Paths 4.2.1 Path Parameters 4.2.2 Inherited Path Parameters 4.2.3 Query Parameters 4.2.4 Redirecting Paths 4.3 路由守护

  • Android使用Flutter实现录音插件

    目录 安卓部分 手动注册 Android和Dart的通讯 安卓录音 Dart module部分 iOS部分 手动注册插件 iOS插件 Dart调用部分 原生提供功能,Dart module 通过 method channel 异步调用 安卓部分 手动注册 Flutter 官方的做法,就是自动注册插件, 很方便 手动注册,体现本文的不同 插件是 AudioRecorderPlugin class MainActivity: FlutterActivity() { override fun onCr

  • Flutter桌面开发windows插件开发

    目录 前言 插件介绍 windows插件编写 Windows插件的一些坑 前言 通过此篇文章,你将了解到: Flutter插件的基本介绍: windows插件开发的真实踩坑经验. 我们都知道,Flutter的定位更多是作为一个跨平台的UI框架,对于原生平台的功能,开发过程中经常需要插件来提供.不幸的是Windows的生态又极其不完整,插件开发必不可少.但网上windows的文章少之又少,所以本篇文章,我们一起来聊聊插件开发的一些技巧. 插件介绍 Flutter的插件主要分两种:package和p

  • Flutter 实现网易云音乐字幕的代码

    没有接触过音乐字幕方面知识的话,会对字幕的实现比较迷茫,什么时候转到下一句?看了这篇文章,你就会明白字幕so easy. 先来一张效果图: 字幕格式 目前市面上有很多种字幕格式,比如srt, ssa, ass(文本形式)和idx+sub(图形格式),但不管哪一种格式都会包含2个属性:时间戳和字幕内容,格式如下: 00:00 歌词: 00:25 我要穿越这片沙漠 00:28 找寻真的自我 00:30 身边只有一匹骆驼陪我 00:34 这片风儿吹过 00:36 那片云儿飘过 上面字幕的意思是:在25

  • iOS开发之UIMenuController使用示例详解

    目录 简介 接口介绍 使用探索 如何创建并显示 UIMenuController 实现 Item 点击事件 菜单 Item 太多??? UIResponderStandardEditActions 协议 添加自定义菜单 箭头的方向 实际使用 总结 简介 UIMenuController 是一个菜单编辑界面,在很多地方都能用到,通常用于剪切.复制.粘贴.选择.全选和删除命令等,也可以自定义想要的操作,它长这样: 接口介绍 open class UIMenuController : NSObject

  • 移动web开发之touch事件实例详解

    前面的话 iOS版Safari为了向开发人员传达一些特殊信息,新增了一些专有事件.因为iOS设备既没有鼠标也没有键盘,所以在为移动Safari开发交互性网页时,常规的鼠标和键盘事件根本不够用.随着Android 中的WebKit的加入,很多这样的专有事件变成了事实标准,导致W3C开始制定Touch Events规范.本文将详细介绍移动端touch事件 概述 包含iOS 2.0软件的iPhone 3G发布时,也包含了一个新版本的Safari浏览器.这款新的移动Safari提供了一些与触摸(touc

  • Flutter Module添加到iOS项目示例详解

    目录 1. 创建flutter module 2. flutter 模块嵌入原生应用 3. flutter模块和原生通信 小结 1. 创建flutter module 摘要:我们实际开发开始作为混合开发,可能会把一个模块使用flutter开发,之后嵌入到iOS项目中.比如说我们的商城模块使用flutter开发,这样android和iOS都可以使用. 我们通常会把iOS项目和 flutter module在同一目录,我们创建一个商城的module flutter create --template

  • Flutter状态管理Provider的使用示例详解

    目录 前言 计数器 全局状态 总结 前言 Provider是三大主流状态管理框架官方推荐使用的框架,它是基于官方数据共享组件InheritedWidget实现的,通过数据改变调用生命周期中的didChangeDependencies()方法,来实现状态的通知改变. InheritedWidget的使用可以参考我之前的这篇Flutter中几种数据传递的应用总结. 计数器 还是以计数器为例,这次通过Provider实现,provider相较于bloc并没有那么强制性分层,所以这里我们自己分为数据层(

  • JSP 开发之 releaseSession的实例详解

    JSP 开发之 releaseSession的实例详解 Hibernate可以实现分页查询,昨天试了一下,分页效果不错.但是发现了一个问题,就是当请求超过20次的时候页面就会卡死.经检查,是卡在分页查询这一块. 应用程序采用struts2 + spring2 + hibernate3架构 连接池配置使用的是c3p0, 最大池大小为20, 很显然是连接池耗尽导致的. 增加连接池大小只是饮鸩止渴,总还有耗尽的时候,必须找到根本原因. Dao类的分页查询方法如下: java 代码  public Li

  • Android音频开发之SurfaceView的使用详解

    目录 SurfaceView 不同点 双缓冲机制 SurfaceHolder 使用 SurfaceView SurfaceView从源码上看继承自View,但在内部实现上SurfaceView和其他View有很多区别. SurfaceView主要作用是提供一个直接绘图表面嵌入到视图结构中,实际上真正做绘制能力的是Surface.因此SurfaceView和宿主窗口是分离的.正常情况下窗口的View共享同一个Window,而Window也对应一个Surface,所有View也就共享同一个Surfa

  • C#面向对象编程中开闭原则的示例详解

    目录 开闭原则 C# 示例 改进 总结 在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原则的子集,在他2000年的论文<设计原则与设计模式>中首次提出. SOLID 原则包含: S:单一功能原则(single-responsibility principle) O:开闭原则(open-closed principle) L:里氏替换原则(Lis

  • 安卓开发之FragmentPagerAdapter和FragmentStatePagerAdapter详解

    目录 FragmentPagerAdapter与FragmentPagerStateAdapter区别点: 一:二者在状态保存有差异:FragmentPagerAdapter并未实现saveState().restoreState() 二:二者在视图管理方法差异: 最近遇到比较奇怪的bug,TableLayout+ViewPager实现点击顶部tab切换viewpager视图.但是在Viewpager设置dapter时,最开始设置的是FragmentPagerAdapter,会导致tab切换后F

  • flutter中使用流式布局示例详解

    目录 简介 Flow和FlowDelegate Flow的应用 总结 简介 我们在开发web应用的时候,有时候为了适应浏览器大小的调整,需要动态对页面的组件进行位置的调整.这时候就会用到flow layout,也就是流式布局. 同样的,在flutter中也有流式布局,这个流式布局的名字叫做Flow.事实上,在flutter中,Flow通常是和FlowDelegate一起使用的,FlowDelegate用来设置Flow子组件的大小和位置,通过使用FlowDelegate.paintChildre可

  • Flutter SizedBox布局组件Widget使用示例详解

    目录 正文 child 的 constrains 确定自己的大小 SizedBox 的命名构造函数们 SizedBox.expand SizedBox.shrink SizedBox.fromSize SizedBox.square 应用场景 为 child 提供 tight 约束. 为 children 之间提供空白. 占位 正文 Flutter Sizedbox 是一个 布局组件,用来给 child 添加 tight 约束的,也可以用来添加空白. width,height是 Sizedbox

随机推荐