Flutter Advent Calendar#2 の11日目です。昨年はEffective Dartまとめで参加させていただきました。今年はMaterialウィジェットについて書こうと思いましたが、量が半端なくなりそうでしたので、掲題のようにターゲットを絞った内容にしてみました。
InkResponse
とInkWell
とInk
どれもタッチ時のsplash(Ripple)エフェクトを表現してくれるウィジェットで、FlutterのMaterial Designのアプリケーションには欠かせないものですが、名前だけでは違いはよくわかりません。本記事ではこれらの違いを詳しく紹介します。
まずはInkResponse
とInkWell
の比較、その後、Ink
について紹介します。
また、ソースコード上ではタッチエフェクト(Ripple Effect)のことをsplashと表現しているため、本記事でもsplashと表現を統一するようにしています。
環境
Flutter 1.22.4
(2020/12/8時点のstable最新版)
TL;DR,
- InkResponse
- ・タッチ時にウィジェット中央に円形のsplashが表示される
- InkWell
- ・InkResponseの継承クラスで、ウィジェットに対してsplashを付けたい場合は基本InkWellの方を使うことが多い
- ・splashは矩形で、タッチ座標がsplashの起点となる
- Ink
- ・Materialウィジェットとsplashを効かせたいウィジェットの間に、不透明なウィジェットがある場合に利用
- ・InkResponseやInkWellと併用する
InkResponse
とInkWell
まずは挙動をみてみます。
InkResponse | InkWell | |
---|---|---|
概念図 | 公式Docより | 公式Docより |
hover/press/focus時のhighlight | ウィジェット中央に円形 | ウィジェット全体の矩形 |
press時のsplash | 円形のままウィジェット中央へ移動 | 矩形の中を同心円状に広がる |
InkResponse
は、ウィジェットの中央に円形のエフェクト(highlight)が表示されます。サンプルは矩形のボタンにしていますが、アイコンボタンなどが合っていると思います。一方、InkWell
はボタンなどでよく見る矩形で一般的なMaterial Designのウィジェットのような挙動をしています。
ここでの用語としてhighlight
とsplash
の違いは把握しておきましょう。highlightはhover/pressの瞬間に表示されるもので、splashはアニメーションを伴うエフェクト表示です。
コードレベルの比較
実は、InkWell
はInkResponse
を継承しており、以下のようになっています。
class InkWell extends InkResponse {
const InkWell({
Key key,
Widget child,
GestureTapCallback onTap,
GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress,
GestureTapDownCallback onTapDown,
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover,
MouseCursor mouseCursor,
(以下略)
}) : super(
key: key,
child: child,
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
onTapDown: onTapDown,
onTapCancel: onTapCancel,
onHighlightChanged: onHighlightChanged,
onHover: onHover,
mouseCursor: mouseCursor,
// パラメータを固定しているのは以下の2項目のみ
containedInkWell: true,
highlightShape: BoxShape.rectangle,
(以下略)
);
}
(InkWell/InkResponseのソースコードはこちら)
InkWell
独自の実装は存在せず、コンストラクタでcontainedInkWell
とhighlightShape
の2つのパラメータを固定しているだけに過ぎません。このパラメータについてみていきます。
containedInkWell
パラメータ
タッチ時のsplashが、ウィジェットの境界に合わせてclipするかどうかを示す、bool型のフラグです。
また、このフラグはsplashの挙動にも影響します。(press/focus/hover時のhighlightは、次のhighlightShapeパラメータで指定します)
- false
- ・InkResponseのデフォルト値
- ・ウィジェットの境界を超えてsplashが広がる
- ・splashはタッチ位置からスタートするが、splashが広がるにつれてウィジェットの中心へ移動
- true
- ・InkWellではtrueが固定で使用されている
- ・splashはウィジェットの境界を超えない(下記例のようにBoxDecorationなどで指定した角丸は考慮されない)
- ・タッチ位置を中心にsplashが広がる
- ・borderRadiusでsplashが広がる矩形の角丸を指定可能
containedInkWell の値 |
borderRadius の値 |
InkResponseの結果 |
---|---|---|
false(デフォルト) | 指定なし(0) | * 矩形の角丸はradius=8です |
true | 指定なし(0) | |
true | 20 |
highlightShape
パラメータ
BoxShape
型(enum)で、press/focus/hover状態の時のhighlight表示の形状を定義します。逆に、先程のタッチ時のsplashには影響しません。
- BoxShape.circle
- ・InkResponseのデフォルト値
- ・highlightはウィジェットの中央に円形で表示される
- BoxShape.rectangle
- ・InkWellではBoxShape.rectangleが固定で使用されている
- ・highlightはウィジェットをfillするような矩形で表示される
- ・borderRadiusでhighlight矩形の角丸を指定可能
highlightShape の値 |
borderRadius の値 |
InkResponseの結果 |
---|---|---|
BoxShape.circle (デフォルト) |
指定なし(0) | * 矩形の角丸はradius=8です |
BoxShape.rectangle |
指定なし(0) | |
BoxShape.rectangle |
24 |
containedInkWell
とhighlightShape
のまとめ
簡単にまとめるとこうなります。
containedInkWell | highlightShape | |
---|---|---|
影響を受けるもの | splashの形 | highlightの形 |
InkResponseのデフォルト値 | false(円状になる) | BoxShape.circle(円状になる) |
InkWellの値 | true(矩形になる) | BoxShape.rectangle(矩形になる) |
ちなみに、英語のinkwell
(wは小文字でひとつの単語)は「インク壺」を示します
以下にCodepenのサンプルコードを示しておきます。
See the Pen wvWVdov by m.kosuke (@kosuke_mtm) on CodePen.
InkResponse
の内部実装
InkResponse
とInkWell
の違いについては以上ですが、内部実装やその他のパラメータについてもいくつか紹介しておきます。
InkResponse
の中身
実はInkResponse
の挙動は_InkResponseStateWidget
→_InkResponseState
へと移譲される形になっています。パラメータもほぼそのまま引き継がれています。
class InkResponse extends StatelessWidget {
@override
Widget build(BuildContext context) {
final _ParentInkResponseState parentState = _ParentInkResponseProvider.of(context);
return _InkResponseStateWidget(...); // InkResponseのパラメータをほぼそのまま引き継ぎます
}
}
class _InkResponseStateWidget extends StatefulWidget {
@override
_InkResponseState createState() => _InkResponseState();
...
}
class _InkResponseState extends State<_InkResponseStateWidget>
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState {
(実装のメイン部分はここに集約されている)
}
以上を前提として、まだ触れていないパラメータについて紹介していきます。以降で紹介するコードは基本的に_InkResponseState
の中の実装となります。
hoverColor
/splashColor
/focusColor
とoverlayColor
パラメータ
hoverColor
/splashColor
/focusColor
は名前そのままで以下の通りです。
パラメータ | 概要 |
---|---|
hoverColor | デスクトップアプリなどでポインタがウィジェットの上に差し掛かった場合に使われるhighlightの色。nullの場合はThemeData.hoverColor が使われる |
splashColor | splashの色。nullの場合はThemeData.splashColor が使われる |
focusColor | ウィジェットにフォーカスが当たっている場合のhighlight色。nullの場合はThemeData.focusColor が使われる |
一方、overlayColor
はMaterialStateProperty
型になっています。
final MaterialStateProperty<Color> overlayColor;
MaterialStateProperty
型は、様々な状態におけるcolorをマッピングして保持しているオブジェクトだとイメージしていただければと思います。そのため、役割はほぼ同じです。overlayColor
が便利となるのは、親ウィジェットからMaterialStateProperty
を受け取る場合などです。
また、overlayColor
が指定されている場合は、こちらが優先して使われます。
MaterialStatePropertyについて
MaterialStateProperty
とは、MaterialState
というenumをkeyに、colorをマッピングしたオブジェクトと考えて良いです(実際には内部で色々な制御が行われていますが、やっていることはHashMapに似ています)。MaterialState
は以下のようなenumで多くの状態を持っていますが、InkResponse
で使われるのはhovered/focused/pressed
のみとなり、それ以外は使われません。
enum MaterialState {
hovered, /// The state when the user drags their mouse cursor over the given widget.
focused, /// The state when the user navigates with the keyboard to a given widget.
pressed, /// The state when the user is actively pressing down on the given widget.
// 以降はInkResponseでは使われない
dragged, /// The state when this widget is being dragged from one place to another by the user.
selected, /// The state when this item has been selected.
disabled, /// The state when this widget disabled and can not be interacted with.
error, /// The state when the widget has entered some form of invalid state.
}
highlightColor
パラメータ
overlayColor
ではcolorが3つ含まれていますが、highlightColor
は含まれていません。これだけは個別で指定する必要があります。理由としては、ソースコードのコメントにて以下のように説明されています。
The [overlayColor] doesn't map a state to [highlightColor] because a separate highlight is not used by the current design guidelines.
See https://material.io/design/interaction/states.html#pressed
Materialデザインのガイドラインでの扱いに依存しているとのことです。
radius
パラメータ
splashやhighlightの円の半径(double型)です。borderRadius
はhighlightShape
が.rectangleのときの角丸の指定ですので、混乱しないようにしましょう。
// splashの半径に利用
InteractiveInkFeature _createInkFeature(Offset globalPosition) {
...
// radiusは直接使われず、splashFactoryの引数に使われる
InteractiveInkFeature splash;
splash = (widget.splashFactory ?? Theme.of(context).splashFactory).create(
...
radius: widget.radius,
);
return splash;
}
// highlightの半径に利用
void updateHighlight(_HighlightType type, { @required bool value, bool callOnHover = true }) {
...
_highlights[type] = InkHighlight(
radius: widget.radius,
...
);
...
}
radiusは_InkResponseState
の中では直接使われることはありません。そのまま、splashFactory(InteractiveInkFeatureFactory
型)やInkHighlight
の引数として使われます。続いてInkHighlight
とsplashFactory
パラメータの説明をしていきます。
InkHighlight
pressed/hover/focus時のhighlight表示に使われます。
enum _HighlightType {
pressed,
hover,
focus,
}
class _InkResponseState ... {
void updateHighlight(_HighlightType type, { @required bool value, bool callOnHover = true }) {
...
_highlights[type] = InkHighlight(
controller: Material.of(context),
referenceBox: referenceBox,
color: getHighlightColorForType(type),
shape: widget.highlightShape,
radius: widget.radius,
borderRadius: widget.borderRadius,
customBorder: widget.customBorder,
rectCallback: widget.getRectCallback(referenceBox),
onRemoved: handleInkRemoval,
textDirection: Directionality.of(context),
fadeDuration: getFadeDurationForType(type),
);
...
}
_HighlightType
というenumで定義された、pressed/hover/focusそれぞれの場合のhighlightが生成されて、フィールドの_highlightsに保持されているようです。pressed/hover/focusのInkHighlight
における差分は、colorとfadeDurationです。この部分は本題と少しそれるため、折りたたみの中に記載します。
typeによるInkHighlightの差分
fadeDurationは簡単で、以下のような実装になっています。
Duration getFadeDurationForType(_HighlightType type) {
switch (type) {
case _HighlightType.pressed:
return const Duration(milliseconds: 200);
case _HighlightType.hover:
case _HighlightType.focus:
return const Duration(milliseconds: 50);
}
assert(false, 'Unhandled $_HighlightType $type');
return null;
}
また、colorは以下のようになっています。
Color getHighlightColorForType(_HighlightType type) {
const Set<MaterialState> focused = <MaterialState>{MaterialState.focused};
const Set<MaterialState> hovered = <MaterialState>{MaterialState.hovered};
switch (type) {
case _HighlightType.pressed:
// overlayColorは参照しない
return widget.highlightColor ?? Theme.of(context).highlightColor;
case _HighlightType.focus:
return widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? Theme.of(context).focusColor;
case _HighlightType.hover:
return widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? Theme.of(context).hoverColor;
}
assert(false, 'Unhandled $_HighlightType $type');
return null;
}
基本的にInkResponse
のhighlightColor
,focusColor
,hoverColor
を使い、nullの場合はThemeから引っ張ってきています。が、focus/hoverの場合はその前にoverlayColor
を参照しています。(前述のとおり、highlightColor
はoverlayColor
に含まれません)
ちなみに何も指定しない場合の色はThemeDataを見るとわかります。
const Color _kLightThemeHighlightColor = Color(0x66BCBCBC);
const Color _kDarkThemeHighlightColor = Color(0x40CCCCCC);
const Color _kLightThemeSplashColor = Color(0x66C8C8C8);
const Color _kDarkThemeSplashColor = Color(0x40CCCCCC);
factory ThemeData({
...
}) {
hoverColor ??= isDark ? Colors.white.withOpacity(0.04) : Colors.black.withOpacity(0.04);
focusColor ??= isDark ? Colors.white.withOpacity(0.12) : Colors.black.withOpacity(0.12);
highlightColor ??= isDark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor;
splashColor ??= isDark ? _kDarkThemeSplashColor : _kLightThemeSplashColor;
...
}
LightTheme(背景が白) | DarkTheme(背景が黒) |
---|---|
InkHighlight
の中でradius
パラメータ(_radius)を使っている箇所は以下のとおりです。
class InkHighlight extends InteractiveInkFeature {
...
void _paintHighlight(Canvas canvas, Rect rect, Paint paint) {
...
switch (_shape) {
case BoxShape.circle:
canvas.drawCircle(rect.center, _radius ?? Material.defaultSplashRadius, paint);
break;
case BoxShape.rectangle:
if (_borderRadius != BorderRadius.zero) {
final RRect clipRRect = RRect.fromRectAndCorners(
rect,
topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight,
bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight,
);
canvas.drawRRect(clipRRect, paint);
} else {
canvas.drawRect(rect, paint);
}
break;
}
canvas.restore();
}
InkResponse
のhighlightShape
(BoxShape型)を参照しており、.circle
の場合は指定された_radius
をそのまま使ってdrawCircleを行っています。nullの場合はMaterial.defaultSplashRadius
(35.0で定義されている)を使っています。
一方、.rectangle
の場合は矩形のため、_radius
は使われません。代わりに_borderRadius
の有無でdrawRect/drawRRectの使い分けが行われています。
highlightColorを赤でわかりやすくしてまとめました。
See the Pen InkHighligh by m.kosuke (@kosuke_mtm) on CodePen.
splashFactory
パラメータ
splashFactory
はInteractiveInkFeatureFactory
型です。定義は以下のとおりです。
abstract class InteractiveInkFeatureFactory {
const InteractiveInkFeatureFactory();
@factory
InteractiveInkFeature create({
@required MaterialInkController controller,
@required RenderBox referenceBox,
@required Offset position,
@required Color color,
@required TextDirection textDirection,
bool containedInkWell = false,
RectCallback rectCallback,
BorderRadius borderRadius,
ShapeBorder customBorder,
double radius,
VoidCallback onRemoved,
});
}
このabstractクラスを継承したものとして、以下の2つが提供されています。
InkSplash.splashFactory | InkRipple.splashFactory |
---|---|
デフォルトで使われる | デフォルトよりも外側へ広がっていく |
containedInkWellがfalseの場合は半径35.0固定、trueの場合は矩形全体がカバーできる半径を計算 | ウィジェット全体が囲われるように広がる |
詳細はコードをみてもらった方がわかりやすいかと思います。順に説明していきます。
InkSplash
class _InkSplashFactory extends InteractiveInkFeatureFactory {
const _InkSplashFactory();
@override
InteractiveInkFeature create({...}) {
return InkSplash(...);
}
}
class InkSplash extends InteractiveInkFeature {
static const InteractiveInkFeatureFactory splashFactory = _InkSplashFactory();
final double _targetRadius;
Animation<double> _radius; // _radiusはアニメーション
AnimationController _radiusController;
InkSplash({...
double radius,
bool containedInkWell = false,
}) : ...
// 引数の[radius]がnullの場合は自前で計算する
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position) {
// 初期化処理。計算された[_targetRadius]を使ってsplashアニメーションを定義
_radius = _radiusController.drive(Tween<double>(
begin: _kSplashInitialSize, // 0.0
end: _targetRadius,
));
...
}
...
}
/// 引数で[radius]が指定されなかった場合は自前で計算し、[_targetRadius]に設定します
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
if (containedInkWell) {
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
return _getSplashRadiusForPositionInSize(size, position);
}
// 35.0で定義されている
return Material.defaultSplashRadius;
}
/// タッチ位置とウィジェットの矩形から、タッチ位置を中心に円を描く際、矩形全体を覆うことのできる半径を計算しています
double _getSplashRadiusForPositionInSize(Size bounds, Offset position) {
final double d1 = (position - bounds.topLeft(Offset.zero)).distance;
final double d2 = (position - bounds.topRight(Offset.zero)).distance;
final double d3 = (position - bounds.bottomLeft(Offset.zero)).distance;
final double d4 = (position - bounds.bottomRight(Offset.zero)).distance;
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
}
言葉で並べると下記のようになります。
- 引数でradiusが指定されなかった場合は_getTargetRadiusにて計算
-
containedInkWell
がtrueの場合は、タッチ位置とウィジェットの矩形から、タッチ位置を中心に円を描く際、矩形全体を覆うことのできる半径を計算(rectCallbackで矩形を指定することも可能) -
containedInkWell
がfalseの場合は35.0の固定値が使用 - 計算された
_targetRadius
を使ってAnimation(_radius
)を生成 - beginは0.0が指定
- endに上で計算された
_targetRadius
が使用される
つまり、
- splashは半径0から指定半径まで広がる
- 半径の指定がない場合は半径35のサイズまで広がる
- ただし、
containedInkWell
=trueの場合は矩形全体が覆われるようにsplashが広がる
という挙動となります。
InkRipple
class _InkRippleFactory extends InteractiveInkFeatureFactory {
const _InkRippleFactory();
@override
InteractiveInkFeature create({...}) {
return InkRipple(...);
}
}
class InkRipple extends InteractiveInkFeature {
static const InteractiveInkFeatureFactory splashFactory = _InkRippleFactory();
final double _targetRadius;
Animation<double> _radius; // _radiusはアニメーション
AnimationController _radiusController;
InkRipple({...
double radius,
bool containedInkWell = false,
}) : ...
_targetRadius = radius ?? _getTargetRadius(referenceBox, containedInkWell, rectCallback, position)
{
// 初期化処理。計算された[_targetRadius]を使ってsplashアニメーションを定義
_radius = _radiusController.drive(
Tween<double>(
begin: _targetRadius * 0.30,
end: _targetRadius + 5.0,
).chain(_easeCurveTween),
);
...
}
}
/// 矩形の対角線距離の半分を半径として算出しています。
/// [InkSplash]と同様の引数を受け取っていますが、containedInkWell、positionは使っていません。
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
final double d1 = size.bottomRight(Offset.zero).distance;
final double d2 = (size.topRight(Offset.zero) - size.bottomLeft(Offset.zero)).distance;
return math.max(d1, d2) / 2.0;
}
流れはほぼInkSplash
と同じですね。
- 引数でradiusが指定されなかった場合は_getTargetRadiusにて計算
- タッチ位置とウィジェットの矩形から、タッチ位置を中心に円を描く際、矩形全体を覆うことのできる半径を計算(rectCallbackで矩形を指定することも可能)
-
containedInkWell
は見ない - 計算された
_targetRadius
を使ってAnimation(_radius
)を生成 - beginは
_targetRadius
の30%サイズ - endは
_targetRadius
プラス5
つまり、必ず矩形全体を覆うサイズまでsplashが広がります。また、splashは矩形の一回り大きなサイズ(+5の分)にすることで見栄えがよくなっています。startはゼロではなく、30%のサイズから開始するのは、アニメーションの動きが大きくなりすぎることを避けるためでしょう。
InkSplash
とInkRipple
の比較
以上をgifにしてまとめてみました。
ここまで読んできて頂いた方ならわかると思いますが35.0
はコード上で定義されたデフォルトの固定値、+5.0
の一回り大きくエフェクトを描画するための固定マージン値です。また、矩形の場合も実際にはウイジェット全体を囲うことのできる円の半径を計算して描画されていますが、矩形状にclipされているので円であることはわかりづらくなっています。
See the Pen InkSplash/InkRipple by m.kosuke (@kosuke_mtm) on CodePen.
Ink
InkResponse
とInkWell
がタッチエフェクトを描画するウィジェットだったのに対し、Ink
単体ではエフェクトの描画はできません。まず、splashの描画方法についてお話して、その上でInk
の役割について説明します。
splashはどこで描画されるのか?
イメージ的には、それぞれのウィジェットの表面に描画されるように思われるかもしれません。しかし、実際はウィジェットツリーの親のMaterial
ウィジェット上で描画されます。つまり、splashの描画面はZ軸方向では他のウィジェットの下に位置することになります。
そのため、InkResponse
/InkWell
は必ずMaterial
ウィジェットの階層下に配置する必要があります。しかし、InkResponse
/InkWell
とMaterial
の間に不透明なウィジェット(例えば色のついたContainer
や画像など)が存在すると、Material
が隠れてしまい、その表面に展開されるsplashが見えなくなってしまいます。
実際にこの事象に悩まされる人は多いんじゃないかと思います。私も過去に悩まされたときに記事を書きました。
Ink
の使い方
そこでInk
の出番です。splashを阻害している不透明なウィジェットの『代わり』にInk
ウィジェットを配置して使います。
主な使い方は以下の3パターンです。
- 背景色を
Material
とは異なる色に変更 -
Decoration
を使ってMaterial
とは異なる背景に変更 - 画像を背景に設定
順に説明していきます。
(1)背景色をMaterial
とは異なる色に変更
Container
で背景色をつけている場合などに使えます。
Material( // 親にMaterialが必須
color: Colors.blue, // Material自体は青を指定
child: Center(
child: Ink( // ここをContainerにするとsplashが効かないので、Inkに変える
color: Colors.yellow, // 背景色はここで指定
width: 200.0,
height: 100.0,
child: InkWell(
onTap: () { },
child: Center(child: Text('YELLOW'))
),
),
),
)
あくまで、『背景色をつけるためにContainer
の代わりにInk
が使える』というだけで、『不透明なウィジェットがあってもInkで無効化できる』わけではないことに注意してください。(下記表参照)
コード | 実行結果 |
---|---|
MaterialとInkWellの間に不透明なウィジェットがある場合 | |
Inkを使って背景色を変更 | |
Inkの誤った使い方 |
*コードは一部省略しています
(2)Decoration
を使ってMaterial
とは異なる背景に変更
colorを使う場合は単色でしたが、グラデーションをかけたい場合などはこのDecorationを使うことになります。
Material(
color: Colors.blue,
child: Ink(
decoration: BoxDecoration( // Decorationを使って背景を変更
gradient: LinearGradient(
colors: [Colors.green, Colors.greenAccent],
),
),
width: 200.0,
height: 50.0,
child: InkWell(
onTap: () {},
child: Center(child: Text('Decoration'))
),
),
);
(3)画像を背景に設定
Material(
color: Colors.grey[800],
child: Center(
child: Ink.image( // Inkのfactoryで画像を指定するものがあります
image: NetworkImage('https://......'),
width: 300.0,
height: 200.0,
child: InkWell(
onTap: () { },
child: Center(child: Text('IMAGE')),
),
),
),
)
(*4)MaterialType.transparency
Ink
を使う方法以外にも、splashを効かせる方法があります。もうひとつMaterial
を置いてしまう方法です。
不透明なウィジェットの上にMaterial
を配置し、そのMaterial
自体は透過にするため、MaterialType.transparency
を指定することで実現できます。
Material(
color: Colors.blue,
child: Container(
color: Colors.yellow, // どうしても不透明なウィジェットがある場合
width: 200.0,
height: 50.0,
child: Material(
type: MaterialType.transparency, // このMaterial自体は透過にする
child: InkWell(
onTap: () { },
child: Center(child: Text('MaterialTypeを使う方法')),
),
),
),
)
ちなみにMaterialType
はenumで、他にcanvas
/card
/circle
/button
があります。
以上のことを確認できるサンプルコードを下記においておきます。
See the Pen How to use Ink by m.kosuke (@kosuke_mtm) on CodePen.
Ink
の実装内容
ここからは、気になる方だけ読んでいただければと思います。
まずInk
のコンストラクタは以下のようになっています。
class Ink extends StatefulWidget {
Ink({
Key key,
this.padding,
Color color,
Decoration decoration,
this.width,
this.height,
this.child,
}) : assert(color == null || decoration == null,
'Cannot provide both a color and a decoration\n'
'The color argument is just a shorthand for "decoration: BoxDecoration(color: color)".'
),
decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
super(key: key);
final Decoration decoration;
assertの部分について、assert(color == null || decoration == null
となっていて、colorまたはdecorationどちらか片方のみが定義されていることが必須となっています。また、color指定時も初期化処理の中でdecorationに変換されてフィールドで保持されるようになっています。
decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null)
Imageを使ったfactoryでも同様にdecorationとして保持されています。
Ink.image({
Key key,
this.padding,
@required ImageProvider image,
ImageErrorListener onImageError,
...
}) : assert(image != null),
decoration = BoxDecoration(
image: DecorationImage(
image: image,
...
),
),
super(key: key);
このように初期化時に生成したdecorationは、splashを描画するInkDecoration
に渡されることでsplashの背景として描画されます。
class Ink extends StatefulWidget {
...
final Decoration decoration;
...
}
class _InkState extends State<Ink> {
InkDecoration _ink;
...
Widget _build(BuildContext context, BoxConstraints constraints) {
if (_ink == null) {
_ink = InkDecoration(
decoration: widget.decoration, // ここでdecorationが使われる
controller: Material.of(context),
referenceBox: context.findRenderObject() as RenderBox,
onRemoved: _handleRemoved,
);
} else {
_ink.decoration = widget.decoration;
}
...
}
}
まとめ
名前のよく似たInk
/InkResponse
/InkWell
の紹介をしました。
長々と難しく書いてしまいましたが、乱暴な言い方をすると、Ripple Effectを使いたい場合はInkWell
を使い、InkWell
のsplashが表示されない場合はInk
の使用を検討する、とまずは覚えていただければ良いのではないでしょうか。まずは一度使った上で読み直していただけると理解が深まるかと思います。
twitterもよろしければフォローお願いします。
@kosuke_mtm