Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Flutterで構築する漫画ビューア / FlutterKaigi 2023
Search
katsu
November 10, 2023
Programming
1
4.3k
Flutterで構築する漫画ビューア / FlutterKaigi 2023
katsu
November 10, 2023
Tweet
Share
Other Decks in Programming
See All in Programming
Monixと常駐プログラムの勘どころ / Scalaわいわい勉強会 #4
stoneream
0
270
アクターシステムに頼らずEvent Sourcingする方法について
j5ik2o
4
180
Stackless и stackful? Корутины и асинхронность в Go
lamodatech
0
640
Fibonacci Function Gallery - Part 1
philipschwarz
PRO
0
200
プロダクトの品質に コミットする / Commit to Product Quality
pekepek
2
770
CSC305 Lecture 25
javiergs
PRO
0
130
短期間での新規プロダクト開発における「コスパの良い」Goのテスト戦略」 / kamakura.go
n3xem
2
170
競技プログラミングへのお誘い@阪大BOOSTセミナー
kotamanegi
0
350
103 Early Hints
sugi_0000
1
220
Discord Bot with AI -for English learners-
xin9le
1
120
[JAWS-UG横浜 #76] イケてるアップデートを宇宙いち早く紹介するよ!
maroon1st
0
450
数十万行のプロジェクトを Scala 2から3に完全移行した
xuwei_k
0
260
Featured
See All Featured
How GitHub (no longer) Works
holman
311
140k
Large-scale JavaScript Application Architecture
addyosmani
510
110k
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
Why Our Code Smells
bkeepers
PRO
335
57k
Bootstrapping a Software Product
garrettdimon
PRO
305
110k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
330
21k
BBQ
matthewcrist
85
9.4k
The Cult of Friendly URLs
andyhume
78
6.1k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
44
9.3k
Designing for Performance
lara
604
68k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
2
290
Intergalactic Javascript Robots from Outer Space
tanoku
270
27k
Transcript
Flutterで構築する漫画ビューア @katsu
自己紹介 • 伊藤 克弘 • エキサイト株式会社 • 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.yaml 9
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. 画像を拡⼤する TransformationController InteractiveViewerに設定する ‧外部からの操作 ‧現在の状態を取得 → 拡⼤状態を判定するのに使う 拡大中の判定を行う 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を作る 40 loadImageで 画像読み込みの処理を実装する 画像データを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 ‧ステータスバー ‧ナビゲーションバー ‧ホームインジケーター システムUI iOS Android ステータスバー
ナビゲーションバー ホームインジケーター 46
4. 全画⾯表⽰に対応する コンテンツの⾼さが変わるため 表⽰を切り替えた際に位置がズレてしまう → UIの裏にも表⽰しておくことで コンテンツの⾼さを⼀定にする UIの裏にコンテンツを表示する 47
4. 全画⾯表⽰に対応する Flutterではデフォルトで⼀部のシステムUIの裏に表⽰される Androidのナビゲーションバーは除く UIの裏にコンテンツを表示する ナビゲーションバーの裏には 表示されない 48
4. 全画⾯表⽰に対応する SystemChromeでSystemUiModeを切り替えてシステムUIを管理する SystemUiMode • edgeToEdge • leanBack • immersive
• immersiveSticky • manual UIの裏にコンテンツを表示する 49
4. 全画⾯表⽰に対応する SystemChromeでSystemUiModeを切り替えてシステムUIを管理する SystemUiMode • edgeToEdge • leanBack • immersive
• immersiveSticky • manual UIの裏にコンテンツを表示する 後ほど説明 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
• manual SystemUiMode 57
4. 全画⾯表⽰に対応する SystemUiMode edgeToEdge leanBack immersive immersiveSticky manual 表示にする 非表示にする
手動で設定する 58
4. 全画⾯表⽰に対応する SystemUiMode edgeToEdge leanBack immersive immersiveSticky manual 表示にする 非表示にする
パラメータで決まる iOSでは全て同じ挙動 59
4. 全画⾯表⽰に対応する edgeToEdge • 全てのシステムUIが表⽰される • コンテンツはシステムUIの裏にも表⽰される SystemUiMode 60
4. 全画⾯表⽰に対応する leanBack • 全てのシステムUIが⾮表⽰になる • 画⾯をタップするとシステムUIが表⽰される 動画再⽣など、基本的に画⾯を触らない場合に有効 SystemUiMode 61
4. 全画⾯表⽰に対応する immersive • 全てのシステムUIが⾮表⽰になる • 端をスワイプするとシステムUIが表⽰される ギャラリーなど、⼤まかな操作をする場合に有効 SystemUiMode 62
4. 全画⾯表⽰に対応する immersiveSticky • 全てのシステムUIが⾮表⽰になる • 端をスワイプするとシステムUIが表⽰される • ⼀定時間経過で再び⾮表⽰になる •
スワイプのイベントをアプリも受け取れる お絵かきなど、画⾯全体を操作する場合に有効 SystemUiMode 63
4. 全画⾯表⽰に対応する manual • 表⽰するシステムUIを指定する • システムUIは上下に分類される どちらも省略した場合はleanBackと同様 SystemUiMode 64
4. 全画⾯表⽰に対応する AppBarの表⽰状態 表⽰する場合はAppBarを、⾮表⽰にする場合はnullを設定する。 UIの表示状態を切り替える 65
4. 全画⾯表⽰に対応する 動作イメージ 66
5. ページを操作する 67
目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5.
ページを操作する 6. 見開きページを表示する 68
5. ページを操作する PageViewはスワイプでページ移動ができるが、 それ以外の操作もサポートしていると操作性が⾼まる • タップでの簡単なページ移動 • スライダーでの素早いページ移動 これらの操作に対応する スワイプ以外でのページ操作
69
5. ページを操作する PageController PageViewを外部から操作する コントローラー経由でページ移動を⾏う ページ操作の基本 70
5. ページを操作する ページ移動には下記のメソッドが使える • jumpTo • jumpToPage • animateTo •
animateToPage • nextPage • previousPage PageControllerの操作 71
5. ページを操作する ページ移動には下記のメソッドが使える • jumpTo • jumpToPage • animateTo •
animateToPage • nextPage • previousPage PageControllerの操作 72 指定したページに瞬時に移動する オフセットもしくはインデックスを指定する
5. ページを操作する ページ移動には下記のメソッドが使える • jumpTo • jumpToPage • animateTo •
animateToPage • nextPage • previousPage PageControllerの操作 73 指定したページにアニメーションで移動する オフセットもしくはインデックスを指定する
5. ページを操作する ページ移動には下記のメソッドが使える • jumpTo • jumpToPage • animateTo •
animateToPage • nextPage • previousPage PageControllerの操作 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. ⾒開きページを表⽰する 表⽰が切り替わった際にページがズレてしまう ページ数の管理 99 index == 2
6. ⾒開きページを表⽰する ページ数の管理 100 表示が切り替わった際に ページを移動させる
6. ⾒開きページを表⽰する ページ数の管理 101 直前の向きを保持しておき 切り替わったかどうかを判定
6. ⾒開きページを表⽰する ページ数の管理 102 表示が切り替わった場合は 対応したページに移動させる
6. ⾒開きページを表⽰する 表⽰が切り替わった際に正しいページが表⽰される ページ数の管理 103
6. ⾒開きページを表⽰する 2枚の画像を1枚に連結する 画像の連結 104
6. ⾒開きページを表⽰する 画像の連結 105 画像を連結するために ImageProviderを変更
6. ⾒開きページを表⽰する 画像の連結 106 名前付きコンストラクタで 画像の指定を追加
6. ⾒開きページを表⽰する 画像の連結 107 比較処理をListに対応
6. ⾒開きページを表⽰する 画像を復号した後に連結する 画像の連結 108
6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 109 pubspec.yaml
6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 110 画像をImage型に デコードする
6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 111 出力先の画像の 大きさを決定する
6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 112 画像の数だけ 順番に連結する
6. ⾒開きページを表⽰する 変更したImageProviderを使⽤する 画像の連結 113
6. ⾒開きページを表⽰する 画像連結の処理が重いのでページ移動の際に画⾯が固まってしまう 画像の連結 114
Futureで⾮同期処理にしても同⼀のスレッドで実⾏される → 重い処理を実⾏するとメインスレッドがブロックされる 重い処理は並列で実⾏する必要がある 6. ⾒開きページを表⽰する 並列処理の実行 115
6. ⾒開きページを表⽰する Isolate Dartで並列処理を⾏うための仕組み • メインスレッドも⼀つの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