Slide 1

Slide 1 text

Flutterで構築する漫画ビューア @katsu

Slide 2

Slide 2 text

自己紹介 ● 伊藤 克弘 ● エキサイト株式会社 ● Android / Flutter エンジニア ● 10年ほどAndroidアプリ開発 ● Flutter歴は1年半ほど 2

Slide 3

Slide 3 text

経緯 株式会社講談社様との協業 週刊漫画誌「モーニング」の電⼦版アプリ 「Dモーニング」を2013年にリリース Flutter化のための技術検証 漫画アプリのFlutter化 3

Slide 4

Slide 4 text

前提事項 ● Flutterバージョンは 3.13.8、Dartバージョンは 3.1.4 を使⽤ ● モバイルアプリのみを対象とする(ウェブなどは対象外) ● ⼀般的な横開きの漫画を対象とする(縦読み漫画などは対象外) ● マテリアルデザインで統⼀する 環境および制約 4

Slide 5

Slide 5 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 5

Slide 6

Slide 6 text

1. 画像を表⽰する 6

Slide 7

Slide 7 text

1. 画像を表⽰する 今回使う画像ファイル ● ファイル名: 001.png ~ 010.png ● サイズ: 980 x 1,400 ● 配置場所: /images/ 使用する画像 7

Slide 8

Slide 8 text

1. 画像を表⽰する ビューアの画⾯としてViewerScreenを⽤意する 画面の作成 8

Slide 9

Slide 9 text

1. 画像を表⽰する アプリのディレクトリを取得するためにpath_providerを使う プラットフォームごとの各種ディレクトリにアクセスできる ファイルの参照 pubspec.yaml 9

Slide 10

Slide 10 text

1. 画像を表⽰する 画像ファイルを⼀覧で取得する ファイルの参照 10

Slide 11

Slide 11 text

1. 画像を表⽰する 画像ファイルを⼀覧で取得する ファイルの参照 アプリケーションディレクトリを取得 11

Slide 12

Slide 12 text

1. 画像を表⽰する 画像ファイルを⼀覧で取得する ファイルの参照 ディレクトリの要素の Listを取得 12

Slide 13

Slide 13 text

1. 画像を表⽰する 画像ファイルを⼀覧で取得する ファイルの参照 1. Fileのみを抽出 2. ファイル名で並び替え 13

Slide 14

Slide 14 text

1. 画像を表⽰する FutureBuilderでFutureからウィジェットを構成する ウィジェットで画像を参照する 14

Slide 15

Slide 15 text

1. 画像を表⽰する FutureBuilderでFutureからウィジェットを構成する ウィジェットで画像を参照する Futureの状態に 応じて処理する 15

Slide 16

Slide 16 text

1. 画像を表⽰する 画像の表⽰にはImageを使う ページ表⽰にはPageViewを使う 画像をPageViewで表示する 16

Slide 17

Slide 17 text

1. 画像を表⽰する PageViewの向きを反転する → reverseをtrueにする 画像をPageViewで表示する 17

Slide 18

Slide 18 text

2. 画像を拡⼤する 18

Slide 19

Slide 19 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 19

Slide 20

Slide 20 text

2. 画像を拡⼤する 画像の操作にはInteractiveViewerを使う 拡大縮小や、拡大状態での移動が可能 InteractiveViewerでの操作 20

Slide 21

Slide 21 text

2. 画像を拡⼤する InteractiveViewerのイベントがPageViewと競合する → 拡⼤したまま横に動かせなくなる 拡⼤中はPageViewのスクロールを無効にする PageViewとInteractiveViewerでの問題 21

Slide 22

Slide 22 text

2. 画像を拡⼤する TransformationController InteractiveViewerに設定する ‧外部からの操作 ‧現在の状態を取得 → 拡⼤状態を判定するのに使う 拡大中の判定を行う 22

Slide 23

Slide 23 text

2. 画像を拡⼤する リスナーで値の変化を検知する getMaxScaleOnAxisで拡⼤率を取得する 拡⼤率が1より⼤きければ拡⼤中とする 拡大中の判定を行う 23

Slide 24

Slide 24 text

2. 画像を拡⼤する PageViewのphysicsでスクロールの反応を定義 AlwaysScrollableScrollPhysics → スクロール可 NeverScrollableScrollPhysics → スクロール不可 PageViewのページ移動の可否を設定する 24

Slide 25

Slide 25 text

2. 画像を拡⼤する ピンチ操作だけではなく、ダブルタップでも拡⼤できるようにする ダブルタップを検出するためにGestureDetectorを追加 ダブルタップでの操作 25

Slide 26

Slide 26 text

2. 画像を拡⼤する ダブルタップでの操作 26

Slide 27

Slide 27 text

2. 画像を拡⼤する ダブルタップでの操作 タップ位置が必要なので onDoubleTapDownを使う 27

Slide 28

Slide 28 text

2. 画像を拡⼤する ダブルタップでの操作 拡大中の場合は初期状態に戻す 28

Slide 29

Slide 29 text

2. 画像を拡⼤する ダブルタップでの操作 (50, 50) (150, 150) (0, 0) (100, 100) (300, 300) x3.0 拡大した分の座標を移動させる 29

Slide 30

Slide 30 text

2. 画像を拡⼤する ダブルタップでの操作 30

Slide 31

Slide 31 text

3. 暗号化された画像を使⽤する 31

Slide 32

Slide 32 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 32

Slide 33

Slide 33 text

3. 暗号化された画像を使⽤する AndroidでもiOSでも、アプリ内のファイルにユーザーがアクセスすることはできない ただし、ユーザーが端末のルート化やジェイルブレイクなどで 強⼒なアクセス権を持った場合はアクセスできてしまう → コンテンツを暗号化して守ることが必要 暗号化の必要性 33

Slide 34

Slide 34 text

3. 暗号化された画像を使⽤する AndroidでもiOSでも、アプリ内のファイルにユーザーがアクセスすることはできない ただし、ユーザーが端末のルート化やジェイルブレイクなどで 強⼒なアクセス権を持った場合はアクセスできてしまう → コンテンツを暗号化して守ることが必要 ファイルになんらかの暗号化を施したという体で進める。 暗号化の必要性 34

Slide 35

Slide 35 text

3. 暗号化された画像を使⽤する 今までは画像ファイルをそのまま読み込んで表⽰していた → ファイルが暗号化されているためそのままでは表⽰できない ImageProviderを拡張して画像の読み込みをカスタマイズする ImageProviderの拡張 35

Slide 36

Slide 36 text

3. 暗号化された画像を使⽤する ImageProviderは画像をどのように処理するのかを定義するクラス いくつかのImageProviderが標準で⽤意されている ● FileImage(ファイルの画像) ● NetworkImage(ネットワーク上の画像) ● MemoryImage(メモリに展開された画像) ⾃作のImageProviderを作成して、画像を復号して読み込めるようにする ImageProviderの拡張 36

Slide 37

Slide 37 text

3. 暗号化された画像を使⽤する 復号用のImageProviderを作る FileImageを参考に 共通部分を作る 37

Slide 38

Slide 38 text

3. 暗号化された画像を使⽤する 復号用のImageProviderを作る 最適化のための比較処理 38

Slide 39

Slide 39 text

3. 暗号化された画像を使⽤する 復号用のImageProviderを作る 画像の識別のために自身を返す 39

Slide 40

Slide 40 text

3. 暗号化された画像を使⽤する 復号用のImageProviderを作る 40 loadImageで 画像読み込みの処理を実装する 画像データをCodecに、 Codecを画像フレームに変換する

Slide 41

Slide 41 text

3. 暗号化された画像を使⽤する 復号用のImageProviderを作る 画像データを展開して復号する Codecに変換する 41

Slide 42

Slide 42 text

3. 暗号化された画像を使⽤する 作成したImageProviderをImageに指定する 復号用のImageProviderを作る 42 復号用のImageProviderを作る

Slide 43

Slide 43 text

4. 全画⾯表⽰に対応する 43

Slide 44

Slide 44 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 44

Slide 45

Slide 45 text

4. 全画⾯表⽰に対応する 全画⾯表⽰ = コンテンツ以外のUIを⾮表⽰にして画⾯全体に表⽰する ● より⼤きく鮮明に表⽰できる ● 余計な要素がなくなり没⼊感が⾼まる 通知の確認や画⾯遷移の際にUIが必要になる → UIの表⽰を切り替えられるようにする 全画面表示とは 45

Slide 46

Slide 46 text

4. 全画⾯表⽰に対応する OSが管理しているUI ‧ステータスバー ‧ナビゲーションバー ‧ホームインジケーター システムUI iOS Android ステータスバー ナビゲーションバー ホームインジケーター 46

Slide 47

Slide 47 text

4. 全画⾯表⽰に対応する コンテンツの⾼さが変わるため 表⽰を切り替えた際に位置がズレてしまう → UIの裏にも表⽰しておくことで コンテンツの⾼さを⼀定にする UIの裏にコンテンツを表示する 47

Slide 48

Slide 48 text

4. 全画⾯表⽰に対応する Flutterではデフォルトで⼀部のシステムUIの裏に表⽰される Androidのナビゲーションバーは除く UIの裏にコンテンツを表示する ナビゲーションバーの裏には 表示されない 48

Slide 49

Slide 49 text

4. 全画⾯表⽰に対応する SystemChromeでSystemUiModeを切り替えてシステムUIを管理する SystemUiMode ● edgeToEdge ● leanBack ● immersive ● immersiveSticky ● manual UIの裏にコンテンツを表示する 49

Slide 50

Slide 50 text

4. 全画⾯表⽰に対応する SystemChromeでSystemUiModeを切り替えてシステムUIを管理する SystemUiMode ● edgeToEdge ● leanBack ● immersive ● immersiveSticky ● manual UIの裏にコンテンツを表示する 後ほど説明 50

Slide 51

Slide 51 text

4. 全画⾯表⽰に対応する edgeToEdge システムUIの裏にコンテンツを表⽰する ビューア画⾯の最初に設定しておく UIの裏にコンテンツを表示する 51

Slide 52

Slide 52 text

4. 全画⾯表⽰に対応する AppBarの裏にコンテンツを表⽰する ScaffoldのextendBodyBehindAppBarをtrueにする UIの裏にコンテンツを表示する 52

Slide 53

Slide 53 text

4. 全画⾯表⽰に対応する 画⾯をタップでUIの表⽰を切り替える UIが表⽰されている → ⾮表⽰に UIが表⽰されていない → 表⽰に UIの表示状態を切り替える 53

Slide 54

Slide 54 text

4. 全画⾯表⽰に対応する UIの表示状態を切り替える 表示状態の変更時に フラグを更新 54 現在の表示状態を 自前で管理

Slide 55

Slide 55 text

4. 全画⾯表⽰に対応する UIの表示状態を切り替える GestureDetectorに タップ時の処理を追加 55

Slide 56

Slide 56 text

4. 全画⾯表⽰に対応する UIの表示状態を切り替える システムUIを非表示にする システムUIを表示する 56

Slide 57

Slide 57 text

4. 全画⾯表⽰に対応する ● edgeToEdge ● leanBack ● immersive ● immersiveSticky ● manual SystemUiMode 57

Slide 58

Slide 58 text

4. 全画⾯表⽰に対応する SystemUiMode edgeToEdge leanBack immersive immersiveSticky manual 表示にする 非表示にする 手動で設定する 58

Slide 59

Slide 59 text

4. 全画⾯表⽰に対応する SystemUiMode edgeToEdge leanBack immersive immersiveSticky manual 表示にする 非表示にする パラメータで決まる iOSでは全て同じ挙動 59

Slide 60

Slide 60 text

4. 全画⾯表⽰に対応する edgeToEdge ● 全てのシステムUIが表⽰される ● コンテンツはシステムUIの裏にも表⽰される SystemUiMode 60

Slide 61

Slide 61 text

4. 全画⾯表⽰に対応する leanBack ● 全てのシステムUIが⾮表⽰になる ● 画⾯をタップするとシステムUIが表⽰される 動画再⽣など、基本的に画⾯を触らない場合に有効 SystemUiMode 61

Slide 62

Slide 62 text

4. 全画⾯表⽰に対応する immersive ● 全てのシステムUIが⾮表⽰になる ● 端をスワイプするとシステムUIが表⽰される ギャラリーなど、⼤まかな操作をする場合に有効 SystemUiMode 62

Slide 63

Slide 63 text

4. 全画⾯表⽰に対応する immersiveSticky ● 全てのシステムUIが⾮表⽰になる ● 端をスワイプするとシステムUIが表⽰される ● ⼀定時間経過で再び⾮表⽰になる ● スワイプのイベントをアプリも受け取れる お絵かきなど、画⾯全体を操作する場合に有効 SystemUiMode 63

Slide 64

Slide 64 text

4. 全画⾯表⽰に対応する manual ● 表⽰するシステムUIを指定する ● システムUIは上下に分類される どちらも省略した場合はleanBackと同様 SystemUiMode 64

Slide 65

Slide 65 text

4. 全画⾯表⽰に対応する AppBarの表⽰状態 表⽰する場合はAppBarを、⾮表⽰にする場合はnullを設定する。 UIの表示状態を切り替える 65

Slide 66

Slide 66 text

4. 全画⾯表⽰に対応する 動作イメージ 66

Slide 67

Slide 67 text

5. ページを操作する 67

Slide 68

Slide 68 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 68

Slide 69

Slide 69 text

5. ページを操作する PageViewはスワイプでページ移動ができるが、 それ以外の操作もサポートしていると操作性が⾼まる ● タップでの簡単なページ移動 ● スライダーでの素早いページ移動 これらの操作に対応する スワイプ以外でのページ操作 69

Slide 70

Slide 70 text

5. ページを操作する PageController PageViewを外部から操作する コントローラー経由でページ移動を⾏う ページ操作の基本 70

Slide 71

Slide 71 text

5. ページを操作する ページ移動には下記のメソッドが使える ● jumpTo ● jumpToPage ● animateTo ● animateToPage ● nextPage ● previousPage PageControllerの操作 71

Slide 72

Slide 72 text

5. ページを操作する ページ移動には下記のメソッドが使える ● jumpTo ● jumpToPage ● animateTo ● animateToPage ● nextPage ● previousPage PageControllerの操作 72 指定したページに瞬時に移動する オフセットもしくはインデックスを指定する

Slide 73

Slide 73 text

5. ページを操作する ページ移動には下記のメソッドが使える ● jumpTo ● jumpToPage ● animateTo ● animateToPage ● nextPage ● previousPage PageControllerの操作 73 指定したページにアニメーションで移動する オフセットもしくはインデックスを指定する

Slide 74

Slide 74 text

5. ページを操作する ページ移動には下記のメソッドが使える ● jumpTo ● jumpToPage ● animateTo ● animateToPage ● nextPage ● previousPage PageControllerの操作 74 隣のページにアニメーションで移動する 内部ではanimateToPageが使われている

Slide 75

Slide 75 text

5. ページを操作する タップでページ移動ができるようにする 左側の25%の範囲をタップしたら次のページに移動 右側の25%の範囲をタップしたら前のページに移動 タップでのページ操作 75

Slide 76

Slide 76 text

5. ページを操作する タップでのページ操作 76

Slide 77

Slide 77 text

5. ページを操作する タップでのページ操作 タップ位置を知るために onTapUpを使う 77

Slide 78

Slide 78 text

5. ページを操作する タップでのページ操作 横軸のタップ位置を取得 ページ移動判定の閾値を算出 78

Slide 79

Slide 79 text

5. ページを操作する タップでのページ操作 左右のタップ時に 隣のページに移動する 79

Slide 80

Slide 80 text

5. ページを操作する タップでのページ操作 UIの表示切替のタップイベントを統 合する 80

Slide 81

Slide 81 text

5. ページを操作する タップでのページ操作 81

Slide 82

Slide 82 text

5. ページを操作する Slider : ⼀定の範囲から値を選択するウィジェット 下記の要件を実装する ‧画⾯下部にスライダーを設置 ‧ページ数を範囲とする ‧選択したページに移動する スライダーでのページ操作 82

Slide 83

Slide 83 text

5. ページを操作する スライダーでのページ操作 83 ・Stackでコンテンツに重ねて表示する ・SafeAreaでシステムUIを避ける ・UIの表示フラグに合わせる SafeAreaなし SafeAreaあり

Slide 84

Slide 84 text

5. ページを操作する スライダーでのページ操作 84 選択中のページを変数で保持する ページの変更時に変数を更新する

Slide 85

Slide 85 text

5. ページを操作する スライダーと選択中のページを紐付ける スライダーでのページ操作 85

Slide 86

Slide 86 text

5. ページを操作する スライダーと選択中のページを紐付ける スライダーでのページ操作 86 値の範囲は0.0 ~ 1.0 選択中のページ/ページ数

Slide 87

Slide 87 text

5. ページを操作する スライダーと選択中のページを紐付ける スライダーでのページ操作 87 値 x ページ数 対象のページに移動

Slide 88

Slide 88 text

5. ページを操作する スライダーを反転させる PageViewのような単純な⽅法はない 計算でページのインデックスを逆順にする → ロジックが少し複雑になる スライダーでのページ操作 88

Slide 89

Slide 89 text

5. ページを操作する スライダーを反転させる PageViewのような単純な⽅法はない 計算でページのインデックスを逆順にする → ロジックが少し複雑になる ウィジェット⾃体を反転させる スライダーでのページ操作 89

Slide 90

Slide 90 text

5. ページを操作する Transform.flip ウィジェットの中⼼を軸に反転させる flipXを指定すると⽔平⽅向に反転 スライダーでのページ操作 90

Slide 91

Slide 91 text

5. ページを操作する スライダーでのページ操作 91

Slide 92

Slide 92 text

6. ⾒開きページを表⽰する 92

Slide 93

Slide 93 text

目次 1. 画像を表示する 2. 画像を拡大する 3. 暗号化された画像を使用する 4. 全画面表示に対応する 5. ページを操作する 6. 見開きページを表示する 93

Slide 94

Slide 94 text

6. ⾒開きページを表⽰する ⾒開きページは向かい合う左右のページで構成される 漫画は⾒開き⽤に構成されていることが多い → 可能な場合はアプリでも⾒開きで表⽰する 見開きページとは 94

Slide 95

Slide 95 text

6. ⾒開きページを表⽰する OrientationBuilderで親ウィジェットの向きを取得する 横向きの場合は⾒開き表⽰にする 見開き表示の判定 95

Slide 96

Slide 96 text

6. ⾒開きページを表⽰する ページ数の管理 96 画面あたりのページ数が 変わるため調整する

Slide 97

Slide 97 text

6. ⾒開きページを表⽰する ページ数の管理 97 見開きの場合はページ数の 最大値を半分として扱う 半分にしたページ数を PageViewとSliderに反映する

Slide 98

Slide 98 text

6. ⾒開きページを表⽰する ページ数の管理 98 選択中のページは 1ページ表示を基準とする 基準の値をSliderの 現在値に反映する

Slide 99

Slide 99 text

6. ⾒開きページを表⽰する 表⽰が切り替わった際にページがズレてしまう ページ数の管理 99 index == 2

Slide 100

Slide 100 text

6. ⾒開きページを表⽰する ページ数の管理 100 表示が切り替わった際に ページを移動させる

Slide 101

Slide 101 text

6. ⾒開きページを表⽰する ページ数の管理 101 直前の向きを保持しておき 切り替わったかどうかを判定

Slide 102

Slide 102 text

6. ⾒開きページを表⽰する ページ数の管理 102 表示が切り替わった場合は 対応したページに移動させる

Slide 103

Slide 103 text

6. ⾒開きページを表⽰する 表⽰が切り替わった際に正しいページが表⽰される ページ数の管理 103

Slide 104

Slide 104 text

6. ⾒開きページを表⽰する 2枚の画像を1枚に連結する 画像の連結 104

Slide 105

Slide 105 text

6. ⾒開きページを表⽰する 画像の連結 105 画像を連結するために ImageProviderを変更

Slide 106

Slide 106 text

6. ⾒開きページを表⽰する 画像の連結 106 名前付きコンストラクタで 画像の指定を追加

Slide 107

Slide 107 text

6. ⾒開きページを表⽰する 画像の連結 107 比較処理をListに対応

Slide 108

Slide 108 text

6. ⾒開きページを表⽰する 画像を復号した後に連結する 画像の連結 108

Slide 109

Slide 109 text

6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 109 pubspec.yaml

Slide 110

Slide 110 text

6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 110 画像をImage型に デコードする

Slide 111

Slide 111 text

6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 111 出力先の画像の 大きさを決定する

Slide 112

Slide 112 text

6. ⾒開きページを表⽰する 画像処理のライブラリを使⽤して画像を連結する 画像の連結 112 画像の数だけ 順番に連結する

Slide 113

Slide 113 text

6. ⾒開きページを表⽰する 変更したImageProviderを使⽤する 画像の連結 113

Slide 114

Slide 114 text

6. ⾒開きページを表⽰する 画像連結の処理が重いのでページ移動の際に画⾯が固まってしまう 画像の連結 114

Slide 115

Slide 115 text

Futureで⾮同期処理にしても同⼀のスレッドで実⾏される → 重い処理を実⾏するとメインスレッドがブロックされる 重い処理は並列で実⾏する必要がある 6. ⾒開きページを表⽰する 並列処理の実行 115

Slide 116

Slide 116 text

6. ⾒開きページを表⽰する Isolate Dartで並列処理を⾏うための仕組み ● メインスレッドも⼀つのIsolateである ● Isolate毎に異なるイベントループが実⾏される ● Isolate間は分断されている → 別のIsolateで処理を実⾏すれば画⾯が固まることはない 並列処理の実行 116

Slide 117

Slide 117 text

6. ⾒開きページを表⽰する Isolateを使う ● Isolate.spawn: 低レイヤーでの操作で、⾊々と⾃前で管理する必要がある ● compute: Flutterの拡張機能で、Isolateをシンプルに扱える ● Isolate.run: Dart 2.19で追加され、computeと同様にIsolateをシンプルに扱える → 今回は⼀番シンプルなIsolate.runを使う 並列処理の実行 117

Slide 118

Slide 118 text

6. ⾒開きページを表⽰する 処理全体をIsolate.runのブロックに含める 並列処理の実行 118

Slide 119

Slide 119 text

6. ⾒開きページを表⽰する 画⾯が固まることがなくなる 並列処理の実行 119

Slide 120

Slide 120 text

最後に 120

Slide 121

Slide 121 text

最後に 今回実装したのは最低限の基本的な機能 → 改善できる点はたくさんある ● OSバージョンごとの最適化 ● 読み込みの最適化、キャッシュ処理 ● 操作の効率化 ● 動作のアニメーション ● etc. さらなる改善 121

Slide 122

Slide 122 text

最後に 今回実装したのは最低限の基本的な機能 → 改善できる点はたくさんある ● OSバージョンごとの最適化 ● 読み込みの最適化、キャッシュ処理 ● 操作の効率化 ● 動作のアニメーション ● etc. さらなる改善 122 最高のサービスを目指して!

Slide 123

Slide 123 text

123