32.9K Views
May 22, 21
スライド概要
【オンライン開催】Flutter Meetup Osaka #7
https://flutter-jp.connpass.com/event/210708/
で発表した件のスライドになります。
Android Engineer at Kyash Inc.
Flutter Webに HTML要素を貼る 株式会社リサーチ・アンド・イノベーション 高田 晴彦 1
自己紹介 ● ● 高田 晴彦 @tfandkusu ○ ● ● ● Qiita、Twitter、Zenn、 GitHub、Note 株式会社リサーチ・アンド・ イノベーション Androidエンジニア スプラトゥーンが好き 2
今回は個人開発の話 スプラトゥーン2のプレイヤー向けに便利な機能を搭載した動画プレイヤーアプリをFlutter Webで開発しました。 ● ● 重要なシーンを AIで頭出し スロー再生 気になる人向けリンク スプラトゥーン2のプレイ動画から、やられたシーンだけ をディープラーニングで自動抽出する(Qiita) スプラトゥーン2の録画ファイルからバトル開始時間を自 動抽出してクリップボードにコピーする(Note) 3
HTMLのvideo要素を貼っている 4
video要素の上にダイアログが表示 5
開発環境 ● ● ● Flutter 2.0.6 Null Safety対応 hooks_riverpod 0.14.0+3 ○ StateNotifierProviderの仕様が0.13.0から変更されている 6
video要素を貼る #1 import 'dart:ui' as ui; ///runAppメソッドから呼ばれる最初のWidget class IkutApp extends StatelessWidget { @override Widget build(BuildContext context) { // video要素を作成 ui.platformViewRegistry.registerViewFactory('video', (viewId) { // シングルトンなvideo要素を作成するメソッド(次のページ) // ちなみにこの中はshadow DOMです。 return getVideoElement(context); }); return MaterialApp( /* 略 */); } } Android StudioではplatformViewRegistryが 赤く表示されてしまうが、ビルドは可能 7
video要素を貼る #2 // HTML要素を使うときは、dart:htmlライブラリをインポートする // Web以外にはビルドできなくなる import 'dart:html'; /// 1つしか無いvideo要素 late VideoElement _ikutVideoElement ; /// 作成されたフラグ bool _ikutVideoElementCreated = false; /// 1つしかないvideo要素を取得する VideoElement getVideoElement(BuildContext context) { if (!_ikutVideoElementCreated) { // video要素を作成する final videoElement = VideoElement(); // シークバーなどのコントロールを表示する(あとで問題になります) videoElement.controls = true; // videoElement.addEventListener でVideo要素のコールバックから // RiverpodのStateNotiferの更新を行い、各Widgetの更新を行っているが省略 _ikutVideoElement = videoElement; _ikutVideoElementCreated = true; } return _ikutVideoElement; } 8
video要素をシングルトンにした理由 横幅の変更でレイアウトが変わった場 合でも、同じvideo要素を張り直して動 画の再生を継続するため。 右の動画が再生出来ない場合は、こちらのQiita記事で見 てください。 Flutterで一定時間後にアニメーションを開始する 「useEffectを使う理由」節 9
video要素を貼る #3 貼りたいところにHtmlElementViewウィジットを設置する。 viewType引数をregisterViewFactoryで指定した物と同じにする。 大きさの制限を付けないと、エラーになる。 Column( children: [ SizedBox( width: 640, height: 360, child: HtmlElementView(viewType: 'video')) ] ) 10
問題あり ダイアログへのクリックがvideo要素に奪われる 「破棄する」ボタンを押したが video要素のシークバーを押したことになって、 動画の再生位置が変わり破棄するボタンが押 せない。 11
コントロールをFlutter側で自作すれば解決 // video要素を作成する final videoElement = VideoElement(); // コントロールは表示しない(デフォルトでは非表示) videoElement.controls = false; クリックできないvideo要素ならばクリックを奪わない Flutter側でシークバーやボリューム調整を実装 12
video要素以外でもこの現象は発生する クリックできるiframeのケースもありえる。 ツイート埋め込みの例 https://github.com/tfandkusu/flutter_web_embed_twitter_sample 例:広告SDK、SNSの埋め込み HTML要素にクリックを奪われなくする方法を 解説 13
pointer_interceptorパッケージを使う クリックをHTML要素に奪われることを防ぎ Flutter Widgetにクリックイベントを届ける https://pub.dev/packages/pointer_interceptor HtmlElementView上のWidgetを PointerInterceptor Widgetで囲んで使う PointerInterceptor( child: ElevatedButton( /* 略 */) ) クリックできる場合 14
HTML要素の上にダイアログを表示する方法 #1 showDialogメソッドのrouteSettings引数で名前を設定する。 showDialog( context: context, routeSettings: RouteSettings(name: "dialog"), builder: (_) => AlertDialog( title: Text('情報'), content: Text('ブックマークしました'), actions: [ TextButton( onPressed: () { // OKボタンでダイアログを閉じる Navigator.of(context).pop(); }, child: Text('OK')) ], )); 15
HTML要素の上にダイアログを表示する方法 #2 ダイアログ開閉状態を持つ、RiverpodのStateNotifierとそのProviderを作成する class MainDialogOpenStateNotifier extends StateNotifier<bool> { MainDialogOpenStateNotifier() : super(false); /// ダイアログが開かれたときに呼ばれる void onOpen() { state = true; } /// ダイアログが閉じられたときに呼ばれる void onClose() { state = false; } } final mainDialogOpenStateNotifierProvider = StateNotifierProvider<MainDialogOpenStateNotifier, bool>((_) { return MainDialogOpenStateNotifier(); }); 16
HTML要素の上にダイアログを表示する方法 #3 NavigatorObserverから先ほどのStateNotiferのメソッドを呼ぶ class MainNavigatorObserver extends NavigatorObserver { BuildContext _context; MainNavigatorObserver(this._context); @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { final name = route.settings.name; if (name == 'dialog') { // ダイアログが開かれた final mainDialogOpenStateNotifier = _context.read(mainDialogOpenStateNotifierProvider .notifier); mainDialogOpenStateNotifier.onOpen(); } } // スペースの都合で、ダイアログが閉じられたときの処理は省略 // 必要な人はこちらで確認してください。 // https://github.com/tfandkusu/flutter_web_embed_twitter_sample/blob/main/lib/main_navigator_observer.dart } 17
HTML要素の上にダイアログを表示する方法 #4 HTML要素をすべて覆うように、ダイアログが表示されているときだけ PointerInterceptor Widgetを設置する // ダイアログ開閉状態をRiverpodのStateNotiferProviderから取得する final isDialogOpen = useProvider(mainDialogOpenStateNotifierProvider ); Stack(children: [ HtmlElementView(viewType: 'video'), Visibility( visible: isDialogOpen, child: Positioned.fill( child: PointerInterceptor(child: Container()))) ]) PointerInterceptor (ダイアログ表示時のみ設置 ) HtmlElementView これで対策完了 18
【補足】ありそうな質問 #1 ● なぜFlutter Webを使ったのか ○ ○ ○ ○ ○ ● スキルセットの問題 Androidエンジニアなのでマテリアルデザインの知識がある CSSよく分からない Flutter WebならばCSSを1行も書かずにマテリアルデザインの 知識で適切な UI/UXを実装できる 学習コストが低い Flutter Webを使うことの問題点 ○ ○ HTML rendererを使うとレイアウトが崩れる ■ テキストボタンのマウスオーバーなど CanvasKit rendererを使うとページを開いたときに一瞬お豆腐 が表示される ■ こっちを選択 ■ お豆腐は妥協 19
【補足】ありそうな質問 #2 ● 動画を再生したいならば、video_playerパッケージでも良かったのではないか ○ ○ ○ 実は作ってから存在に気がついた 再生位置をコールバックする仕組みが無い まだこのアプリについては HtmlElementViewを無くすことはできない 20
まとめ ● ● ● ● ● Flutter WebにはHtmlElementViewを使うことでHTML要素を貼ることができる HTML要素はDartで作成することができる HTML要素がクリックできる場合は、手前のFlutter Widgetに対するクリックを奪わ れるのでPointerInterceptor Widgetでブロックする ダイアログがあるときはNavigatorObserverとStateNotifierProviderを組み合わ せる 今回の内容はTwitterの埋め込みの上にFlutterのボタンやダイアログを表示する こちらのサンプルで確認できる ○ https://github.com/tfandkusu/flutter_web_embed_twitter_sample 21