LoginSignup
103
50

More than 3 years have passed since last update.

[Flutter] InkResponse と InkWell と Ink、違いを説明できますか?

Last updated at Posted at 2020-12-10

Flutter Advent Calendar#2 の11日目です。昨年はEffective Dartまとめで参加させていただきました。今年はMaterialウィジェットについて書こうと思いましたが、量が半端なくなりそうでしたので、掲題のようにターゲットを絞った内容にしてみました。

InkResponseInkWellInk

どれもタッチ時のsplash(Ripple)エフェクトを表現してくれるウィジェットで、FlutterのMaterial Designのアプリケーションには欠かせないものですが、名前だけでは違いはよくわかりません。本記事ではこれらの違いを詳しく紹介します。

まずはInkResponseInkWellの比較、その後、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と併用する

InkResponseInkWell

まずは挙動をみてみます。

InkResponse InkWell
inkresponse.gif inkwell.gif
概念図 InkResponse公式Docより InkWell公式Docより
hover/press/focus時のhighlight ウィジェット中央に円形 ウィジェット全体の矩形
press時のsplash 円形のままウィジェット中央へ移動 矩形の中を同心円状に広がる

InkResponseは、ウィジェットの中央に円形のエフェクト(highlight)が表示されます。サンプルは矩形のボタンにしていますが、アイコンボタンなどが合っていると思います。一方、InkWellはボタンなどでよく見る矩形で一般的なMaterial Designのウィジェットのような挙動をしています。

ここでの用語としてhighlightsplashの違いは把握しておきましょう。highlightはhover/pressの瞬間に表示されるもので、splashはアニメーションを伴うエフェクト表示です。

コードレベルの比較

実は、InkWellInkResponseを継承しており、以下のようになっています。

ink_well.dart
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独自の実装は存在せず、コンストラクタでcontainedInkWellhighlightShapeの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) contained_false.gif* 矩形の角丸はradius=8です
true 指定なし(0) contained_true.gif
true 20 contained_true_r.gif

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) shape_circle.gif* 矩形の角丸はradius=8です
BoxShape.rectangle 指定なし(0) shape_rect.gif
BoxShape.rectangle 24 shape_rrect.gif

containedInkWellhighlightShapeのまとめ

簡単にまとめるとこうなります。

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の内部実装

InkResponseInkWellの違いについては以上ですが、内部実装やその他のパラメータについてもいくつか紹介しておきます。

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/focusColoroverlayColorパラメータ

hoverColor/splashColor/focusColorは名前そのままで以下の通りです。

パラメータ 概要
hoverColor デスクトップアプリなどでポインタがウィジェットの上に差し掛かった場合に使われるhighlightの色。nullの場合はThemeData.hoverColorが使われる
splashColor splashの色。nullの場合はThemeData.splashColorが使われる
focusColor ウィジェットにフォーカスが当たっている場合のhighlight色。nullの場合はThemeData.focusColorが使われる

一方、overlayColorMaterialStateProperty型になっています。

final MaterialStateProperty<Color> overlayColor;

MaterialStateProperty型は、様々な状態におけるcolorをマッピングして保持しているオブジェクトだとイメージしていただければと思います。そのため、役割はほぼ同じです。overlayColorが便利となるのは、親ウィジェットからMaterialStatePropertyを受け取る場合などです。

また、overlayColorが指定されている場合は、こちらが優先して使われます。

MaterialStatePropertyについて

MaterialStatePropertyとは、MaterialStateというenumをkeyに、colorをマッピングしたオブジェクトと考えて良いです(実際には内部で色々な制御が行われていますが、やっていることはHashMapに似ています)。MaterialStateは以下のようなenumで多くの状態を持っていますが、InkResponseで使われるのはhovered/focused/pressedのみとなり、それ以外は使われません。

materia_state.dart
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型)です。borderRadiushighlightShapeが.rectangleのときの角丸の指定ですので、混乱しないようにしましょう。

_InkResponseState
  // 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の引数として使われます。続いてInkHighlightsplashFactoryパラメータの説明をしていきます。

InkHighlight

pressed/hover/focus時のhighlight表示に使われます。

_InkResponseState

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は簡単で、以下のような実装になっています。

_InkResponseState
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は以下のようになっています。

_InkResponseState
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;
}

基本的にInkResponsehighlightColor,focusColor,hoverColorを使い、nullの場合はThemeから引っ張ってきています。が、focus/hoverの場合はその前にoverlayColorを参照しています。(前述のとおり、highlightColoroverlayColorに含まれません)

ちなみに何も指定しない場合の色はThemeDataを見るとわかります。

theme_data.dart
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(背景が黒)
image.png image.png

InkHighlightの中でradiusパラメータ(_radius)を使っている箇所は以下のとおりです。

ink_highlight.dart
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();
  }

InkResponsehighlightShape(BoxShape型)を参照しており、.circleの場合は指定された_radiusをそのまま使ってdrawCircleを行っています。nullの場合はMaterial.defaultSplashRadius(35.0で定義されている)を使っています。
一方、.rectangleの場合は矩形のため、_radiusは使われません。代わりに_borderRadiusの有無でdrawRect/drawRRectの使い分けが行われています。

highlightColorを赤でわかりやすくしてまとめました。

highlightShape radius borderRadius 描画形状
.circle null (使用しない) drawCircle(35.0)image.png
.circle 指定あり (使用しない) drawCircle(radius)image.png
.rectangle (使用しない) null drawRect()image.png
.rectangle (使用しない) 指定あり drawRRect(borderRadius)image.png

See the Pen InkHighligh by m.kosuke (@kosuke_mtm) on CodePen.

splashFactoryパラメータ

splashFactoryInteractiveInkFeatureFactory型です。定義は以下のとおりです。

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
デフォルトで使われる デフォルトよりも外側へ広がっていく
splash_splash.gif splash_ripple.gif
containedInkWellがfalseの場合は半径35.0固定、trueの場合は矩形全体がカバーできる半径を計算 ウィジェット全体が囲われるように広がる

詳細はコードをみてもらった方がわかりやすいかと思います。順に説明していきます。

InkSplash
InkSplash.splashFactory
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();
}

言葉で並べると下記のようになります。

  1. 引数でradiusが指定されなかった場合は_getTargetRadiusにて計算
    1. containedInkWellがtrueの場合は、タッチ位置とウィジェットの矩形から、タッチ位置を中心に円を描く際、矩形全体を覆うことのできる半径を計算(rectCallbackで矩形を指定することも可能)
    2. containedInkWellがfalseの場合は35.0の固定値が使用
  2. 計算された_targetRadiusを使ってAnimation(_radius)を生成
    1. beginは0.0が指定
    2. endに上で計算された_targetRadiusが使用される

つまり、
- splashは半径0から指定半径まで広がる
- 半径の指定がない場合は半径35のサイズまで広がる
- ただし、containedInkWell=trueの場合は矩形全体が覆われるようにsplashが広がる
という挙動となります。

InkRipple
InkRipple.splashFactory
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と同じですね。

  1. 引数でradiusが指定されなかった場合は_getTargetRadiusにて計算
    1. タッチ位置とウィジェットの矩形から、タッチ位置を中心に円を描く際、矩形全体を覆うことのできる半径を計算(rectCallbackで矩形を指定することも可能)
    2. containedInkWellは見ない
  2. 計算された_targetRadiusを使ってAnimation(_radius)を生成
    1. beginは_targetRadiusの30%サイズ
    2. endは_targetRadiusプラス5

つまり、必ず矩形全体を覆うサイズまでsplashが広がります。また、splashは矩形の一回り大きなサイズ(+5の分)にすることで見栄えがよくなっています。startはゼロではなく、30%のサイズから開始するのは、アニメーションの動きが大きくなりすぎることを避けるためでしょう。

InkSplashInkRippleの比較

以上をgifにしてまとめてみました。

InkSplash InkRipple
デフォルト ink.gifhighlight/splashの半径は35.0 ripple.gifhighlightの半径は35.0、splashは矩形+5.0の円
radius=24指定 ink_r.gifhighlight/splashの半径は指定した24.0 ripple_r.gifhighlightは24.0、splashは24.0+5.0
containedInkWell=true ink_well.gifhighlightの半径は35.0、splashは矩形状 ripple_well.gifhighlightの半径は35.0、splashは矩形状
radius指定/containedInkWell=true ink_r_well.gifhighlight/splashの半径は24.0 ripple_r_well.gifhighlightの半径は24.0、splashの半径は24+5

ここまで読んできて頂いた方ならわかると思いますが35.0はコード上で定義されたデフォルトの固定値、+5.0の一回り大きくエフェクトを描画するための固定マージン値です。また、矩形の場合も実際にはウイジェット全体を囲うことのできる円の半径を計算して描画されていますが、矩形状にclipされているので円であることはわかりづらくなっています。

See the Pen InkSplash/InkRipple by m.kosuke (@kosuke_mtm) on CodePen.


Ink

InkResponseInkWellがタッチエフェクトを描画するウィジェットだったのに対し、Ink単体ではエフェクトの描画はできません。まず、splashの描画方法についてお話して、その上でInkの役割について説明します。

splashはどこで描画されるのか?

イメージ的には、それぞれのウィジェットの表面に描画されるように思われるかもしれません。しかし、実際はウィジェットツリーの親のMaterialウィジェット上で描画されます。つまり、splashの描画面はZ軸方向では他のウィジェットの下に位置することになります。

そのため、InkResponse/InkWellは必ずMaterialウィジェットの階層下に配置する必要があります。しかし、InkResponse/InkWellMaterialの間に不透明なウィジェット(例えば色のついたContainerや画像など)が存在すると、Materialが隠れてしまい、その表面に展開されるsplashが見えなくなってしまいます。
実際にこの事象に悩まされる人は多いんじゃないかと思います。私も過去に悩まされたときに記事を書きました。

Inkの使い方

そこでInkの出番です。splashを阻害している不透明なウィジェットの『代わり』にInkウィジェットを配置して使います。
主な使い方は以下の3パターンです。

  1. 背景色をMaterialとは異なる色に変更
  2. Decorationを使ってMaterialとは異なる背景に変更
  3. 画像を背景に設定

順に説明していきます。

(1)背景色をMaterialとは異なる色に変更

Containerで背景色をつけている場合などに使えます。

Inkの使用例:(1)背景色を付けたい場合
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の間に不透明なウィジェットがある場合image.png ink_err.gif
Inkを使って背景色を変更image.png ink_ok.gif
Inkの誤った使い方image.png ink_err2.gif

*コードは一部省略しています

(2)Decorationを使ってMaterialとは異なる背景に変更

colorを使う場合は単色でしたが、グラデーションをかけたい場合などはこのDecorationを使うことになります。

Inkの使用例:(2)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)画像を背景に設定

Inkの使用例:(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のコンストラクタは以下のようになっています。

ink_decoration.dart
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の背景として描画されます。

ink_decoration.dart
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

103
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
103
50