赞
踩
Flutter的愿景是一般的开发者只需要开发Flutter代码就能实现跨平台的应用,官方提供了一些插件,也有很多可以可以直接拿来使用的第三方插件。
但是现实是现实,例如当遇到定制化的功能时,编写插件是不可避免的。譬如我们有一个自定义协议的蓝牙功能,这个功能在Flutter中就不可能直接拿来使用了,需要编写插件让Flutter进行调用。本文我们将来看看Flutter插件是如何实现的。
本文我们用Flutter来仿写网易云音乐的播放页面的功能,其中音乐的播放,音乐的暂停,快进,音乐的时长获取,音乐播放的进度等功能我们需要用原生代码编写插件来实现。
提示:本文用音乐播放器的插件只是为了提供一个编写Flutter插件的思路和方法,当需要自己编写插件的时候可以方便的来实现。播放音视频的Flutter插件已经有一些优秀的三方库已经实现了。
说明:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHbXg5gX-1620631043776)(https://flutter.dev/images/PlatformChannels.png)]
我们从上面的官方架构图可以看出,Flutter和Native代码是通过MethodChannel
进行通信的。
AudioPlayer
, 然后定义为单例模式class AudioPlayer {
// 单例
factory AudioPlayer() => _getInstance();
static AudioPlayer get instance => _getInstance();
static AudioPlayer _instance;
AudioPlayer._internal() {}
static AudioPlayer _getInstance() {
if (_instance == null) {
_instance = new AudioPlayer._internal();
}
return _instance;
}
}
MethodChannel
class AudioPlayer {
static final channel = const MethodChannel("netmusic.com/audio_player");
}
MethodChannel
名字要有意义,其组成遵循"域名"+"/"+“功能”,随意写就显得不够专业。
MethodChannel
的invokeMethod
实现播放音乐/// 播放
Future<int> play() async {
final result = await channel.invokeMethod("play", {'url': audioUrl});
return result ?? 0;
}
play
就是方法名,{'url': audioUrl}
就是参数invokeMethod
是异步的,所以返回值需要用Future
包裹。
MethodChannel
的invokeMethod
实现暂停音乐/// 暂停
Future<int> pause() async {
final result = await channel.invokeMethod("pause", {'url': audioUrl});
return result ?? 0;
}
MethodChannel
的invokeMethod
实现继续播放音乐/// 继续播放
Future<int> resume() async {
final result = await channel.invokeMethod("resume", {'url': audioUrl});
return result ?? 0;
}
MethodChannel
的invokeMethod
实现拖动播放位置/// 拖动播放位置
Future<int> seek(int time) async {
final result = await channel.invokeMethod("seek", {
'position': time,
});
return result ?? 0;
}
前提:需要用Xcode打开iOS项目,这是开始编写的基础。
PlayerWrapper
class PlayerWrapper: NSObject {
var vc: FlutterViewController
var channel: FlutterMethodChannel
var player: AVPlayer?
}
AppDelegate
中初始化PlayerWrapper
,并将FlutterViewController
作为初始化参数。@objc class AppDelegate: FlutterAppDelegate { // 持有播放器 var playerWrapper: PlayerWrapper? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // 初始化播放器 let controller : FlutterViewController = window?.rootViewController as! FlutterViewController playerWrapper = PlayerWrapper(vc: controller) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
- FlutterAppDelegate的根视图就是一个FlutterViewController,这个我们在以前的文章中有介绍;
- FlutterViewController中有一个FlutterBinaryMessenger,创建
FlutterMethodChannel
时需要,所以将其传入PlayerWrapper
。
FlutterMethodChannel
class PlayerWrapper: NSObject {
init(vc: FlutterViewController) {
self.vc = vc
channel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger)
super.init()
}
}
name
的值必须和Flutter中的对应,否则是没法通信的;binaryMessenger
就使用FlutterViewController的FlutterBinaryMessenger,前面提到过。
由于是被动接收,所以可以想象的实现是注册一个回调函数,接收Flutter端的调用方法和参数。
init(vc: FlutterViewController) { //... channel.setMethodCallHandler(handleFlutterMessage); } // 从Flutter传过来的方法 public func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) { // 1. 获取方法名和参数 let method = call.method let args = call.arguments as? [String: Any] if method == "play" { // 2.1 确保有url参数 guard let url = args?["url"] as! String? else { result(0) return } player?.pause() // 2.2 确保有url参数正确 guard let audioURL = URL.init(string: url) else { result(0) return } // 2.3 根据url初始化播放内容,然后开始进行播放 let asset = AVAsset.init(url: audioURL) let item = AVPlayerItem.init(asset: asset); player = AVPlayer(playerItem: item); player?.play(); // 2.4 定时检测播放进度 player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: nil, using: { [weak self] (time) in // *********回调Flutter当前播放进度********* self?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)]) }) keyVakueObservation?.invalidate() // 2.5 监测播放状态 keyVakueObservation = item.observe(\AVPlayerItem.status) { [weak self] (playerItem, change) in let status = playerItem.status if status == .readyToPlay { // *********回调Flutter当前播放内容的总长度********* if let time = self?.player?.currentItem?.asset.duration { self?.channel.invokeMethod("onDuration", arguments: ["value": time.value / Int64(time.timescale)]) } } else if status == .failed { // *********回调Flutter当前播放出现错误********* self?.channel.invokeMethod("onError", arguments: ["value": "pley failed"]) } } // 2.6 监测播放完成 notificationObservation = NotificationCenter.default.addObserver( forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item,** queue: nil ) { [weak self] (notification) in self?.channel.invokeMethod("onComplete", arguments: []) }** result(1) } else if method == "pause" || method == "stop" { // 3 暂停 player?.pause() result(1) } else if method == "resume" { // 4 继续播放 player?.play() result(1) } else if method == "seek" { guard let position = args?["position"] as! Int? else { result(0) return } // 4 拖动到某处进行播放 let seekTime: CMTime = CMTimeMake(value: Int64(position), timescale: 1) player?.seek(to: seekTime); } }
handleFlutterMessage
这个回调函数有两个参数:FlutterMethodCall
接收Flutter传过来的方法名method
和参数arguments
,FlutterResult
可以返回调用的结果,例如result(1)
就给Flutter返回了1
这个结果。- 获取到
FlutterMethodCall
的方法名和参数后就可以进行处理了,我们以play
为例:
- 根据url初始化播放内容,然后开始进行播放;
- 通过
player.addPeriodicTimeObserver
方法检测播放进度,然后通过FlutterMethodChannel
的invokeMethod
方法传递当前的进度给Flutter端,方法名是onPosition
,参数是当前进度;- 后面还有一列逻辑:例如监听播放状态,监听播放完成等。
目前为止,iOS端的代码完成了。接下来就是Flutter端接收iOS端的方法和参数了。
iOS端向Flutter端发送了onPosition
(当前播放进度),onComplete
(播放完成),onDuration
(当前歌曲的总长度)和onError
(播放出现错误)等几个方法调用。
AudioPlayer._internal() { channel.setMethodCallHandler(nativePlatformCallHandler); } /// Native主动调用的方法 Future<void> nativePlatformCallHandler(MethodCall call) async { try { // 获取参数 final callArgs = call.arguments as Map<dynamic, dynamic>; print('nativePlatformCallHandler call ${call.method} $callArgs'); switch (call.method) { case 'onPosition': final time = callArgs['value'] as int; _currentPlayTime = time; _currentPlayTimeController.add(_currentPlayTime); break; case 'onComplete': this.updatePlayerState(PlayerState.COMPLETED); break; case 'onDuration': final time = callArgs['value'] as int; _totalPlayTime = time; _totalPlayTimeController.add(totalPlayTime); break; case 'onError': final error = callArgs['value'] as String; this.updatePlayerState(PlayerState.STOPPED); _errorController.add(error); break; } } catch (ex) { print('Unexpected error: $ex'); } }
- 注册回调也是使用
setMethodCallHandler
方法,MethodCall
对应的也包含方法名和参数;- 获取到对应的数据后Flutter就可进行数据的展示了。
我们以onDuration
(当前歌曲的总长度)为例进行介绍。
class AudioPlayer { // 1. 记录下总时间 int _totalPlayTime = 0; int get totalPlayTime => _totalPlayTime; // 2. 代表歌曲时长的流 final StreamController<int> _totalPlayTimeController = StreamController<int>.broadcast(); Stream<int> get onTotalTimeChanged => _totalPlayTimeController.stream; Future<void> nativePlatformCallHandler(MethodCall call) async { try { final callArgs = call.arguments as Map<dynamic, dynamic>; print('nativePlatformCallHandler call ${call.method} $callArgs'); switch (call.method) { // 3. 记录下总时间和推送更新 case 'onDuration': final time = callArgs['value'] as int; _totalPlayTime = time; _totalPlayTimeController.add(totalPlayTime); break; } } catch (ex) { print('Unexpected error: $ex'); } } }
_totalPlayTime
记录下总播放时长;_totalPlayTimeController
是总播放时长的流,当调用add
方法时,onTotalTimeChanged
的监听者就能收到新的值;
StreamBuilder(
initialData: "00:00",
stream: AudioPlayer().onTotalTimeChanged,
builder: (context, snapshot) {
if (!snapshot.hasData)
return Text(
"00:00",
style: TextStyle(color: Colors.white70),
);
return Text(
AudioPlayer().totalPlayTimeStr,
style: TextStyle(color: Colors.white70),
);
},
),
监听
AudioPlayer().onTotalTimeChanged
的数据变化,然后最新的值展示在Text
上。
import 'dart:async'; import 'package:flutter/services.dart'; import 'package:netmusic_flutter/music_item.dart'; class AudioPlayer { // 定义一个MethodChannel static final channel = const MethodChannel("netmusic.com/audio_player"); // 单例 factory AudioPlayer() => _getInstance(); static AudioPlayer get instance => _getInstance(); static AudioPlayer _instance; AudioPlayer._internal() { // 初始化 channel.setMethodCallHandler(nativePlatformCallHandler); } static AudioPlayer _getInstance() { if (_instance == null) { _instance = new AudioPlayer._internal(); } return _instance; } // 播放状态 PlayerState _playerState = PlayerState.STOPPED; PlayerState get playerState => _playerState; // 时间 int _totalPlayTime = 0; int _currentPlayTime = 0; int get totalPlayTime => _totalPlayTime; int get currentPlayTime => _currentPlayTime; String get totalPlayTimeStr => formatTime(_totalPlayTime); String get currentPlayTimeStr => formatTime(_currentPlayTime); // 歌曲 MusicItem _item; set item(MusicItem item) { _item = item; } String get audioUrl { return _item != null ? "https://music.163.com/song/media/outer/url?id=${_item.id}.mp3" : ""; } Future<int> togglePlay() async { if (_playerState == PlayerState.PLAYING) { return pause(); } else { return play(); } } /// 播放 Future<int> play() async { if (_item == null) return 0; // 如果是停止状态 if (_playerState == PlayerState.STOPPED || _playerState == PlayerState.COMPLETED) { // 更新状态 this.updatePlayerState(PlayerState.PLAYING); final result = await channel.invokeMethod("play", {'url': audioUrl}); return result ?? 0; } else if (_playerState == PlayerState.PAUSED) { return resume(); } return 0; } /// 继续播放 Future<int> resume() async { // 更新状态 this.updatePlayerState(PlayerState.PLAYING); final result = await channel.invokeMethod("resume", {'url': audioUrl}); return result ?? 0; } /// 暂停 Future<int> pause() async { // 更新状态 this.updatePlayerState(PlayerState.PAUSED); final result = await channel.invokeMethod("pause", {'url': audioUrl}); return result ?? 0; } /// 停止 Future<int> stop() async { // 更新状态 this.updatePlayerState(PlayerState.STOPPED); final result = await channel.invokeMethod("stop"); return result ?? 0; } /// 播放 Future<int> seek(int time) async { // 更新状态 this.updatePlayerState(PlayerState.PLAYING); final result = await channel.invokeMethod("seek", { 'position': time, }); return result ?? 0; } /// Native主动调用的方法 Future<void> nativePlatformCallHandler(MethodCall call) async { try { // 获取参数 final callArgs = call.arguments as Map<dynamic, dynamic>; print('nativePlatformCallHandler call ${call.method} $callArgs'); switch (call.method) { case 'onPosition': final time = callArgs['value'] as int; _currentPlayTime = time; _currentPlayTimeController.add(_currentPlayTime); break; case 'onComplete': this.updatePlayerState(PlayerState.COMPLETED); break; case 'onDuration': final time = callArgs['value'] as int; _totalPlayTime = time; _totalPlayTimeController.add(totalPlayTime); break; case 'onError': final error = callArgs['value'] as String; this.updatePlayerState(PlayerState.STOPPED); _errorController.add(error); break; } } catch (ex) { print('Unexpected error: $ex'); } } // 播放状态 final StreamController<PlayerState> _stateController = StreamController<PlayerState>.broadcast(); Stream<PlayerState> get onPlayerStateChanged => _stateController.stream; // Video的时长和当前位置时间变化 final StreamController<int> _totalPlayTimeController = StreamController<int>.broadcast(); Stream<int> get onTotalTimeChanged => _totalPlayTimeController.stream; final StreamController<int> _currentPlayTimeController = StreamController<int>.broadcast(); Stream<int> get onCurrentTimeChanged => _currentPlayTimeController.stream; // 发生错误 final StreamController<String> _errorController = StreamController<String>(); Stream<String> get onError => _errorController.stream; // 更新播放状态 void updatePlayerState(PlayerState state, {bool stream = true}) { _playerState = state; if (stream) { _stateController.add(state); } } // 这里需要关闭流 void dispose() { _stateController.close(); _currentPlayTimeController.close(); _totalPlayTimeController.close(); _errorController.close(); } // 格式化时间 String formatTime(int time) { int min = (time ~/ 60); int sec = time % 60; String minStr = min < 10 ? "0$min" : "$min"; String secStr = sec < 10 ? "0$sec" : "$sec"; return "$minStr:$secStr"; } } /// 播放状态 enum PlayerState { STOPPED, PLAYING, PAUSED, COMPLETED, }
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { var playerWrapper: PlayerWrapper? override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // 播放器 let controller : FlutterViewController = window?.rootViewController as! FlutterViewController playerWrapper = PlayerWrapper(vc: controller) GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
import Foundation import Flutter import AVKit import CoreMedia class PlayerWrapper: NSObject { var vc: FlutterViewController var channel: FlutterMethodChannel var player: AVPlayer? var keyVakueObservation: NSKeyValueObservation? var notificationObservation: NSObjectProtocol? init(vc: FlutterViewController) { self.vc = vc channel = FlutterMethodChannel(name: "netmusic.com/audio_player", binaryMessenger: vc.binaryMessenger) super.init() channel.setMethodCallHandler(handleFlutterMessage); } // 从Flutter传过来的方法 public func handleFlutterMessage(_ call: FlutterMethodCall, result: @escaping FlutterResult) { let method = call.method let args = call.arguments as? [String: Any] if method == "play" { guard let url = args?["url"] as! String? else { NSLog("无播放地址") result(0) return } player?.pause() guard let audioURL = URL.init(string: url) else { NSLog("播放地址错误") result(0) return } let asset = AVAsset.init(url: audioURL) let item = AVPlayerItem.init(asset: asset); player = AVPlayer(playerItem: item); player?.play(); player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: nil, using: { [weak self] (time) in self?.channel.invokeMethod("onPosition", arguments: ["value": time.value / Int64(time.timescale)]) }) keyVakueObservation?.invalidate() keyVakueObservation = item.observe(\AVPlayerItem.status) { [weak self] (playerItem, change) in let status = playerItem.status if status == .readyToPlay { if let time = self?.player?.currentItem?.asset.duration { self?.channel.invokeMethod("onDuration", arguments: ["value": time.value / Int64(time.timescale)]) } } else if status == .failed { self?.channel.invokeMethod("onError", arguments: ["value": "pley failed"]) } } notificationObservation = NotificationCenter.default.addObserver( forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: nil ) { [weak self] (notification) in self?.channel.invokeMethod("onComplete", arguments: []) } result(1) } else if method == "pause" || method == "stop" { player?.pause() result(1) } else if method == "resume" { player?.play() result(1) } else if method == "seek" { guard let position = args?["position"] as! Int? else { NSLog("无播放时间") result(0) return } let seekTime: CMTime = CMTimeMake(value: Int64(position), timescale: 1) player?.seek(to: seekTime); } } }
有没有感觉编写插件其实也很简单,附上所有Flutter代码,下篇介绍Android的插件编写。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。