當團隊準備着手作 APP 時,咱們把目標對準了 Flutter,尤爲近期 Flutter 的使用熱度一直不斷攀升。因爲第一次使用 Flutter,就想經過本身的實踐去提高本身的能力。java
在作 APP 時,咱們用到了視頻播放器,當前使用官方提供的插件「video_player」https://github.com/flutter/plugins/tree/master/packages/video_player,可能該插件在國外沒什麼問題,但國內不少視頻播放器作的很精良,自定義功能很齊全。android
舉一個例子:國內的 APP 全屏播放視頻時,幾乎都是橫向全屏的,但官方提供的插件在 iOS 端是豎向直播的,效果很很差。git
所以萌生了本身想作一個視頻播放插件:github
要求緩存
- Android 和 iOS 端都是使用原生開發,體驗效果好;
- 儘量使用 GitHub Star 靠前的第三方開源插件,減輕本身的開發工做量;
根據以上的「2」要求,我主要找到了 lipangit/JiaoZiVideoPlayer
和 newyjp/JPVideoPlayer
app
好了,全部鋪墊都作好了,咱們開始一步步實現插件開發吧~async
1. 建立插件ide
flutter create --org com.***.test --template=plugin bms_video_player
2. 建立關聯類函數
在 lib/bms_video_player.dart
文件中建立 BmsVideoPlayerController
類,用於和原生代碼關聯:佈局
class BmsVideoPlayerController { MethodChannel _channel; BmsVideoPlayerController.init(int id) { _channel = new MethodChannel('bms_video_player_$id'); } Future<void> loadUrl(String url) async { assert(url != null); return _channel.invokeMethod('loadUrl', url); } }
這裏存在的 MethodChannel
有待於下一次好好研究研究。
3. 建立 Callback
typedef void BmsVideoPlayerCreatedCallback(BmsVideoPlayerController controller);
4. 建立 Widget 佈局
建立 Widget,用於添加原生布局:
class BmsVideoPlayer extends StatefulWidget { final BmsVideoPlayerCreatedCallback onCreated; final x; final y; final width; final height; BmsVideoPlayer({ Key key, @required this.onCreated, @required this.x, @required this.y, @required this.width, @required this.height, }); @override State<StatefulWidget> createState() => _VideoPlayerState(); } class _VideoPlayerState extends State<BmsVideoPlayer> { @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, child: nativeView(), onHorizontalDragStart: (DragStartDetails details) { print("onHorizontalDragStart: ${details.globalPosition}"); // if (!controller.value.initialized) { // return; // } // _controllerWasPlaying = controller.value.isPlaying; // if (_controllerWasPlaying) { // controller.pause(); // } }, onHorizontalDragUpdate: (DragUpdateDetails details) { print("onHorizontalDragUpdate: ${details.globalPosition}"); print(details.globalPosition); // if (!controller.value.initialized) { // return; // } // seekToRelativePosition(details.globalPosition); }, onHorizontalDragEnd: (DragEndDetails details) { print("onHorizontalDragEnd"); // if (_controllerWasPlaying) { // controller.play(); // } }, onTapDown: (TapDownDetails details) { print("onTapDown: ${details.globalPosition}"); }, ); } nativeView() { if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.bms_video_player/view', onPlatformViewCreated: onPlatformViewCreated, creationParams: <String,dynamic>{ "x": widget.x, "y": widget.y, "width": widget.width, "height": widget.height, }, creationParamsCodec: const StandardMessageCodec(), ); } else { return UiKitView( viewType: 'plugins.bms_video_player/view', onPlatformViewCreated: onPlatformViewCreated, creationParams: <String,dynamic>{ "x": widget.x, "y": widget.y, "width": widget.width, "height": widget.height, }, creationParamsCodec: const StandardMessageCodec(), ); } } Future<void> onPlatformViewCreated(id) async { if (widget.onCreated == null) { return; } widget.onCreated(new BmsVideoPlayerController.init(id)); } }
這裏的 AndroidView
和 UiKitView
字如其意,不一樣的系統使用不一樣的 widget。
其中,AndroidView
和 UiKitView
都自帶幾個參數,如:
onPlatformViewCreated
);下面開始,根據 iOS 和 Android 分別註冊插件和實現功能,首先是 Android。
5.1 註冊 ViewFactory
在 BmsVideoPlayerPlugin
類中註冊 ViewFactory
:new VideoViewFactory(registrar)
,並命名爲 「plugins.bms_video_player/view」:
public static void registerWith(Registrar registrar) { registrar.platformViewRegistry() .registerViewFactory("plugins.bms_video_player/view", new VideoViewFactory(registrar)); }
5.2 建立 VideoViewFactory
該 VideoViewFactory
類須要集成類 PlatformViewFactory
,實現函數:create(Context context, int viewId, Object args)
:
public class VideoViewFactory extends PlatformViewFactory { private final Registrar registrar; public VideoViewFactory(Registrar registrar) { super(StandardMessageCodec.INSTANCE); this.registrar = registrar; } @Override public PlatformView create(Context context, int viewId, Object args) { return new VideoView(context, viewId, args, this.registrar); } }
開始咱們的正餐了,建立實現類 VideoView
。
5.3 VideoView
public class VideoView implements PlatformView, MethodCallHandler { private final JzvdStd jzvdStd; private final MethodChannel methodChannel; private final Registrar registrar; VideoView(Context context, int viewId, Object args, Registrar registrar) { this.registrar = registrar; this.jzvdStd = getJzvStd(registrar, args); this.methodChannel = new MethodChannel(registrar.messenger(), "bms_video_player_" + viewId); this.methodChannel.setMethodCallHandler(this); } @Override public View getView() { return jzvdStd; } @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { switch (methodCall.method) { case "loadUrl": String url = methodCall.arguments.toString(); jzvdStd.setUp(url, "", Jzvd.SCREEN_NORMAL); break; default: result.notImplemented(); } } @Override public void dispose() {} private JzvdStd getJzvStd(Registrar registrar, Object args) { JzvdStd view = (JzvdStd) LayoutInflater.from(registrar.activity()).inflate(R.layout.jz_video, null); return view; } }
直接分析代碼:
return
原生 View,也就是咱們使用的第三方插件:JzvdStd。第二個接口「MethodCallHandler」,用於處理從 Dart 發過來的請求函數,如本文建立的函數:loadUrl
return
的 JzvdStd
,使用 xml:<?xml version="1.0" encoding="utf-8"?> <cn.jzvd.JzvdStd xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/jz_video" android:layout_width="match_parent" android:layout_height="match_parent" />
5.4 引入第三方插件
固然,咱們須要在 build.gradle
最後加入插件:
dependencies { implementation 'cn.jzvd:jiaozivideoplayer:7.0_preview' }
至此,咱們的 Android 端就算完成了,接下來看看 iOS 端。
6.1 註冊 ViewFactory
一樣的,在類 BmsVideoPlayerPlugin
中註冊 VideoViewFactory
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { VideoViewFactory* factory = [[VideoViewFactory alloc] initWithMessenger:registrar.messenger]; [registrar registerViewFactory:factory withId:@"plugins.bms_video_player/view"]; }
6.2 建立 VideoViewFactory
#import "VideoViewFactory.h" #import "BMSVideoPlayerViewController.h" @implementation VideoViewFactory { NSObject<FlutterBinaryMessenger>* _messenger; } - (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger { self = [super init]; if (self) { _messenger = messenger; } return self; } - (NSObject<FlutterMessageCodec>*)createArgsCodec { return [FlutterStandardMessageCodec sharedInstance]; } - (nonnull NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args { BMSVideoPlayerViewController* viewController = [[BMSVideoPlayerViewController alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger]; return viewController; } @end
代碼仍是很簡單,重點往下看 BMSVideoPlayerViewController
6.3 BMSVideoPlayerViewController
#import "BMSVideoPlayerViewController.h" #import <JPVideoPlayer/JPVideoPlayerKit.h> @interface BMSVideoPlayerViewController ()<JPVideoPlayerDelegate> @end @implementation BMSVideoPlayerViewController { UIView * _videoView; int64_t _viewId; FlutterMethodChannel* _channel; } #pragma mark - life cycle - (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger { if ([super init]) { _viewId = viewId; _videoView = [UIView new]; _videoView.backgroundColor = [UIColor greenColor]; NSDictionary *dic = args; CGFloat x = [dic[@"x"] floatValue]; CGFloat y = [dic[@"y"] floatValue]; CGFloat width = [dic[@"width"] floatValue]; CGFloat height = [dic[@"height"] floatValue]; _videoView.frame = CGRectMake(x, y, width, height); _videoView.jp_videoPlayerDelegate = self; NSString* channelName = [NSString stringWithFormat:@"bms_video_player_%lld", viewId]; _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; __weak __typeof__(self) weakSelf = self; [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { [weakSelf onMethodCall:call result:result]; }]; } return self; } - (nonnull UIView *)view { return _videoView; } - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([[call method] isEqualToString:@"loadUrl"]) { [self onLoadUrl:call result:result]; } else { result(FlutterMethodNotImplemented); } } - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { NSString* url = [call arguments]; if (![self loadUrl:url]) { result([FlutterError errorWithCode:@"loadUrl_failed" message:@"Failed parsing the URL" details:[NSString stringWithFormat:@"URL was: '%@'", url]]); } else { result(nil); } } - (bool)loadUrl:(NSString*)url { NSURL* nsUrl = [NSURL URLWithString:url]; if (!nsUrl) { return false; } [_videoView jp_playVideoWithURL:nsUrl bufferingIndicator:nil controlView:nil progressView:nil configuration:^(UIView *view, JPVideoPlayerModel *playerModel) { // self.muteSwitch.on = ![self.videoContainer jp_muted]; }]; return true; } #pragma mark - JPVideoPlayerDelegate - (BOOL)shouldAutoReplayForURL:(nonnull NSURL *)videoURL { return true; } @end
其實,代碼實現都很簡單,惟一和 Android 端不同的就是控件的建立不同,Android 的我直接用 xml,iOS 的主要是須要定義 Frame 大小,我嘗試使用函數傳遞的 frame 值,貌似無論用。若是有人知道問題所在,歡迎告知我!
最後,和 Android 同樣,引入咱們使用的第三方插件:
6.4 引入 JPVideoPlayer
在文件 bms_video_player.podspec
引入:
s.dependency 'JPVideoPlayer'
7. 連接調用
看「4」的建立 widget 後的回調函數:
Future<void> onPlatformViewCreated(id) async { if (widget.onCreated == null) { return; } widget.onCreated(new BmsVideoPlayerController.init(id)); }
直接 new BmsVideoPlayerController.init(id)
,即建立了 channel
:
MethodChannel _channel; BmsVideoPlayerController.init(int id) { _channel = new MethodChannel('bms_video_player_$id'); } Future<void> loadUrl(String url) async { assert(url != null); return _channel.invokeMethod('loadUrl', url); }
有了 channel
天然和原生代碼串聯起來了,同時建立 loadUrl
函數供外界調用。
8. 測試使用
藉此,咱們的插件實現了基本功能了,寫個 demo,測試下效果:
import 'package:flutter/material.dart'; import 'package:bms_video_player/bms_video_player.dart'; void main() => runApp(MyApp()); class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { var viewPlayerController; @override void initState() { super.initState(); } @override Widget build(BuildContext context) { var x = 0.0; var y = 0.0; var width = 400.0; var height = width * 9.0 / 16.0; BmsVideoPlayer videoPlayer = new BmsVideoPlayer( onCreated: onViewPlayerCreated, x: x, y: y, width: width, height: height ); return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text('Plugin example app'), ), body: Container( child: videoPlayer, width: width, height: height ) ), ); } void onViewPlayerCreated(viewPlayerController) { this.viewPlayerController = viewPlayerController; this.viewPlayerController.loadUrl("https://www.****.com/****.mp4"); } }
相信這代碼不用多解釋了,引入咱們的插件 widget,而後調用 loadUrl
函數,傳入咱們的視頻連接,便可開始播放了。
iOS 效果
Android 效果
第一次使用 Flutter,第一次實現基本的插件功能,寫的比較粗糙,但相信基本的寫法都在裏面了。接下來就是實現播放視頻的全部功能,如:暫停/播放,小窗口播放、全屏播放、緩存、靜音等。
還有,就是如何實現 Dart 和原生代碼進行通信的。
未完待續,敬請期待