Slide 1

Slide 1 text

FlutterKaigi 2023 NAVITIME JAPAN / 松村航裕 Add Material touch ripples

Slide 2

Slide 2 text

• Android / iOS / Flutter • 個⼈でもFlutter製アプリを公開 • X(Twitter) / @kosuke_mtm  2 松村航裕 / Kosuke Matsumura ⾃⼰紹介

Slide 3

Slide 3 text

設⽴⽇  3 会社概要 NAVITIME JAPAN 2000年 3⽉ 1⽇ 約420名 (2023年10⽉現在) 社員数 約80%がエンジニア 経営理念 経路探索エンジンの技術で 
 世界の産業に奉仕する

Slide 4

Slide 4 text

 4 事業領域 NAVITIME JAPAN メディア‧トラベル事業 ドライブ事業 ツーリング事業 バス‧ウォーキング事業 キャリア協業事業 MaaS/CASE/交通データ事業 ビジネスナビタイム事業 ソリューション事業 公共交通事業 運転代⾏事業 ロケーションマーケティング事業 スポーツビジネス事業

Slide 5

Slide 5 text

 5 事業領域 NAVITIME JAPAN メディア‧トラベル事業 ドライブ事業 ツーリング事業 バス‧ウォーキング事業 キャリア協業事業 MaaS/CASE/交通データ事業 ビジネスナビタイム事業 ソリューション事業 公共交通事業 運転代⾏事業 ロケーションマーケティング事業 スポーツビジネス事業

Slide 6

Slide 6 text

 6 事業領域 / Flutter採⽤事業 NAVITIME JAPAN メディア‧トラベル事業 ドライブ事業 ツーリング事業 バス‧ウォーキング事業 キャリア協業事業 MaaS/CASE/交通データ事業 ビジネスナビタイム事業 ソリューション事業 公共交通事業 運転代⾏事業 ロケーションマーケティング事業 スポーツビジネス事業

Slide 7

Slide 7 text

 7 事業領域 / Flutter採⽤事業 NAVITIME JAPAN メディア‧トラベル事業 ドライブ事業 ツーリング事業 バス‧ウォーキング事業 キャリア協業事業 MaaS/CASE/交通データ事業 ビジネスナビタイム事業 ソリューション事業 公共交通事業 運転代⾏事業 ロケーションマーケティング事業 スポーツビジネス事業 ナビタイムのコアである地図やナビゲーションは、 
 Android/iOSそれぞれの独⾃ライブラリを開発‧運⽤しているため、 
 ネイティブ実装のアプリの⽅が多い https://speakerdeck.com/navitimejapan/ fl utterhanabitaimuziyapanfalsexin-gui-apurikai-fa-dehui-ku

Slide 8

Slide 8 text

• Jリーグをもっと楽しく! • 2022年夏リリース • Flutter製アプリ • スポンサーブースに展⽰  8 ユニタビ NAVITIME JAPAN https://site.uni-tabi.jp/ アプリDLリンク

Slide 9

Slide 9 text

みなさんはこんなことありませんか?  9

Slide 10

Slide 10 text

みなさんはこんなことありませんか?  10 タッチエフェクトがはみ出ているウィジェット タッチエフェクトがないウィジェット 本セッションでは、タッチエフェクトの仕組みを理解し、 
 このような事象を避ける実装⽅法を理解することをゴールとしています。

Slide 11

Slide 11 text

Agenda • タッチエフェクトとは • Material Design 3 Guideline • State Layer • 内部実装の解説 • Material • InkWell / InkResponse • タッチエフェクトの付け⽅ • Ink  11 資料中のソースコードはFlutter 3 . 1 3 . 3 のものです。また、簡略化のため⼀部省略および改変しています。 https://github.com/ fl utter/ fl utter/tree/ 3 . 1 3 . 3 /packages/ fl utter/lib/src

Slide 12

Slide 12 text

タッチエフェクトとは  12

Slide 13

Slide 13 text

ユーザーがオブジェクト(図中のContainer)をタッチしたことを視覚的に認識できるようにしたもの  13 Material Design 3 Guideline タッチエフェクトとは https://m 3 .material.io/foundations/interaction/states/state-layers コンテンツ State layer Container = Materialウィジェット = Materialウィジェットの表⾯ = Materialウィジェットのchild

Slide 14

Slide 14 text

要素の表層を覆う半透過のレイヤーで、透過度で状態を⽰す 
  14 State layer タッチエフェクトとは https://m 3 .material.io/foundations/interaction/states/state-layers#ec 6 8 aa 4 0 -c 1 aa- 4 1 0 a-b 6 7 7 -e 8 3 f 6 f 2 ba 0 2 1 Hover 不透明度 8 % Focus 不透明度 1 2 % Press 不透明度 1 2 % Drag 不透明度 1 6 % State layerの⾊は、 
 基本的にはonColorが使われる

Slide 15

Slide 15 text

 15 State layer タッチエフェクトとは コンテンツ State layer Container = Materialウィジェット = Materialウィジェットの表⾯ = Materialウィジェットのchild https://m 3 .material.io/foundations/interaction/states/state-layers タッチエフェクトもState layerで描画されます。 次は、Materialウィジェットの内部実装を追ってState layerの実態を探ります。

Slide 16

Slide 16 text

class Material extends StatefulWidget { … @override Widget build(BuildContext context) { … contents = NotificationListener( onNotification: (notification) { … }, child: _InkFeatures( absorbHitTest: widget.type != MaterialType.transparency, color: backgroundColor, child: contents, ), ); …  16 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM

Slide 17

Slide 17 text

class Material extends StatefulWidget { … @override Widget build(BuildContext context) { … contents = NotificationListener( onNotification: (notification) { … }, child: _InkFeatures( absorbHitTest: widget.type != MaterialType.transparency, color: backgroundColor, child: contents, ), ); …  17 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT

Slide 18

Slide 18 text

 18 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT class _InkFeatures extends SingleChildRenderObjectWidget { … @override _RenderInkFeatures createRenderObject(BuildContext context) { return _RenderInkFeatures(…); } @override void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) { … } }

Slide 19

Slide 19 text

 19 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT @3FOEFS*OL'FBUVSFT class _InkFeatures extends SingleChildRenderObjectWidget { … @override _RenderInkFeatures createRenderObject(BuildContext context) { return _RenderInkFeatures(…); } @override void updateRenderObject(BuildContext context, _RenderInkFeatures renderObject) { … } }

Slide 20

Slide 20 text

 20 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } } @3FOEFS*OL'FBUVSFT

Slide 21

Slide 21 text

 21 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } } -JTU*OL'FBUVSF @3FOEFS*OL'FBUVSFT

Slide 22

Slide 22 text

class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } }  22 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF *OL'FBUVSFͷ഑ྻΛ࢖ͬͯQBJOU͍ͯ͠Δ @3FOEFS*OL'FBUVSFT

Slide 23

Slide 23 text

class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } }  23 Materialウィジェット MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF *OL'FBUVSFͷ഑ྻΛ࢖ͬͯQBJOU͍ͯ͠Δ ʮ4UBUF-BZFSʢ.BUFSJBMͷද໘ʣʯͱ͸ @3FOEFS*OL'FBUVSFTͷ͜ͱ @3FOEFS*OL'FBUVSFT *OL'FBUVSF͕ λονΤϑΣΫτͷඳըΛ୲͏

Slide 24

Slide 24 text

class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } }  24 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController @3FOEFS*OL'FBUVSFT

Slide 25

Slide 25 text

class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } }  25 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT /// An interface for creating /// [InkSplash]s and [InkHighlight]s on a [Material]. /// /// Typically obtained via [Material.of]. abstract class MaterialInkController { void addInkFeature(InkFeature feature); … } -JTU*OL'FBUVSF MaterialInkController addInkFeature @3FOEFS*OL'FBUVSFT

Slide 26

Slide 26 text

class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController { … List? _inkFeatures; @override void addInkFeature(InkFeature feature) { … _inkFeatures!.add(feature); } @override void paint(PaintingContext context, Offset offset) { … for (final InkFeature inkFeature in inkFeatures) { inkFeature._paint(canvas); } } }  26 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT /// An interface for creating /// [InkSplash]s and [InkHighlight]s on a [Material]. /// /// Typically obtained via [Material.of]. abstract class MaterialInkController { void addInkFeature(InkFeature feature); … } -JTU*OL'FBUVSF MaterialInkController addInkFeature .BUFSJBM*OL$POUSPMMFSʹBEE*OL'FBUVSF͕։͍͍ͯΔ @3FOEFS*OL'FBUVSFT

Slide 27

Slide 27 text

 27 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature 8JEHFU class Material extends StatefulWidget { static MaterialInkController of(BuildContext context) { final MaterialInkController? controller = maybeOf(context); return controller!; } Material.of(context) @3FOEFS*OL'FBUVSFT .BUFSJBMPGͰ .BUFSJBM*OL$POUSPMMFSʹΞΫηεՄೳ

Slide 28

Slide 28 text

class InkResponse extends StatelessWidget { … InkFeature _createSplash(…) { final MaterialInkController inkController = Material.of(context); … InkFeature? splash; splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create( controller: inkController,…); return splash; } }  28 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature *OL8FMM*OL3FTQPOTF @3FOEFS*OL'FBUVSFT ※InkResponseはInkWellの基底クラス

Slide 29

Slide 29 text

class InkResponse extends StatelessWidget { … InkFeature _createSplash(…) { final MaterialInkController inkController = Material.of(context); … InkFeature? splash; splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create( controller: inkController,…); return splash; } }  29 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature *OL8FMM*OL3FTQPOTF @3FOEFS*OL'FBUVSFT ※InkResponseはInkWellの基底クラス *OL'FBUVSFΛੜ੒͢ΔTQMBTI'BDUPSZʹ .BUFSJBM*OL$POUSPMMFSΛ౉͍ͯ͠Δ

Slide 30

Slide 30 text

class InkRipple extends InkFeature { InkRipple({ required MaterialInkController controller, … }) : … { … controller.addInkFeature(this); }  30 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature *OL3JQQMFFYUFOET *OL'FBUVSF @3FOEFS*OL'FBUVSFT ίϯετϥΫλͰ.BUFSJBM*OL$POUSPMMFSΛड͚औΓ ॳظԽ࣌ʹࣗ෼ΛBEE*OL'FBUVSF͍ͯ͠Δ splashFactory.create() *OL8FMM*OL3FTQPOTF ※⼀例としてInkRippleを取り上げています

Slide 31

Slide 31 text

class InkRipple extends InkFeature { InkRipple({ required MaterialInkController controller, … }) : … { … controller.addInkFeature(this); }  31 InkFeatureの登録 MaterialウィジェットにおけるState layer .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature @3FOEFS*OL'FBUVSFT ίϯετϥΫλͰ.BUFSJBM*OL$POUSPMMFSΛड͚औΓ ॳظԽ࣌ʹࣗ෼ΛBEE*OL'FBUVSF͍ͯ͠Δ splashFactory.create() *OL8FMM*OL3FTQPOTF ※⼀例としてInkRippleを取り上げています *OL8FMMͰੜ੒͞Εͨ*OL3JQQMF౳͕ɺ @3FOEFS*OL'FBUVSFT্Ͱ λονΤϑΣΫτͱͯ͠ඳը͞ΕΔ *OL3JQQMFFYUFOET *OL'FBUVSF

Slide 32

Slide 32 text

 32 概念図 タッチエフェクトとは .BUFSJBM *OL8FMM

Slide 33

Slide 33 text

 33 概念図 タッチエフェクトとは *OL8FMM .BUFSJBM

Slide 34

Slide 34 text

 34 概念図 タッチエフェクトとは @3FOEFS*OL'FBUVSFT .BUFSJBMද໘ͷ@3FOEFS*OL'FBUVSFT .BUFSJBM *OL8FMM

Slide 35

Slide 35 text

 35 概念図 タッチエフェクトとは @3FOEFS*OL'FBUVSFT *OL'FBUVSF *OL8FMMͰ*OL'FBUVSFΛੜ੒ .BUFSJBMද໘ͷ@3FOEFS*OL'FBUVSFT *OL8FMM .BUFSJBM

Slide 36

Slide 36 text

 36 概念図 タッチエフェクトとは *OL'FBUVSF @3FOEFS*OL'FBUVSFT *OL8FMM .BUFSJBM

Slide 37

Slide 37 text

 37 概念図 タッチエフェクトとは *OL'FBUVSF @3FOEFS*OL'FBUVSFT λονΤϑΣΫτͷඳը͸ @3FOEFS*OL'FBUVSFTͷ$BOWBT্ *OL8FMM .BUFSJBM

Slide 38

Slide 38 text

 38 概念図 タッチエフェクトとは *OL'FBUVSF @3FOEFS*OL'FBUVSFT $PMPSFE#PYͳͲ *OL8FMM .BUFSJBM λονΤϑΣΫτ͸ඳը͞Ε͍ͯΔ $PMPSFE#PY ෆಁաͳϨΠϠʔ ͕अຐͰ λονΤϑΣΫτ͸ݟ͑ͳ͍

Slide 39

Slide 39 text

InkWell / InkResponse  39

Slide 40

Slide 40 text

InkWell / InkResponse オブジェクトにタッチエフェクトを付与するには、⼀般的にInkWellを使います  40 class InkWell extends InkResponse { const InkWell({ super.key, super.child, super.onTap, super.splashFactory, … bool?… enableFeedback = true, }) : super( containedInkWell: true, highlightShape: BoxShape.rectangle, enableFeedback: enableFeedback ?? true, ); } *OL8FMM͸*OL3FTQPOTFΛܧঝ ྆ऀͱ΋λονΤϑΣΫτΛ෇༩͢ΔͨΊͷ΋ͷ

Slide 41

Slide 41 text

class InkWell extends InkResponse { const InkWell({ super.key, super.child, super.onTap, super.splashFactory, … bool?… enableFeedback = true, }) : super( containedInkWell: true, highlightShape: BoxShape.rectangle, enableFeedback: enableFeedback ?? true, ); } InkWell / InkResponse オブジェクトにタッチエフェクトを付与するには、⼀般的にInkWellを使います  41 違いは2つのパラメータくらいで、 雑に⾔うとInkResponseは円形、InkWellは矩形のエフェクトが発⽣します。

Slide 42

Slide 42 text

 42 highlightShapeパラメータ InkWell / InkResponse IJHIMJHIU4IBQFͷ஋ ϋΠϥΠτͷදࣔ DJSDMF 
 *OL3FTQPOTFͰ࢖༻ ԁܗ SFDUBOHMF *OL8FMMͰ࢖༻ ۣܗ ۣܗͷؙ֯͸CPSEFS3BEJVTύϥϝʔλͰࢦఆՄೳ press/focus/hover時のハイライトの挙動を定義します enum BoxShape { rectangle, circle, // Don't add more, instead create a new ShapeBorder. } #PY4IBQF͸ۣܗ͔ԁͷ୒Ͱɺ ࠓޙ૿͑Δ͜ͱ΋૝ఆ͞Ε͍ͯͳ͍

Slide 43

Slide 43 text

 43 containedInkWellパラメータ InkWell / InkResponse DPOUBJOFE*OL8FMMͷ஋ λονΤϑΣΫτͷڍಈ '"-4& *OL3FTQPOTFͰ࢖༻ ԁܗ λονҐஔ͔Βঃʑʹ 
 ΢ΟδΣοτதԝ΁ͱ޲͔͏ 536& *OL8FMMͰ࢖༻ ۣܗ λονҐஔ͔ΒΤϑΣΫτ͕޿͕Δ ۣܗͷؙ֯͸CPSEFS3BEJVTύϥϝʔλͰࢦఆՄೳ タッチエフェクトの挙動を定義します 元は、タッチエフェクトがウィジェットの境界を越えるかどうか、という意味のフラグ

Slide 44

Slide 44 text

基本はInkWell(矩形)を使うことが多くなると思います。 円形のエフェクトやカスタマイズが必要な場合に、InkResponseの利⽤を検討してください。 また、InkResponseを使う場合はhighlightShapeパラメータとcontainedInkWellパラメータが 
 ⼀致していないと、下記のような⾒た⽬になるため注意が必要です。  44 InkWell と InkResponse の使い分け InkWell / InkResponse

Slide 45

Slide 45 text

GestureDetectorは、細やかなタッチイベントのハンドリングには便利。 タッチエフェクトをつけることはできないので、onTapは基本InkWellを使う⽅が⾃然。 ちなみに、InkResponseも内部でGestureDetectorを使っています。  45 GestureDetectorとの使い分け InkWell / InkResponse

Slide 46

Slide 46 text

TableRowInkWell Tableで使う 
 Row全体にタッチエフェクトをかけることができる 
 _IndicatorInkWell NavigatorRailで内部的に使われている  46 派⽣クラス InkWell / InkResponse

Slide 47

Slide 47 text

 47 SplashFactory InkWell / InkResponse abstract class InteractiveInkFeatureFactory { … @factory InteractiveInkFeature create({ required MaterialInkController controller, required RenderBox referenceBox, required Color color, bool containedInkWell = false, … }); } タッチエフェクトの挙動を決めるInkFeature(InteractiveInkFeature)を⽣成するfactoryを定義します このfactoryで⽣成されるInteractiveInkFeatureは 主に InkSplash/InkRipple/InkSparkle (,NoSplash) TQMBTI'BDUPSZͷܕ͸ *OUFSBDUJWF*OL'FBUVSF'BDUPSZܕ

Slide 48

Slide 48 text

 48 InkSplash / InkRipple / InkSparkle SplashFactory factory ThemeData() { ... final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 ? useInkSparkle ? InkSparkle.splashFactory : InkRipple.splashFactory : InkSplash.splashFactory; → InkSparkleを使うのはAndroid M 3 ではInkSparkleかInkRipple

Slide 49

Slide 49 text

factory ThemeData() { ... final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 ? useInkSparkle ? InkSparkle.splashFactory : InkRipple.splashFactory : InkSplash.splashFactory;  49 InkSplash / InkRipple / InkSparkle SplashFactory M 3 ではInkSparkleかInkRipple M 2 はInkSplash → InkSparkleを使うのはAndroid

Slide 50

Slide 50 text

factory ThemeData() { ... final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 ? useInkSparkle ? InkSparkle.splashFactory : InkRipple.splashFactory : InkSplash.splashFactory;  50 InkSplash / InkRipple / InkSparkle SplashFactory M 2 M 3 (Android以外) M 3 
 Android ※InkSplashはShaderを使うため、Web⾮対応 【タッチ時の挙動】 M 3 になって速くなった Sparkleによりキラキラしている

Slide 51

Slide 51 text

 51 InkSplash / InkRipple / InkSparkle SplashFactory タッチ後、すぐ指を離すと 
 InkSplashのみタッチエフェクトがキャンセルされます 【タッチキャンセル時の挙動】 M 2 M 3 (Android以外) M 3 
 Android factory ThemeData() { ... final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 ? useInkSparkle ? InkSparkle.splashFactory : InkRipple.splashFactory : InkSplash.splashFactory;

Slide 52

Slide 52 text

Android 1 2 から登場したタッチエフェクトの表現。 
 β版だとsparkleが強かったが、 
 stable版でだいぶ落ち着いた表現に。  52 InkSparkle SplashFactory https://github.com/ fl utter/ fl utter/issues/ 8 2 8 5 0 https:// 9 to 5 google.com/ 2 0 2 1 / 0 7 / 1 4 /android- 1 2 -ripple-sparkle/ Android 1 2 β版のタッチエフェクトの様⼦ FlutterでもM 3 対応の⼀環として登場 
 ※M 3 のガイドラインには載っていない

Slide 53

Slide 53 text

Materialウィジェットを使う  53

Slide 54

Slide 54 text

Materialウィジェットを使う みてきたように、InkWellを使うためにはMaterialが必要となります。 しかし⼤半のウィジェットは、内部的にMaterial (+ InkWell)を持っているため、 
 特に難しいことを考えずにタッチエフェクトを使うことができるようになっています。 ただし、ウィジェットのchildに不透過なレイヤーがあると、 
 やはりタッチエフェクトは⾒えなくなります。 いくつか例を⾒ていきます。  54

Slide 55

Slide 55 text

 55 いつものCounterアプリ Materialウィジェットを使う class MyApp extends StatelessWidget { … @override Widget build(BuildContext context) { return MaterialApp( home: const MyHomePage(title: 'Home Page'), ); } } class MyHomePage extends StatefulWidget { … @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.title)), body: Center( … class Scaffold extends StatefulWidget { … @override Widget build(BuildContext context) { … return _ScaffoldScope( hasDrawer: hasDrawer, geometryNotifier: _geometryNotifier, child: ScrollNotificationObserver( child: Material( child:… .BUFSJBM"QQ͸ɺ.BUFSJBMͱ͸ผ෺ 4DB PMEͷதʹ.BUFSJBM͕͍ΔͨΊɺ 4DB PMEԼͰ͸*OL8FMMΛ࢖͑Δ

Slide 56

Slide 56 text

内部的にMaterialを持っているので、Cardの下に不透過なレイヤ ーがあってもCard上でタッチエフェクトが表⽰できる。 ただし、⾓丸になっているためInkWellを使う場合は注意が必要 ※タッチエフェクトがCardを超えて表⽰される  56 Card Materialウィジェットを使う class Card extends StatelessWidget { … @override Widget build(BuildContext context) { … return Semantics( child: Container( child: Material( type: MaterialType.card, shape: shape ?? cardTheme.shape ?? defaults.shape, …

Slide 57

Slide 57 text

 57 Card Materialウィジェットを使う Card( clipBehavior: Clip.hardEdge, child: InkWell( child: …, onTap: () { … }, ), ) clipBehaviorで .none以外を指定する必要がある  57

Slide 58

Slide 58 text

 58 TextButton / OutlinedButton / ElevatedButton Materialウィジェットを使う class ElevatedButton extends ButtonStyleButton { } abstract class ButtonStyleButton extends StatefulWidget { @override Widget build(BuildContext context) { … final Widget result = ConstrainedBox( child: Material( shape: resolvedShape!.copyWith(side: resolvedSide), child: InkWell( onTap: widget.onPressed, customBorder: resolvedShape.copyWith(side: resolvedSide), child: … ), ), ); return Semantics(child: _InputPadding(child: result)); } } #VUUPO܈͸ɺ#VUUPO4UZMF#VUUPOΛܧঝ ಺෦Ͱ.BUFSJBM *OL8FMMΛ࣋ͭ

Slide 59

Slide 59 text

class ElevatedButton extends ButtonStyleButton { } abstract class ButtonStyleButton extends StatefulWidget { @override Widget build(BuildContext context) { … final Widget result = ConstrainedBox( child: Material( shape: resolvedShape!.copyWith(side: resolvedSide), child: InkWell( onTap: widget.onPressed, customBorder: resolvedShape.copyWith(side: resolvedSide), child: … ), ), ); return Semantics(child: _InputPadding(child: result)); } }  59 TextButton / OutlinedButton / ElevatedButton Materialウィジェットを使う $BSEͷ৔߹͸$MJQͷࢦఆ͕ඞཁ͕ͩͬͨɺ #VUUPOͷ৔߹͸#PSEFS4JEF͕ࢦఆ͞Ε͍ͯΔͨΊɺෆཁ

Slide 60

Slide 60 text

 60 IconButton(M 3 ) Materialウィジェットを使う class IconButton extends StatelessWidget { @override Widget build(BuildContext context) { if (theme.useMaterial3) { return _SelectableIconButton(…); } … } } class _SelectableIconButton extends StatefulWidget { @override Widget build(BuildContext context) { return _IconButtonM3(…); } } class _IconButtonM3 extends ButtonStyleButton {…} IconButtonはStatelessWidgetだが、M 3 の場合は内部的に ButtonStyleButtonを使っているため、実装はほぼ同じ。

Slide 61

Slide 61 text

 61 IconButton(M 2 ) Materialウィジェットを使う class IconButton extends StatelessWidget { @override Widget build(BuildContext context) { if (theme.useMaterial3) { return _SelectableIconButton(…); } … return Semantics( child: InkResponse( radius: splashRadius ?? … child: result, …), … ); } } .ͱ͸ҟͳΓɺ *OL3FTQPOTF͕࢖ΘΕ͍ͯΔ Materialウィジェットは内包していないので、 
 不透過レイヤーが下にあるとタッチエフェクトは表⽰されません

Slide 62

Slide 62 text

 62 IconButton(M 2 vs M 3 ) Materialウィジェットを使う M 2 M 3 *OL8FMM *OL3FTQPOTF

Slide 63

Slide 63 text

 63 IconButton(M 2 vs M 3 ) / タッチエフェクトの挙動 Materialウィジェットを使う M 2 M 3 特に指定しないとサイズ固定( 3 5 pt) 
 円形 アイコンサイズに応じたサイズになる 
 実は矩形 *OL8FMM *OL3FTQPOTF

Slide 64

Slide 64 text

 64 IconButton(M 2 vs M 3 ) / タッチエフェクトの挙動 Materialウィジェットを使う M 2 M 3 IconButton( onPressed: () {}, icon: Container( padding: const EdgeInsets.symmetric(vertical: 16), color: Colors.yellowAccent.withAlpha(128), child: Icon( Icons.flutter_dash_sharp, ), ), *DPOʹ1BEEJOHΛՃ͑ͯΈΔͱʜ

Slide 65

Slide 65 text

 65 IconButton(M 2 vs M 3 ) / タッチエフェクトの挙動 Materialウィジェットを使う M 2 M 3 円のサイズが固定のため、 
 Paddingの分だけ下寄りになってしまう 矩形のため、アイコンの形状によっては 
 楕円のようになることもある *OL8FMM *OL3FTQPOTF

Slide 66

Slide 66 text

 66 IconButton(M 2 vs M 3 ) / タッチエフェクトの挙動 Materialウィジェットを使う M 2 M 3 円のサイズが固定のため、 
 Paddingの分だけ下寄りになってしまう 矩形のため、アイコンの形状によっては 
 楕円のようになることもある *OL8FMM *OL3FTQPOTF ɹ.ͷ*DPO#VUUPOɿίϯςϯπͷαΠζͱ ɹɹɹɹɹɹɹɹɹɹɹɹɹෆಁաϨΠϠʔʹ஫ҙ ɹ.ͷ*DPO#VUUPOɿλονΤϑΣΫτͷܗঢ়ʹ஫ҙ

Slide 67

Slide 67 text

ここまでInkWellの中⾝を⾒てきました。 最後に、不透過なレイヤーがあるためにタッチエフェクトが⾒えない場合の 
 対応を⾒ていきます。  67

Slide 68

Slide 68 text

不透過レイヤーがある場合の タッチエフェクトの付け⽅  68

Slide 69

Slide 69 text

Inkを使う 不透過なレイヤーでタッチエフェクトが⾒えない場合、 
 Inkを代⽤することでタッチエフェクトが⾒えるようにできることがあります 簡単にいうと、不透過なレイヤーを加える代わりに、 
 InkによってMaterialウィジェットの⾊を変更することができます。  69

Slide 70

Slide 70 text

class Ink extends StatefulWidget { Ink({ Color? color, Decoration? decoration, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null); … final Decoration? decoration; @override Widget build(BuildContext context) { return Padding( child: Builder(builder: _build), ); } …  70 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController *OL @3FOEFS*OL'FBUVSFT

Slide 71

Slide 71 text

class Ink extends StatefulWidget { Ink({ Color? color, Decoration? decoration, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null); … final Decoration? decoration; @override Widget build(BuildContext context) { return Padding( child: Builder(builder: _build), ); } …  71 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController *OL @3FOEFS*OL'FBUVSFT .BUFSJBM΢ΟδΣοτʹ ృΓ͍ͨ$PMPS%FDPSBUJPOΛࢦఆ

Slide 72

Slide 72 text

class Ink extends StatefulWidget { Ink({ Color? color, Decoration? decoration, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null); … final Decoration? decoration; @override Widget build(BuildContext context) { return Padding( child: Builder(builder: _build), ); } …  72 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController *OL @3FOEFS*OL'FBUVSFT $PMPSΛࢦఆͨ͠৔߹΋ %FDPSBUJPOʹม׵

Slide 73

Slide 73 text

class Ink extends StatefulWidget { Ink({ Color? color, Decoration? decoration, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null); … final Decoration? decoration; @override Widget build(BuildContext context) { return Padding( child: Builder(builder: _build), ); } …  73 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController *OL @3FOEFS*OL'FBUVSFT *OLࣗମ͸ඳը͞Εͳ͍ %FDPSBUJPO͸࢖ΘΕͳ͍

Slide 74

Slide 74 text

class Ink extends StatefulWidget { Ink({ Color? color, Decoration? decoration, this.child, }) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null); … final Decoration? decoration; @override Widget build(BuildContext context) { return Padding( child: Builder(builder: _build), ); } … Widget _build(BuildContext context) { … _ink = InkDecoration( decoration: widget.decoration, controller: Material.of(context), ); return widget.child; }  74 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController *OL @3FOEFS*OL'FBUVSFT EFDPSBUJPO͸ *OL%FDPSBUJPO *OL'FBUVSF ʹ౉͞ΕΔ

Slide 75

Slide 75 text

class InkDecoration extends InkFeature { InkDecoration({ required Decoration? decoration, required super.controller, … }) : … controller.addInkFeature(this); }  75 内部実装 Ink .BUFSJBM @*OL'FBUVSFT *OL *OL%FDPSBUJPO @3FOEFS*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController

Slide 76

Slide 76 text

class InkDecoration extends InkFeature { InkDecoration({ required Decoration? decoration, required super.controller, … }) : … controller.addInkFeature(this); }  76 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature *OL *OL%FDPSBUJPO @3FOEFS*OL'FBUVSFT *OL಺Ͱੜ੒͞Εͨ%FDPSBUJPOͰ $BOWBTʹඳը͢Δ*OL'FBUVSFΛੜ੒

Slide 77

Slide 77 text

class InkDecoration extends InkFeature { InkDecoration({ required Decoration? decoration, required super.controller, … }) : … controller.addInkFeature(this); }  77 内部実装 Ink .BUFSJBM @*OL'FBUVSFT -JTU*OL'FBUVSF MaterialInkController addInkFeature *OL *OL%FDPSBUJPO @3FOEFS*OL'FBUVSFT *OL'FBUVSFΛ @3FOEFS*OL'FBUVSFTʹొ࿥

Slide 78

Slide 78 text

 78 内部実装(図解) Ink *OL'FBUVSF @3FOEFS*OL'FBUVSFT $PMPSFE#PYͳͲ .BUFSJBM *OL8FMM

Slide 79

Slide 79 text

 79 内部実装(図解) Ink *OL'FBUVSF @3FOEFS*OL'FBUVSFT .BUFSJBM *OL $PMPSFE#PYΛ*OL DPMPS ʹࠩ͠ସ͑Δ *OL8FMM

Slide 80

Slide 80 text

 80 内部実装(図解) Ink *OL'FBUVSF @3FOEFS*OL'FBUVSFT *OL8FMM .BUFSJBM *OL%FDPSBUJPO *OL

Slide 81

Slide 81 text

 81 内部実装(図解) Ink *OL'FBUVSF @3FOEFS*OL'FBUVSFT *OL8FMM .BUFSJBM *OL%FDPSBUJPO *OL *OLࣗମ͸ಁա .BUFSJBMͷද໘ͷ৭ %FDPSBUJPO Λ ม͑Δ͜ͱ͕Ͱ͖Δ

Slide 82

Slide 82 text

 82 ⾊を変えたい場合 Ink ColoredBox( color: Colors.lightGreenAccent, child: InkWell( onTap: () {…}, child: …, ), ) ColoredBoxやContainerで指定しているcolor部分を、Inkに差し替える Ink( color: Colors.lightGreenAccent, child: InkWell( onTap: () {…}, child: …, ), )

Slide 83

Slide 83 text

 83 画像の上にタッチエフェクトをつけたい場合 Ink Ink.image({ this.padding, required ImageProvider image, this.child, }) : decoration = BoxDecoration( image: DecorationImage(image: image, …), ); 画像を指定すると、color同様にDecorationに変換される

Slide 84

Slide 84 text

 84 画像の上にタッチエフェクトをつけたい場合 Ink Ink.image({ this.padding, required ImageProvider image, this.child, }) : decoration = BoxDecoration( image: DecorationImage(image: image, …), ); 画像を指定すると、color同様にDecorationに変換される UJQT 画像の⾓丸は、Ink.imageではできない 
 その場合、BoxDecorationを⾃作するのが早い Ink( decoration: BoxDecoration( image: DecorationImage(image: imageProvider), borderRadius: BorderRadius.all(Radius.circular(8)), ), )

Slide 85

Slide 85 text

Ink.image({ this.padding, required ImageProvider image, this.child, }) : decoration = BoxDecoration( image: DecorationImage(image: image, …), ); Ink( decoration: BoxDecoration( image: DecorationImage(image: imageProvider), borderRadius: BorderRadius.all(Radius.circular(8)), ), )  85 画像の上にタッチエフェクトをつけたい場合 Ink UJQT 画像の⾓丸は、Ink.imageではできない 
 その場合、BoxDecorationを⾃作するのが早い ؙ֯ࢦఆ 画像を指定すると、color同様にDecorationに変換される

Slide 86

Slide 86 text

Materialを使う  86 難しいことを考えずに、Materialウィジェットをもう⼀枚置くのも、OK

Slide 87

Slide 87 text

 87 MaterialType.transparency Material Material( type: MaterialType.transparency, child: …, ) MaterialTypeにtransparencyが⽤意されている enum MaterialType { /// ۣܗɻσϑΥϧτ canvas, /// ؙ֯ͷۣܗɻCardͰ࢖͏ card, /// ԁܗɻσϑΥϧτͰ͸৭ͳ͠ɻʢFABͰ࢖͏ʣ circle, /// ؙ֯ͷۣܗɻσϑΥϧτͰ৭ͳ͠ɻʢϘλϯͰ࢖͏ʣ button, /// ಁ໌Ͱink splashes ΍ highlightsΛඳը͢Δɻ transparency }

Slide 88

Slide 88 text

 88 MaterialType.transparencyによる挙動の違い Material @override Widget build(BuildContext context) { final Color? backgroundColor = _getBackgroundColor(context); assert( backgroundColor != null || widget.type == MaterialType.transparency, 'If Material type is not MaterialType.transparency, ‘a color must either be passed in through the ‘`color` property, or be defined in the theme', ); … if (widget.type == MaterialType.transparency) { return _transparentInterior( shape: shape, clipBehavior: widget.clipBehavior, contents: contents, ); } return _MaterialInterior(…); } USBOTQBSFODZͷ৔߹ɺ CBDLHSPVOE$PMPS͸OVMMͰ͋Δඞཁ͕͋Δ

Slide 89

Slide 89 text

 89 MaterialType.transparencyによる挙動の違い Material @override Widget build(BuildContext context) { final Color? backgroundColor = _getBackgroundColor(context); assert( backgroundColor != null || widget.type == MaterialType.transparency, 'If Material type is not MaterialType.transparency, ‘a color must either be passed in through the ‘`color` property, or be defined in the theme', ); … if (widget.type == MaterialType.transparency) { return _transparentInterior( shape: shape, clipBehavior: widget.clipBehavior, contents: contents, ); } return _MaterialInterior(…); } FMFWBUJPO΍TIBQF͕มߋ͞Εͨͱ͖ʹ "OJNBUJPOΛ࣮ߦ͢Δ*NQMJDJUMZ"OJNBUFE8JEHFU

Slide 90

Slide 90 text

@override Widget build(BuildContext context) { final Color? backgroundColor = _getBackgroundColor(context); assert( backgroundColor != null || widget.type == MaterialType.transparency, 'If Material type is not MaterialType.transparency, ‘a color must either be passed in through the ‘`color` property, or be defined in the theme', ); … if (widget.type == MaterialType.transparency) { return _transparentInterior( shape: shape, clipBehavior: widget.clipBehavior, contents: contents, ); } return _MaterialInterior(…); }  90 MaterialType.transparencyによる挙動の違い Material static Widget _transparentInterior({… }) { final _ShapeBorderPaint child = _ShapeBorderPaint(…); return ClipPath( clipper: ShapeBorderClipper( shape: shape, textDirection:Directionality.maybeOf(context) ), … ); }

Slide 91

Slide 91 text

@override Widget build(BuildContext context) { final Color? backgroundColor = _getBackgroundColor(context); assert( backgroundColor != null || widget.type == MaterialType.transparency, 'If Material type is not MaterialType.transparency, ‘a color must either be passed in through the ‘`color` property, or be defined in the theme', ); … if (widget.type == MaterialType.transparency) { return _transparentInterior( shape: shape, clipBehavior: widget.clipBehavior, contents: contents, ); } return _MaterialInterior(…); } static Widget _transparentInterior({… }) { final _ShapeBorderPaint child = _ShapeBorderPaint(…); return ClipPath( clipper: ShapeBorderClipper( shape: shape, textDirection:Directionality.maybeOf(context) ), … ); }  91 MaterialType.transparencyによる挙動の違い Material USBOTQBSFODZͷ৔߹ɺ୯७ͳ4IBQF#PSEFS$MJQQFS 㱺.BUFSJBMಛ༗ͷ"OJNBUJPO͕ແޮʹͳ͍ͬͯΔ

Slide 92

Slide 92 text

 92 Material or Ink ? Material Inkで代⽤できるならそれに越したことはないけど、 
 Buttonなどでも内部的にMaterialを使⽤しているので、そこまで神経質になる必要はない。 特に汎⽤的なカスタムWidgetを作っている場合などは、 
 むしろ深く考えずにMaterialを使った⽅が良いと、個⼈的には思います。 class ElevatedButton extends ButtonStyleButton { } abstract class ButtonStyleButton extends StatefulWidget { @override Widget build(BuildContext context) { … final Widget result = ConstrainedBox( child: Material( child: InkWell( …

Slide 93

Slide 93 text

Summary  93

Slide 94

Slide 94 text

Summary • タッチエフェクトは、Materialウィジェットの表⾯(_RenderInkFeatures)で描画される • タッチエフェクトはM 2 とM 3 で挙動が少し異なる • タッチエフェクトが表⽰されない場合はInkをうまく使う • 難しく考えずに、Materialを使ってしまってもOK!  94

Slide 95

Slide 95 text

Thank you for your attention!  95

Slide 96

Slide 96 text

参考 • Material Design 
 https://m 3 .material.io/ • InkResponse と InkWell と Ink、違いを説明できますか? 
 https://qiita.com/mkosuke/items/e 5 0 6 2 5 6 5 1 5 1 7 9 d 0 f 4 2 1 b • Flutterはナビタイム ジャパンの新規アプリ開発で輝く 
 https://speakerdeck.com/navitimejapan/ fl utterhanabitaimuziyapanfalsexin-gui-apurikai- fa-dehui-ku  96

Slide 97

Slide 97 text

FlutterKaigi 2023