├── .gitattributes ├── .gitignore ├── .idea ├── libraries │ └── Flutter_Plugins.xml ├── modules.xml ├── runConfigurations │ └── example_lib_main_dart.xml └── vcs.xml ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_images ├── f53502b3.png ├── ios.gif ├── ios_error.png └── video2gif_20191118_101627.gif ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── record │ └── wilson │ └── flutter │ └── com │ └── flutter_plugin_record │ ├── FlutterPluginRecordPlugin.kt │ ├── timer │ ├── ITimer.java │ ├── ITimerChangeCallback.java │ ├── MTimer.java │ └── TimerUtils.java │ └── utils │ ├── AudioHandler.java │ ├── DateUtils.java │ ├── DialogUtil.java │ ├── FileTool.java │ ├── LogUtils.java │ ├── PlayState.java │ ├── PlayUtilsPlus.java │ └── RecorderUtil.java ├── example ├── .flutter-plugins-dependencies ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── record │ │ │ │ │ └── wilson │ │ │ │ │ └── flutter │ │ │ │ │ └── com │ │ │ │ │ └── flutter_plugin_record_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ └── styles.xml │ │ │ │ └── xml │ │ │ │ └── network_security_config.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── res │ │ └── values │ │ │ └── strings_en.arb │ └── settings.gradle ├── ios │ ├── Flutter │ │ ├── .last_build_id │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Flutter.podspec │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── generated │ │ └── i18n.dart │ ├── main.dart │ ├── path_provider_screen.dart │ ├── record_mp3_screen.dart │ ├── record_screen.dart │ └── wechat_record_screen.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── flutter_plugin_record.iml ├── images ├── voice_volume_1.png ├── voice_volume_2.png ├── voice_volume_3.png ├── voice_volume_4.png ├── voice_volume_5.png ├── voice_volume_6.png └── voice_volume_7.png ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── DPAudioPlayer.h │ ├── DPAudioPlayer.m │ ├── DPAudioRecorder.h │ ├── DPAudioRecorder.m │ ├── FlutterPluginRecordPlugin.h │ ├── FlutterPluginRecordPlugin.m │ ├── JX_GCDTimerManager.h │ └── JX_GCDTimerManager.m └── flutter_plugin_record.podspec ├── lib ├── const │ ├── play_state.dart │ ├── record_state.dart │ └── response.dart ├── flutter_plugin_record.dart ├── index.dart ├── utils │ └── common_toast.dart └── widgets │ ├── custom_overlay.dart │ └── voice_widget.dart ├── pubspec.lock ├── pubspec.yaml ├── test └── flutter_plugin_record_test.dart └── 飞云之下.wav /.gitattributes: -------------------------------------------------------------------------------- 1 | *.yml linguist-language=Dart 2 | *.java linguist-language=Dart 3 | *.m linguist-language=Dart 4 | *.h linguist-language=Dart -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | .idea/ -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | 3 | * 录音时长类型修改为 Double(和 iOS 端保持一致) 4 | 5 | ## 1.0.0 6 | 7 | * 添加空安全相关逻辑 8 | * 适配 flutter 2.0 9 | 10 | ## 0.3.5 11 | 12 | * 修复ios 音频监听灵敏性问题 13 | 14 | ## 0.3.4 15 | 16 | * 修复根据路径录制bug 17 | 18 | ## 0.3.3 19 | 20 | * 修复根据路径录制bug 21 | 22 | ## 0.3.2 23 | 24 | * 修复android 获取权限bug 25 | 26 | ## 0.3.1 27 | 28 | * 更新IOS podfile 29 | 30 | ## 0.3.0 31 | * 添加支持录制mp3 32 | 33 | ## 0.2.5 34 | * 修复权限申请bug 35 | ## 0.2.4 36 | * 修复播放音频文件的bug。 37 | ## 0.2.3 38 | * 修复bug 39 | ## 0.2.2 40 | * 修复bug 41 | ## 0.2.1 42 | * 补充说明文档 43 | ## 0.2.0 44 | * 实现暂停播放和继续播放功能 45 | ## 0.1.9 46 | 47 | * 注释掉初始化插件就申请权限的问题 48 | 49 | ## 0.1.8 50 | 51 | * 添加支持 Android 和IOS 播放在线wav音频 52 | 53 | ## 0.1.7 54 | 55 | * 修复bug在未使用录音功能前,通过playByPath播发音频,音频可以正常播放,但无法监听到播放结束 56 | 57 | ## 0.1.6 58 | 59 | * 添加android 在开始录制时进行权限验证判断 60 | ## 0.1.5 61 | 62 | * 实现根据传递的路径进行语音录制 63 | 64 | ## 0.1.4 65 | 66 | * 解决 android 申请权限失败问题 67 | * 解决 快速点击发送语音录制停止不了问题 68 | 69 | ## 0.1.3 70 | 71 | * 实现播放完成的回调监听 72 | 73 | 74 | ## 0.1.2 75 | 76 | * 实现播放指定路径录音文件 77 | ## 0.1.1 78 | 79 | * 格式代码 80 | ## 0.1.0 81 | 82 | * 修复提示bug 83 | 84 | ## 0.0.9 85 | 86 | * 添加记录录制时间的功能 87 | 88 | ## 0.0.8 89 | 90 | * 重构项目为oc项目 解决播放oc 工程无法使用问题 91 | ## 0.0.7 92 | 93 | * 更新readme 94 | 95 | ## 0.0.6 96 | 97 | * 适配android 9.0 98 | * 解决ios集成到oc项目不成功问题 99 | 100 | ## 0.0.5 101 | 102 | * 添加 readme 说明 103 | 104 | ## 0.0.4 105 | 106 | * 添加example readme 107 | 108 | ## 0.0.3 109 | 110 | * 添加引入方式 111 | 112 | ## 0.0.2 113 | 114 | * 添加license 115 | 116 | ## 0.0.1 117 | 118 | * 初始化项目 119 | 120 | 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | 4 | # 使用Flutter实现 仿微信录音的插件 5 | 插件支持android 和IOS 6 | 7 | 8 | ------- 9 | 插件提供的功能 10 | 录制 11 | 1. 录制语音, 12 | 2. 播放录音, 13 | 3. 录制声音大小的监听 14 | 4. 提供录制时长的监听 15 | 5. 提供类似微信的录制组件 16 | 6. 提供播放音频结束的监听 17 | 7. 提供根据传递的路径进行语音录制 18 | 8. 提供录制wav,mp3格式 具体可参考example 19 | 20 | 播放 21 | 1. 提供播放指定路径的音频文件 22 | 2. 提供播放指定Url地址的wav,MP3格式文件 23 | 3. 提供播放完成的回调监听 24 | 4. 提供暂停和继续播放的功能 25 | 5. 提供停止播放的功能 26 | 27 | 28 | 29 | 30 | ## 1,引入 31 | 在pubspec.yaml 文件上引入如下配置 32 | 33 | 34 | 引入方式1(引入最新的版本) 35 | flutter_plugin_record: 36 | git: 37 | url: https://github.com/yxwandroid/flutter_plugin_record.git 38 | 39 | 引入方式2 (引入指定某次commit) 40 | flutter_plugin_record: 41 | git: 42 | url: https://github.com/yxwandroid/flutter_plugin_record.git 43 | ref: 29c02b15835907879451ad9f8f88c357149c6085 44 | 45 | 引入方式3 (引入Flutter仓库的library) 46 | 47 | dependencies: 48 | flutter_plugin_record: ^1.0.1 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ### 使用 59 | ### 1, 初始化录制 60 | #### 1.1, 初始化录制(wav) 61 | 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 62 | 63 | //实例化对象 64 | FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); 65 | // 初始化 66 | recordPlugin.init() 67 | 68 | 69 | #### 1.2, 初始化录制(Mp3) 70 | 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 71 | 72 | //实例化对象 73 | FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); 74 | // 初始化 75 | recordPlugin.initRecordMp3() 76 | 77 | ### 2, 开始录制 78 | 79 | recordPlugin.start() 80 | 81 | ### 3, 停止录制 82 | 83 | recordPlugin.stop() 84 | 85 | ### 4, 播放 86 | 87 | #### 1,播放 88 | 89 | recordPlugin.play() 90 | 91 | #### 2, 暂停和继续播放 92 | 93 | recordPlugin.pausePlay(); 94 | 95 | #### 3, 停止播放 96 | 97 | recordPlugin.stopPlay(); 98 | 99 | ### 5, 根据传递的路径进行语音录制 100 | 101 | recordPlugin.startByWavPath(wavPath); 102 | 103 | ### 6, 根据传递的路径或则Url进行语音播放 104 | 105 | 106 | /// 107 | /// 参数 path 播放音频的地址 108 | /// 109 | ///path 为 url类型对应的在线播放地址 https://linjuli-app-audio.oss-cn-hangzhou.aliyuncs.com/audio/50c39c768b534ce1ba25d837ed153824.wav 110 | ///path 对应本地文件路径对应的是本地文件播放肚子 /sdcard/flutterdemo/wiw.wav 111 | /// 参数 type 112 | /// 当path 为url type为 url 113 | /// 当path 为本地地址 type为 file 114 | /// 115 | Future playByPath(String path, String type) async { 116 | return await _invokeMethod('playByPath', { 117 | "play": "play", 118 | "path": path, 119 | "type": type, 120 | }); 121 | } 122 | 123 | ### 7, 释放资源 124 | 可以在页面退出的时候进行资源释放 比如在 dispose方法中调用如下代码 125 | 126 | recordPlugin.dispose() 127 | 128 | 129 | 130 | ### 4,回调监听 131 | 1,初始化回调监听 132 | 133 | 134 | ///初始化方法的监听 135 | recordPlugin.responseFromInit.listen((data) { 136 | if (data) { 137 | print("初始化成功"); 138 | } else { 139 | print("初始化失败"); 140 | } 141 | }); 142 | 143 | 144 | 2,开始录制停止录制监听 145 | 146 | /// 开始录制或结束录制的监听 147 | recordPlugin.response.listen((data) { 148 | if (data.msg == "onStop") { 149 | ///结束录制时会返回录制文件的地址方便上传服务器 150 | print("onStop " + data.path); 151 | } else if (data.msg == "onStart") { 152 | print("onStart --"); 153 | } 154 | }); 155 | 156 | 3,录制声音大小回调监听 157 | 158 | 159 | ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 160 | recordPlugin.responseFromAmplitude.listen((data) { 161 | var voiceData = double.parse(data.msg); 162 | var tempVoice = ""; 163 | if (voiceData > 0 && voiceData < 0.1) { 164 | tempVoice = "images/voice_volume_2.png"; 165 | } else if (voiceData > 0.2 && voiceData < 0.3) { 166 | tempVoice = "images/voice_volume_3.png"; 167 | } else if (voiceData > 0.3 && voiceData < 0.4) { 168 | tempVoice = "images/voice_volume_4.png"; 169 | } else if (voiceData > 0.4 && voiceData < 0.5) { 170 | tempVoice = "images/voice_volume_5.png"; 171 | } else if (voiceData > 0.5 && voiceData < 0.6) { 172 | tempVoice = "images/voice_volume_6.png"; 173 | } else if (voiceData > 0.6 && voiceData < 0.7) { 174 | tempVoice = "images/voice_volume_7.png"; 175 | } else if (voiceData > 0.7 && voiceData < 1) { 176 | tempVoice = "images/voice_volume_7.png"; 177 | } 178 | setState(() { 179 | voiceIco = tempVoice; 180 | if(overlayEntry!=null){ 181 | overlayEntry.markNeedsBuild(); 182 | } 183 | }); 184 | 185 | print("振幅大小 " + voiceData.toString() + " " + voiceIco); 186 | }); 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 4,播放声音完成的监听监听 197 | 198 | 199 | recordPlugin.responsePlayStateController.listen((data){ 200 | print("播放路径 " + data.playPath ); 201 | print("播放状态 " + data.playState ); 202 | }); 203 | 204 | 205 | 206 | ## 2,录制组件的使用 207 | 208 | 组件使用效果 209 | 210 | android效果 211 | 212 | 213 | 214 | 215 | IOS效果 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | ### 1,在使用的页面进行导入package 225 | 226 | import 'package:flutter_plugin_record/index.dart'; 227 | 228 | 229 | 230 | 231 | 232 | ### 2,在使用的地方引入VoiceWidget组件 233 | 234 | VoiceWidget(), 235 | 236 | 237 | VoiceWidget({startRecord: Function, stopRecord: Function}) { 238 | 239 | 240 | 241 | startRecord 开始录制的回调 242 | 243 | stopRecord 停止录制的回调 返回的path是录制成功之后文件的保存地址 244 | 245 | 246 | 247 | ## IOS配置注意事项 248 | 249 | ### ios集成的的时候需要在info.list添加 250 | 251 | 252 | NSMicrophoneUsageDescription 253 | 打开话筒 254 | 255 | 256 | NSAppTransportSecurity 257 | 258 | NSAllowsArbitraryLoads 259 | 260 | 261 | 262 | 263 | 264 | ### ios release 打包失败配置注意事项 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | ## android配置注意事项 273 | 274 | ### android 集成的的时候需要在application标签下添加 275 | 276 | 277 | tools:replace="android:label" 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | ## TODO 287 | 288 | * [ ] 双声道切换 单声道切换 289 | 290 | ## 感谢 291 | 292 | 293 | [肖中旺](https://github.com/xzw421771880)对IOS 播放在线Wav的支持 294 | 295 | 296 | ## 作者的其他开源项目推荐 297 | 298 | 299 | [基于腾讯云点播封装的flutter版的播放器插件 ](https://github.com/yxwandroid/flutter_tencentplayer_plus) 300 | 301 | [Flutter 二维码扫描插件](https://github.com/yxwandroid/flutter_plugin_qrcode) 302 | 303 | [抖音开发平台SDK Flutter插件](https://github.com/yxwandroid/flutter_plugin_douyin_open) 304 | 305 | [FLutter地图插件](https://github.com/yxwandroid/flutter_amap_location) 306 | 307 | [Flutter 模板工程](https://github.com/yxwandroid/flutter_app_redux.git) 308 | 309 | ## 关注公众号获取更多内容 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /README_images/f53502b3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/README_images/f53502b3.png -------------------------------------------------------------------------------- /README_images/ios.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/README_images/ios.gif -------------------------------------------------------------------------------- /README_images/ios_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/README_images/ios_error.png -------------------------------------------------------------------------------- /README_images/video2gif_20191118_101627.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/README_images/video2gif_20191118_101627.gif -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'record.wilson.flutter.com.flutter_plugin_record' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | // ext.kotlin_version = '1.2.71' 6 | ext.kotlin_version = '1.3.50' 7 | 8 | repositories { 9 | google() 10 | jcenter() 11 | } 12 | 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:3.2.1' 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | rootProject.allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | maven { url 'https://jitpack.io' } 24 | } 25 | } 26 | 27 | apply plugin: 'com.android.library' 28 | apply plugin: 'kotlin-android' 29 | 30 | android { 31 | compileSdkVersion 29 32 | 33 | sourceSets { 34 | main.java.srcDirs += 'src/main/kotlin' 35 | } 36 | defaultConfig { 37 | minSdkVersion 19 38 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 39 | } 40 | lintOptions { 41 | disable 'InvalidPackage' 42 | } 43 | } 44 | 45 | dependencies { 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 47 | implementation 'com.github.shaoshuai904:RecordWav:1.0.2' 48 | implementation "androidx.appcompat:appcompat:1.0.0" 49 | implementation 'com.github.adrielcafe:AndroidAudioConverter:0.0.8' 50 | } 51 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_plugin_record' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/ITimer.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.timer; 2 | 3 | 4 | 5 | public interface ITimer { 6 | void startTimer(); 7 | 8 | void pauseTimer(); 9 | 10 | void resumeTimer(); 11 | 12 | void stopTimer(); 13 | } 14 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/ITimerChangeCallback.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.timer; 2 | 3 | public interface ITimerChangeCallback { 4 | void onTimeChange(long time); 5 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/MTimer.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.timer; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.util.Timer; 6 | import java.util.TimerTask; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | public class MTimer implements ITimer { 10 | //--real timer-- 11 | private Timer timer; 12 | //--default task-- 13 | private TimerTask task; 14 | //--default initdelay-- 15 | private long initDelay = 0l; 16 | //--default delay-- 17 | private long delay = 0l; 18 | //--call back-- 19 | private ITimerChangeCallback[] callbacks = null; 20 | //--real time-- 21 | private AtomicLong time;//时间的记录工具 22 | 23 | //--action-- 24 | private static final int START = 0; 25 | private static final int PAUSE = 1; 26 | private static final int RESUME = 2; 27 | private static final int STOP = 3; 28 | 29 | private int status = STOP;//默认是stop 30 | 31 | private MTimer(long initDelay, long delay, ITimerChangeCallback[] callbacks) { 32 | this.initDelay = initDelay; 33 | this.delay = delay; 34 | this.callbacks = callbacks; 35 | } 36 | 37 | //-----------------外部方法------------------------ 38 | 39 | /** 40 | * 用于生成MTimer 对象 41 | * 42 | * @return --MTimer 43 | */ 44 | public static Builder makeTimerBuilder() { 45 | return new Builder(); 46 | } 47 | 48 | /** 49 | * 开启 timer 50 | */ 51 | @Override 52 | public void startTimer() { 53 | //判断当前是不是stop,是的话开始运行 54 | if (status != STOP) { 55 | return; 56 | } 57 | //切换当前状态为 start 58 | status = START; 59 | realStartTimer(true); 60 | } 61 | 62 | /** 63 | * 暂停timer 64 | */ 65 | @Override 66 | public void pauseTimer() { 67 | //判断当前是不是start 是不是resume,如果是其中一个就可以 68 | if (status != START && status != RESUME) { 69 | return; 70 | } 71 | //切换当前状态为 pause 72 | status = PAUSE; 73 | realStopTimer(false); 74 | } 75 | 76 | /** 77 | * 重启timer 78 | */ 79 | @Override 80 | public void resumeTimer() { 81 | //判断当前是不是pause ,如果是则恢复 82 | if (status != PAUSE) { 83 | return; 84 | } 85 | //切换当前状态为 resume 86 | status = RESUME; 87 | realStartTimer(false); 88 | } 89 | 90 | /** 91 | * 关闭timer 92 | */ 93 | @Override 94 | public void stopTimer() { 95 | //无论当前处于那种状态都可以stop 96 | status = STOP; 97 | realStopTimer(true); 98 | } 99 | 100 | //-----------------内部方法------------------------ 101 | 102 | /** 103 | * timer 真正的开始方法 104 | * 105 | * @param isToZero --是否清除数据 106 | */ 107 | private void realStartTimer(boolean isToZero) { 108 | //清空记录时间 109 | if (isToZero) { 110 | time = new AtomicLong(0); 111 | } 112 | //重新生成timer、task 113 | if (timer == null && task == null) { 114 | timer = new Timer(); 115 | task = createTask(); 116 | timer.scheduleAtFixedRate(task, initDelay, delay); 117 | } 118 | } 119 | 120 | /** 121 | * timer 真正的关闭方法 122 | * 123 | * @param isToZero --是否清除数据 124 | */ 125 | private void realStopTimer(boolean isToZero) { 126 | //清空记录时间 127 | if (isToZero) { 128 | time = new AtomicLong(0); 129 | } 130 | //关闭当前的timer 131 | if (timer != null) { 132 | timer.purge(); 133 | timer.cancel(); 134 | timer = null; 135 | 136 | } 137 | //关闭当前任务 138 | if (task != null) { 139 | task.cancel(); 140 | task = null; 141 | } 142 | } 143 | 144 | 145 | /** 146 | * 判断是否设置监听回调 147 | * 148 | * @return -- true 表示设置了回调,反之表示没设置 149 | */ 150 | private boolean checkCallback() { 151 | return callbacks != null && callbacks.length > 0; 152 | } 153 | 154 | /** 155 | * 创建task 156 | * 157 | * @return 158 | */ 159 | private TimerTask createTask() { 160 | TimerTask task = new TimerTask() { 161 | @Override 162 | public void run() { 163 | time.incrementAndGet(); 164 | notifyCallback(time); 165 | } 166 | }; 167 | return task; 168 | } 169 | 170 | /** 171 | * 通知callback 172 | * 173 | * @param time --间距走的次数(花费时间=次数*delay+initDelay) 174 | */ 175 | private void notifyCallback(AtomicLong time) { 176 | if (checkCallback()) { 177 | for (ITimerChangeCallback callback : callbacks) { 178 | callback.onTimeChange(time.longValue()); 179 | } 180 | } 181 | } 182 | 183 | public static class Builder { 184 | 185 | //--default initdelay-- 186 | private long initDelay = 0l; 187 | //--default delay-- 188 | private long delay = 0l; 189 | //--call back-- 190 | private ITimerChangeCallback[] callbacks = null; 191 | //--tag-- 192 | private String tag; 193 | 194 | public Builder setTag(String tag) { 195 | if (TextUtils.isEmpty(tag)) { 196 | throw new NullPointerException("设置的tag无效!=>setTag(String tag)"); 197 | } 198 | this.tag = tag; 199 | return this; 200 | } 201 | 202 | 203 | /** 204 | * 设置执行当前任务的时候首次执行时的延迟时间 205 | * 206 | * @param initDelay --首次执行的延迟时间(ms) 207 | */ 208 | public Builder setInitDelay(long initDelay) { 209 | this.initDelay = initDelay; 210 | return this; 211 | } 212 | 213 | /** 214 | * 设置时间回调 215 | * 216 | * @param callbacks 217 | */ 218 | public Builder setCallbacks(ITimerChangeCallback... callbacks) { 219 | this.callbacks = callbacks; 220 | return this; 221 | } 222 | 223 | /** 224 | * 设置后续的延迟时间 225 | * 226 | * @param delay --后续延迟时间(ms) 227 | */ 228 | public Builder setDelay(long delay) { 229 | this.delay = delay; 230 | return this; 231 | } 232 | 233 | /** 234 | * 外部会重用此对象,所以需要重置其参数 235 | */ 236 | public void reset() { 237 | tag = null; 238 | initDelay = 0l; 239 | delay = 0l; 240 | callbacks = null; 241 | } 242 | 243 | /** 244 | * 最终的生成方法,如果不调用此处,timer无法运行 245 | */ 246 | public MTimer build() { 247 | //--check delay-- 248 | if (initDelay < 0 || delay < 0) { 249 | throw new AssertionError("initDelay或delay 不允许小于0"); 250 | } 251 | //--build timer-- 252 | MTimer timer = new MTimer(initDelay, delay, callbacks); 253 | //--add to cache-- 254 | if (!TextUtils.isEmpty(tag)) { 255 | TimerUtils.addTimerToCache(tag, timer); 256 | } 257 | //--return timer-- 258 | return timer; 259 | } 260 | 261 | } 262 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/timer/TimerUtils.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.timer; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import java.util.Locale; 7 | import java.util.WeakHashMap; 8 | /** 9 | * description: 定时器工具类 10 | * author: Simon 11 | * created at 2017/8/10 上午9:47 12 | */ 13 | 14 | public final class TimerUtils { 15 | //--tag-- 16 | private static final String TAG = "TimerUtils"; 17 | //--err info-- 18 | private static final String ERR_INFO = "未找到对应的MTimer,确认是否设置过Tag!=>new Builder().setTag(String tag)"; 19 | //--cache-- 20 | private static WeakHashMap cacheTimerMap = new WeakHashMap<>(); 21 | //--action-- 22 | private static final int START = 0; 23 | private static final int PAUSE = 1; 24 | private static final int RESUME = 2; 25 | private static final int STOP = 3; 26 | 27 | //--recycle build-- 28 | private static final MTimer.Builder BUILDER = new MTimer.Builder(); 29 | 30 | private TimerUtils() { 31 | throw new AssertionError("you can't init me!"); 32 | } 33 | 34 | /** 35 | * 注意此方法,会重复利用Builder 对象,所以每次build()完成后再重新使用该方法!! 36 | * 37 | * @return --builder 38 | */ 39 | public static MTimer.Builder makeBuilder() { 40 | BUILDER.reset();//每次执行的时候都会重置一次 41 | return BUILDER; 42 | } 43 | 44 | /** 45 | * 开启timer ,时间清零 46 | */ 47 | public static void startTimer(String tag) { 48 | actionTimer(START, tag); 49 | } 50 | 51 | /** 52 | * 恢复timer,不清零 53 | */ 54 | public static void resumeTimer(String tag) { 55 | actionTimer(RESUME, tag); 56 | } 57 | 58 | /** 59 | * 暂停timer 60 | */ 61 | public static void pauseTimer(String tag) { 62 | actionTimer(PAUSE, tag); 63 | } 64 | 65 | /** 66 | * 关闭 timer 67 | */ 68 | public static void stopTimer(String tag) { 69 | actionTimer(STOP, tag); 70 | } 71 | 72 | /** 73 | * 格式化 时间 格式为 hh:mm:ss 74 | * 75 | * @param cnt 76 | * @return 77 | */ 78 | public static String formatTime(long cnt) { 79 | long hour = cnt / 3600; 80 | long min = cnt % 3600 / 60; 81 | long second = cnt % 60; 82 | return String.format(Locale.CHINA, "%02d:%02d:%02d", hour, min, second); 83 | } 84 | 85 | //------------------------私有方法/内部类------------------------------ 86 | 87 | /** 88 | * 添加timer到缓存 89 | * 90 | * @param tag --tag 91 | * @param timer --timer 92 | */ 93 | public static void addTimerToCache(String tag, MTimer timer) { 94 | if (cacheTimerMap == null) { 95 | cacheTimerMap = new WeakHashMap<>(); 96 | } 97 | cacheTimerMap.put(tag, timer); 98 | } 99 | 100 | /** 101 | * 真正的执行方法 102 | * 103 | * @param action --行为 104 | * @param tag --tag 105 | */ 106 | private static void actionTimer(int action, String tag) { 107 | //-----check tag---- 108 | if (!checkTag(tag)) { 109 | Log.e(TAG, "The tag is empty or null!"); 110 | return; 111 | } 112 | //-----check timer---- 113 | MTimer timer = findMTimerByTag(tag); 114 | if (timer == null) { 115 | Log.e(TAG, "Can't found timer by tag!"); 116 | return; 117 | } 118 | 119 | //-----action timer---- 120 | switch (action) { 121 | case START: 122 | timer.startTimer(); 123 | break; 124 | case RESUME: 125 | timer.resumeTimer(); 126 | break; 127 | case PAUSE: 128 | timer.pauseTimer(); 129 | break; 130 | case STOP: 131 | timer.stopTimer(); 132 | break; 133 | } 134 | 135 | 136 | } 137 | 138 | 139 | /** 140 | * 通过tag获取mtimer 141 | * 142 | * @param tag --设置的tag 143 | * @return --MTimer 144 | */ 145 | private static MTimer findMTimerByTag(String tag) { 146 | if (!checkTag(tag) || cacheTimerMap == null || cacheTimerMap.size() == 0) {//tag无效,没有缓存数据,返回null 147 | return null; 148 | } else {//反之根据tag返回 149 | return cacheTimerMap.get(tag); 150 | } 151 | } 152 | 153 | /** 154 | * 判断tag 是否有效 155 | * 156 | * @param tag --tag 157 | * @return true表示有效,反之无效 158 | */ 159 | private static boolean checkTag(String tag) { 160 | return !TextUtils.isEmpty(tag); 161 | } 162 | 163 | 164 | } 165 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/DateUtils.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.utils; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | 6 | public class DateUtils { 7 | public static String dateToString(Date date) { 8 | String str = "yyyyMMddhhmmss"; 9 | SimpleDateFormat format = new SimpleDateFormat(str); 10 | String dateFormat = format.format(date); 11 | return dateFormat; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/DialogUtil.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.utils; 2 | 3 | import android.app.Activity; 4 | import android.app.Dialog; 5 | import android.content.Context; 6 | import android.content.DialogInterface; 7 | import android.content.Intent; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.app.AlertDialog; 11 | /** 12 | * 对话框管理 13 | * 打开app应用程序信息界面 14 | */ 15 | public class DialogUtil { 16 | 17 | public static void Dialog(final Activity activity, String content) { 18 | Dialog deleteDialog = new AlertDialog.Builder(activity) 19 | .setTitle("提示") 20 | .setMessage("请进入应用信息界面开启录音权限") 21 | .setPositiveButton("确定", 22 | new DialogInterface.OnClickListener() { 23 | public void onClick(DialogInterface dialog, int which) { 24 | startSetting(activity); 25 | } 26 | }) 27 | .create(); 28 | deleteDialog.setCanceledOnTouchOutside(false); 29 | deleteDialog.setCancelable(false); 30 | deleteDialog.show(); 31 | } 32 | 33 | 34 | /** 35 | * 启动app设置应用程序信息界面 36 | */ 37 | public static void startSetting(Context context) { 38 | Intent intent = new Intent(); 39 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 40 | // intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 41 | if (Build.VERSION.SDK_INT >= 9) { 42 | intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS"); 43 | intent.setData(Uri.fromParts("package", context.getPackageName(), null)); 44 | } else if (Build.VERSION.SDK_INT <= 8) { 45 | intent.setAction(Intent.ACTION_VIEW); 46 | intent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails"); 47 | intent.putExtra(context.getPackageName(), context.getPackageName()); 48 | } 49 | context.startActivity(intent); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/PlayState.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.utils; 2 | 3 | public enum PlayState { 4 | prepare, start, pause, complete 5 | } 6 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/PlayUtilsPlus.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.utils; 2 | 3 | import android.media.MediaPlayer; 4 | 5 | 6 | public class PlayUtilsPlus { 7 | PlayStateChangeListener playStateChangeListener; 8 | MediaPlayer player; 9 | 10 | public PlayUtilsPlus() { 11 | } 12 | 13 | public void setPlayStateChangeListener(PlayStateChangeListener listener) { 14 | this.playStateChangeListener = listener; 15 | // this.playStateChangeListener.onPlayStateChange(PlayState.prepare); 16 | } 17 | 18 | public void startPlaying(String filePath) { 19 | try { 20 | isPause=false; 21 | this.player = new MediaPlayer(); 22 | this.player.setDataSource(filePath); 23 | this.player.prepareAsync(); 24 | this.player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 25 | public void onPrepared(MediaPlayer mp) { 26 | PlayUtilsPlus.this.player.start(); 27 | } 28 | }); 29 | if (this.playStateChangeListener != null) { 30 | // this.playStateChangeListener.onPlayStateChange(PlayState.start); 31 | } 32 | 33 | this.player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 34 | public void onCompletion(MediaPlayer mp) { 35 | PlayUtilsPlus.this.stopPlaying(); 36 | } 37 | }); 38 | } catch (Exception var3) { 39 | var3.printStackTrace(); 40 | } 41 | 42 | } 43 | Boolean isPause = false; 44 | 45 | public boolean pausePlay() { 46 | try { 47 | if (this.player.isPlaying() && !isPause) { 48 | this.player.pause(); 49 | isPause = true; 50 | } else { 51 | this.player.start(); 52 | isPause = false; 53 | } 54 | 55 | } catch (Exception var2) { 56 | var2.printStackTrace(); 57 | } 58 | return isPause ; 59 | } 60 | 61 | public void stopPlaying() { 62 | try { 63 | if (this.player != null) { 64 | this.player.stop(); 65 | this.player.reset(); 66 | this.player=null; 67 | if (this.playStateChangeListener != null) { 68 | this.playStateChangeListener.onPlayStateChange(PlayState.complete); 69 | } 70 | } 71 | } catch (Exception var2) { 72 | var2.printStackTrace(); 73 | } 74 | 75 | } 76 | 77 | public boolean isPlaying() { 78 | try { 79 | return this.player != null && this.player.isPlaying(); 80 | } catch (Exception var2) { 81 | return false; 82 | } 83 | } 84 | 85 | public interface PlayStateChangeListener { 86 | void onPlayStateChange(PlayState playState); 87 | } 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /android/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record/utils/RecorderUtil.java: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record.utils; 2 | 3 | import android.os.Environment; 4 | import android.util.Log; 5 | 6 | import com.maple.recorder.recording.AudioChunk; 7 | import com.maple.recorder.recording.AudioRecordConfig; 8 | import com.maple.recorder.recording.MsRecorder; 9 | import com.maple.recorder.recording.PullTransport; 10 | import com.maple.recorder.recording.Recorder; 11 | 12 | import java.io.File; 13 | import java.util.Date; 14 | 15 | 16 | public class RecorderUtil { 17 | 18 | Recorder recorder; 19 | public static String rootPath = "/yun_ke_fu/flutter/wav_file/"; 20 | String voicePath; 21 | PlayUtilsPlus playUtils; 22 | 23 | RecordListener recordListener; 24 | PlayStateListener playStateListener; 25 | 26 | public RecorderUtil() { 27 | initVoice(); 28 | } 29 | 30 | 31 | 32 | public RecorderUtil(String path) { 33 | voicePath =path; 34 | } 35 | 36 | 37 | 38 | 39 | 40 | public void addPlayAmplitudeListener(RecordListener recordListener) { 41 | this.recordListener = recordListener; 42 | } 43 | public void addPlayStateListener(PlayStateListener playStateListener) { 44 | this.playStateListener = playStateListener; 45 | } 46 | 47 | private void initVoice() { 48 | initPath(); 49 | initVoicePath(); 50 | initRecorder(); 51 | } 52 | 53 | 54 | 55 | //初始化存储路径 56 | private void initPath() { 57 | String ROOT = "";// /storage/emulated/0 58 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 59 | ROOT = Environment.getExternalStorageDirectory().getPath(); 60 | Log.e("voice", "系统方法:" + ROOT); 61 | } 62 | rootPath = ROOT + rootPath; 63 | 64 | File lrcFile = new File(rootPath); 65 | if (!lrcFile.exists()) { 66 | lrcFile.mkdirs(); 67 | } 68 | 69 | Log.e("voice", "初始存储路径" + rootPath); 70 | } 71 | 72 | 73 | private void initVoicePath() { 74 | String forDate = DateUtils.dateToString(new Date()); 75 | String name = "wav-" + forDate; 76 | voicePath = rootPath + name + ".wav"; 77 | Log.e("voice", "初始化语音路径" + voicePath); 78 | 79 | 80 | } 81 | 82 | 83 | private void initRecorder() { 84 | recorder = MsRecorder.wav( 85 | new File(voicePath), 86 | new AudioRecordConfig.Default(), 87 | new PullTransport.Default() 88 | .setOnAudioChunkPulledListener(new PullTransport.OnAudioChunkPulledListener() { 89 | @Override 90 | public void onAudioChunkPulled(AudioChunk audioChunk) { 91 | if (recordListener != null) { 92 | recordListener.onPlayAmplitude(audioChunk.maxAmplitude()); 93 | } 94 | } 95 | }) 96 | 97 | ); 98 | } 99 | 100 | public void startRecord() { 101 | if (recordListener != null) { 102 | recordListener.onVoicePathSuccess(voicePath); 103 | } 104 | 105 | recorder.stopRecording(); 106 | recorder.startRecording(); 107 | } 108 | 109 | public void stopRecord() { 110 | recorder.stopRecording(); 111 | } 112 | 113 | public void playVoice() { 114 | if (playUtils == null) { 115 | playUtils = new PlayUtilsPlus(); 116 | playUtils.setPlayStateChangeListener(new PlayUtilsPlus.PlayStateChangeListener() { 117 | @Override 118 | public void onPlayStateChange(PlayState playState) { 119 | playStateListener.playState(playState); 120 | } 121 | }); 122 | } 123 | if(playUtils.isPlaying()) { 124 | playUtils.stopPlaying(); 125 | } 126 | playUtils.startPlaying(voicePath); 127 | } 128 | 129 | public boolean pausePlay(){ 130 | LogUtils.LOGD("wilson","pausePlay"); 131 | boolean isPlaying = playUtils.pausePlay(); 132 | return isPlaying; 133 | } 134 | 135 | public void stopPlay(){ 136 | LogUtils.LOGD("wilson","stopPlay"); 137 | playUtils.stopPlaying(); 138 | } 139 | 140 | public interface RecordListener { 141 | void onPlayAmplitude(Double amplitude); 142 | 143 | void onVoicePathSuccess(String voicePath); 144 | 145 | } 146 | 147 | public interface PlayStateListener { 148 | 149 | void playState(PlayState playState); 150 | 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /example/.flutter-plugins-dependencies: -------------------------------------------------------------------------------- 1 | {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_plugin_record","path":"/Users/wilson/aochuang/FlutterDemo/flutter_plugin_record/","dependencies":[]},{"name":"path_provider","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.24/","dependencies":[]},{"name":"shared_preferences","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.12+4/","dependencies":[]}],"android":[{"name":"flutter_plugin_record","path":"/Users/wilson/aochuang/FlutterDemo/flutter_plugin_record/","dependencies":[]},{"name":"path_provider","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.6.24/","dependencies":[]},{"name":"shared_preferences","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.12+4/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-0.0.4+6/","dependencies":[]},{"name":"shared_preferences_macos","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_macos-0.0.1+11/","dependencies":[]}],"linux":[{"name":"path_provider_linux","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-0.0.1+2/","dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-0.0.2+4/","dependencies":["path_provider_linux"]}],"windows":[{"name":"path_provider_windows","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-0.0.4+3/","dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-0.0.1+3/","dependencies":["path_provider_windows"]}],"web":[{"name":"shared_preferences_web","path":"/Users/wilson/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-0.1.2+7/","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_plugin_record","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos","path_provider_linux","path_provider_windows"]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_macos","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_linux","shared_preferences_macos","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_macos","dependencies":[]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2021-03-24 11:27:57.607425","version":"2.0.3"} -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_plugin_record_example 2 | 3 | 4 | ### 使用 5 | 6 | ### 1, 初始化录制 7 | 8 | 9 | 10 | 可以在页面初始化的时候进行初始化比如: 在initState方法中进行初始化 11 | 12 | //实例化对象 13 | FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); 14 | // 初始化 15 | recordPlugin.init() 16 | 17 | ### 2, 开始录制 18 | 19 | recordPlugin.start() 20 | ### 3, 停止录制 21 | recordPlugin.stop() 22 | ### 4, 播放 暂停,停止播放 23 | #### 1,播放 24 | 25 | recordPlugin.play() 26 | 27 | #### 2, 暂停和继续播放 28 | 29 | recordPlugin.pausePlay(); 30 | 31 | #### 3, 停止播放 32 | 33 | recordPlugin.stopPlay(); 34 | 35 | ### 3, 释放资源 36 | 可以在页面退出的时候进行资源释放 比如在 dispose方法中调用如下代码 37 | 38 | recordPlugin.dispose() 39 | 40 | ### 4,回调监听 41 | 1,初始化回调监听 42 | 43 | 44 | ///初始化方法的监听 45 | recordPlugin.responseFromInit.listen((data) { 46 | if (data) { 47 | print("初始化成功"); 48 | } else { 49 | print("初始化失败"); 50 | } 51 | }); 52 | 53 | 54 | 2,开始录制停止录制监听 55 | 56 | /// 开始录制或结束录制的监听 57 | recordPlugin.response.listen((data) { 58 | if (data.msg == "onStop") { 59 | ///结束录制时会返回录制文件的地址方便上传服务器 60 | print("onStop " + data.path); 61 | } else if (data.msg == "onStart") { 62 | print("onStart --"); 63 | } 64 | }); 65 | 66 | 3,录制声音大小回调监听 67 | 68 | 69 | ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 70 | recordPlugin.responseFromAmplitude.listen((data) { 71 | var voiceData = double.parse(data.msg); 72 | var tempVoice = ""; 73 | if (voiceData > 0 && voiceData < 0.1) { 74 | tempVoice = "images/voice_volume_2.png"; 75 | } else if (voiceData > 0.2 && voiceData < 0.3) { 76 | tempVoice = "images/voice_volume_3.png"; 77 | } else if (voiceData > 0.3 && voiceData < 0.4) { 78 | tempVoice = "images/voice_volume_4.png"; 79 | } else if (voiceData > 0.4 && voiceData < 0.5) { 80 | tempVoice = "images/voice_volume_5.png"; 81 | } else if (voiceData > 0.5 && voiceData < 0.6) { 82 | tempVoice = "images/voice_volume_6.png"; 83 | } else if (voiceData > 0.6 && voiceData < 0.7) { 84 | tempVoice = "images/voice_volume_7.png"; 85 | } else if (voiceData > 0.7 && voiceData < 1) { 86 | tempVoice = "images/voice_volume_7.png"; 87 | } 88 | setState(() { 89 | voiceIco = tempVoice; 90 | if(overlayEntry!=null){ 91 | overlayEntry.markNeedsBuild(); 92 | } 93 | }); 94 | 95 | print("振幅大小 " + voiceData.toString() + " " + voiceIco); 96 | }); 97 | 98 | 99 | ## 2,录制组件的使用 100 | 101 | 102 | ### 1,在使用的页面进行导入package 103 | 104 | import 'package:flutter_plugin_record/index.dart'; 105 | 106 | 107 | 108 | 109 | 110 | ### 2,在使用的地方引入VoiceWidget组件 111 | 112 | new VoiceWidget(), 113 | 114 | 115 | 116 | ## TODO 117 | 118 | * [x] 实现发送语音时间按下抬起时间很短提示 119 | * [x] 优化代码 120 | * [x] 实现录制完成文件路径回调功能,方面使用者可以把录音文件上传服务器 121 | 122 | 123 | ## 关注公众号获取更多内容 124 | 125 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20190926100941125.jpg) 126 | 127 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 29 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "record.wilson.flutter.com.flutter_plugin_record_example" 42 | minSdkVersion 19 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 17 | 20 | 27 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/record/wilson/flutter/com/flutter_plugin_record_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package record.wilson.flutter.com.flutter_plugin_record_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // ext.kotlin_version = '1.2.71' 3 | ext.kotlin_version = '1.3.50' 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:3.5.3' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 18 09:02:45 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/res/values/strings_en.arb: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | dcb78416cfbfe60288e8c774d67c139c -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'High-performance, high-fidelity mobile apps.' 10 | s.homepage = 'https://flutter.io' 11 | s.license = { :type => 'MIT' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '8.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_plugin_record (0.0.1): 4 | - Flutter 5 | - path_provider (0.0.1): 6 | - Flutter 7 | - shared_preferences (0.0.1): 8 | - Flutter 9 | 10 | DEPENDENCIES: 11 | - Flutter (from `Flutter`) 12 | - flutter_plugin_record (from `.symlinks/plugins/flutter_plugin_record/ios`) 13 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 14 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 15 | 16 | EXTERNAL SOURCES: 17 | Flutter: 18 | :path: Flutter 19 | flutter_plugin_record: 20 | :path: ".symlinks/plugins/flutter_plugin_record/ios" 21 | path_provider: 22 | :path: ".symlinks/plugins/path_provider/ios" 23 | shared_preferences: 24 | :path: ".symlinks/plugins/shared_preferences/ios" 25 | 26 | SPEC CHECKSUMS: 27 | Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c 28 | flutter_plugin_record: 562ded56f3a109d769e72c3ef52ef20d835493d4 29 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 30 | shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d 31 | 32 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 33 | 34 | COCOAPODS: 1.10.1 35 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | flutter_plugin_record_example 20 | CFBundlePackageType 21 | APPL 22 | CFBundleShortVersionString 23 | $(FLUTTER_BUILD_NAME) 24 | CFBundleSignature 25 | ???? 26 | CFBundleVersion 27 | $(CURRENT_PROJECT_VERSION) 28 | LSRequiresIPhoneOS 29 | 30 | NSMicrophoneUsageDescription 31 | 打开话筒 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIViewControllerBasedStatusBarAppearance 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /example/lib/generated/i18n.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | // ignore_for_file: non_constant_identifier_names 7 | // ignore_for_file: camel_case_types 8 | // ignore_for_file: prefer_single_quotes 9 | 10 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 11 | class S implements WidgetsLocalizations { 12 | const S(); 13 | 14 | static S current; 15 | 16 | static const GeneratedLocalizationsDelegate delegate = 17 | GeneratedLocalizationsDelegate(); 18 | 19 | static S of(BuildContext context) => Localizations.of(context, S); 20 | 21 | @override 22 | TextDirection get textDirection => TextDirection.ltr; 23 | 24 | } 25 | 26 | class $en extends S { 27 | const $en(); 28 | } 29 | 30 | class GeneratedLocalizationsDelegate extends LocalizationsDelegate { 31 | const GeneratedLocalizationsDelegate(); 32 | 33 | List get supportedLocales { 34 | return const [ 35 | Locale("en", ""), 36 | ]; 37 | } 38 | 39 | LocaleListResolutionCallback listResolution({Locale fallback, bool withCountry = true}) { 40 | return (List locales, Iterable supported) { 41 | if (locales == null || locales.isEmpty) { 42 | return fallback ?? supported.first; 43 | } else { 44 | return _resolve(locales.first, fallback, supported, withCountry); 45 | } 46 | }; 47 | } 48 | 49 | LocaleResolutionCallback resolution({Locale fallback, bool withCountry = true}) { 50 | return (Locale locale, Iterable supported) { 51 | return _resolve(locale, fallback, supported, withCountry); 52 | }; 53 | } 54 | 55 | @override 56 | Future load(Locale locale) { 57 | final String lang = getLang(locale); 58 | if (lang != null) { 59 | switch (lang) { 60 | case "en": 61 | S.current = const $en(); 62 | return SynchronousFuture(S.current); 63 | default: 64 | // NO-OP. 65 | } 66 | } 67 | S.current = const S(); 68 | return SynchronousFuture(S.current); 69 | } 70 | 71 | @override 72 | bool isSupported(Locale locale) => _isSupported(locale, true); 73 | 74 | @override 75 | bool shouldReload(GeneratedLocalizationsDelegate old) => false; 76 | 77 | /// 78 | /// Internal method to resolve a locale from a list of locales. 79 | /// 80 | Locale _resolve(Locale locale, Locale fallback, Iterable supported, bool withCountry) { 81 | if (locale == null || !_isSupported(locale, withCountry)) { 82 | return fallback ?? supported.first; 83 | } 84 | 85 | final Locale languageLocale = Locale(locale.languageCode, ""); 86 | if (supported.contains(locale)) { 87 | return locale; 88 | } else if (supported.contains(languageLocale)) { 89 | return languageLocale; 90 | } else { 91 | final Locale fallbackLocale = fallback ?? supported.first; 92 | return fallbackLocale; 93 | } 94 | } 95 | 96 | /// 97 | /// Returns true if the specified locale is supported, false otherwise. 98 | /// 99 | bool _isSupported(Locale locale, bool withCountry) { 100 | if (locale != null) { 101 | for (Locale supportedLocale in supportedLocales) { 102 | // Language must always match both locales. 103 | if (supportedLocale.languageCode != locale.languageCode) { 104 | continue; 105 | } 106 | 107 | // If country code matches, return this locale. 108 | if (supportedLocale.countryCode == locale.countryCode) { 109 | return true; 110 | } 111 | 112 | // If no country requirement is requested, check if this locale has no country. 113 | if (true != withCountry && (supportedLocale.countryCode == null || supportedLocale.countryCode.isEmpty)) { 114 | return true; 115 | } 116 | } 117 | } 118 | return false; 119 | } 120 | } 121 | 122 | String getLang(Locale l) => l == null 123 | ? null 124 | : l.countryCode != null && l.countryCode.isEmpty 125 | ? l.languageCode 126 | : l.toString(); 127 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_plugin_record_example/path_provider_screen.dart'; 3 | import 'package:flutter_plugin_record_example/record_mp3_screen.dart'; 4 | import 'package:flutter_plugin_record_example/record_screen.dart'; 5 | import 'package:flutter_plugin_record_example/wechat_record_screen.dart'; 6 | 7 | void main() => runApp(MyApp()); 8 | 9 | class MyApp extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return new MaterialApp( 13 | title: 'Flutter Demo', 14 | theme: new ThemeData( 15 | primarySwatch: Colors.blue, 16 | ), 17 | home: new MyHomePage(title: 'Flutter Demo Home Page'), 18 | routes: { 19 | "RecordScreen": (BuildContext context) => new RecordScreen(), 20 | "RecordMp3Screen": (BuildContext context) => new RecordMp3Screen(), 21 | "WeChatRecordScreen": (BuildContext context) => 22 | new WeChatRecordScreen(), 23 | "PathProviderScreen": (BuildContext context) => 24 | new PathProviderScreen(), 25 | }, 26 | ); 27 | } 28 | } 29 | 30 | class MyHomePage extends StatefulWidget { 31 | MyHomePage({Key key, this.title}) : super(key: key); 32 | final String title; 33 | 34 | @override 35 | _MyHomePageState createState() => new _MyHomePageState(); 36 | } 37 | 38 | class _MyHomePageState extends State { 39 | @override 40 | void initState() { 41 | super.initState(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return new Scaffold( 47 | appBar: new AppBar( 48 | title: new Text("flutter版微信语音录制实现"), 49 | ), 50 | body: new Center( 51 | child: new Column( 52 | mainAxisSize: MainAxisSize.min, 53 | children: [ 54 | new FlatButton( 55 | onPressed: () { 56 | Navigator.pushNamed(context, "RecordScreen"); 57 | }, 58 | child: new Text("进入语音录制界面")), 59 | new FlatButton( 60 | onPressed: () { 61 | Navigator.pushNamed(context, "RecordMp3Screen"); 62 | }, 63 | child: new Text("进入录制mp3模式")), 64 | new FlatButton( 65 | onPressed: () { 66 | Navigator.pushNamed(context, "WeChatRecordScreen"); 67 | }, 68 | child: new Text("进入仿微信录制界面")), 69 | new FlatButton( 70 | onPressed: () { 71 | Navigator.pushNamed(context, "PathProviderScreen"); 72 | }, 73 | child: new Text("进入文件路径获取界面")), 74 | ], 75 | ), 76 | ), 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /example/lib/path_provider_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:path_provider/path_provider.dart'; 5 | 6 | class PathProviderScreen extends StatefulWidget { 7 | PathProviderScreen({Key key, this.title}) : super(key: key); 8 | final String title; 9 | 10 | @override 11 | _PathProviderScreenState createState() => _PathProviderScreenState(); 12 | } 13 | 14 | class _PathProviderScreenState extends State { 15 | Future _tempDirectory; 16 | Future _appSupportDirectory; 17 | Future _appLibraryDirectory; 18 | Future _appDocumentsDirectory; 19 | Future _externalDocumentsDirectory; 20 | Future> _externalStorageDirectories; 21 | Future> _externalCacheDirectories; 22 | 23 | void _requestTempDirectory() { 24 | setState(() { 25 | _tempDirectory = getTemporaryDirectory(); 26 | }); 27 | } 28 | 29 | Widget _buildDirectory( 30 | BuildContext context, AsyncSnapshot snapshot) { 31 | Text text = const Text(''); 32 | if (snapshot.connectionState == ConnectionState.done) { 33 | if (snapshot.hasError) { 34 | text = Text('Error: ${snapshot.error}'); 35 | } else if (snapshot.hasData) { 36 | text = Text('path: ${snapshot.data.path}'); 37 | } else { 38 | text = const Text('path unavailable'); 39 | } 40 | } 41 | return Padding(padding: const EdgeInsets.all(16.0), child: text); 42 | } 43 | 44 | Widget _buildDirectories( 45 | BuildContext context, AsyncSnapshot> snapshot) { 46 | Text text = const Text(''); 47 | if (snapshot.connectionState == ConnectionState.done) { 48 | if (snapshot.hasError) { 49 | text = Text('Error: ${snapshot.error}'); 50 | } else if (snapshot.hasData) { 51 | final String combined = 52 | snapshot.data.map((Directory d) => d.path).join(', '); 53 | text = Text('paths: $combined'); 54 | } else { 55 | text = const Text('path unavailable'); 56 | } 57 | } 58 | return Padding(padding: const EdgeInsets.all(16.0), child: text); 59 | } 60 | 61 | void _requestAppDocumentsDirectory() { 62 | setState(() { 63 | _appDocumentsDirectory = getApplicationDocumentsDirectory(); 64 | }); 65 | } 66 | 67 | void _requestAppSupportDirectory() { 68 | setState(() { 69 | _appSupportDirectory = getApplicationSupportDirectory(); 70 | }); 71 | } 72 | 73 | void _requestAppLibraryDirectory() { 74 | setState(() { 75 | _appLibraryDirectory = getLibraryDirectory(); 76 | }); 77 | } 78 | 79 | void _requestExternalStorageDirectory() { 80 | setState(() { 81 | _externalDocumentsDirectory = getExternalStorageDirectory(); 82 | }); 83 | } 84 | 85 | void _requestExternalStorageDirectories(StorageDirectory type) { 86 | setState(() { 87 | _externalStorageDirectories = getExternalStorageDirectories(type: type); 88 | }); 89 | } 90 | 91 | void _requestExternalCacheDirectories() { 92 | setState(() { 93 | _externalCacheDirectories = getExternalCacheDirectories(); 94 | }); 95 | } 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Scaffold( 100 | appBar: AppBar( 101 | title: Text("获取文件路径界面"), 102 | ), 103 | body: Center( 104 | child: ListView( 105 | children: [ 106 | Padding( 107 | padding: const EdgeInsets.all(16.0), 108 | child: RaisedButton( 109 | child: const Text('Get Temporary Directory'), 110 | onPressed: _requestTempDirectory, 111 | ), 112 | ), 113 | FutureBuilder( 114 | future: _tempDirectory, builder: _buildDirectory), 115 | Padding( 116 | padding: const EdgeInsets.all(16.0), 117 | child: RaisedButton( 118 | child: const Text('Get Application Documents Directory'), 119 | onPressed: _requestAppDocumentsDirectory, 120 | ), 121 | ), 122 | FutureBuilder( 123 | future: _appDocumentsDirectory, builder: _buildDirectory), 124 | Padding( 125 | padding: const EdgeInsets.all(16.0), 126 | child: RaisedButton( 127 | child: const Text('Get Application Support Directory'), 128 | onPressed: _requestAppSupportDirectory, 129 | ), 130 | ), 131 | FutureBuilder( 132 | future: _appSupportDirectory, builder: _buildDirectory), 133 | Padding( 134 | padding: const EdgeInsets.all(16.0), 135 | child: RaisedButton( 136 | child: const Text('Get Application Library Directory'), 137 | onPressed: _requestAppLibraryDirectory, 138 | ), 139 | ), 140 | FutureBuilder( 141 | future: _appLibraryDirectory, builder: _buildDirectory), 142 | Padding( 143 | padding: const EdgeInsets.all(16.0), 144 | child: RaisedButton( 145 | child: Text( 146 | '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directory"}'), 147 | onPressed: 148 | Platform.isIOS ? null : _requestExternalStorageDirectory, 149 | ), 150 | ), 151 | FutureBuilder( 152 | future: _externalDocumentsDirectory, builder: _buildDirectory), 153 | Column(children: [ 154 | Padding( 155 | padding: const EdgeInsets.all(16.0), 156 | child: RaisedButton( 157 | child: Text( 158 | '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directories"}'), 159 | onPressed: Platform.isIOS 160 | ? null 161 | : () { 162 | _requestExternalStorageDirectories( 163 | StorageDirectory.music, 164 | ); 165 | }, 166 | ), 167 | ), 168 | ]), 169 | FutureBuilder>( 170 | future: _externalStorageDirectories, 171 | builder: _buildDirectories), 172 | Column(children: [ 173 | Padding( 174 | padding: const EdgeInsets.all(16.0), 175 | child: RaisedButton( 176 | child: Text( 177 | '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Cache Directories"}'), 178 | onPressed: 179 | Platform.isIOS ? null : _requestExternalCacheDirectories, 180 | ), 181 | ), 182 | ]), 183 | FutureBuilder>( 184 | future: _externalCacheDirectories, builder: _buildDirectories), 185 | ], 186 | ), 187 | ), 188 | ); 189 | } 190 | } -------------------------------------------------------------------------------- /example/lib/record_mp3_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flustars/flustars.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_plugin_record/flutter_plugin_record.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | 8 | class RecordMp3Screen extends StatefulWidget { 9 | @override 10 | _RecordMp3ScreenState createState() => _RecordMp3ScreenState(); 11 | } 12 | 13 | class _RecordMp3ScreenState extends State { 14 | FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); 15 | 16 | String filePath = ""; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | 22 | ///初始化方法的监听 23 | recordPlugin.responseFromInit.listen((data) { 24 | if (data) { 25 | print("初始化成功"); 26 | } else { 27 | print("初始化失败"); 28 | } 29 | }); 30 | 31 | /// 开始录制或结束录制的监听 32 | recordPlugin.response.listen((data) { 33 | if (data.msg == "onStop") { 34 | ///结束录制时会返回录制文件的地址方便上传服务器 35 | print("onStop 文件路径" + data.path); 36 | filePath = data.path; 37 | print("onStop 时长 " + data.audioTimeLength.toString()); 38 | } else if (data.msg == "onStart") { 39 | print("onStart --"); 40 | } else { 41 | print("--" + data.msg); 42 | } 43 | }); 44 | 45 | ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 46 | recordPlugin.responseFromAmplitude.listen((data) { 47 | var voiceData = double.parse(data.msg); 48 | print("振幅大小 " + voiceData.toString()); 49 | }); 50 | 51 | recordPlugin.responsePlayStateController.listen((data) { 52 | print("播放路径 " + data.playPath); 53 | print("播放状态 " + data.playState); 54 | }); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return Scaffold( 60 | appBar: AppBar( 61 | title: const Text('录制mp3'), 62 | ), 63 | body: Center( 64 | child: Column( 65 | children: [ 66 | FlatButton( 67 | child: Text("初始化录制mp3"), 68 | onPressed: () { 69 | _initRecordMp3(); 70 | }, 71 | ), 72 | FlatButton( 73 | child: Text("开始录制"), 74 | onPressed: () { 75 | start(); 76 | }, 77 | ), 78 | FlatButton( 79 | child: Text("根据路径录制mp3文件"), 80 | onPressed: () { 81 | _requestAppDocumentsDirectory(); 82 | }, 83 | ), 84 | FlatButton( 85 | child: Text("停止录制"), 86 | onPressed: () { 87 | stop(); 88 | }, 89 | ), 90 | FlatButton( 91 | child: Text("播放"), 92 | onPressed: () { 93 | play(); 94 | }, 95 | ), 96 | FlatButton( 97 | child: Text("播放本地指定路径录音文件"), 98 | onPressed: () { 99 | playByPath(filePath,"file"); 100 | }, 101 | ), 102 | FlatButton( 103 | child: Text("播放网络mp3文件"), 104 | onPressed: () { 105 | playByPath("https://test-1259809289.cos.ap-nanjing.myqcloud.com/temp.mp3","url"); 106 | }, 107 | ), 108 | FlatButton( 109 | child: Text("暂停|继续播放"), 110 | onPressed: () { 111 | pause(); 112 | }, 113 | ), 114 | FlatButton( 115 | child: Text("停止播放"), 116 | onPressed: () { 117 | stopPlay(); 118 | }, 119 | ), 120 | ], 121 | ), 122 | ), 123 | ); 124 | } 125 | 126 | void _requestAppDocumentsDirectory() { 127 | // if(Platform.isIOS){ 128 | // //ios相关代码 129 | // setState(() { 130 | // getApplicationDocumentsDirectory().then((value) { 131 | // String nowDataTimeStr = DateUtil.getNowDateMs().toString(); 132 | // String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; 133 | // startByWavPath(wavPath); 134 | // }); 135 | // }); 136 | // }else if(Platform.isAndroid){ 137 | // //android相关代码 138 | // } 139 | 140 | setState(() { 141 | getApplicationDocumentsDirectory().then((value) { 142 | String nowDataTimeStr = DateUtil.getNowDateMs().toString(); 143 | // TODO 注意IOS 传递的Mp3路径一定是以 .MP3 结尾 144 | String wavPath =""; 145 | if (Platform.isIOS) { 146 | wavPath = value.path + "/" + nowDataTimeStr+".MP3"; 147 | }else{ 148 | wavPath = value.path + "/" + nowDataTimeStr; 149 | } 150 | startByWavPath(wavPath); 151 | }); 152 | }); 153 | } 154 | 155 | ///初始化语音录制的方法 156 | void _initRecordMp3() async { 157 | recordPlugin.initRecordMp3(); 158 | } 159 | 160 | ///开始语音录制的方法 161 | void start() async { 162 | recordPlugin.start(); 163 | } 164 | 165 | ///根据传递的路径进行语音录制 166 | void startByWavPath(String wavPath) async { 167 | recordPlugin.startByWavPath(wavPath); 168 | } 169 | 170 | ///停止语音录制的方法 171 | void stop() { 172 | recordPlugin.stop(); 173 | } 174 | 175 | ///播放语音的方法 176 | void play() { 177 | recordPlugin.play(); 178 | } 179 | 180 | ///播放指定路径录音文件 url为iOS播放网络语音,file为播放本地语音文件 181 | void playByPath(String path,String type) { 182 | recordPlugin.playByPath(path,type); 183 | } 184 | 185 | ///暂停|继续播放 186 | void pause() { 187 | recordPlugin.pausePlay(); 188 | } 189 | 190 | @override 191 | void dispose() { 192 | /// 当界面退出的时候是释放录音资源 193 | recordPlugin.dispose(); 194 | super.dispose(); 195 | } 196 | 197 | void stopPlay() { 198 | recordPlugin.stopPlay(); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /example/lib/record_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flustars/flustars.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_plugin_record/flutter_plugin_record.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | 8 | class RecordScreen extends StatefulWidget { 9 | @override 10 | _RecordScreenState createState() => _RecordScreenState(); 11 | } 12 | 13 | class _RecordScreenState extends State { 14 | FlutterPluginRecord recordPlugin = new FlutterPluginRecord(); 15 | 16 | String filePath = ""; 17 | 18 | @override 19 | void initState() { 20 | super.initState(); 21 | 22 | ///初始化方法的监听 23 | recordPlugin.responseFromInit.listen((data) { 24 | if (data) { 25 | print("初始化成功"); 26 | } else { 27 | print("初始化失败"); 28 | } 29 | }); 30 | 31 | /// 开始录制或结束录制的监听 32 | recordPlugin.response.listen((data) { 33 | if (data.msg == "onStop") { 34 | ///结束录制时会返回录制文件的地址方便上传服务器 35 | print("onStop 文件路径" + data.path); 36 | filePath = data.path; 37 | print("onStop 时长 " + data.audioTimeLength.toString()); 38 | } else if (data.msg == "onStart") { 39 | print("onStart --"); 40 | } else { 41 | print("--" + data.msg); 42 | } 43 | }); 44 | 45 | ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 46 | recordPlugin.responseFromAmplitude.listen((data) { 47 | var voiceData = double.parse(data.msg); 48 | print("振幅大小 " + voiceData.toString()); 49 | }); 50 | 51 | recordPlugin.responsePlayStateController.listen((data) { 52 | print("播放路径 " + data.playPath); 53 | print("播放状态 " + data.playState); 54 | }); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return Scaffold( 60 | appBar: AppBar( 61 | title: const Text('录制wav'), 62 | ), 63 | body: Center( 64 | child: Column( 65 | children: [ 66 | FlatButton( 67 | child: Text("初始化"), 68 | onPressed: () { 69 | _init(); 70 | }, 71 | ), 72 | FlatButton( 73 | child: Text("开始录制"), 74 | onPressed: () { 75 | start(); 76 | }, 77 | ), 78 | FlatButton( 79 | child: Text("根据路径录制wav文件"), 80 | onPressed: () { 81 | _requestAppDocumentsDirectory(); 82 | }, 83 | ), 84 | FlatButton( 85 | child: Text("停止录制"), 86 | onPressed: () { 87 | stop(); 88 | }, 89 | ), 90 | FlatButton( 91 | child: Text("播放"), 92 | onPressed: () { 93 | play(); 94 | }, 95 | ), 96 | FlatButton( 97 | child: Text("播放本地指定路径录音文件"), 98 | onPressed: () { 99 | playByPath(filePath,"file"); 100 | }, 101 | ), 102 | FlatButton( 103 | child: Text("播放网络wav文件"), 104 | onPressed: () { 105 | playByPath("https://test-1259809289.cos.ap-nanjing.myqcloud.com/test.wav","url"); 106 | }, 107 | ), 108 | FlatButton( 109 | child: Text("暂停|继续播放"), 110 | onPressed: () { 111 | pause(); 112 | }, 113 | ), 114 | FlatButton( 115 | child: Text("停止播放"), 116 | onPressed: () { 117 | stopPlay(); 118 | }, 119 | ), 120 | ], 121 | ), 122 | ), 123 | ); 124 | } 125 | 126 | void _requestAppDocumentsDirectory() { 127 | // if(Platform.isIOS){ 128 | // //ios相关代码 129 | // setState(() { 130 | // getApplicationDocumentsDirectory().then((value) { 131 | // String nowDataTimeStr = DateUtil.getNowDateMs().toString(); 132 | // String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; 133 | // startByWavPath(wavPath); 134 | // }); 135 | // }); 136 | // }else if(Platform.isAndroid){ 137 | // //android相关代码 138 | // } 139 | 140 | setState(() { 141 | getApplicationDocumentsDirectory().then((value) { 142 | String nowDataTimeStr = DateUtil.getNowDateMs().toString(); 143 | String wavPath = value.path + "/" + nowDataTimeStr + ".wav"; 144 | print(wavPath); 145 | startByWavPath(wavPath); 146 | }); 147 | }); 148 | } 149 | 150 | ///初始化语音录制的方法 151 | void _init() async { 152 | recordPlugin.init(); 153 | } 154 | 155 | ///开始语音录制的方法 156 | void start() async { 157 | recordPlugin.start(); 158 | } 159 | 160 | ///根据传递的路径进行语音录制 161 | void startByWavPath(String wavPath) async { 162 | recordPlugin.startByWavPath(wavPath); 163 | } 164 | 165 | ///停止语音录制的方法 166 | void stop() { 167 | recordPlugin.stop(); 168 | } 169 | 170 | ///播放语音的方法 171 | void play() { 172 | recordPlugin.play(); 173 | } 174 | 175 | ///播放指定路径录音文件 url为iOS播放网络语音,file为播放本地语音文件 176 | void playByPath(String path,String type) { 177 | recordPlugin.playByPath(path,type); 178 | } 179 | 180 | ///暂停|继续播放 181 | void pause() { 182 | recordPlugin.pausePlay(); 183 | } 184 | 185 | @override 186 | void dispose() { 187 | /// 当界面退出的时候是释放录音资源 188 | recordPlugin.dispose(); 189 | super.dispose(); 190 | } 191 | 192 | void stopPlay() { 193 | recordPlugin.stopPlay(); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /example/lib/wechat_record_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_plugin_record/index.dart'; 3 | 4 | class WeChatRecordScreen extends StatefulWidget { 5 | @override 6 | _WeChatRecordScreenState createState() => _WeChatRecordScreenState(); 7 | } 8 | 9 | class _WeChatRecordScreenState extends State { 10 | String toastShow = "悬浮框"; 11 | OverlayEntry overlayEntry; 12 | 13 | showView(BuildContext context) { 14 | if (overlayEntry == null) { 15 | overlayEntry = new OverlayEntry(builder: (content) { 16 | return Positioned( 17 | top: MediaQuery.of(context).size.height * 0.5 - 80, 18 | left: MediaQuery.of(context).size.width * 0.5 - 80, 19 | child: Material( 20 | child: Center( 21 | child: Opacity( 22 | opacity: 0.8, 23 | child: Container( 24 | width: 100, 25 | height: 100, 26 | decoration: BoxDecoration( 27 | color: Color(0xff77797A), 28 | borderRadius: BorderRadius.all(Radius.circular(20.0)), 29 | ), 30 | child: Column( 31 | children: [ 32 | Container( 33 | // padding: EdgeInsets.only(right: 20, left: 20, top: 0), 34 | child: Text( 35 | toastShow, 36 | style: TextStyle( 37 | fontStyle: FontStyle.normal, 38 | color: Colors.white, 39 | fontSize: 14, 40 | ), 41 | ), 42 | ) 43 | ], 44 | ), 45 | ), 46 | ), 47 | ), 48 | ), 49 | ); 50 | }); 51 | Overlay.of(context).insert(overlayEntry); 52 | } 53 | } 54 | 55 | startRecord() { 56 | print("开始录制"); 57 | } 58 | 59 | stopRecord(String path, double audioTimeLength) { 60 | print("结束束录制"); 61 | print("音频文件位置" + path); 62 | print("音频录制时长" + audioTimeLength.toString()); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Scaffold( 68 | appBar: AppBar( 69 | title: Text("仿微信发送语音"), 70 | ), 71 | body: Container( 72 | child: Column( 73 | children: [ 74 | new FlatButton( 75 | onPressed: () { 76 | showView(context); 77 | }, 78 | child: new Text("悬浮组件")), 79 | new FlatButton( 80 | onPressed: () { 81 | if (overlayEntry != null) { 82 | overlayEntry.remove(); 83 | overlayEntry = null; 84 | } 85 | }, 86 | child: new Text("隐藏悬浮组件")), 87 | new FlatButton( 88 | onPressed: () { 89 | setState(() { 90 | toastShow = "111"; 91 | if (overlayEntry != null) { 92 | overlayEntry.markNeedsBuild(); 93 | } 94 | }); 95 | }, 96 | child: new Text("悬浮窗状态更新")), 97 | new VoiceWidget( 98 | startRecord: startRecord, 99 | stopRecord: stopRecord, 100 | // 加入定制化Container的相关属性 101 | height: 40.0, 102 | ), 103 | ], 104 | ), 105 | ), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.15.0" 46 | common_utils: 47 | dependency: transitive 48 | description: 49 | name: common_utils 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "1.2.1" 53 | convert: 54 | dependency: transitive 55 | description: 56 | name: convert 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.1" 60 | crypto: 61 | dependency: transitive 62 | description: 63 | name: crypto 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "3.0.0" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | url: "https://pub.flutter-io.cn" 72 | source: hosted 73 | version: "0.1.3" 74 | decimal: 75 | dependency: transitive 76 | description: 77 | name: decimal 78 | url: "https://pub.flutter-io.cn" 79 | source: hosted 80 | version: "0.3.5" 81 | fake_async: 82 | dependency: transitive 83 | description: 84 | name: fake_async 85 | url: "https://pub.flutter-io.cn" 86 | source: hosted 87 | version: "1.2.0" 88 | ffi: 89 | dependency: transitive 90 | description: 91 | name: ffi 92 | url: "https://pub.flutter-io.cn" 93 | source: hosted 94 | version: "0.1.3" 95 | file: 96 | dependency: transitive 97 | description: 98 | name: file 99 | url: "https://pub.flutter-io.cn" 100 | source: hosted 101 | version: "5.2.1" 102 | flustars: 103 | dependency: "direct main" 104 | description: 105 | name: flustars 106 | url: "https://pub.flutter-io.cn" 107 | source: hosted 108 | version: "0.3.3" 109 | flutter: 110 | dependency: "direct main" 111 | description: flutter 112 | source: sdk 113 | version: "0.0.0" 114 | flutter_plugin_record: 115 | dependency: "direct dev" 116 | description: 117 | path: ".." 118 | relative: true 119 | source: path 120 | version: "1.0.0" 121 | flutter_test: 122 | dependency: "direct dev" 123 | description: flutter 124 | source: sdk 125 | version: "0.0.0" 126 | flutter_web_plugins: 127 | dependency: transitive 128 | description: flutter 129 | source: sdk 130 | version: "0.0.0" 131 | intl: 132 | dependency: transitive 133 | description: 134 | name: intl 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "0.16.1" 138 | js: 139 | dependency: transitive 140 | description: 141 | name: js 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "0.6.3" 145 | matcher: 146 | dependency: transitive 147 | description: 148 | name: matcher 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "0.12.10" 152 | meta: 153 | dependency: transitive 154 | description: 155 | name: meta 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.3.0" 159 | path: 160 | dependency: transitive 161 | description: 162 | name: path 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "1.8.0" 166 | path_provider: 167 | dependency: "direct main" 168 | description: 169 | name: path_provider 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "1.6.24" 173 | path_provider_linux: 174 | dependency: transitive 175 | description: 176 | name: path_provider_linux 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "0.0.1+2" 180 | path_provider_macos: 181 | dependency: transitive 182 | description: 183 | name: path_provider_macos 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "0.0.4+6" 187 | path_provider_platform_interface: 188 | dependency: transitive 189 | description: 190 | name: path_provider_platform_interface 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "1.0.4" 194 | path_provider_windows: 195 | dependency: transitive 196 | description: 197 | name: path_provider_windows 198 | url: "https://pub.flutter-io.cn" 199 | source: hosted 200 | version: "0.0.4+3" 201 | platform: 202 | dependency: transitive 203 | description: 204 | name: platform 205 | url: "https://pub.flutter-io.cn" 206 | source: hosted 207 | version: "2.2.1" 208 | plugin_platform_interface: 209 | dependency: transitive 210 | description: 211 | name: plugin_platform_interface 212 | url: "https://pub.flutter-io.cn" 213 | source: hosted 214 | version: "1.0.3" 215 | process: 216 | dependency: transitive 217 | description: 218 | name: process 219 | url: "https://pub.flutter-io.cn" 220 | source: hosted 221 | version: "3.0.13" 222 | rational: 223 | dependency: transitive 224 | description: 225 | name: rational 226 | url: "https://pub.flutter-io.cn" 227 | source: hosted 228 | version: "0.3.8" 229 | shared_preferences: 230 | dependency: transitive 231 | description: 232 | name: shared_preferences 233 | url: "https://pub.flutter-io.cn" 234 | source: hosted 235 | version: "0.5.12+4" 236 | shared_preferences_linux: 237 | dependency: transitive 238 | description: 239 | name: shared_preferences_linux 240 | url: "https://pub.flutter-io.cn" 241 | source: hosted 242 | version: "0.0.2+4" 243 | shared_preferences_macos: 244 | dependency: transitive 245 | description: 246 | name: shared_preferences_macos 247 | url: "https://pub.flutter-io.cn" 248 | source: hosted 249 | version: "0.0.1+11" 250 | shared_preferences_platform_interface: 251 | dependency: transitive 252 | description: 253 | name: shared_preferences_platform_interface 254 | url: "https://pub.flutter-io.cn" 255 | source: hosted 256 | version: "1.0.4" 257 | shared_preferences_web: 258 | dependency: transitive 259 | description: 260 | name: shared_preferences_web 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "0.1.2+7" 264 | shared_preferences_windows: 265 | dependency: transitive 266 | description: 267 | name: shared_preferences_windows 268 | url: "https://pub.flutter-io.cn" 269 | source: hosted 270 | version: "0.0.1+3" 271 | sky_engine: 272 | dependency: transitive 273 | description: flutter 274 | source: sdk 275 | version: "0.0.99" 276 | source_span: 277 | dependency: transitive 278 | description: 279 | name: source_span 280 | url: "https://pub.flutter-io.cn" 281 | source: hosted 282 | version: "1.8.0" 283 | sp_util: 284 | dependency: transitive 285 | description: 286 | name: sp_util 287 | url: "https://pub.flutter-io.cn" 288 | source: hosted 289 | version: "1.0.1" 290 | stack_trace: 291 | dependency: transitive 292 | description: 293 | name: stack_trace 294 | url: "https://pub.flutter-io.cn" 295 | source: hosted 296 | version: "1.10.0" 297 | stream_channel: 298 | dependency: transitive 299 | description: 300 | name: stream_channel 301 | url: "https://pub.flutter-io.cn" 302 | source: hosted 303 | version: "2.1.0" 304 | string_scanner: 305 | dependency: transitive 306 | description: 307 | name: string_scanner 308 | url: "https://pub.flutter-io.cn" 309 | source: hosted 310 | version: "1.1.0" 311 | synchronized: 312 | dependency: transitive 313 | description: 314 | name: synchronized 315 | url: "https://pub.flutter-io.cn" 316 | source: hosted 317 | version: "2.2.0+2" 318 | term_glyph: 319 | dependency: transitive 320 | description: 321 | name: term_glyph 322 | url: "https://pub.flutter-io.cn" 323 | source: hosted 324 | version: "1.2.0" 325 | test_api: 326 | dependency: transitive 327 | description: 328 | name: test_api 329 | url: "https://pub.flutter-io.cn" 330 | source: hosted 331 | version: "0.2.19" 332 | typed_data: 333 | dependency: transitive 334 | description: 335 | name: typed_data 336 | url: "https://pub.flutter-io.cn" 337 | source: hosted 338 | version: "1.3.0" 339 | uuid: 340 | dependency: transitive 341 | description: 342 | name: uuid 343 | url: "https://pub.flutter-io.cn" 344 | source: hosted 345 | version: "3.0.2" 346 | vector_math: 347 | dependency: transitive 348 | description: 349 | name: vector_math 350 | url: "https://pub.flutter-io.cn" 351 | source: hosted 352 | version: "2.1.0" 353 | win32: 354 | dependency: transitive 355 | description: 356 | name: win32 357 | url: "https://pub.flutter-io.cn" 358 | source: hosted 359 | version: "1.7.4" 360 | xdg_directories: 361 | dependency: transitive 362 | description: 363 | name: xdg_directories 364 | url: "https://pub.flutter-io.cn" 365 | source: hosted 366 | version: "0.1.0" 367 | sdks: 368 | dart: ">=2.12.0-29.10.beta <3.0.0" 369 | flutter: ">=1.12.13+hotfix.5" 370 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_plugin_record_example 2 | description: The flutter voice recording plug-in,provides the recording animation and the recording successfully returns to the recording file path 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^0.1.3 31 | path_provider: ^1.6.8 32 | flustars: ^0.3.2 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | 38 | flutter_plugin_record: 39 | path: ../ 40 | 41 | # For information on the generic Dart part of this file, see the 42 | # following page: https://dart.dev/tools/pub/pubspec 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | 47 | # The following line ensures that the Material Icons font is 48 | # included with your application, so that you can use the icons in 49 | # the material Icons class. 50 | uses-material-design: true 51 | 52 | # To add assets to your application, add an assets section, like this: 53 | # assets: 54 | # - images/a_dot_burr.jpeg 55 | # - images/a_dot_ham.jpeg 56 | 57 | # An image asset can refer to one or more resolution-specific "variants", see 58 | # https://flutter.dev/assets-and-images/#resolution-aware. 59 | 60 | # For details regarding adding assets from package dependencies, see 61 | # https://flutter.dev/assets-and-images/#from-packages 62 | 63 | # To add custom fonts to your application, add a fonts section here, 64 | # in this "flutter" section. Each entry in this list should have a 65 | # "family" key with the font family name, and a "fonts" key with a 66 | # list giving the asset and other descriptors for the font. For 67 | # example: 68 | # fonts: 69 | # - family: Schyler 70 | # fonts: 71 | # - asset: fonts/Schyler-Regular.ttf 72 | # - asset: fonts/Schyler-Italic.ttf 73 | # style: italic 74 | # - family: Trajan Pro 75 | # fonts: 76 | # - asset: fonts/TrajanPro.ttf 77 | # - asset: fonts/TrajanPro_Bold.ttf 78 | # weight: 700 79 | # 80 | # For details regarding fonts from package dependencies, 81 | # see https://flutter.dev/custom-fonts/#from-packages 82 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_plugin_record_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /flutter_plugin_record.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /images/voice_volume_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_1.png -------------------------------------------------------------------------------- /images/voice_volume_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_2.png -------------------------------------------------------------------------------- /images/voice_volume_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_3.png -------------------------------------------------------------------------------- /images/voice_volume_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_4.png -------------------------------------------------------------------------------- /images/voice_volume_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_5.png -------------------------------------------------------------------------------- /images/voice_volume_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_6.png -------------------------------------------------------------------------------- /images/voice_volume_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/images/voice_volume_7.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/DPAudioPlayer.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | typedef void(^PlayCompleteBlock)(void); 5 | 6 | typedef void(^StartPlayingBlock)(BOOL isPlaying); 7 | 8 | @interface DPAudioPlayer : NSObject 9 | 10 | /** 11 | 播放完成回调 12 | */ 13 | @property (nonatomic, copy) PlayCompleteBlock playComplete; 14 | 15 | /** 16 | 开始播放回调 17 | */ 18 | @property (nonatomic, copy) StartPlayingBlock startPlaying; 19 | 20 | + (DPAudioPlayer *)sharedInstance; 21 | 22 | /** 23 | 播放data格式录音 24 | 25 | @param data 录音data 26 | */ 27 | - (void)startPlayWithData:(NSData *)data; 28 | 29 | /** 30 | 停止播放 31 | */ 32 | - (void)stopPlaying; 33 | 34 | /// 暂停播放 35 | - (bool)pausePlaying; 36 | 37 | 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /ios/Classes/DPAudioPlayer.m: -------------------------------------------------------------------------------- 1 | 2 | #import "DPAudioPlayer.h" 3 | #import 4 | #import 5 | 6 | @interface DPAudioPlayer () 7 | { 8 | BOOL isPlaying; 9 | } 10 | 11 | @property (nonatomic, strong) AVAudioPlayer *audioPlayer; 12 | 13 | @end 14 | 15 | @implementation DPAudioPlayer 16 | 17 | static DPAudioPlayer *playerManager = nil; 18 | + (DPAudioPlayer *)sharedInstance 19 | { 20 | static dispatch_once_t onceToken; 21 | 22 | dispatch_once(&onceToken,^{ 23 | playerManager = [[DPAudioPlayer alloc] init]; 24 | }); 25 | return playerManager; 26 | } 27 | 28 | - (instancetype)init 29 | { 30 | if (self) { 31 | //创建缓存录音文件到Tmp 32 | NSString *wavPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryPlayer.wav"]; 33 | NSString *amrPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AMRtemporaryPlayer.amr"]; 34 | 35 | if (![[NSFileManager defaultManager]fileExistsAtPath:wavPlayerFilePath]) { 36 | [[NSData data] writeToFile:wavPlayerFilePath atomically:YES]; 37 | } 38 | if (![[NSFileManager defaultManager]fileExistsAtPath:amrPlayerFilePath]) { 39 | [[NSData data] writeToFile:amrPlayerFilePath atomically:YES]; 40 | } 41 | 42 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(proximityStateDidChange) name:UIDeviceProximityStateDidChangeNotification object:nil]; 43 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; 44 | [[AVAudioSession sharedInstance] setActive:YES error:nil]; 45 | 46 | } 47 | return self; 48 | } 49 | 50 | - (void)startPlayWithData:(NSData *)data 51 | { 52 | // if (isPlaying) return; 53 | //打开红外传感器 54 | [[UIDevice currentDevice] setProximityMonitoringEnabled:YES]; 55 | AVAudioSession *session = [AVAudioSession sharedInstance]; 56 | [session setActive:true error:nil]; 57 | [session setCategory:AVAudioSessionCategoryPlayback error:nil]; 58 | // //默认情况下扬声器播放 59 | // AVAudioSessionPortOverride portOverride = AVAudioSessionPortOverrideNone; 60 | // [[AVAudioSession sharedInstance] overrideOutputAudioPort:portOverride error:nil]; 61 | 62 | //self.audioPlayer = [[AVAudioPlayer alloc]initWithData:[self conversionAMRDataToWAVData:data] error:nil]; 63 | 64 | if (isPlaying){ 65 | [self.audioPlayer stop]; 66 | self.audioPlayer = nil; 67 | isPlaying = NO; 68 | } 69 | self.audioPlayer = [[AVAudioPlayer alloc]initWithData:data error:nil]; 70 | self.audioPlayer.meteringEnabled = YES; 71 | self.audioPlayer.delegate = self; 72 | self.audioPlayer.volume = 1.0; 73 | self.audioPlayer.numberOfLoops = 0; 74 | [self.audioPlayer prepareToPlay]; 75 | [self.audioPlayer play]; 76 | 77 | if ([self.audioPlayer isPlaying]) { 78 | isPlaying = YES; 79 | if (self.startPlaying) { 80 | self.startPlaying(YES); 81 | } 82 | } else { 83 | isPlaying = NO; 84 | if (self.startPlaying) { 85 | self.startPlaying(NO); 86 | } 87 | } 88 | } 89 | 90 | //暂停播放 91 | - (bool)pausePlaying 92 | { 93 | if (isPlaying){ 94 | //关闭红外传感器 95 | [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; 96 | [self.audioPlayer pause]; 97 | isPlaying = NO; 98 | }else{ 99 | [self.audioPlayer play]; 100 | isPlaying = YES; 101 | } 102 | 103 | return isPlaying; 104 | 105 | } 106 | 107 | 108 | - (void)stopPlaying 109 | { 110 | if (!isPlaying) return; 111 | //关闭红外传感器 112 | [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; 113 | [self.audioPlayer stop]; 114 | self.audioPlayer = nil; 115 | isPlaying = NO; 116 | } 117 | 118 | - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 119 | { 120 | if (flag) { 121 | [self stopPlaying]; 122 | if (self.playComplete) { 123 | self.playComplete(); 124 | } 125 | } 126 | } 127 | 128 | - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer*)player error:(NSError *)error{ 129 | //解码错误执行的动作 130 | NSLog(@""); 131 | } 132 | 133 | //- (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player 134 | //{ 135 | // isPlaying = NO; 136 | // [player stop]; 137 | //} 138 | 139 | ////转换amr文件类型data为wav文件类型data 140 | //- (NSData *)conversionAMRDataToWAVData:(NSData *)amrData 141 | //{ 142 | // 143 | // NSString *wavPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryPlayer.wav"]; 144 | // NSString *amrPlayerFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AMRtemporaryPlayer.amr"]; 145 | // 146 | // //amr的data写入文件 147 | // [amrData writeToFile:amrPlayerFilePath atomically:YES]; 148 | // //将AMR文件转码成WAVE文件 149 | // amr_file_to_wave_file([amrPlayerFilePath cStringUsingEncoding:NSUTF8StringEncoding], 150 | // [wavPlayerFilePath cStringUsingEncoding:NSUTF8StringEncoding]); 151 | // 152 | // //得到转码后wav的data 153 | // return [NSData dataWithContentsOfFile:wavPlayerFilePath]; 154 | //} 155 | 156 | - (void)proximityStateDidChange 157 | { 158 | if ([UIDevice currentDevice].proximityState) { 159 | NSLog(@"有物品靠近"); 160 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil]; 161 | } else { 162 | NSLog(@"有物品离开"); 163 | [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; 164 | } 165 | } 166 | 167 | 168 | - (void)dealloc 169 | { 170 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceProximityStateDidChangeNotification object:nil]; 171 | [[UIDevice currentDevice] setProximityMonitoringEnabled:NO]; 172 | } 173 | 174 | @end 175 | -------------------------------------------------------------------------------- /ios/Classes/DPAudioRecorder.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | typedef void(^AudioRecorderFinishRecordingBlock)(NSData *data, NSTimeInterval audioTimeLength,NSString *path); 5 | 6 | typedef void(^AudioStartRecordingBlock)(BOOL isRecording); 7 | 8 | typedef void(^AudioRecordingFailBlock)(NSString *reason); 9 | 10 | typedef void(^AudioSpeakPowerBlock)(float power); 11 | 12 | /// 录制语音 13 | @interface DPAudioRecorder : NSObject 14 | 15 | /// 录制完成的回调 16 | @property (nonatomic, copy) AudioRecorderFinishRecordingBlock audioRecorderFinishRecording; 17 | /// 开始录制回调 18 | @property (nonatomic, copy) AudioStartRecordingBlock audioStartRecording; 19 | /// 录制失败回调 20 | @property (nonatomic, copy) AudioRecordingFailBlock audioRecordingFail; 21 | /// 音频值测量回调 22 | @property (nonatomic, copy) AudioSpeakPowerBlock audioSpeakPower; 23 | 24 | 25 | + (DPAudioRecorder *)sharedInstance; 26 | 27 | /// 传递录制文件路径 28 | - (void)initByWavPath:(NSString*) wavPath; 29 | - (void)initByMp3; 30 | 31 | /// 开始录音方法 32 | - (void)startRecording; 33 | 34 | /// 停止录音方法 35 | - (void)stopRecording; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /ios/Classes/DPAudioRecorder.m: -------------------------------------------------------------------------------- 1 | 2 | #import "DPAudioRecorder.h" 3 | #import "DPAudioPlayer.h" 4 | #import 5 | #import 6 | #import "JX_GCDTimerManager.h" 7 | 8 | 9 | 10 | #define MAX_RECORDER_TIME 2100 //最大录制时间 11 | #define MIN_RECORDER_TIME 1 // 最小录制时间 12 | 13 | #define TimerName @"audioTimer_999" 14 | 15 | //定义音频枚举类型 16 | typedef NS_ENUM(NSUInteger, CSVoiceType) { 17 | CSVoiceTypeWav, 18 | CSVoiceTypeAmr 19 | }; 20 | 21 | static const CSVoiceType preferredVoiceType = CSVoiceTypeWav; 22 | 23 | 24 | @interface DPAudioRecorder () 25 | { 26 | BOOL isRecording; 27 | dispatch_source_t timer; 28 | NSTimeInterval __block audioTimeLength; //录音时长 29 | } 30 | @property (nonatomic, strong) AVAudioRecorder *audioRecorder; 31 | @property (nonatomic, strong) NSString *originWaveFilePath; 32 | @end 33 | 34 | @implementation DPAudioRecorder 35 | 36 | static DPAudioRecorder *recorderManager = nil; 37 | 38 | 39 | + (DPAudioRecorder *)sharedInstance 40 | { 41 | static dispatch_once_t onceToken; 42 | 43 | dispatch_once(&onceToken,^{ 44 | recorderManager = [[DPAudioRecorder alloc] init]; 45 | }); 46 | 47 | return recorderManager; 48 | } 49 | 50 | 51 | /// 默认构造方法 52 | - (instancetype)init 53 | { 54 | if (self = [super init]) { 55 | //创建缓存录音文件到Tmp 56 | NSString *wavRecordFilePath = [self createWaveFilePath]; 57 | if (![[NSFileManager defaultManager] fileExistsAtPath:wavRecordFilePath]) { 58 | [[NSData data] writeToFile:wavRecordFilePath atomically:YES]; 59 | } 60 | self.originWaveFilePath = wavRecordFilePath; 61 | 62 | NSLog(@"ios------初始化默认录制文件路径---%@",wavRecordFilePath); 63 | 64 | } 65 | return self; 66 | } 67 | 68 | 69 | - (void) initByMp3{ 70 | //创建缓存录音文件到Tmp 71 | NSString *mp3RecordFilePath = [self createMp3FilePath]; 72 | if (![[NSFileManager defaultManager] fileExistsAtPath:mp3RecordFilePath]) { 73 | [[NSData data] writeToFile:mp3RecordFilePath atomically:YES]; 74 | } 75 | self.originWaveFilePath = mp3RecordFilePath; 76 | 77 | NSLog(@"ios------初始化录制文件路径---%@",mp3RecordFilePath); 78 | 79 | } 80 | 81 | - (NSString *) createMp3FilePath { 82 | return [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryRadio.MP3"]; 83 | 84 | } 85 | - (NSString *) createWaveFilePath { 86 | return [NSTemporaryDirectory() stringByAppendingPathComponent:@"WAVtemporaryRadio.wav"]; 87 | 88 | } 89 | 90 | /// 根据传递过来的文件路径创建wav录制文件路径 91 | /// @param wavPath 传递的文件路径 92 | - (void)initByWavPath:(NSString *) wavPath{ 93 | 94 | NSString *wavRecordFilePath = wavPath; 95 | if (![[NSFileManager defaultManager] fileExistsAtPath:wavRecordFilePath]) { 96 | [[NSData data] writeToFile:wavRecordFilePath atomically:YES]; 97 | } 98 | 99 | self.originWaveFilePath = wavRecordFilePath; 100 | NSLog(@"ios-----传递的录制文件路径-------- %@",wavRecordFilePath); 101 | } 102 | 103 | /// 开始录制方法 104 | - (void)startRecording 105 | { 106 | if (isRecording) return; 107 | 108 | [[DPAudioPlayer sharedInstance]stopPlaying]; 109 | //开始录音 110 | [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil]; 111 | // //默认情况下扬声器播放 112 | AVAudioSessionPortOverride portOverride = AVAudioSessionPortOverrideNone; 113 | [[AVAudioSession sharedInstance] overrideOutputAudioPort:portOverride error:nil]; 114 | 115 | [[AVAudioSession sharedInstance] setActive:YES error:nil]; 116 | 117 | [self.audioRecorder prepareToRecord]; 118 | 119 | [self.audioRecorder record]; 120 | 121 | if ([self.audioRecorder isRecording]) { 122 | isRecording = YES; 123 | [self activeTimer]; 124 | if (self.audioStartRecording) { 125 | self.audioStartRecording(YES); 126 | } 127 | } else { 128 | if (self.audioStartRecording) { 129 | self.audioStartRecording(NO); 130 | } 131 | } 132 | 133 | 134 | [self createPickSpeakPowerTimer]; 135 | } 136 | 137 | - (void)stopRecording; 138 | { 139 | if (!isRecording) return; 140 | // try!AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) 141 | [self shutDownTimer]; 142 | [self.audioRecorder stop]; 143 | self.audioRecorder = nil; 144 | 145 | //设置播放语音为k公开模式 146 | AVAudioSession *avAudioSession = [AVAudioSession sharedInstance]; 147 | [avAudioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil]; 148 | 149 | } 150 | 151 | - (void)activeTimer 152 | { 153 | 154 | //录音时长 155 | audioTimeLength = 0; 156 | NSTimeInterval timeInterval = 0.1; 157 | __weak typeof(self) weakSelf = self; 158 | [[JX_GCDTimerManager sharedInstance] scheduledDispatchTimerWithName:TimerName timeInterval:timeInterval queue:nil repeats:YES actionOption:AbandonPreviousAction action:^{ 159 | __strong typeof(weakSelf) strongSelf = weakSelf; 160 | strongSelf->audioTimeLength += timeInterval; 161 | if (strongSelf->audioTimeLength >= MAX_RECORDER_TIME) { //大于等于 MAX_RECORDER_TIME 秒停止 162 | [strongSelf stopRecording]; 163 | } 164 | }]; 165 | } 166 | 167 | - (void)shutDownTimer 168 | { 169 | [[JX_GCDTimerManager sharedInstance] cancelAllTimer];//定时器停止 170 | } 171 | 172 | - (AVAudioRecorder *)audioRecorder { 173 | if (!_audioRecorder) { 174 | 175 | //暂存录音文件路径 176 | NSString *wavRecordFilePath = self.originWaveFilePath; 177 | NSLog(@"%@", wavRecordFilePath); 178 | NSDictionary *param = 179 | @{AVSampleRateKey:@8000.0, //采样率 180 | AVFormatIDKey:@(kAudioFormatLinearPCM),//音频格式 181 | AVLinearPCMBitDepthKey:@16, //采样位数 默认 16 182 | AVNumberOfChannelsKey:@1, // 通道的数目 183 | AVEncoderAudioQualityKey:@(AVAudioQualityMin), 184 | AVEncoderBitRateKey:@16000, 185 | // AVEncoderBitRateStrategyKey:AVAudioBitRateStrategy_VariableConstrained 186 | }; 187 | 188 | NSError *initError; 189 | NSURL *fileURL = [NSURL fileURLWithPath:wavRecordFilePath]; 190 | _audioRecorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:param error:&initError]; 191 | if (initError) { 192 | NSLog(@"AVAudioRecorder initError:%@", initError.localizedDescription); 193 | } 194 | _audioRecorder.delegate = self; 195 | _audioRecorder.meteringEnabled = YES; 196 | } 197 | return _audioRecorder; 198 | } 199 | 200 | #pragma mark - AVAudioRecorder 201 | 202 | - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 203 | { 204 | //暂存录音文件路径 205 | NSString *wavRecordFilePath = self.originWaveFilePath; 206 | NSLog(@"录音暂存位置 %@ ",wavRecordFilePath); 207 | NSData *cacheAudioData; 208 | switch (preferredVoiceType) { 209 | case CSVoiceTypeWav: 210 | cacheAudioData = [NSData dataWithContentsOfFile:wavRecordFilePath]; 211 | break; 212 | } 213 | 214 | //大于最小录音时长时,发送数据 215 | if (audioTimeLength > MIN_RECORDER_TIME) { 216 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 217 | NSUInteger location = 4100; 218 | NSData *body = [cacheAudioData subdataWithRange:NSMakeRange(location, cacheAudioData.length - location)]; 219 | NSMutableData *data1 = WriteWavFileHeader(body.length + 44, 8000, 1, 16).mutableCopy; 220 | [data1 appendData:body]; 221 | // NSLog(@"date1date1date1date1[0-200]:%@", [data1 subdataWithRange:NSMakeRange(0, 200)]); 222 | 223 | dispatch_sync(dispatch_get_main_queue(), ^{ 224 | if (self.audioRecorderFinishRecording) { 225 | self.audioRecorderFinishRecording(data1, self->audioTimeLength,wavRecordFilePath); 226 | } 227 | }); 228 | }); 229 | } else { 230 | if (self.audioRecordingFail) { 231 | self.audioRecordingFail(@"录音时长小于设定最短时长"); 232 | } 233 | } 234 | 235 | isRecording = NO; 236 | 237 | //取消定时器 238 | if (timer) { 239 | dispatch_source_cancel(timer); 240 | timer = NULL; 241 | } 242 | } 243 | 244 | NSData* WriteWavFileHeader(long lengthWithHeader, int sampleRate, int channels, int PCMBitDepth) { 245 | Byte header[44]; 246 | header[0] = 'R'; // RIFF/WAVE header 247 | header[1] = 'I'; 248 | header[2] = 'F'; 249 | header[3] = 'F'; 250 | long totalDataLen = lengthWithHeader - 8; 251 | header[4] = (Byte) (totalDataLen & 0xff); //file-size (equals file-size - 8) 252 | header[5] = (Byte) ((totalDataLen >> 8) & 0xff); 253 | header[6] = (Byte) ((totalDataLen >> 16) & 0xff); 254 | header[7] = (Byte) ((totalDataLen >> 24) & 0xff); 255 | header[8] = 'W'; // Mark it as type "WAVE" 256 | header[9] = 'A'; 257 | header[10] = 'V'; 258 | header[11] = 'E'; 259 | header[12] = 'f'; // Mark the format section 'fmt ' chunk 260 | header[13] = 'm'; 261 | header[14] = 't'; 262 | header[15] = ' '; 263 | header[16] = 16; // 4 bytes: size of 'fmt ' chunk, Length of format data. Always 16 264 | header[17] = 0; 265 | header[18] = 0; 266 | header[19] = 0; 267 | header[20] = 1; // format = 1 ,Wave type PCM 268 | header[21] = 0; 269 | header[22] = (Byte) channels; // channels 270 | header[23] = 0; 271 | header[24] = (Byte) (sampleRate & 0xff); 272 | header[25] = (Byte) ((sampleRate >> 8) & 0xff); 273 | header[26] = (Byte) ((sampleRate >> 16) & 0xff); 274 | header[27] = (Byte) ((sampleRate >> 24) & 0xff); 275 | int byteRate = sampleRate * channels * PCMBitDepth >> 3; 276 | header[28] = (Byte) (byteRate & 0xff); 277 | header[29] = (Byte) ((byteRate >> 8) & 0xff); 278 | header[30] = (Byte) ((byteRate >> 16) & 0xff); 279 | header[31] = (Byte) ((byteRate >> 24) & 0xff); 280 | header[32] = (Byte) (channels * PCMBitDepth >> 3); // block align 281 | header[33] = 0; 282 | header[34] = PCMBitDepth; // bits per sample 283 | header[35] = 0; 284 | header[36] = 'd'; //"data" marker 285 | header[37] = 'a'; 286 | header[38] = 't'; 287 | header[39] = 'a'; 288 | long totalAudioLen = lengthWithHeader - 44; 289 | header[40] = (Byte) (totalAudioLen & 0xff); //data-size (equals file-size - 44). 290 | header[41] = (Byte) ((totalAudioLen >> 8) & 0xff); 291 | header[42] = (Byte) ((totalAudioLen >> 16) & 0xff); 292 | header[43] = (Byte) ((totalAudioLen >> 24) & 0xff); 293 | return [[NSData alloc] initWithBytes:header length:44];; 294 | } 295 | 296 | //音频值测量 297 | - (void)createPickSpeakPowerTimer 298 | { 299 | timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); 300 | dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC, 1ull * NSEC_PER_SEC); 301 | 302 | __weak __typeof(self) weakSelf = self; 303 | 304 | dispatch_source_set_event_handler(timer, ^{ 305 | __strong __typeof(weakSelf) _self = weakSelf; 306 | 307 | [_self->_audioRecorder updateMeters]; 308 | double lowPassResults = pow(10, (0.05 * [_self->_audioRecorder averagePowerForChannel:0])); 309 | if (_self.audioSpeakPower) { 310 | _self.audioSpeakPower(lowPassResults); 311 | } 312 | }); 313 | 314 | dispatch_resume(timer); 315 | } 316 | 317 | - (void)dealloc 318 | { 319 | if (isRecording) [self.audioRecorder stop]; 320 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 321 | } 322 | 323 | @end 324 | -------------------------------------------------------------------------------- /ios/Classes/FlutterPluginRecordPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FlutterPluginRecordPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/FlutterPluginRecordPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FlutterPluginRecordPlugin.h" 2 | #import "DPAudioRecorder.h" 3 | #import "DPAudioPlayer.h" 4 | 5 | 6 | @implementation FlutterPluginRecordPlugin{ 7 | FlutterMethodChannel *_channel; 8 | FlutterResult _result; 9 | FlutterMethodCall *_call; 10 | NSData *wavData; 11 | NSString *audioPath; 12 | BOOL _isInit;//是否执行初始化的标识 13 | } 14 | 15 | + (void)registerWithRegistrar:(NSObject *)registrar { 16 | FlutterMethodChannel *channel = [FlutterMethodChannel 17 | methodChannelWithName:@"flutter_plugin_record" 18 | binaryMessenger:[registrar messenger]]; 19 | 20 | FlutterPluginRecordPlugin *instance = [[FlutterPluginRecordPlugin alloc] initWithChannel:channel]; 21 | [registrar addMethodCallDelegate:instance channel:channel]; 22 | } 23 | 24 | - (instancetype)initWithChannel:(FlutterMethodChannel *)channel { 25 | self = [super init]; 26 | if (self) { 27 | _channel = channel; 28 | _isInit = NO; 29 | } 30 | return self; 31 | } 32 | 33 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result{ 34 | _result = result; 35 | _call = call; 36 | NSString *method = call.method; 37 | if ([@"init" isEqualToString:method]) { 38 | [self initRecord ]; 39 | }else if([@"initRecordMp3" isEqualToString:method]){ 40 | [self initMp3Record]; 41 | }else if([@"startByWavPath" isEqualToString:method]){ 42 | [self startByWavPath]; 43 | }else if([@"start" isEqualToString:method]){ 44 | [self start ]; 45 | }else if([@"stop" isEqualToString:method]){ 46 | [self stop ]; 47 | }else if([@"play" isEqualToString:method]){ 48 | [self play ]; 49 | }else if([@"pause" isEqualToString:method]){ 50 | [self pausePlay ]; 51 | }else if([@"playByPath" isEqualToString:method]){ 52 | [self playByPath]; 53 | }else if([@"stopPlay" isEqualToString:method]){ 54 | [self stopPlay]; 55 | }else{ 56 | result(FlutterMethodNotImplemented); 57 | } 58 | 59 | } 60 | 61 | 62 | 63 | //初始化录制mp3 64 | - (void) initMp3Record{ 65 | [DPAudioRecorder.sharedInstance initByMp3]; 66 | [self initRecord]; 67 | 68 | } 69 | 70 | ///初始化语音录制的方法 初始化录制完成的回调,开始录制的回调,录制失败的回调,录制音量大小的回调 71 | /// 注意未初始化的话 Flutter 不能监听到上述回调事件 72 | - (void) initRecord{ 73 | _isInit = YES; 74 | 75 | DPAudioRecorder.sharedInstance.audioRecorderFinishRecording = ^void (NSData *data, NSTimeInterval audioTimeLength,NSString *path){ 76 | self->audioPath =path; 77 | self->wavData = data; 78 | NSLog(@"ios voice onStop"); 79 | NSDictionary *args = [self->_call arguments]; 80 | NSString *mId = [args valueForKey:@"id"]; 81 | 82 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: 83 | @"success", @"result", 84 | mId, @"id", 85 | path, @"voicePath", 86 | [NSString stringWithFormat:@"%.20lf", audioTimeLength], @"audioTimeLength", 87 | nil]; 88 | [self->_channel invokeMethod:@"onStop" arguments:dict3]; 89 | 90 | }; 91 | 92 | DPAudioRecorder.sharedInstance.audioStartRecording = ^void(BOOL isRecording){ 93 | NSLog(@"ios voice start audioStartRecording"); 94 | }; 95 | DPAudioRecorder.sharedInstance.audioRecordingFail = ^void(NSString *reason){ 96 | 97 | NSLog(@"ios voice %@", reason); 98 | 99 | }; 100 | DPAudioRecorder.sharedInstance.audioSpeakPower = ^void(float power){ 101 | NSLog(@"ios voice %f",power); 102 | NSString *powerStr = [NSString stringWithFormat:@"%f", power]; 103 | NSDictionary *args = [self->_call arguments]; 104 | NSString *mId = [args valueForKey:@"id"]; 105 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: 106 | @"success",@"result", 107 | mId ,@"id", 108 | powerStr,@"amplitude", 109 | nil]; 110 | [self->_channel invokeMethod:@"onAmplitude" arguments:dict3]; 111 | }; 112 | 113 | NSLog(@"ios voice init"); 114 | NSDictionary *args = [_call arguments]; 115 | NSString *mId = [args valueForKey:@"id"]; 116 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; 117 | [_channel invokeMethod:@"onInit" arguments:dict3]; 118 | } 119 | 120 | 121 | 122 | /// 开始录制的方法 123 | - (void) start{ 124 | if (!_isInit) { 125 | NSLog(@"ios-------未初始化录制方法- initRecord--"); 126 | return; 127 | } 128 | NSLog(@"ios--------start record -----function--- start----"); 129 | [DPAudioRecorder.sharedInstance startRecording]; 130 | 131 | NSDictionary *args = [_call arguments]; 132 | NSString *mId = [args valueForKey:@"id"]; 133 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; 134 | [_channel invokeMethod:@"onStart" arguments:dict3]; 135 | } 136 | 137 | /// 根据文件路径进行录制 138 | - (void) startByWavPath{ 139 | if (!_isInit) { 140 | NSLog(@"ios-------未初始化录制方法- initRecord--"); 141 | return; 142 | } 143 | NSDictionary *args = [_call arguments]; 144 | NSString *mId = [args valueForKey:@"id"]; 145 | NSString *wavPath = [args valueForKey:@"wavPath"]; 146 | 147 | NSLog(@"ios--------start record -----function--- startByWavPath----%@", wavPath); 148 | 149 | [DPAudioRecorder.sharedInstance initByWavPath:wavPath]; 150 | [DPAudioRecorder.sharedInstance startRecording]; 151 | 152 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; 153 | [_channel invokeMethod:@"onStart" arguments:dict3]; 154 | } 155 | 156 | 157 | 158 | /// 停止录制的方法 159 | - (void) stop{ 160 | if (!_isInit) { 161 | NSLog(@"ios-------未初始化录制方法- initRecord--"); 162 | return; 163 | } 164 | NSLog(@"ios--------stop record -----function--- stop----"); 165 | [DPAudioRecorder.sharedInstance stopRecording]; 166 | } 167 | 168 | 169 | 170 | /// 播放录制完成的音频 171 | - (void) play{ 172 | 173 | NSLog(@"ios------play voice by warData----function---play--"); 174 | [DPAudioPlayer.sharedInstance startPlayWithData:self->wavData]; 175 | DPAudioPlayer.sharedInstance.playComplete = ^void(){ 176 | NSLog(@"ios-----播放完成----by play"); 177 | NSDictionary *args = [self->_call arguments]; 178 | NSString *mId = [args valueForKey:@"id"]; 179 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:self->audioPath,@"playPath",@"complete",@"playState",mId,@"id", nil]; 180 | [self->_channel invokeMethod:@"onPlayState" arguments:dict3]; 181 | }; 182 | 183 | 184 | NSDictionary *args = [_call arguments]; 185 | NSString *mId = [args valueForKey:@"id"]; 186 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; 187 | [_channel invokeMethod:@"onPlay" arguments:dict3]; 188 | } 189 | - (void)stopPlay{ 190 | [DPAudioPlayer.sharedInstance stopPlaying]; 191 | 192 | } 193 | - (void) pausePlay{ 194 | 195 | NSLog(@"ios------pausePlay----function---pausePlay--"); 196 | bool isPlaying = [DPAudioPlayer.sharedInstance pausePlaying]; 197 | 198 | NSDictionary *args = [_call arguments]; 199 | NSString *mId = [args valueForKey:@"id"]; 200 | NSString *isPlayingStr = nil; 201 | if (isPlaying) { 202 | isPlayingStr = @"true"; 203 | }else{ 204 | isPlayingStr = @"false"; 205 | } 206 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys: 207 | @"success",@"result", 208 | isPlayingStr,@"isPlaying", 209 | mId,@"id", 210 | nil]; 211 | [_channel invokeMethod:@"pausePlay" arguments:dict3]; 212 | } 213 | 214 | /// 根据指定路径播放音频 215 | - (void) playByPath{ 216 | NSLog(@"ios------play voice by path-----function---playByPath---"); 217 | NSDictionary *args = [_call arguments]; 218 | NSString *filePath = [args valueForKey:@"path"]; 219 | 220 | NSString *typeStr = [args valueForKey:@"type"]; 221 | NSData *data; 222 | if ([typeStr isEqualToString:@"url"]) { 223 | data =[[NSData alloc]initWithContentsOfURL:[NSURL URLWithString:filePath]]; 224 | }else if([typeStr isEqualToString:@"file"]){ 225 | data= [NSData dataWithContentsOfFile:filePath]; 226 | 227 | } 228 | 229 | [DPAudioPlayer.sharedInstance startPlayWithData:data]; 230 | DPAudioPlayer.sharedInstance.playComplete = ^void(){ 231 | NSLog(@"ios-----播放完成----by playbyPath---"); 232 | NSDictionary *args = [self->_call arguments]; 233 | NSString *mId = [args valueForKey:@"id"]; 234 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:filePath,@"playPath",@"complete",@"playState",mId,@"id", nil]; 235 | [self->_channel invokeMethod:@"onPlayState" arguments:dict3]; 236 | }; 237 | 238 | NSString *mId = [args valueForKey:@"id"]; 239 | NSDictionary *dict3 = [NSDictionary dictionaryWithObjectsAndKeys:@"success",@"result",mId,@"id", nil]; 240 | [_channel invokeMethod:@"onPlay" arguments:dict3]; 241 | } 242 | 243 | 244 | @end 245 | 246 | -------------------------------------------------------------------------------- /ios/Classes/JX_GCDTimerManager.h: -------------------------------------------------------------------------------- 1 | 2 | #import 3 | 4 | typedef enum : NSUInteger { 5 | AbandonPreviousAction, // 废除之前的任务 6 | MergePreviousAction // 将之前的任务合并到新的任务中 7 | } ActionOption; 8 | 9 | @interface JX_GCDTimerManager : NSObject 10 | 11 | + (JX_GCDTimerManager *)sharedInstance; 12 | 13 | /** 14 | 启动一个timer,默认精度为0.1秒 15 | 16 | @param timerName timer的名称,作为唯一标识 17 | @param interval 执行的时间间隔 18 | @param queue timer将被放入的队列,也就是最终action执行的队列。传入nil将自动放到一个子线程队列中 19 | @param repeats timer是否循环调用 20 | @param option 多次schedule同一个timer时的操作选项(目前提供将之前的任务废除或合并的选项) 21 | @param action 时间间隔到点时执行的block 22 | */ 23 | - (void)scheduledDispatchTimerWithName:(NSString *)timerName 24 | timeInterval:(double)interval 25 | queue:(dispatch_queue_t)queue 26 | repeats:(BOOL)repeats 27 | actionOption:(ActionOption)option 28 | action:(dispatch_block_t)action; 29 | 30 | /** 31 | 撤销某个timer 32 | 33 | @param timerName timer的名称,作为唯一标识 34 | */ 35 | - (void)cancelTimerWithName:(NSString *)timerName; 36 | 37 | /** 38 | 撤销所有的timer 39 | */ 40 | - (void)cancelAllTimer; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /ios/Classes/JX_GCDTimerManager.m: -------------------------------------------------------------------------------- 1 | 2 | #import "JX_GCDTimerManager.h" 3 | 4 | @interface JX_GCDTimerManager() 5 | 6 | @property (nonatomic, strong) NSMutableDictionary *timerContainer; 7 | @property (nonatomic, strong) NSMutableDictionary *actionBlockCache; 8 | 9 | @end 10 | 11 | @implementation JX_GCDTimerManager 12 | 13 | #pragma mark - Public Method 14 | 15 | + (JX_GCDTimerManager *)sharedInstance 16 | { 17 | static JX_GCDTimerManager *_gcdTimerManager = nil; 18 | static dispatch_once_t onceToken; 19 | 20 | dispatch_once(&onceToken,^{ 21 | _gcdTimerManager = [[JX_GCDTimerManager alloc] init]; 22 | }); 23 | 24 | return _gcdTimerManager; 25 | } 26 | 27 | - (void)scheduledDispatchTimerWithName:(NSString *)timerName 28 | timeInterval:(double)interval 29 | queue:(dispatch_queue_t)queue 30 | repeats:(BOOL)repeats 31 | actionOption:(ActionOption)option 32 | action:(dispatch_block_t)action 33 | { 34 | if (nil == timerName) 35 | return; 36 | 37 | if (nil == queue) 38 | queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 39 | 40 | dispatch_source_t timer = [self.timerContainer objectForKey:timerName]; 41 | if (!timer) { 42 | timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); 43 | dispatch_resume(timer); 44 | [self.timerContainer setObject:timer forKey:timerName]; 45 | } 46 | 47 | /* timer精度为0.1秒 */ 48 | dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); 49 | 50 | __weak typeof(self) weakSelf = self; 51 | 52 | switch (option) { 53 | 54 | case AbandonPreviousAction: 55 | { 56 | /* 移除之前的action */ 57 | [weakSelf removeActionCacheForTimer:timerName]; 58 | 59 | dispatch_source_set_event_handler(timer, ^{ 60 | action(); 61 | 62 | if (!repeats) { 63 | [weakSelf cancelTimerWithName:timerName]; 64 | } 65 | }); 66 | } 67 | break; 68 | 69 | case MergePreviousAction: 70 | { 71 | /* cache本次的action */ 72 | [self cacheAction:action forTimer:timerName]; 73 | 74 | dispatch_source_set_event_handler(timer, ^{ 75 | NSMutableArray *actionArray = [self.actionBlockCache objectForKey:timerName]; 76 | [actionArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { 77 | dispatch_block_t actionBlock = obj; 78 | actionBlock(); 79 | }]; 80 | [weakSelf removeActionCacheForTimer:timerName]; 81 | 82 | if (!repeats) { 83 | [weakSelf cancelTimerWithName:timerName]; 84 | } 85 | }); 86 | } 87 | break; 88 | } 89 | } 90 | 91 | - (void)cancelTimerWithName:(NSString *)timerName 92 | { 93 | dispatch_source_t timer = [self.timerContainer objectForKey:timerName]; 94 | 95 | if (!timer) { 96 | return; 97 | } 98 | 99 | [self.timerContainer removeObjectForKey:timerName]; 100 | dispatch_source_cancel(timer); 101 | 102 | [self.actionBlockCache removeObjectForKey:timerName]; 103 | } 104 | 105 | - (void)cancelAllTimer 106 | { 107 | // Fast Enumeration 108 | [self.timerContainer enumerateKeysAndObjectsUsingBlock:^(NSString *timerName, dispatch_source_t timer, BOOL *stop) { 109 | [self.timerContainer removeObjectForKey:timerName]; 110 | dispatch_source_cancel(timer); 111 | }]; 112 | } 113 | 114 | #pragma mark - Property 115 | 116 | - (NSMutableDictionary *)timerContainer 117 | { 118 | if (!_timerContainer) { 119 | _timerContainer = [[NSMutableDictionary alloc] init]; 120 | } 121 | return _timerContainer; 122 | } 123 | 124 | - (NSMutableDictionary *)actionBlockCache 125 | { 126 | if (!_actionBlockCache) { 127 | _actionBlockCache = [[NSMutableDictionary alloc] init]; 128 | } 129 | return _actionBlockCache; 130 | } 131 | 132 | #pragma mark - Private Method 133 | 134 | - (void)cacheAction:(dispatch_block_t)action forTimer:(NSString *)timerName 135 | { 136 | id actionArray = [self.actionBlockCache objectForKey:timerName]; 137 | 138 | if (actionArray && [actionArray isKindOfClass:[NSMutableArray class]]) { 139 | [(NSMutableArray *)actionArray addObject:action]; 140 | }else { 141 | NSMutableArray *array = [NSMutableArray arrayWithObject:action]; 142 | [self.actionBlockCache setObject:array forKey:timerName]; 143 | } 144 | } 145 | 146 | - (void)removeActionCacheForTimer:(NSString *)timerName 147 | { 148 | if (![self.actionBlockCache objectForKey:timerName]) 149 | return; 150 | 151 | [self.actionBlockCache removeObjectForKey:timerName]; 152 | } 153 | 154 | @end 155 | -------------------------------------------------------------------------------- /ios/flutter_plugin_record.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | #use_frameworks! 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_plugin_record' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter plugin.' 9 | s.description = <<-DESC 10 | A new Flutter plugin. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | # s.swift_version = '4.2' 17 | s.source_files = 'Classes/**/*.{h,m}' 18 | s.public_header_files = 'Classes/**/*.h' 19 | # s.vendored_libraries = 'Classes/libopencore-amrnb.a' 20 | s.dependency 'Flutter' 21 | s.framework = "AVFoundation" 22 | s.ios.deployment_target = '8.0' 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/const/play_state.dart: -------------------------------------------------------------------------------- 1 | class PlayState { 2 | String playState; 3 | String playPath; 4 | 5 | PlayState(this.playState, this.playPath); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /lib/const/record_state.dart: -------------------------------------------------------------------------------- 1 | class RecordState { 2 | ///发送到原生端的方法名 3 | static String init = "init"; 4 | static String start = "start"; 5 | static String startByWavPath = "startByWavPath"; 6 | static String stop = "stop"; 7 | static String play = "play"; 8 | static String playByPath = "playByPath"; 9 | 10 | ///原生端的回调方法名 11 | static String onInit = "onInit"; 12 | static String onStart = "onStart"; 13 | static String onStop = "onStop"; 14 | static String onPlay = "onPlay"; 15 | static String onAmplitude = "onAmplitude"; 16 | static String onPlayState = "onPlayState"; 17 | } 18 | -------------------------------------------------------------------------------- /lib/const/response.dart: -------------------------------------------------------------------------------- 1 | class RecordResponse { 2 | bool? success; 3 | String? path; 4 | String? msg; 5 | String? key; 6 | double? audioTimeLength; 7 | 8 | RecordResponse( 9 | {this.success, this.path, this.msg, this.key, this.audioTimeLength}); 10 | // RecordResponse({this.success, this.path,this.msg,this.key}); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/flutter_plugin_record.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_plugin_record/const/play_state.dart'; 5 | import 'package:flutter_plugin_record/const/response.dart'; 6 | import 'package:uuid/uuid.dart'; 7 | 8 | class FlutterPluginRecord { 9 | final MethodChannel _channel = const MethodChannel('flutter_plugin_record'); 10 | 11 | static final _uuid = new Uuid(); 12 | String id = ''; 13 | static final alis = new Map(); 14 | 15 | FlutterPluginRecord() { 16 | id = _uuid.v4(); 17 | alis[id] = this; 18 | // print("--------FlutterPluginRecord init"); 19 | } 20 | 21 | ///Flutter 调用原生初始化 22 | Future _invokeMethod(String method, 23 | [Map arguments = const {}]) { 24 | Map withId = Map.of(arguments); 25 | withId['id'] = id; 26 | _channel.setMethodCallHandler(_handler); 27 | return _channel.invokeMethod(method, withId); 28 | } 29 | 30 | ///初始化init的回调 31 | StreamController _responseInitController = 32 | new StreamController.broadcast(); 33 | 34 | Stream get responseFromInit => _responseInitController.stream; 35 | 36 | ///开始录制 停止录制的回调监听 37 | StreamController _responseController = 38 | new StreamController.broadcast(); 39 | 40 | Stream get response => _responseController.stream; 41 | 42 | ///音量高低的回调 43 | StreamController _responseAmplitudeController = 44 | new StreamController.broadcast(); 45 | 46 | Stream get responseFromAmplitude => 47 | _responseAmplitudeController.stream; 48 | 49 | ///播放状态监听 50 | StreamController _responsePlayStateController = 51 | new StreamController.broadcast(); 52 | 53 | Stream get responsePlayStateController => 54 | _responsePlayStateController.stream; 55 | 56 | ///原生回调 57 | static Future _handler(MethodCall methodCall) async{ 58 | // print("--------FlutterPluginRecord " + methodCall.method); 59 | 60 | String id = (methodCall.arguments as Map)['id']; 61 | FlutterPluginRecord recordPlugin = alis[id] ?? FlutterPluginRecord(); 62 | switch (methodCall.method) { 63 | case "onInit": 64 | bool flag = false; 65 | if ("success" == methodCall.arguments["result"]) { 66 | flag = true; 67 | } 68 | recordPlugin._responseInitController.add(flag); 69 | break; 70 | case "onStart": 71 | if ("success" == methodCall.arguments["result"]) { 72 | RecordResponse res = new RecordResponse( 73 | success: true, 74 | path: "", 75 | msg: "onStart", 76 | key: methodCall.arguments["key"].toString(), 77 | ); 78 | recordPlugin._responseController.add(res); 79 | } 80 | 81 | break; 82 | case "onStop": 83 | if ("success" == methodCall.arguments["result"]) { 84 | RecordResponse res = new RecordResponse( 85 | success: true, 86 | path: methodCall.arguments["voicePath"].toString(), 87 | audioTimeLength: 88 | double.parse(methodCall.arguments["audioTimeLength"]), 89 | msg: "onStop", 90 | key: methodCall.arguments["key"].toString(), 91 | ); 92 | recordPlugin._responseController.add(res); 93 | } 94 | 95 | break; 96 | case "onPlay": 97 | RecordResponse res = new RecordResponse( 98 | success: true, 99 | path: "", 100 | msg: "开始播放", 101 | key: methodCall.arguments["key"].toString(), 102 | ); 103 | recordPlugin._responseController.add(res); 104 | break; 105 | case "onAmplitude": 106 | if ("success" == methodCall.arguments["result"]) { 107 | RecordResponse res = new RecordResponse( 108 | success: true, 109 | path: "", 110 | msg: methodCall.arguments["amplitude"].toString(), 111 | key: methodCall.arguments["key"].toString(), 112 | ); 113 | recordPlugin._responseAmplitudeController.add(res); 114 | } 115 | break; 116 | case "onPlayState": 117 | var playState = methodCall.arguments["playState"]; 118 | var playPath = methodCall.arguments["playPath"]; 119 | PlayState res = new PlayState(playState, playPath); 120 | recordPlugin._responsePlayStateController.add(res); 121 | break; 122 | case "pausePlay": 123 | //暂停或继续播放 124 | var isPlaying = methodCall.arguments["isPlaying"]; 125 | PlayState res = new PlayState(isPlaying, ""); 126 | recordPlugin._responsePlayStateController.add(res); 127 | break; 128 | default: 129 | print("default"); 130 | break; 131 | } 132 | return null; 133 | } 134 | 135 | //初始化 136 | Future init() async { 137 | return await _invokeMethod('init', { 138 | "init": "init", 139 | }); 140 | } 141 | 142 | //初始化 143 | Future initRecordMp3() async { 144 | return await _invokeMethod('initRecordMp3', { 145 | "initRecordMp3": "initRecordMp3", 146 | }); 147 | } 148 | 149 | Future start() async { 150 | return await _invokeMethod('start', { 151 | "start": "start", 152 | }); 153 | } 154 | 155 | Future startByWavPath(String wavPath) async { 156 | return await _invokeMethod('startByWavPath', { 157 | "wavPath": wavPath, 158 | }); 159 | } 160 | 161 | Future stop() async { 162 | return await _invokeMethod('stop', { 163 | "stop": "stop", 164 | }); 165 | } 166 | 167 | Future play() async { 168 | return await _invokeMethod('play', { 169 | "play": "play", 170 | }); 171 | } 172 | 173 | // Future playByPath(String path) async { 174 | // return await _invokeMethod('playByPath', { 175 | // "play": "play", 176 | // "path": path, 177 | // }); 178 | // } 179 | 180 | /// 181 | /// 参数 path 播放音频的地址 182 | /// 183 | ///path 为 url类型对应的在线播放地址 https://linjuli-app-audio.oss-cn-hangzhou.aliyuncs.com/audio/50c39c768b534ce1ba25d837ed153824.wav 184 | ///path 对应本地文件路径对应的是本地文件播放肚子 /sdcard/flutterdemo/wiw.wav 185 | /// 参数 type 186 | /// 当path 为url type为 url 187 | /// 当path 为本地地址 type为 file 188 | /// 189 | Future playByPath(String path, String type) async { 190 | return await _invokeMethod('playByPath', { 191 | "play": "play", 192 | "path": path, 193 | "type": type, 194 | }); 195 | } 196 | 197 | ///暂停播放 198 | Future pausePlay() async { 199 | return await _invokeMethod('pause', { 200 | "pause": "pause", 201 | }); 202 | } 203 | 204 | /// 提供停止播放的功能 205 | Future stopPlay() async { 206 | return await _invokeMethod('stopPlay', {}); 207 | } 208 | 209 | dispose() { 210 | // stopPlay(); 211 | _responseInitController.close(); 212 | _responseController.close(); 213 | _responseAmplitudeController.close(); 214 | _responsePlayStateController.close(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/index.dart: -------------------------------------------------------------------------------- 1 | export 'const/record_state.dart'; 2 | export 'flutter_plugin_record.dart'; 3 | export 'utils/common_toast.dart'; 4 | export 'widgets/custom_overlay.dart'; 5 | export 'widgets/voice_widget.dart'; 6 | -------------------------------------------------------------------------------- /lib/utils/common_toast.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_plugin_record/widgets/custom_overlay.dart'; 5 | 6 | class CommonToast { 7 | static showView({ 8 | BuildContext? context, 9 | String? msg, 10 | TextStyle? style, 11 | Widget? icon, 12 | Duration duration = const Duration(seconds: 1), 13 | int count = 3, 14 | Function? onTap, 15 | }) { 16 | OverlayEntry? overlayEntry; 17 | int _count = 0; 18 | 19 | void removeOverlay() { 20 | overlayEntry?.remove(); 21 | overlayEntry = null; 22 | } 23 | 24 | if (overlayEntry == null) { 25 | overlayEntry = new OverlayEntry(builder: (content) { 26 | return Container( 27 | child: GestureDetector( 28 | onTap: () { 29 | if (onTap != null) { 30 | removeOverlay(); 31 | onTap(); 32 | } 33 | }, 34 | child: CustomOverlay( 35 | icon: Column( 36 | children: [ 37 | Padding( 38 | child: icon, 39 | padding: const EdgeInsets.only( 40 | bottom: 10.0, 41 | ), 42 | ), 43 | Container( 44 | // padding: EdgeInsets.only(right: 20, left: 20, top: 0), 45 | child: Text( 46 | msg ?? '', 47 | style: style ?? 48 | TextStyle( 49 | fontStyle: FontStyle.normal, 50 | color: Colors.white, 51 | fontSize: 16, 52 | ), 53 | ), 54 | ) 55 | ], 56 | ), 57 | ), 58 | ), 59 | ); 60 | }); 61 | Overlay.of(context!)!.insert(overlayEntry!); 62 | if (onTap != null) return; 63 | Timer.periodic(duration, (timer) { 64 | _count++; 65 | if (_count == count) { 66 | _count = 0; 67 | timer.cancel(); 68 | removeOverlay(); 69 | } 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/widgets/custom_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomOverlay extends StatelessWidget { 4 | final Widget? icon; 5 | final BoxDecoration decoration; 6 | final double width; 7 | final double height; 8 | const CustomOverlay({ 9 | Key? key, 10 | this.icon, 11 | this.decoration = const BoxDecoration( 12 | color: Color(0xff77797A), 13 | borderRadius: BorderRadius.all(Radius.circular(20.0)), 14 | ), 15 | this.width = 160, 16 | this.height = 160, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Positioned( 22 | top: MediaQuery.of(context).size.height * 0.5 - width / 2, 23 | left: MediaQuery.of(context).size.width * 0.5 - height / 2, 24 | child: Material( 25 | type: MaterialType.transparency, 26 | child: Center( 27 | child: Opacity( 28 | opacity: 0.8, 29 | child: Container( 30 | width: width, 31 | height: height, 32 | decoration: decoration, 33 | child: icon, 34 | ), 35 | ), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/widgets/voice_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_plugin_record/flutter_plugin_record.dart'; 5 | import 'package:flutter_plugin_record/utils/common_toast.dart'; 6 | 7 | import 'custom_overlay.dart'; 8 | 9 | typedef startRecord = Future Function(); 10 | typedef stopRecord = Future Function(); 11 | 12 | class VoiceWidget extends StatefulWidget { 13 | final Function? startRecord; 14 | final Function? stopRecord; 15 | final double? height; 16 | final EdgeInsets? margin; 17 | final Decoration? decoration; 18 | 19 | /// startRecord 开始录制回调 stopRecord回调 20 | const VoiceWidget( 21 | {Key? key, 22 | this.startRecord, 23 | this.stopRecord, 24 | this.height, 25 | this.decoration, 26 | this.margin}) 27 | : super(key: key); 28 | 29 | @override 30 | _VoiceWidgetState createState() => _VoiceWidgetState(); 31 | } 32 | 33 | class _VoiceWidgetState extends State { 34 | // 倒计时总时长 35 | int _countTotal = 12; 36 | double starty = 0.0; 37 | double offset = 0.0; 38 | bool isUp = false; 39 | String textShow = "按住说话"; 40 | String toastShow = "手指上滑,取消发送"; 41 | String voiceIco = "images/voice_volume_1.png"; 42 | 43 | ///默认隐藏状态 44 | bool voiceState = true; 45 | FlutterPluginRecord? recordPlugin; 46 | Timer? _timer; 47 | int _count = 0; 48 | OverlayEntry? overlayEntry; 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | recordPlugin = new FlutterPluginRecord(); 54 | 55 | _init(); 56 | 57 | ///初始化方法的监听 58 | recordPlugin?.responseFromInit.listen((data) { 59 | if (data) { 60 | print("初始化成功"); 61 | } else { 62 | print("初始化失败"); 63 | } 64 | }); 65 | 66 | /// 开始录制或结束录制的监听 67 | recordPlugin?.response.listen((data) { 68 | if (data.msg == "onStop") { 69 | ///结束录制时会返回录制文件的地址方便上传服务器 70 | print("onStop " + data.path!); 71 | if (widget.stopRecord != null) 72 | widget.stopRecord!(data.path, data.audioTimeLength); 73 | } else if (data.msg == "onStart") { 74 | print("onStart --"); 75 | if (widget.startRecord != null) widget.startRecord!(); 76 | } 77 | }); 78 | 79 | ///录制过程监听录制的声音的大小 方便做语音动画显示图片的样式 80 | recordPlugin!.responseFromAmplitude.listen((data) { 81 | var voiceData = double.parse(data.msg ?? ''); 82 | setState(() { 83 | if (voiceData > 0 && voiceData < 0.1) { 84 | voiceIco = "images/voice_volume_2.png"; 85 | } else if (voiceData > 0.2 && voiceData < 0.3) { 86 | voiceIco = "images/voice_volume_3.png"; 87 | } else if (voiceData > 0.3 && voiceData < 0.4) { 88 | voiceIco = "images/voice_volume_4.png"; 89 | } else if (voiceData > 0.4 && voiceData < 0.5) { 90 | voiceIco = "images/voice_volume_5.png"; 91 | } else if (voiceData > 0.5 && voiceData < 0.6) { 92 | voiceIco = "images/voice_volume_6.png"; 93 | } else if (voiceData > 0.6 && voiceData < 0.7) { 94 | voiceIco = "images/voice_volume_7.png"; 95 | } else if (voiceData > 0.7 && voiceData < 1) { 96 | voiceIco = "images/voice_volume_7.png"; 97 | } else { 98 | voiceIco = "images/voice_volume_1.png"; 99 | } 100 | if (overlayEntry != null) { 101 | overlayEntry!.markNeedsBuild(); 102 | } 103 | }); 104 | 105 | print("振幅大小 " + voiceData.toString() + " " + voiceIco); 106 | }); 107 | } 108 | 109 | ///显示录音悬浮布局 110 | buildOverLayView(BuildContext context) { 111 | if (overlayEntry == null) { 112 | overlayEntry = new OverlayEntry(builder: (content) { 113 | return CustomOverlay( 114 | icon: Column( 115 | children: [ 116 | Container( 117 | margin: const EdgeInsets.only(top: 10), 118 | child: _countTotal - _count < 11 119 | ? Center( 120 | child: Padding( 121 | padding: const EdgeInsets.only(bottom: 15.0), 122 | child: Text( 123 | (_countTotal - _count).toString(), 124 | style: TextStyle( 125 | fontSize: 70.0, 126 | color: Colors.white, 127 | ), 128 | ), 129 | ), 130 | ) 131 | : new Image.asset( 132 | voiceIco, 133 | width: 100, 134 | height: 100, 135 | package: 'flutter_plugin_record', 136 | ), 137 | ), 138 | Container( 139 | // padding: const EdgeInsets.only(right: 20, left: 20, top: 0), 140 | child: Text( 141 | toastShow, 142 | style: TextStyle( 143 | fontStyle: FontStyle.normal, 144 | color: Colors.white, 145 | fontSize: 14, 146 | ), 147 | ), 148 | ) 149 | ], 150 | ), 151 | ); 152 | }); 153 | Overlay.of(context)!.insert(overlayEntry!); 154 | } 155 | } 156 | 157 | showVoiceView() { 158 | setState(() { 159 | textShow = "松开结束"; 160 | voiceState = false; 161 | }); 162 | 163 | ///显示录音悬浮布局 164 | buildOverLayView(context); 165 | 166 | start(); 167 | } 168 | 169 | hideVoiceView() { 170 | if (_timer!.isActive) { 171 | if (_count < 1) { 172 | CommonToast.showView( 173 | context: context, 174 | msg: '说话时间太短', 175 | icon: Text( 176 | '!', 177 | style: TextStyle(fontSize: 80, color: Colors.white), 178 | )); 179 | isUp = true; 180 | } 181 | _timer?.cancel(); 182 | _count = 0; 183 | } 184 | 185 | setState(() { 186 | textShow = "按住说话"; 187 | voiceState = true; 188 | }); 189 | 190 | stop(); 191 | if (overlayEntry != null) { 192 | overlayEntry?.remove(); 193 | overlayEntry = null; 194 | } 195 | 196 | if (isUp) { 197 | print("取消发送"); 198 | } else { 199 | print("进行发送"); 200 | } 201 | } 202 | 203 | moveVoiceView() { 204 | // print(offset - start); 205 | setState(() { 206 | isUp = starty - offset > 100 ? true : false; 207 | if (isUp) { 208 | textShow = "松开手指,取消发送"; 209 | toastShow = textShow; 210 | } else { 211 | textShow = "松开结束"; 212 | toastShow = "手指上滑,取消发送"; 213 | } 214 | }); 215 | } 216 | 217 | ///初始化语音录制的方法 218 | void _init() async { 219 | recordPlugin?.init(); 220 | } 221 | 222 | ///开始语音录制的方法 223 | void start() async { 224 | recordPlugin?.start(); 225 | } 226 | 227 | ///停止语音录制的方法 228 | void stop() { 229 | recordPlugin?.stop(); 230 | } 231 | 232 | @override 233 | Widget build(BuildContext context) { 234 | return Container( 235 | child: GestureDetector( 236 | onLongPressStart: (details) { 237 | starty = details.globalPosition.dy; 238 | _timer = Timer.periodic(Duration(milliseconds: 1000), (t) { 239 | _count++; 240 | print('_count is 👉 $_count'); 241 | if (_count == _countTotal) { 242 | hideVoiceView(); 243 | } 244 | }); 245 | showVoiceView(); 246 | }, 247 | onLongPressEnd: (details) { 248 | hideVoiceView(); 249 | }, 250 | onLongPressMoveUpdate: (details) { 251 | offset = details.globalPosition.dy; 252 | moveVoiceView(); 253 | }, 254 | child: Container( 255 | height: widget.height ?? 60, 256 | // color: Colors.blue, 257 | decoration: widget.decoration ?? 258 | BoxDecoration( 259 | borderRadius: new BorderRadius.circular(6.0), 260 | border: Border.all(width: 1.0, color: Colors.grey.shade200), 261 | ), 262 | margin: widget.margin ?? EdgeInsets.fromLTRB(50, 0, 50, 20), 263 | child: Center( 264 | child: Text( 265 | textShow, 266 | ), 267 | ), 268 | ), 269 | ), 270 | ); 271 | } 272 | 273 | @override 274 | void dispose() { 275 | recordPlugin?.dispose(); 276 | _timer?.cancel(); 277 | super.dispose(); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.5.0" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.2.0" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.15.0" 46 | crypto: 47 | dependency: transitive 48 | description: 49 | name: crypto 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "3.0.0" 53 | fake_async: 54 | dependency: transitive 55 | description: 56 | name: fake_async 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "1.2.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "0.12.10" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.3.0" 84 | path: 85 | dependency: transitive 86 | description: 87 | name: path 88 | url: "https://pub.flutter-io.cn" 89 | source: hosted 90 | version: "1.8.0" 91 | sky_engine: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.99" 96 | source_span: 97 | dependency: transitive 98 | description: 99 | name: source_span 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "1.8.0" 103 | stack_trace: 104 | dependency: transitive 105 | description: 106 | name: stack_trace 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "1.10.0" 110 | stream_channel: 111 | dependency: transitive 112 | description: 113 | name: stream_channel 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "2.1.0" 117 | string_scanner: 118 | dependency: transitive 119 | description: 120 | name: string_scanner 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.1.0" 124 | term_glyph: 125 | dependency: transitive 126 | description: 127 | name: term_glyph 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.2.0" 131 | test_api: 132 | dependency: transitive 133 | description: 134 | name: test_api 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "0.2.19" 138 | typed_data: 139 | dependency: transitive 140 | description: 141 | name: typed_data 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "1.3.0" 145 | uuid: 146 | dependency: "direct main" 147 | description: 148 | name: uuid 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "3.0.2" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "2.1.0" 159 | sdks: 160 | dart: ">=2.12.0-29.10.beta <3.0.0" 161 | flutter: ">=1.12.0" 162 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: flutter_plugin_record 3 | description: The flutter voice recording plug-in,provides the recording animation and the recording successfully returns to the recording file path 4 | version: 1.0.1 5 | #author: wilson 6 | homepage: https://github.com/yxwandroid/flutter_plugin_record 7 | #publish_to: none 8 | 9 | 10 | environment: 11 | sdk: ">=2.12.0-29.10.beta <3.0.0" 12 | flutter: ">=1.12.0" 13 | 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | uuid: ^3.0.0 19 | 20 | dev_dependencies: 21 | flutter_test: 22 | sdk: flutter 23 | 24 | # For information on the generic Dart part of this file, see the 25 | # following page: https://dart.dev/tools/pub/pubspec 26 | 27 | # The following section is specific to Flutter. 28 | flutter: 29 | # This section identifies this Flutter project as a plugin project. 30 | # The androidPackage and pluginClass identifiers should not ordinarily 31 | # be modified. They are used by the tooling to maintain consistency when 32 | # adding or updating assets for this project. 33 | plugin: 34 | # androidPackage: record.wilson.flutter.com.flutter_plugin_record 35 | # pluginClass: FlutterPluginRecordPlugin 36 | platforms: 37 | android: 38 | package: record.wilson.flutter.com.flutter_plugin_record 39 | pluginClass: FlutterPluginRecordPlugin 40 | ios: 41 | pluginClass: FlutterPluginRecordPlugin 42 | 43 | uses-material-design: true 44 | assets: 45 | - images/voice_volume_1.png 46 | - images/voice_volume_2.png 47 | - images/voice_volume_3.png 48 | - images/voice_volume_4.png 49 | - images/voice_volume_5.png 50 | - images/voice_volume_6.png 51 | - images/voice_volume_7.png 52 | 53 | # To add assets to your plugin package, add an assets section, like this: 54 | # assets: 55 | # - images/a_dot_burr.jpeg 56 | # - images/a_dot_ham.jpeg 57 | # 58 | # For details regarding assets in packages, see 59 | # https://flutter.dev/assets-and-images/#from-packages 60 | # 61 | # An image asset can refer to one or more resolution-specific "variants", see 62 | # https://flutter.dev/assets-and-images/#resolution-aware. 63 | 64 | # To add custom fonts to your plugin package, add a fonts section here, 65 | # in this "flutter" section. Each entry in this list should have a 66 | # "family" key with the font family name, and a "fonts" key with a 67 | # list giving the asset and other descriptors for the font. For 68 | # example: 69 | # fonts: 70 | # - family: Schyler 71 | # fonts: 72 | # - asset: fonts/Schyler-Regular.ttf 73 | # - asset: fonts/Schyler-Italic.ttf 74 | # style: italic 75 | # - family: Trajan Pro 76 | # fonts: 77 | # - asset: fonts/TrajanPro.ttf 78 | # - asset: fonts/TrajanPro_Bold.ttf 79 | # weight: 700 80 | # 81 | # For details regarding fonts in packages, see 82 | # https://flutter.dev/custom-fonts/#from-packages 83 | -------------------------------------------------------------------------------- /test/flutter_plugin_record_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | const MethodChannel channel = MethodChannel('flutter_plugin_record'); 6 | 7 | setUp(() { 8 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 9 | return '42'; 10 | }); 11 | }); 12 | 13 | tearDown(() { 14 | channel.setMockMethodCallHandler(null); 15 | }); 16 | 17 | // test('getPlatformVersion', () async { 18 | // expect(await FlutterPluginRecord.platformVersion, '42'); 19 | // }); 20 | } 21 | -------------------------------------------------------------------------------- /飞云之下.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yxwandroid/flutter_plugin_record/8e6f3cf9ebad75ef3bc9b45bb16ef31c49596195/飞云之下.wav --------------------------------------------------------------------------------