background

An interesting bug I recently encountered while developing the Flutter project is that if a page pops up a Dialog during an InkWell animation, InkWell’s animation will not disappear, as shown in the upper right corner of the image below. Take this as an opportunity to explore and analyze InkWell’s source code

An overview of the

InkWell is a Widget that can be used to create Material touch waves with Flutter, the equivalent of Ripple in Android

InkWellInheritance relationships

InkWellThe source code

class InkWell extends InkResponse {
  /// Creates an ink well.
  ///
  /// Must have an ancestor [Material] widget in which to cause ink reactions.
  ///
  /// The [enableFeedback] and [excludeFromSemantics] arguments must not be
  /// null.
  constInkWell({ Key key, Widget child, ... omitbool enableFeedback = true.bool excludeFromSemantics = false,}) :super( key: key, child: child, ... Omit containedInkWell:true, highlightShape: BoxShape.rectangle, ... EnableFeedback: enableFeedback, excludeFromSemantics: excludeFromSemantics,); }Copy the code

The source code is very simple. It’s an InkResponse with a specific attribute value, a special case of InkResponse

InkWellthat

child
highlight
splash

Animation analysis based onInkResponse

Analysis methods

From the display effect, animation starts after touching InkWell, so start with GestureDetector

@overrideWidget build(BuildContext context) { ... omitreturn GestureDetector(
    onTapDown: enabled ? _handleTapDown : null,
    onTap: enabled ? () => _handleTap(context) : null,
    onTapCancel: enabled ? _handleTapCancel : null, onDoubleTap: widget.onDoubleTap ! =null ? _handleDoubleTap : null, onLongPress: widget.onLongPress ! =null ? () => _handleLongPress(context) : null,
    behavior: HitTestBehavior.opaque,
    child: widget.child,
    excludeFromSemantics: widget.excludeFromSemantics,
  );
}
Copy the code

Looking at the onTapDown callback, _createInkFeature(Details) and updateHighlight(true) start the Splash splash and Highlight background animations, respectively

void _handleTapDown(TapDownDetails details) {
  finalInteractiveInkFeature splash = _createInkFeature(details); _splashes ?? = HashSet<InteractiveInkFeature>(); _splashes.add(splash); _currentSplash = splash;if(widget.onTapDown ! =null) {
    widget.onTapDown(details);
  }
  updateKeepAlive();
  updateHighlight(true);
}
Copy the code

Then look at _createInkFeature(Details). The water ripple animation spreads around the touch point, The TapDownDetails argument to _handleTapDown(TapDownDetails) provides a pointer position; If you click on the create method inside the InteractiveInkFeature, it will enter the source code directly. In fact, it is a parent class, and the animation implementation is empty. What really makes Splash splash splash is its subclass, InkSplash

InteractiveInkFeature _createInkFeature(TapDownDetails details) { ... Omit splash = (widget.splashFactory?? Theme.of(context).splashFactory).create( referenceBox: referenceBox, position: position, ... Omitted);return splash;
}
Copy the code

Then we go to updateHighlight(true), and InkHighlight animates the background of highlight

void updateHighlight(boolvalue) { ... omitif (_lastHighlight == null) {
      finalRenderBox referenceBox = context.findRenderObject(); _lastHighlight = InkHighlight( controller: Material.of(context), referenceBox: referenceBox, ... Omit updateKeepAlive (); }else {
      _lastHighlight.activate();
    }
    ... 省略
  }
Copy the code

animation

Inheritance relationships

You can see that these two are actually brothers, they share a common ancestor

The InteractiveInkFeature defines two empty methods and implements an ink color get and set method, indicating that the animation interface definition is still in the upper interface, namely InkFeature

abstract class InteractiveInkFeature extends InkFeature {... omitvoid confirm() {
  }
  void cancel() {
  }
  /// The ink's color.
  Color get color => _color;
  Color _color;
  set color(Color value) {
    if (value == _color)
      return; _color = value; controller.markNeedsPaint(); }}Copy the code

The key interface method is paintFeature(). Next, let’s look at the implementation of InkSplash and InkHighlight

abstract class InkFeature {... omit///
  /// The transform argument gives the coordinate conversion from the coordinate
  /// system of the canvas to the coordinate system of the [referenceBox].
  @protected
  void paintFeature(Canvas canvas, Matrix4 transform);
}
Copy the code

InkSplash,InkHighlight

@override
void paintFeature(Canvas canvas, Matrix4 transform) {
  // Get the background color. _alpha is Animation
      
        and splash controls the color from light to dark
      
  finalPaint paint = Paint().. color = color.withAlpha(_alpha.value);// The center point of the water ripple effect, from which it diffuses outward
  Offset center = _position;
  if (_repositionToReferenceBox)
    center = Offset.lerp(center, referenceBox.size.center(Offset.zero), _radiusController.value);
    // Matrix transformation
  final Offset originOffset = MatrixUtils.getAsTranslation(transform);
  canvas.save();
  if (originOffset == null) {
    canvas.transform(transform.storage);
  } else {
    canvas.translate(originOffset.dx, originOffset.dy);
  }
  // Define the water ripple boundary
  if(_clipCallback ! =null) {
    final Rect rect = _clipCallback();
    if(_customBorder ! =null) {
      canvas.clipPath(_customBorder.getOuterPath(rect, textDirection: _textDirection));
    } else if(_borderRadius ! = BorderRadius.zero) { canvas.clipRRect(RRect.fromRectAndCorners( rect, topLeft: _borderRadius.topLeft, topRight: _borderRadius.topRight, bottomLeft: _borderRadius.bottomLeft, bottomRight: _borderRadius.bottomRight, )); }else{ canvas.clipRect(rect); }}_radius is Animation
      
       . The diffusion effect of the water ripple is caused by the change of its value from small to large
      
  canvas.drawCircle(center, _radius.value, paint);
  canvas.restore();
}
Copy the code

InkHighlight is relatively simple. It works the same way as InkSplash, except that the animation only changes the color transparency

Open animation

InkWell’s source code has a comment at the beginning of this article, which is a key piece of information. By tracing the callers of InkFeature’s paintFeature() method, you can see that the result points to _MaterialState

 /// Must have an ancestor [Material] widget in which to cause ink reactions.
Copy the code
class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController {... omitList<InkFeature> _inkFeatures;

  // addInkFeature() is called at the end of both the InkSplash and InkHighlight constructors.
  @override
  void addInkFeature(InkFeature feature) {
    assert(! feature._debugDisposed);assert(feature._controller == this); _inkFeatures ?? = <InkFeature>[];assert(! _inkFeatures.contains(feature)); _inkFeatures.add(feature); markNeedsPaint(); }// InkFeature dispose() call _removeFeature()
  void _removeFeature(InkFeature feature) {
    assert(_inkFeatures ! =null);
    _inkFeatures.remove(feature);
    markNeedsPaint();
  }
  
  @override
  void paint(PaintingContext context, Offset offset) {
    if(_inkFeatures ! =null && _inkFeatures.isNotEmpty) {
      final Canvas canvas = context.canvas;
      canvas.save();
      canvas.translate(offset.dx, offset.dy);
      canvas.clipRect(Offset.zero & size);
      // Loop over all inkfeatures and call their _paint() to draw the display
      for (InkFeature inkFeature in _inkFeatures)
        inkFeature._paint(canvas);
      canvas.restore();
    }
    super.paint(context, offset); }}Copy the code
class _MaterialState extends State<Material> with TickerProviderStateMixin {
  final GlobalKey _inkFeatureRenderer = GlobalKey(debugLabel: 'ink renderer'); . omit@overrideWidget build(BuildContext context) { ... Omit onNotification: (LayoutChangedNotification notification) {// _MaterialState builds with splash splash and Highlight background animation, which confirms the annotation that InkWell must have a Material ancestor in the drawing tree
        final _RenderInkFeatures renderer = _inkFeatureRenderer.currentContext.findRenderObject();
        renderer._didChangeLayout();
        return true;
      },
      child: _InkFeatures(
        key: _inkFeatureRenderer,
        color: backgroundColor,
        child: contents,
        vsync: this)); . Omit}}Copy the code

End of the animation

There are two main times when an animation ends. Go back to InkResponse and look at a piece of source code

class InkResponse extends StatefulWidget {... omitvoid_handleTap(BuildContext context) { _currentSplash? .confirm(); _currentSplash =null;
    updateHighlight(false);
    if(widget.onTap ! =null) {
      if(widget.enableFeedback) Feedback.forTap(context); widget.onTap(); }}void_handleTapCancel() { _currentSplash? .cancel(); _currentSplash =null;
    if(widget.onTapCancel ! =null) {
      widget.onTapCancel();
    }
    updateHighlight(false);
  }

  void_handleDoubleTap() { _currentSplash? .confirm(); _currentSplash =null;
    if(widget.onDoubleTap ! =null)
      widget.onDoubleTap();
  }

  void_handleLongPress(BuildContext context) { _currentSplash? .confirm(); _currentSplash =null;
    if(widget.onLongPress ! =null) {
      if(widget.enableFeedback) Feedback.forLongPress(context); widget.onLongPress(); }}@override
  void deactivate() {
    if(_splashes ! =null) {
      final Set<InteractiveInkFeature> splashes = _splashes;
      _splashes = null;
      for (InteractiveInkFeature splash in splashes)
        splash.dispose();
      _currentSplash = null;
    }
    assert(_currentSplash == null); _lastHighlight? .dispose(); _lastHighlight =null;
    super.deactivate();
  }

  @overrideWidget build(BuildContext context) { ... omitreturn GestureDetector(
      onTapDown: enabled ? _handleTapDown : null,
      onTap: enabled ? () => _handleTap(context) : null,
      onTapCancel: enabled ? _handleTapCancel : null, onDoubleTap: widget.onDoubleTap ! =null ? _handleDoubleTap : null, onLongPress: widget.onLongPress ! =null ? () => _handleLongPress(context) : null, behavior: HitTestBehavior.opaque, child: widget.child, excludeFromSemantics: widget.excludeFromSemantics, ); }}Copy the code
  • GestureDetectorCalled directly or indirectly in a callback methodInkFeaturedispose()
  • StateThe life cycledeactivate()Method (application back to background or page jump will be called, pop upDialogWill not call) directly or indirectlyInkFeaturedispose()

conclusion

  • InkWellIn response toGestureDetectoronTapDown()Created during the callbackInkSplash,InkHighlight(all isInkFeatureSubclasses, each implementedpaintFeature())
  • InkSplash,InkHighlightAdd yourself to create_RenderInkFeaturesInkFeatureIn the queue
  • InkWellMaterialAncestors inbuild()“Will be called_RenderInkFeaturespaint()
  • _RenderInkFeaturespaint()Will traverseInkFeatureQueue and callInkFeaturepaintFeature()Draw animation Effects
  • GestureDetectorThe callback method orStateThe life cycledeactivate()Method is called directly or indirectlyInkFeaturedispose()
  • InkFeaturedispose()Transform itself from_RenderInkFeaturesInkFeatureQueue removed, animation ends

@123lxw123, the copyright of this article belongs to Zaihui RESEARCH and development team, welcome to reprint, please reserve the source.