Flutterで構築する漫画ビューア@katsu
View Slide
自己紹介● 伊藤 克弘● エキサイト株式会社● Android / Flutter エンジニア● 10年ほどAndroidアプリ開発● Flutter歴は1年半ほど2
経緯株式会社講談社様との協業週刊漫画誌「モーニング」の電⼦版アプリ「Dモーニング」を2013年にリリースFlutter化のための技術検証漫画アプリのFlutter化3
前提事項● Flutterバージョンは 3.13.8、Dartバージョンは 3.1.4 を使⽤● モバイルアプリのみを対象とする(ウェブなどは対象外)● ⼀般的な横開きの漫画を対象とする(縦読み漫画などは対象外)● マテリアルデザインで統⼀する環境および制約4
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する5
1. 画像を表⽰する6
1. 画像を表⽰する今回使う画像ファイル● ファイル名: 001.png ~ 010.png● サイズ: 980 x 1,400● 配置場所: /images/使用する画像7
1. 画像を表⽰するビューアの画⾯としてViewerScreenを⽤意する画面の作成8
1. 画像を表⽰するアプリのディレクトリを取得するためにpath_providerを使うプラットフォームごとの各種ディレクトリにアクセスできるファイルの参照pubspec.yaml9
1. 画像を表⽰する画像ファイルを⼀覧で取得するファイルの参照10
1. 画像を表⽰する画像ファイルを⼀覧で取得するファイルの参照アプリケーションディレクトリを取得11
1. 画像を表⽰する画像ファイルを⼀覧で取得するファイルの参照ディレクトリの要素の Listを取得12
1. 画像を表⽰する画像ファイルを⼀覧で取得するファイルの参照1. Fileのみを抽出2. ファイル名で並び替え13
1. 画像を表⽰するFutureBuilderでFutureからウィジェットを構成するウィジェットで画像を参照する14
1. 画像を表⽰するFutureBuilderでFutureからウィジェットを構成するウィジェットで画像を参照するFutureの状態に応じて処理する15
1. 画像を表⽰する画像の表⽰にはImageを使うページ表⽰にはPageViewを使う画像をPageViewで表示する16
1. 画像を表⽰するPageViewの向きを反転する→ reverseをtrueにする画像をPageViewで表示する17
2. 画像を拡⼤する18
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する19
2. 画像を拡⼤する画像の操作にはInteractiveViewerを使う拡大縮小や、拡大状態での移動が可能InteractiveViewerでの操作20
2. 画像を拡⼤するInteractiveViewerのイベントがPageViewと競合する→ 拡⼤したまま横に動かせなくなる拡⼤中はPageViewのスクロールを無効にするPageViewとInteractiveViewerでの問題21
2. 画像を拡⼤するTransformationControllerInteractiveViewerに設定する‧外部からの操作‧現在の状態を取得→ 拡⼤状態を判定するのに使う拡大中の判定を行う22
2. 画像を拡⼤するリスナーで値の変化を検知するgetMaxScaleOnAxisで拡⼤率を取得する拡⼤率が1より⼤きければ拡⼤中とする拡大中の判定を行う23
2. 画像を拡⼤するPageViewのphysicsでスクロールの反応を定義AlwaysScrollableScrollPhysics→ スクロール可NeverScrollableScrollPhysics→ スクロール不可PageViewのページ移動の可否を設定する24
2. 画像を拡⼤するピンチ操作だけではなく、ダブルタップでも拡⼤できるようにするダブルタップを検出するためにGestureDetectorを追加ダブルタップでの操作25
2. 画像を拡⼤するダブルタップでの操作26
2. 画像を拡⼤するダブルタップでの操作タップ位置が必要なのでonDoubleTapDownを使う27
2. 画像を拡⼤するダブルタップでの操作拡大中の場合は初期状態に戻す28
2. 画像を拡⼤するダブルタップでの操作(50, 50)(150, 150)(0, 0)(100, 100)(300, 300)x3.0拡大した分の座標を移動させる29
2. 画像を拡⼤するダブルタップでの操作30
3. 暗号化された画像を使⽤する31
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する32
3. 暗号化された画像を使⽤するAndroidでもiOSでも、アプリ内のファイルにユーザーがアクセスすることはできないただし、ユーザーが端末のルート化やジェイルブレイクなどで強⼒なアクセス権を持った場合はアクセスできてしまう→ コンテンツを暗号化して守ることが必要暗号化の必要性33
3. 暗号化された画像を使⽤するAndroidでもiOSでも、アプリ内のファイルにユーザーがアクセスすることはできないただし、ユーザーが端末のルート化やジェイルブレイクなどで強⼒なアクセス権を持った場合はアクセスできてしまう→ コンテンツを暗号化して守ることが必要ファイルになんらかの暗号化を施したという体で進める。暗号化の必要性34
3. 暗号化された画像を使⽤する今までは画像ファイルをそのまま読み込んで表⽰していた→ ファイルが暗号化されているためそのままでは表⽰できないImageProviderを拡張して画像の読み込みをカスタマイズするImageProviderの拡張35
3. 暗号化された画像を使⽤するImageProviderは画像をどのように処理するのかを定義するクラスいくつかのImageProviderが標準で⽤意されている● FileImage(ファイルの画像)● NetworkImage(ネットワーク上の画像)● MemoryImage(メモリに展開された画像)⾃作のImageProviderを作成して、画像を復号して読み込めるようにするImageProviderの拡張36
3. 暗号化された画像を使⽤する復号用のImageProviderを作るFileImageを参考に共通部分を作る37
3. 暗号化された画像を使⽤する復号用のImageProviderを作る最適化のための比較処理38
3. 暗号化された画像を使⽤する復号用のImageProviderを作る画像の識別のために自身を返す39
3. 暗号化された画像を使⽤する復号用のImageProviderを作る40loadImageで画像読み込みの処理を実装する画像データをCodecに、Codecを画像フレームに変換する
3. 暗号化された画像を使⽤する復号用のImageProviderを作る画像データを展開して復号するCodecに変換する41
3. 暗号化された画像を使⽤する作成したImageProviderをImageに指定する復号用のImageProviderを作る42復号用のImageProviderを作る
4. 全画⾯表⽰に対応する43
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する44
4. 全画⾯表⽰に対応する全画⾯表⽰ = コンテンツ以外のUIを⾮表⽰にして画⾯全体に表⽰する● より⼤きく鮮明に表⽰できる● 余計な要素がなくなり没⼊感が⾼まる通知の確認や画⾯遷移の際にUIが必要になる→ UIの表⽰を切り替えられるようにする全画面表示とは45
4. 全画⾯表⽰に対応するOSが管理しているUI‧ステータスバー‧ナビゲーションバー‧ホームインジケーターシステムUIiOSAndroidステータスバーナビゲーションバー ホームインジケーター46
4. 全画⾯表⽰に対応するコンテンツの⾼さが変わるため表⽰を切り替えた際に位置がズレてしまう→ UIの裏にも表⽰しておくことでコンテンツの⾼さを⼀定にするUIの裏にコンテンツを表示する47
4. 全画⾯表⽰に対応するFlutterではデフォルトで⼀部のシステムUIの裏に表⽰されるAndroidのナビゲーションバーは除くUIの裏にコンテンツを表示するナビゲーションバーの裏には表示されない48
4. 全画⾯表⽰に対応するSystemChromeでSystemUiModeを切り替えてシステムUIを管理するSystemUiMode● edgeToEdge● leanBack● immersive● immersiveSticky● manualUIの裏にコンテンツを表示する49
4. 全画⾯表⽰に対応するSystemChromeでSystemUiModeを切り替えてシステムUIを管理するSystemUiMode● edgeToEdge● leanBack● immersive● immersiveSticky● manualUIの裏にコンテンツを表示する後ほど説明50
4. 全画⾯表⽰に対応するedgeToEdgeシステムUIの裏にコンテンツを表⽰するビューア画⾯の最初に設定しておくUIの裏にコンテンツを表示する51
4. 全画⾯表⽰に対応するAppBarの裏にコンテンツを表⽰するScaffoldのextendBodyBehindAppBarをtrueにするUIの裏にコンテンツを表示する52
4. 全画⾯表⽰に対応する画⾯をタップでUIの表⽰を切り替えるUIが表⽰されている → ⾮表⽰にUIが表⽰されていない → 表⽰にUIの表示状態を切り替える53
4. 全画⾯表⽰に対応するUIの表示状態を切り替える表示状態の変更時にフラグを更新54現在の表示状態を自前で管理
4. 全画⾯表⽰に対応するUIの表示状態を切り替えるGestureDetectorにタップ時の処理を追加55
4. 全画⾯表⽰に対応するUIの表示状態を切り替えるシステムUIを非表示にするシステムUIを表示する56
4. 全画⾯表⽰に対応する● edgeToEdge● leanBack● immersive● immersiveSticky● manualSystemUiMode57
4. 全画⾯表⽰に対応するSystemUiModeedgeToEdge leanBackimmersiveimmersiveStickymanual表示にする 非表示にする 手動で設定する58
4. 全画⾯表⽰に対応するSystemUiModeedgeToEdge leanBackimmersiveimmersiveStickymanual表示にする 非表示にする パラメータで決まるiOSでは全て同じ挙動59
4. 全画⾯表⽰に対応するedgeToEdge● 全てのシステムUIが表⽰される● コンテンツはシステムUIの裏にも表⽰されるSystemUiMode60
4. 全画⾯表⽰に対応するleanBack● 全てのシステムUIが⾮表⽰になる● 画⾯をタップするとシステムUIが表⽰される動画再⽣など、基本的に画⾯を触らない場合に有効SystemUiMode61
4. 全画⾯表⽰に対応するimmersive● 全てのシステムUIが⾮表⽰になる● 端をスワイプするとシステムUIが表⽰されるギャラリーなど、⼤まかな操作をする場合に有効SystemUiMode62
4. 全画⾯表⽰に対応するimmersiveSticky● 全てのシステムUIが⾮表⽰になる● 端をスワイプするとシステムUIが表⽰される● ⼀定時間経過で再び⾮表⽰になる● スワイプのイベントをアプリも受け取れるお絵かきなど、画⾯全体を操作する場合に有効SystemUiMode63
4. 全画⾯表⽰に対応するmanual● 表⽰するシステムUIを指定する● システムUIは上下に分類されるどちらも省略した場合はleanBackと同様SystemUiMode64
4. 全画⾯表⽰に対応するAppBarの表⽰状態表⽰する場合はAppBarを、⾮表⽰にする場合はnullを設定する。UIの表示状態を切り替える65
4. 全画⾯表⽰に対応する動作イメージ66
5. ページを操作する67
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する68
5. ページを操作するPageViewはスワイプでページ移動ができるが、それ以外の操作もサポートしていると操作性が⾼まる● タップでの簡単なページ移動● スライダーでの素早いページ移動これらの操作に対応するスワイプ以外でのページ操作69
5. ページを操作するPageControllerPageViewを外部から操作するコントローラー経由でページ移動を⾏うページ操作の基本70
5. ページを操作するページ移動には下記のメソッドが使える● jumpTo● jumpToPage● animateTo● animateToPage● nextPage● previousPagePageControllerの操作71
5. ページを操作するページ移動には下記のメソッドが使える● jumpTo● jumpToPage● animateTo● animateToPage● nextPage● previousPagePageControllerの操作72指定したページに瞬時に移動するオフセットもしくはインデックスを指定する
5. ページを操作するページ移動には下記のメソッドが使える● jumpTo● jumpToPage● animateTo● animateToPage● nextPage● previousPagePageControllerの操作73指定したページにアニメーションで移動するオフセットもしくはインデックスを指定する
5. ページを操作するページ移動には下記のメソッドが使える● jumpTo● jumpToPage● animateTo● animateToPage● nextPage● previousPagePageControllerの操作74隣のページにアニメーションで移動する内部ではanimateToPageが使われている
5. ページを操作するタップでページ移動ができるようにする左側の25%の範囲をタップしたら次のページに移動右側の25%の範囲をタップしたら前のページに移動タップでのページ操作75
5. ページを操作するタップでのページ操作76
5. ページを操作するタップでのページ操作タップ位置を知るためにonTapUpを使う77
5. ページを操作するタップでのページ操作横軸のタップ位置を取得ページ移動判定の閾値を算出78
5. ページを操作するタップでのページ操作左右のタップ時に隣のページに移動する79
5. ページを操作するタップでのページ操作UIの表示切替のタップイベントを統合する80
5. ページを操作するタップでのページ操作81
5. ページを操作するSlider : ⼀定の範囲から値を選択するウィジェット下記の要件を実装する‧画⾯下部にスライダーを設置‧ページ数を範囲とする‧選択したページに移動するスライダーでのページ操作82
5. ページを操作するスライダーでのページ操作83・Stackでコンテンツに重ねて表示する・SafeAreaでシステムUIを避ける・UIの表示フラグに合わせるSafeAreaなしSafeAreaあり
5. ページを操作するスライダーでのページ操作84選択中のページを変数で保持するページの変更時に変数を更新する
5. ページを操作するスライダーと選択中のページを紐付けるスライダーでのページ操作85
5. ページを操作するスライダーと選択中のページを紐付けるスライダーでのページ操作86値の範囲は0.0 ~ 1.0選択中のページ/ページ数
5. ページを操作するスライダーと選択中のページを紐付けるスライダーでのページ操作87値 x ページ数対象のページに移動
5. ページを操作するスライダーを反転させるPageViewのような単純な⽅法はない計算でページのインデックスを逆順にする→ ロジックが少し複雑になるスライダーでのページ操作88
5. ページを操作するスライダーを反転させるPageViewのような単純な⽅法はない計算でページのインデックスを逆順にする→ ロジックが少し複雑になるウィジェット⾃体を反転させるスライダーでのページ操作89
5. ページを操作するTransform.flipウィジェットの中⼼を軸に反転させるflipXを指定すると⽔平⽅向に反転スライダーでのページ操作90
5. ページを操作するスライダーでのページ操作91
6. ⾒開きページを表⽰する92
目次1. 画像を表示する2. 画像を拡大する3. 暗号化された画像を使用する4. 全画面表示に対応する5. ページを操作する6. 見開きページを表示する93
6. ⾒開きページを表⽰する⾒開きページは向かい合う左右のページで構成される漫画は⾒開き⽤に構成されていることが多い→ 可能な場合はアプリでも⾒開きで表⽰する見開きページとは94
6. ⾒開きページを表⽰するOrientationBuilderで親ウィジェットの向きを取得する横向きの場合は⾒開き表⽰にする見開き表示の判定95
6. ⾒開きページを表⽰するページ数の管理96画面あたりのページ数が変わるため調整する
6. ⾒開きページを表⽰するページ数の管理97見開きの場合はページ数の最大値を半分として扱う半分にしたページ数をPageViewとSliderに反映する
6. ⾒開きページを表⽰するページ数の管理98選択中のページは1ページ表示を基準とする基準の値をSliderの現在値に反映する
6. ⾒開きページを表⽰する表⽰が切り替わった際にページがズレてしまうページ数の管理99index == 2
6. ⾒開きページを表⽰するページ数の管理100表示が切り替わった際にページを移動させる
6. ⾒開きページを表⽰するページ数の管理101直前の向きを保持しておき切り替わったかどうかを判定
6. ⾒開きページを表⽰するページ数の管理102表示が切り替わった場合は対応したページに移動させる
6. ⾒開きページを表⽰する表⽰が切り替わった際に正しいページが表⽰されるページ数の管理103
6. ⾒開きページを表⽰する2枚の画像を1枚に連結する画像の連結104
6. ⾒開きページを表⽰する画像の連結105画像を連結するためにImageProviderを変更
6. ⾒開きページを表⽰する画像の連結106名前付きコンストラクタで画像の指定を追加
6. ⾒開きページを表⽰する画像の連結107比較処理をListに対応
6. ⾒開きページを表⽰する画像を復号した後に連結する画像の連結108
6. ⾒開きページを表⽰する画像処理のライブラリを使⽤して画像を連結する画像の連結109pubspec.yaml
6. ⾒開きページを表⽰する画像処理のライブラリを使⽤して画像を連結する画像の連結110画像をImage型にデコードする
6. ⾒開きページを表⽰する画像処理のライブラリを使⽤して画像を連結する画像の連結111出力先の画像の大きさを決定する
6. ⾒開きページを表⽰する画像処理のライブラリを使⽤して画像を連結する画像の連結112画像の数だけ順番に連結する
6. ⾒開きページを表⽰する変更したImageProviderを使⽤する画像の連結113
6. ⾒開きページを表⽰する画像連結の処理が重いのでページ移動の際に画⾯が固まってしまう画像の連結114
Futureで⾮同期処理にしても同⼀のスレッドで実⾏される→ 重い処理を実⾏するとメインスレッドがブロックされる重い処理は並列で実⾏する必要がある6. ⾒開きページを表⽰する並列処理の実行115
6. ⾒開きページを表⽰するIsolateDartで並列処理を⾏うための仕組み● メインスレッドも⼀つのIsolateである● Isolate毎に異なるイベントループが実⾏される● Isolate間は分断されている→ 別のIsolateで処理を実⾏すれば画⾯が固まることはない並列処理の実行116
6. ⾒開きページを表⽰するIsolateを使う● Isolate.spawn:低レイヤーでの操作で、⾊々と⾃前で管理する必要がある● compute:Flutterの拡張機能で、Isolateをシンプルに扱える● Isolate.run:Dart 2.19で追加され、computeと同様にIsolateをシンプルに扱える→ 今回は⼀番シンプルなIsolate.runを使う並列処理の実行117
6. ⾒開きページを表⽰する処理全体をIsolate.runのブロックに含める並列処理の実行118
6. ⾒開きページを表⽰する画⾯が固まることがなくなる並列処理の実行119
最後に120
最後に今回実装したのは最低限の基本的な機能→ 改善できる点はたくさんある● OSバージョンごとの最適化● 読み込みの最適化、キャッシュ処理● 操作の効率化● 動作のアニメーション● etc.さらなる改善121
最後に今回実装したのは最低限の基本的な機能→ 改善できる点はたくさんある● OSバージョンごとの最適化● 読み込みの最適化、キャッシュ処理● 操作の効率化● 動作のアニメーション● etc.さらなる改善122最高のサービスを目指して!
123