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
InkWell
Inheritance relationships
InkWell
The 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
InkWell
that
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
GestureDetector
Called directly or indirectly in a callback methodInkFeature
的dispose()
State
The life cycledeactivate()
Method (application back to background or page jump will be called, pop upDialog
Will not call) directly or indirectlyInkFeature
的dispose()
conclusion
InkWell
In response toGestureDetector
的onTapDown()
Created during the callbackInkSplash
,InkHighlight
(all isInkFeature
Subclasses, each implementedpaintFeature()
)InkSplash
,InkHighlight
Add yourself to create_RenderInkFeatures
的InkFeature
In the queueInkWell
的Material
Ancestors inbuild()
“Will be called_RenderInkFeatures
的paint()
_RenderInkFeatures
的paint()
Will traverseInkFeature
Queue and callInkFeature
的paintFeature()
Draw animation EffectsGestureDetector
The callback method orState
The life cycledeactivate()
Method is called directly or indirectlyInkFeature
的dispose()
InkFeature
的dispose()
Transform itself from_RenderInkFeatures
的InkFeature
Queue removed, animation ends
@123lxw123, the copyright of this article belongs to Zaihui RESEARCH and development team, welcome to reprint, please reserve the source.