Android使用Flutter实现录音插件

目录
  • 安卓部分
    • 手动注册
    • Android和Dart的通讯
    • 安卓录音
    • Dart module部分
  • iOS部分
    • 手动注册插件
    • iOS插件
    • Dart调用部分

原生提供功能,Dart module 通过 method channel 异步调用

安卓部分

手动注册

Flutter 官方的做法,就是自动注册插件,

很方便

手动注册,体现本文的不同

插件是 AudioRecorderPlugin

class MainActivity: FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        flutterEngine!!.plugins.add(AudioRecorderPlugin())
    }
}

Android和Dart的通讯

主要是消息回调

下文依次是,

  • 开始录音
  • 结束录音
  • 正在录音
  • 是否有录音权限

注意,这里的录音权限包含两个,麦克风的权限,和存储权限

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
  switch (call.method) {
    case "start":
      Log.d(LOG_TAG, "Start");
      Log.d(LOG_TAG, "11111____");
      String path = call.argument("path");
      mExtension = call.argument("extension");
      startTime = Calendar.getInstance().getTime();
      if (path != null) {
        mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + path;
      } else {
        Log.d(LOG_TAG, "11111____222");
        String fileName = String.valueOf(startTime.getTime());
        mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileName + mExtension;
      }
      Log.d(LOG_TAG, mFilePath);
      startRecording();
      isRecording = true;
      result.success(null);
      break;
    case "stop":
      Log.d(LOG_TAG, "Stop");
      stopRecording();
      long duration = Calendar.getInstance().getTime().getTime() - startTime.getTime();
      Log.d(LOG_TAG, "Duration : " + String.valueOf(duration));
      isRecording = false;
      HashMap<String, Object> recordingResult = new HashMap<>();
      recordingResult.put("duration", duration);
      recordingResult.put("path", mFilePath);
      recordingResult.put("audioOutputFormat", mExtension);
      result.success(recordingResult);
      break;
    case "isRecording":
      Log.d(LOG_TAG, "Get isRecording");
      result.success(isRecording);
      break;
    case "hasPermissions":
      Log.d(LOG_TAG, "Get hasPermissions");
      Context context = _flutterBinding.getApplicationContext();
      PackageManager pm = context.getPackageManager();
      int hasStoragePerm = pm.checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context.getPackageName());
      int hasRecordPerm = pm.checkPermission(Manifest.permission.RECORD_AUDIO, context.getPackageName());
      boolean hasPermissions = hasStoragePerm == PackageManager.PERMISSION_GRANTED && hasRecordPerm == PackageManager.PERMISSION_GRANTED;
      result.success(hasPermissions);
      break;
    default:
      result.notImplemented();
      break;
  }
}

安卓录音

使用 wav 的封装格式,用 AudioRecord;

其他封装格式,用 MediaRecorder

上面两个播放器,有开始录音和结束录音功能;

暂停录音和恢复录音,则多次开始和结束,再把文件拼接在一起

Dart module部分

建立 MethodChannel, 异步调用上面的原生功能

class AudioRecorder {
  static const MethodChannel _channel = const MethodChannel('audio_recorder');
  static LocalFileSystem fs = LocalFileSystem();
  static Future start(String path, AudioOutputFormat audioOutputFormat) async {
    String extension;
    if (path != null) {
      if (audioOutputFormat != null) {
        if (_convertStringInAudioOutputFormat(p.extension(path)) !=
            audioOutputFormat) {
          extension = _convertAudioOutputFormatInString(audioOutputFormat);
          path += extension;
        } else {
          extension = p.extension(path);
        }
      } else {
        if (_isAudioOutputFormat(p.extension(path))) {
          extension = p.extension(path);
        } else {
          extension = ".m4a"; // default value
          path += extension;
        }
      }
      File file = fs.file(path);
      if (await file.exists()) {
        throw new Exception("A file already exists at the path :" + path);
      } else if (!await file.parent.exists()) {
        throw new Exception("The specified parent directory does not exist");
      }
    } else {
      extension = ".m4a"; // default value
    }
    return _channel
        .invokeMethod('start', {"path": path, "extension": extension});
  }
  static Future<Recording?> stop() async {
    // 把原生带出来的信息,放入字典中
    Map<String, dynamic> response =
        Map.from(await _channel.invokeMethod('stop'));
    if (response != null) {
      int duration = response['duration'];
      String fmt = response['audioOutputFormat'];
      AudioOutputFormat? outputFmt = _convertStringInAudioOutputFormat(fmt);
      if (fmt != null && outputFmt != null) {
        Recording recording = new Recording(
            new Duration(milliseconds: duration),
            response['path'],
            outputFmt,
            response['audioOutputFormat']);
        return recording;
      }
    } else {
      return null;
    }
  }

iOS部分

手动注册插件

这里的插件名, 为 SwiftAudioRecorderPlugin

public class SwiftAudioRecorderPlugin: NSObject, FlutterPlugin {
    var isRecording = false
    var hasPermissions = false
    var mExtension = ""
    var mPath = ""
    var startTime: Date!
    var audioRecorder: AVAudioRecorder?
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "audio_recorder", binaryMessenger: registrar.messenger())
    let instance = SwiftAudioRecorderPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }
  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
        case "start":
            print("start")
            let dic = call.arguments as! [String : Any]
            mExtension = dic["extension"] as? String ?? ""
            mPath = dic["path"] as? String ?? ""
            startTime = Date()
            let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
            if mPath == "" {
                mPath = documentsPath + "/" + String(Int(startTime.timeIntervalSince1970)) + ".m4a"
            }
            else{
                mPath = documentsPath + "/" + mPath
            }
            print("path: " + mPath)
            let settings = [
                AVFormatIDKey: getOutputFormatFromString(mExtension),
                AVSampleRateKey: 12000,
                AVNumberOfChannelsKey: 1,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]
            do {
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
                try AVAudioSession.sharedInstance().setActive(true)

                let recorder = try AVAudioRecorder(url: URL(string: mPath)!, settings: settings)
                recorder.delegate = self
                recorder.record()
                audioRecorder = recorder
            } catch {
                print("fail")
                result(FlutterError(code: "", message: "Failed to record", details: nil))
            }
            isRecording = true
            result(nil)
        case "pause":
            audioRecorder?.pause()
            result(nil)
        case "resume":
            audioRecorder?.record()
            result(nil)
        case "stop":
            print("stop")
            audioRecorder?.stop()
            audioRecorder = nil
            let duration = Int(Date().timeIntervalSince(startTime as Date) * 1000)
            isRecording = false
            var recordingResult = [String : Any]()
            recordingResult["duration"] = duration
            recordingResult["path"] = mPath
            recordingResult["audioOutputFormat"] = mExtension
            result(recordingResult)
        case "isRecording":
            print("isRecording")
            result(isRecording)
        case "hasPermissions":
            print("hasPermissions")
        switch AVAudioSession.sharedInstance().recordPermission{
            case AVAudioSession.RecordPermission.granted:
                print("granted")
                hasPermissions = true
            case AVAudioSession.RecordPermission.denied:
                print("denied")
                hasPermissions = false
            case AVAudioSession.RecordPermission.undetermined:
                print("undetermined")
                AVAudioSession.sharedInstance().requestRecordPermission() { [unowned self] allowed in
                    DispatchQueue.main.async {
                        if allowed {
                            self.hasPermissions = true
                        } else {
                            self.hasPermissions = false
                        }
                    }
                }
            default:()
            }
            result(hasPermissions)
        default:
            result(FlutterMethodNotImplemented)
        }
      }
    }

iOS插件

逻辑与安卓插件类似,

因为 iOS 的 AVAudioRecorderpauseresume 操作,支持友好,

所以增添了暂停和恢复录音功能

iOS 端的权限比安卓权限,少一个

仅需要录音麦克风权限

public class SwiftAudioRecorderPlugin: NSObject, FlutterPlugin {
    var isRecording = false
    var hasPermissions = false
    var mExtension = ""
    var mPath = ""
    var startTime: Date!
    var audioRecorder: AVAudioRecorder?
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "audio_recorder", binaryMessenger: registrar.messenger())
    let instance = SwiftAudioRecorderPlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }
  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
        case "start":
            print("start")
            let dic = call.arguments as! [String : Any]
            mExtension = dic["extension"] as? String ?? ""
            mPath = dic["path"] as? String ?? ""
            startTime = Date()
            let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
            if mPath == "" {
                mPath = documentsPath + "/" + String(Int(startTime.timeIntervalSince1970)) + ".m4a"
            }
            else{
                mPath = documentsPath + "/" + mPath
            }
            print("path: " + mPath)
            let settings = [
                AVFormatIDKey: getOutputFormatFromString(mExtension),
                AVSampleRateKey: 12000,
                AVNumberOfChannelsKey: 1,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]
            do {
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.defaultToSpeaker)
                try AVAudioSession.sharedInstance().setActive(true)
                let recorder = try AVAudioRecorder(url: URL(string: mPath)!, settings: settings)
                recorder.delegate = self
                recorder.record()
                audioRecorder = recorder
            } catch {
                print("fail")
                result(FlutterError(code: "", message: "Failed to record", details: nil))
            }
            isRecording = true
            result(nil)
        case "pause":
            audioRecorder?.pause()
            result(nil)
        case "resume":
            audioRecorder?.record()
            result(nil)
        case "stop":
            print("stop")
            audioRecorder?.stop()
            audioRecorder = nil
            let duration = Int(Date().timeIntervalSince(startTime as Date) * 1000)
            isRecording = false
            var recordingResult = [String : Any]()
            recordingResult["duration"] = duration
            recordingResult["path"] = mPath
            recordingResult["audioOutputFormat"] = mExtension
            result(recordingResult)
        case "isRecording":
            print("isRecording")
            result(isRecording)
        case "hasPermissions":
            print("hasPermissions")
        switch AVAudioSession.sharedInstance().recordPermission{
            case AVAudioSession.RecordPermission.granted:
                print("granted")
                hasPermissions = true
            case AVAudioSession.RecordPermission.denied:
                print("denied")
                hasPermissions = false
            case AVAudioSession.RecordPermission.undetermined:
                print("undetermined")
                AVAudioSession.sharedInstance().requestRecordPermission() { [unowned self] allowed in
                    DispatchQueue.main.async {
                        if allowed {
                            self.hasPermissions = true
                        } else {
                            self.hasPermissions = false
                        }
                    }
                }
            default:()
            }
            result(hasPermissions)
        default:
            result(FlutterMethodNotImplemented)
        }
      }
    }

Dart调用部分

通过判断平台,Platform.isIOS,

给 iOS 设备,增加完善的功能

@override
Widget build(BuildContext context) {
  final VoidCallback tapFirst;
  if (Platform.isAndroid && name == kEnd) {
    tapFirst = _audioEnd;
  } else {
    tapFirst = _audioGoOn;
  }
  List<Widget> views = [
    ElevatedButton(
      child: Text(
        name,
        style: Theme.of(context).textTheme.headline4,
      ),
      onPressed: tapFirst,
    )
  ];
  if (Platform.isIOS && name != kStarted) {
    views.add(SizedBox(height: 80));
    views.add(ElevatedButton(
      child: Text(
        kEnd,
        style: Theme.of(context).textTheme.headline4,
      ),
      onPressed: _audioEnd,
    ));
  }
  return Scaffold(
    appBar: AppBar(
      // Here we take the value from the MyHomePage object that was created by
      // the App.build method, and use it to set our appbar title.
      title: Text(widget.title),
    ),
    body: Center(
      // Center is a layout widget. It takes a single child and positions it
      // in the middle of the parent.
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: views,
      ),
    ), // This trailing comma makes auto-formatting nicer for build methods.
  );
}

github repo

到此这篇关于Android使用Flutter实现录音插件的文章就介绍到这了,更多相关Android Flutter录音内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Android Flutter实现有趣的页面滚动效果

    目录 CustomScrollView 简介 改造原代码 让导航栏更有趣 改造后的代码 其他效果 总结 在Flutter 高仿一个某支付价值几个亿的页面这一篇中,我们使用了 ListView 将几个 GridView 组合在一起实现了不同可滑动组件的粘合,但是这里必须要设置禁止 GridView 的滑动,防止多个滑动组件的冲突.这种方式写起来不太方便,事实上 Flutter 提供了 CustomScrollView 来粘合多个滑动组件,并且可以实现更有趣的滑动效果. CustomScrollVi

  • Android利用Flutter实现立体旋转效果

    目录 前言 ImageShader 简介 构建 ui.Image对象 使用 ImageShader 填充形状 立体旋转效果实现 总结 前言 之前我们提到了 CustomPaint er 的 Paint 可以使用渐变(GradientShader)来填充绘制的图形,本篇我们来介绍使用图片填充,并且配合动画实现“立体”旋转效果,之所以给“立体”加上引号,是因为实际是通过填充图片自身的光影效果旋转后看起来像是立体效果一样.下面是实现的效果图. ImageShader 简介 ImageShader 的定

  • Android Flutter利用CustomPaint绘制基本图形详解

    目录 绘制矩形 绘制圆形 绘制椭圆 绘制任意形状 绘制弧形 总结 上一篇我们介绍了 CustomPaint 的基本概念和使用,可以看到 CustomPaint 其实和 前端的 Canvas基本上是一样的,实际上前端 Canvas 支持的绘制方法 CustomPaint 都支持,毕竟 CustomPaint 其实也是基于 Canvas 实现的.本篇我们来介绍 CustomPaint 基本图形的绘制. 绘制矩形 绘制矩形比较简单,方法定义如下: void drawRect(Rect rect, Pa

  • Android Flutter实现GIF动画效果的方法详解

    目录 前言 交错动画机制 代码实现 Interval 介绍 总结 前言 我们之前介绍了不少有关动画的篇章.前面介绍的动画都是只有一个动画效果,那如果我们想对某个组件实现一组动效,比如下面的效果,该怎么办? staggered animation 这个时候我们需要用到组合动效, Flutter 提供了交错动画(Staggered Animation)的方式实现.对于多个 Anmation 对象,可以共用一个 AnimationController,然后在不同的时间段执行动画效果.这就有点像 GIF

  • Android Flutter绘制有趣的 loading加载动画

    目录 前言 效果1:圆环内滚动的球 效果2:双轨运动 效果3:钟摆运动 总结 前言 在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动.然而,大部分的 App的 loading 就是下面这种千篇一律的效果 —— 俗称“转圈”. 本篇我们利用Flutter 的 PathMetric来玩几个有趣的 loading 效果. 效果1:圆环内滚动的球 如上图所示,一个红色的小球在蓝色的圆环内滚动,而且在往

  • Android Flutter实现弹幕效果

    目录 前言 通用弹幕实现方案 ListView弹幕方案实现 基本框架 轮播滚动 轮询算法 点击事件 前言 需求要点如下: 弹幕行数为3行,每条弹幕相互依靠但不存在重叠 每条弹幕可交互点击跳转 滚动速度恒定 触摸不可暂停播放 弹幕数据固定一百条且支持轮询播放 弹幕排序规则如下: 1 4 7 2 5 8 3 6 9 通用弹幕实现方案 Flutter Dev Package已有开源弹幕实现组件,这里举例barrage_page的实现方式(大多数实现底层逻辑基本一样). 基本架构采用Stack然后向布局

  • Android Flutter制作交错动画的示例代码

    目录 前言 动画解析 编码实现 总结 前言 之前一篇我们讲了 Flutter组合动画实现的方式 —— 交错动画.借助 GIF 和绘图技巧是可以做到类似 GIF 那种效果的.本篇我们来一个应用实例,我们让轮子在草地滚动着前进,而且还能粘上“绿色的草”,运行效果如下动画所示. 动画解析 上面实现的效果实际上由三个动画组成: 轮子前进的动画 轮子滚动 轮子的边缘颜色渐变(由黑色变成绿色) 这三个动画是同时进行的,因此需要使用到交错动画,即使用一个 AnimationController来控制三个 Tw

  • Android Flutter实现"斑马纹"背景的示例代码

    目录 最终效果图 实现思维 斑马纹(45°角,向左倾斜) 画笔 斑马纹坐标位置计算 圆角裁剪(如果需要) 作为背景 代码 使用处 main_page.dart 斑马纹具体实现类 zebra_stripes_back.dart 计算过程解释 由于工作中项目需求,需要将H5转换为Flutter代码. 其中的斑马纹背景需要根据接口返回的颜色来渲染,所以不能只是图片形式,无法通过decoration属性配置图片背景板. 楼主这边想到的方法就是通过 实现一个canvas绘制斑马纹类.使用Stack布局,将

  • Android Flutter表格组件Table的使用详解

    目录 Table.TabRow.TabCell 小结 之前开发中用到的表格,本篇文章主要介绍如何在页面中使用表格做一个记录. Table组件不同于其它Flex布局,它是直接继承的RenderObjectWidget的.相当于是一个独立的组件,区别与其他系列组件. Table.TabRow.TabCell 惯例,先看下Table相关的构造方法: Table({ Key? key, this.children = const <TableRow>[],//行列表 表示多少行 this.column

  • Android使用Flutter实现录音插件

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

  • Android 集成Flutter

    目录 Android 集成Flutter 1, Hello Flutter 2, 引入 Flutter 模块 3,使用Flutter 3.1 添加依赖 3.2 运行Flutter页面 3.2.1 添加Flutter页面 4,Flutter APK 解析 5,踩过的坑 Android 集成Flutter Flutter 作为 Google 开源的新一代跨平台.高性能 UI 框架,旨在帮助开发者高效地构建出跨平台的.UI 与交互体验一致的精美应用,推出后一直倍受开发者的青睐. 当需要开发一个全新的应

  • Android基于Flutter编写文件下载管理器

    目录 前言 Dio 的下载方法 download 监测下载进度 取消下载 删除已经下载的文件 调试过程中遇到的一些错误 运行结果及代码 总结 前言 文件下载在很多类型的应用中会涉及,例如音乐.文档.包括图片(只是图片可以使用一些组件完成无感知的下载).本篇介绍使用 Dio 的下载方法完成文件的下载,涉及到的内容如下: Dio 插件的 download 方法介绍: 使用 download 的回调方法监测下载进度: 使用 CancelToken 取消正在下载的任务: 删除已下载的文件: path_p

  • Android集成Flutter

    目录 Android 集成Flutter 1, Hello Flutter 2, 引入 Flutter 模块 3,使用Flutter 3.1 添加依赖 3.2 运行Flutter页面 4,Flutter APK 解析 5,踩过的坑 Android 集成Flutter Flutter 作为 Google 开源的新一代跨平台.高性能 UI 框架,旨在帮助开发者高效地构建出跨平台的.UI 与交互体验一致的精美应用,推出后一直倍受开发者的青睐. 当需要开发一个全新的应用时,我们可以很方便地从零开始,完全

  • Android利用Flutter path绘制粽子的示例代码

    目录 前言 绘制 基本轮廓 粽叶 嘴巴 眼睛 腮红 手&脚 头巾 咸甜是一家 发声 动画控制嘴巴开合 用到的技术点 总结 前言 大家好,端午将至,首先提前祝小伙伴端午安康,端午作为中华民族的非常重要的传统节日,粽子那是必不可少的,但是你真的知道粽子的历史吗? 今天跟随本篇文章用Flutter path画一个会科普节日的的粽子吧- 绘制 基本轮廓 首先我们需要将粽子的基本轮廓绘制出来,通过图片可以看到粽子的轮廓是一个圆圆的三角形状, 本篇文章所有的图形都是用纯Path路径制作,这里我们可以将粽子的

  • Android开发Flutter 桌面应用窗口化实战示例

    目录 前言 一.应用窗口的常规配置 应用窗口化 自定义窗口导航栏 美化应用窗口 二.windows平台特定交互 注册表操作 执行控制台指令 实现应用单例 三.桌面应用的交互习惯 按钮点击态 获取应用启动参数 四.写在最后 前言 通过此篇文章,你可以编写出一个完整桌面应用的窗口框架. 你将了解到: Flutter在开发windows和Android桌面应用初始阶段,应用窗口的常规配置: windows平台特定交互的实现,如:执行控制台指令,windows注册表,应用单例等: 桌面应用的交互习惯,如

  • android编程实现电话录音的方法

    本文实例讲述了android编程实现电话录音的方法.分享给大家供大家参考.具体如下: 在清单文件AndroidManifest.xml中添加权限: <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <!-- 在SDCard中创建与删除文件权限 --> <uses-permission android:name="android.permission.MOUN

  • Android编程检测手机录音权限是否打开的方法

    本文实例讲述了Android编程检测手机录音权限是否打开的方法.分享给大家供大家参考,具体如下: 6.0之前的权限检测只是检测到是否在清单文件中注册 Boolean flag = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.RECORD_AUDIO", "包名")); Boolean flag = PermissionChecker.checkSelfPer

  • android studio 安装完成ButterKnife插件却无法使用(解决方案)

    ButterKnife 算是一款知名老牌 Android 开发框架了,通过注解绑定视图,避免了 findViewById() 的操作,广受好评!由于它是在编译时对注解进行解析完成相关代码的生成,所以在项目编译时会略耗时,但不会影响运行时的性能. 很多朋友在android studio 安装完成ButterKnife插件后,却无法使用.今天小编把我的解决方法分享出来供大家参考下. 1.在设置里找到插件正常安装好 2.选择activity_main右键Generate菜单中没有相应的插件选项 3.我

  • Flutter permission_handler 权限插件的使用详解

    编译环境:Flutter 版本v1.12.hotfix9 dart SDK:2.7.2 1 pubspec.yaml中引入: #  权限   permission_handler: ^3.2.0 ios中info.plist配置(根据权限情况使用): <!-- Permission options for the `location` group --> <key>NSLocationWhenInUseUsageDescription</key> <string&

随机推荐