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表格组件Table的使用详解

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

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

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

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

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

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

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

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

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

  • Android Flutter实现弹幕效果

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

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

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

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

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

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

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

  • 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&

  • Android仿微信录音功能(录音后的raw文件转mp3文件)

    现在很多时候需要用到录音,然后如果我们的App是ios和android两端的话,就要考虑录音的文件在两端都能使用,这个时候就需要适配,两端的录音文件都要是mp3文件,这样才能保证两边都能播放. 针对这个,封装了一个简单可用的录音控件. 使用方法: 1.在xml文件中添加 <ant.muxi.com.audiodemo.view.SoundTextView android:id="@+id/record_audio" android:text="按住开始录音"

  • 详解Android 扫描条形码(Zxing插件)

    使用Android Studio 一.在build.gradle(Module:app)添加代码  下载,调用插件 apply plugin: 'com.android.application' android { compileSdkVersion 24 buildToolsVersion "24.0.1" defaultConfig { applicationId "com.example.ly.scanrfid" minSdkVersion 19 target

  • Android AccessibilityService实现微信抢红包插件

    在你的手机更多设置或者高级设置中,我们会发现有个无障碍的功能,很多人不知道这个功能具体是干嘛的,其实这个功能是为了增强用户界面以帮助残障人士,或者可能暂时无法与设备充分交互的人们 它的具体实现是通过AccessibilityService服务运行在后台中,通过AccessibilityEvent接收指定事件的回调.这样的事件表示用户在界面中的一些状态转换,例如:焦点改变了,一个按钮被点击,等等.这样的服务可以选择请求活动窗口的内容的能力.简单的说AccessibilityService就是一个后

  • 为Android Studio编写自定义Gradle插件的教程

    Google已经建议Android开发全部转向Android Studio开发,Android Studio 是使用gradle编译.打包的,那么问题来了,gradle可是有一堆东西...,为了彻底了解gradle,今天就来学习下如何写自己的gradle插件(当然插件源码是使用groovy写的),先看如下代码目录: 如上图所示,plugin目录是插件源码目录,sample是用来测试插件的. 1.在目录plugin/src/main/groovy/com/micky/gradle/下新建插件类My

  • Android使用MediaRecorder实现录音及播放

    现在项目中有使用到音视频相关技术,在参考了网上各种大牛的资料及根据自己项目实际情况(兼容安卓6.0以上版本动态权限管理等),把声音录制及播放相关代码做个记录. public class MediaRecorderActivity extends BaseActivity { private Button start_tv; private ListView listView; //线程操作 private ExecutorService mExecutorService; //录音API pri

  • android使用flutter的ListView实现滚动列表的示例代码

    现如今打开一个 App,比如头条.微博,都会有长列表,随着我们不断地滑动,视窗内的内容也会不断地更新.今天就用 Flutter 实现一下这种效果. 这里的表现其实就相当于有一个固定长度的容器,然后超出的内容是不可见的,只有当你向上或向下滑动屏幕时,视窗外看不见的内容才会出现在视窗中.如果在 web 开发时,是需要容器加上样式 overflow: auto; 要想用 Flutter 实现,其实也是很简单的,因为 Flutter 为我们提供了 ListView 组件. ListView 主要有以下几

随机推荐

其他