Unverified Commit ac3189c3 authored by chunhtai's avatar chunhtai Committed by GitHub

Remove RenderEditable dependency from TextSelectionHandleOverlay (#97967)

parent 3afbec88
...@@ -62,10 +62,6 @@ enum TextSelectionHandleType { ...@@ -62,10 +62,6 @@ enum TextSelectionHandleType {
collapsed, collapsed,
} }
/// The text position that a give selection handle manipulates. Dragging the
/// [start] handle always moves the [start]/[baseOffset] of the selection.
enum _TextSelectionHandlePosition { start, end }
/// Signature for when a pointer that's dragging to select text has moved again. /// Signature for when a pointer that's dragging to select text has moved again.
/// ///
/// The first argument [startDetails] contains the details of the event that /// The first argument [startDetails] contains the details of the event that
...@@ -262,7 +258,7 @@ class TextSelectionOverlay { ...@@ -262,7 +258,7 @@ class TextSelectionOverlay {
required this.renderObject, required this.renderObject,
this.selectionControls, this.selectionControls,
bool handlesVisible = false, bool handlesVisible = false,
this.selectionDelegate, required this.selectionDelegate,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
this.onSelectionHandleTapped, this.onSelectionHandleTapped,
this.clipboardStatus, this.clipboardStatus,
...@@ -312,7 +308,7 @@ class TextSelectionOverlay { ...@@ -312,7 +308,7 @@ class TextSelectionOverlay {
/// The delegate for manipulating the current selection in the owning /// The delegate for manipulating the current selection in the owning
/// text field. /// text field.
final TextSelectionDelegate? selectionDelegate; final TextSelectionDelegate selectionDelegate;
/// Determines the way that drag start behavior is handled. /// Determines the way that drag start behavior is handled.
/// ///
...@@ -412,8 +408,8 @@ class TextSelectionOverlay { ...@@ -412,8 +408,8 @@ class TextSelectionOverlay {
return; return;
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.start)), OverlayEntry(builder: (BuildContext context) => _buildStartHandle(context)),
OverlayEntry(builder: (BuildContext context) => _buildHandle(context, _TextSelectionHandlePosition.end)), OverlayEntry(builder: (BuildContext context) => _buildEndHandle(context)),
]; ];
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)! Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
...@@ -507,29 +503,61 @@ class TextSelectionOverlay { ...@@ -507,29 +503,61 @@ class TextSelectionOverlay {
_toolbarController.dispose(); _toolbarController.dispose();
} }
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) { Widget _buildStartHandle(BuildContext context) {
final Widget handle; final Widget handle;
final TextSelectionControls? selectionControls = this.selectionControls; final TextSelectionControls? selectionControls = this.selectionControls;
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) || if (selectionControls == null)
selectionControls == null) handle = Container();
else {
handle = Visibility(
visible: handlesVisible,
child: _SelectionHandleOverlay(
type: _chooseType(
renderObject.textDirection,
TextSelectionHandleType.left,
TextSelectionHandleType.right,
),
handleLayerLink: startHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped,
onSelectionHandleDragStart: _handleSelectionStartHandleDragStart,
onSelectionHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
selectionControls: selectionControls,
visibility: renderObject.selectionStartInViewport,
preferredLineHeight: renderObject.preferredLineHeight,
glyphHeight: _getStartGlyphHeight(),
dragStartBehavior: dragStartBehavior,
)
);
}
return ExcludeSemantics(
child: handle,
);
}
Widget _buildEndHandle(BuildContext context) {
final Widget handle;
final TextSelectionControls? selectionControls = this.selectionControls;
if (_selection.isCollapsed || selectionControls == null)
handle = Container(); // hide the second handle when collapsed handle = Container(); // hide the second handle when collapsed
else { else {
handle = Visibility( handle = Visibility(
visible: handlesVisible, visible: handlesVisible,
child: _TextSelectionHandleOverlay( child: _SelectionHandleOverlay(
onSelectionHandleChanged: (TextSelection newSelection) { type: _chooseType(
_handleSelectionHandleChanged(newSelection, position); renderObject.textDirection,
}, TextSelectionHandleType.right,
TextSelectionHandleType.left,
),
handleLayerLink: endHandleLayerLink,
onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleTapped: onSelectionHandleTapped,
startHandleLayerLink: startHandleLayerLink, onSelectionHandleDragStart: _handleSelectionEndHandleDragStart,
endHandleLayerLink: endHandleLayerLink, onSelectionHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
renderObject: renderObject,
selection: _selection,
selectionControls: selectionControls, selectionControls: selectionControls,
position: position, visibility: renderObject.selectionEndInViewport,
preferredLineHeight: renderObject.preferredLineHeight,
glyphHeight: _getEndGlyphHeight(),
dragStartBehavior: dragStartBehavior, dragStartBehavior: dragStartBehavior,
selectionDelegate: selectionDelegate!, )
),
); );
} }
return ExcludeSemantics( return ExcludeSemantics(
...@@ -537,6 +565,100 @@ class TextSelectionOverlay { ...@@ -537,6 +565,100 @@ class TextSelectionOverlay {
); );
} }
double? _getStartGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
final int firstSelectedGraphemeExtent;
Rect? startHandleRect;
// Only calculate handle rects if the text in the previous frame
// is the same as the text in the current frame. This is done because
// widget.renderObject contains the renderEditable from the previous frame.
// If the text changed between the current and previous frames then
// widget.renderObject.getRectForComposingRange might fail. In cases where
// the current frame is different from the previous we fall back to
// renderObject.preferredLineHeight.
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
startHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.start, end: _selection.start + firstSelectedGraphemeExtent));
}
return startHandleRect?.height;
}
double? _getEndGlyphHeight() {
final InlineSpan span = renderObject.text!;
final String prevText = span.toPlainText();
final String currText = selectionDelegate.textEditingValue.text;
final int lastSelectedGraphemeExtent;
Rect? endHandleRect;
// See the explanation in _getStartGlyphHeight.
if (prevText == currText && _selection != null && _selection.isValid && !_selection.isCollapsed) {
final String selectedGraphemes = _selection.textInside(currText);
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
endHandleRect = renderObject.getRectForComposingRange(TextRange(start: _selection.end - lastSelectedGraphemeExtent, end: _selection.end));
}
return endHandleRect?.height;
}
late Offset _dragEndPosition;
void _handleSelectionEndHandleDragStart(DragStartDetails details) {
final Size handleSize = selectionControls!.getHandleSize(
renderObject.preferredLineHeight,
);
_dragEndPosition = details.globalPosition + Offset(0.0, -handleSize.height);
}
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
_dragEndPosition += details.delta;
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
if (_selection.isCollapsed) {
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: true);
return;
}
final TextSelection newSelection = TextSelection(
baseOffset: _selection.baseOffset,
extentOffset: position.offset,
);
if (newSelection.baseOffset >= newSelection.extentOffset)
return; // don't allow order swapping.
_handleSelectionHandleChanged(newSelection, isEnd: true);
}
late Offset _dragStartPosition;
void _handleSelectionStartHandleDragStart(DragStartDetails details) {
final Size handleSize = selectionControls!.getHandleSize(
renderObject.preferredLineHeight,
);
_dragStartPosition = details.globalPosition + Offset(0.0, -handleSize.height);
}
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
_dragStartPosition += details.delta;
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
if (_selection.isCollapsed) {
_handleSelectionHandleChanged(TextSelection.fromPosition(position), isEnd: false);
return;
}
final TextSelection newSelection = TextSelection(
baseOffset: position.offset,
extentOffset: _selection.extentOffset,
);
if (newSelection.baseOffset >= newSelection.extentOffset)
return; // don't allow order swapping.
_handleSelectionHandleChanged(newSelection, isEnd: false);
}
Widget _buildToolbar(BuildContext context) { Widget _buildToolbar(BuildContext context) {
if (selectionControls == null) if (selectionControls == null)
return Container(); return Container();
...@@ -581,7 +703,7 @@ class TextSelectionOverlay { ...@@ -581,7 +703,7 @@ class TextSelectionOverlay {
renderObject.preferredLineHeight, renderObject.preferredLineHeight,
midpoint, midpoint,
endpoints, endpoints,
selectionDelegate!, selectionDelegate,
clipboardStatus!, clipboardStatus!,
renderObject.lastSecondaryTapDownPosition, renderObject.lastSecondaryTapDownPosition,
); );
...@@ -592,67 +714,67 @@ class TextSelectionOverlay { ...@@ -592,67 +714,67 @@ class TextSelectionOverlay {
); );
} }
void _handleSelectionHandleChanged(TextSelection newSelection, _TextSelectionHandlePosition position) { void _handleSelectionHandleChanged(TextSelection newSelection, {required bool isEnd}) {
final TextPosition textPosition; final TextPosition textPosition = isEnd ? newSelection.extent : newSelection.base;
switch (position) { selectionDelegate.userUpdateTextEditingValue(
case _TextSelectionHandlePosition.start:
textPosition = newSelection.base;
break;
case _TextSelectionHandlePosition.end:
textPosition = newSelection.extent;
break;
}
selectionDelegate!.userUpdateTextEditingValue(
_value.copyWith(selection: newSelection), _value.copyWith(selection: newSelection),
SelectionChangedCause.drag, SelectionChangedCause.drag,
); );
selectionDelegate!.bringIntoView(textPosition); selectionDelegate.bringIntoView(textPosition);
}
TextSelectionHandleType _chooseType(
TextDirection textDirection,
TextSelectionHandleType ltrType,
TextSelectionHandleType rtlType,
) {
if (_selection.isCollapsed)
return TextSelectionHandleType.collapsed;
assert(textDirection != null);
switch (textDirection) {
case TextDirection.ltr:
return ltrType;
case TextDirection.rtl:
return rtlType;
}
} }
} }
/// This widget represents a single draggable text selection handle. /// This widget represents a single draggable selection handle.
class _TextSelectionHandleOverlay extends StatefulWidget { class _SelectionHandleOverlay extends StatefulWidget {
const _TextSelectionHandleOverlay({ /// Create selection overlay.
const _SelectionHandleOverlay({
Key? key, Key? key,
required this.selection, required this.type,
required this.position, required this.handleLayerLink,
required this.startHandleLayerLink, this.onSelectionHandleTapped,
required this.endHandleLayerLink, this.onSelectionHandleDragStart,
required this.renderObject, this.onSelectionHandleDragUpdate,
required this.onSelectionHandleChanged,
required this.onSelectionHandleTapped,
required this.selectionControls, required this.selectionControls,
required this.selectionDelegate, required this.visibility,
required this.preferredLineHeight,
this.glyphHeight,
this.dragStartBehavior = DragStartBehavior.start, this.dragStartBehavior = DragStartBehavior.start,
}) : super(key: key); }) : super(key: key);
final TextSelection selection; final LayerLink handleLayerLink;
final _TextSelectionHandlePosition position;
final LayerLink startHandleLayerLink;
final LayerLink endHandleLayerLink;
final RenderEditable renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback? onSelectionHandleTapped; final VoidCallback? onSelectionHandleTapped;
final ValueChanged<DragStartDetails>? onSelectionHandleDragStart;
final ValueChanged<DragUpdateDetails>? onSelectionHandleDragUpdate;
final TextSelectionControls selectionControls; final TextSelectionControls selectionControls;
final ValueListenable<bool> visibility;
final double preferredLineHeight;
final double? glyphHeight;
final TextSelectionHandleType type;
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
final TextSelectionDelegate selectionDelegate;
@override @override
_TextSelectionHandleOverlayState createState() => _TextSelectionHandleOverlayState(); State<_SelectionHandleOverlay> createState() => _SelectionHandleOverlayState();
ValueListenable<bool> get _visibility {
switch (position) {
case _TextSelectionHandlePosition.start:
return renderObject.selectionStartInViewport;
case _TextSelectionHandlePosition.end:
return renderObject.selectionEndInViewport;
}
}
} }
class _TextSelectionHandleOverlayState class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with SingleTickerProviderStateMixin {
extends State<_TextSelectionHandleOverlay> with SingleTickerProviderStateMixin {
late Offset _dragPosition;
late AnimationController _controller; late AnimationController _controller;
Animation<double> get _opacity => _controller.view; Animation<double> get _opacity => _controller.view;
...@@ -664,11 +786,11 @@ class _TextSelectionHandleOverlayState ...@@ -664,11 +786,11 @@ class _TextSelectionHandleOverlayState
_controller = AnimationController(duration: TextSelectionOverlay.fadeDuration, vsync: this); _controller = AnimationController(duration: TextSelectionOverlay.fadeDuration, vsync: this);
_handleVisibilityChanged(); _handleVisibilityChanged();
widget._visibility.addListener(_handleVisibilityChanged); widget.visibility.addListener(_handleVisibilityChanged);
} }
void _handleVisibilityChanged() { void _handleVisibilityChanged() {
if (widget._visibility.value) { if (widget.visibility.value) {
_controller.forward(); _controller.forward();
} else { } else {
_controller.reverse(); _controller.reverse();
...@@ -676,126 +798,30 @@ class _TextSelectionHandleOverlayState ...@@ -676,126 +798,30 @@ class _TextSelectionHandleOverlayState
} }
@override @override
void didUpdateWidget(_TextSelectionHandleOverlay oldWidget) { void didUpdateWidget(_SelectionHandleOverlay oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
oldWidget._visibility.removeListener(_handleVisibilityChanged); oldWidget.visibility.removeListener(_handleVisibilityChanged);
_handleVisibilityChanged(); _handleVisibilityChanged();
widget._visibility.addListener(_handleVisibilityChanged); widget.visibility.addListener(_handleVisibilityChanged);
} }
@override @override
void dispose() { void dispose() {
widget._visibility.removeListener(_handleVisibilityChanged); widget.visibility.removeListener(_handleVisibilityChanged);
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
void _handleDragStart(DragStartDetails details) {
final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight,
);
_dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
}
void _handleDragUpdate(DragUpdateDetails details) {
_dragPosition += details.delta;
final TextPosition position = widget.renderObject.getPositionForPoint(_dragPosition);
if (widget.selection.isCollapsed) {
widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
return;
}
final TextSelection newSelection;
switch (widget.position) {
case _TextSelectionHandlePosition.start:
newSelection = TextSelection(
baseOffset: position.offset,
extentOffset: widget.selection.extentOffset,
);
break;
case _TextSelectionHandlePosition.end:
newSelection = TextSelection(
baseOffset: widget.selection.baseOffset,
extentOffset: position.offset,
);
break;
}
if (newSelection.baseOffset >= newSelection.extentOffset)
return; // don't allow order swapping.
widget.onSelectionHandleChanged(newSelection);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final LayerLink layerLink;
final TextSelectionHandleType type;
switch (widget.position) {
case _TextSelectionHandlePosition.start:
layerLink = widget.startHandleLayerLink;
type = _chooseType(
widget.renderObject.textDirection,
TextSelectionHandleType.left,
TextSelectionHandleType.right,
);
break;
case _TextSelectionHandlePosition.end:
// For collapsed selections, we shouldn't be building the [end] handle.
assert(!widget.selection.isCollapsed);
layerLink = widget.endHandleLayerLink;
type = _chooseType(
widget.renderObject.textDirection,
TextSelectionHandleType.right,
TextSelectionHandleType.left,
);
break;
}
// On some platforms we may want to calculate the start and end handles
// separately so they scale for the selected content.
//
// For the start handle we compute the rectangles that encompass the range
// of the first full selected grapheme cluster at the beginning of the selection.
//
// For the end handle we compute the rectangles that encompass the range
// of the last full selected grapheme cluster at the end of the selection.
//
// Only calculate start/end handle rects if the text in the previous frame
// is the same as the text in the current frame. This is done because
// widget.renderObject contains the renderEditable from the previous frame.
// If the text changed between the current and previous frames then
// widget.renderObject.getRectForComposingRange might fail. In cases where
// the current frame is different from the previous we fall back to
// widget.renderObject.preferredLineHeight.
final InlineSpan span = widget.renderObject.text!;
final String prevText = span.toPlainText();
final String currText = widget.selectionDelegate.textEditingValue.text;
final int firstSelectedGraphemeExtent;
final int lastSelectedGraphemeExtent;
final TextSelection selection = widget.selection;
Rect? startHandleRect;
Rect? endHandleRect;
if (prevText == currText && selection != null && selection.isValid && !selection.isCollapsed) {
final String selectedGraphemes = selection.textInside(currText);
firstSelectedGraphemeExtent = selectedGraphemes.characters.first.length;
lastSelectedGraphemeExtent = selectedGraphemes.characters.last.length;
assert(firstSelectedGraphemeExtent <= selectedGraphemes.length && lastSelectedGraphemeExtent <= selectedGraphemes.length);
startHandleRect = widget.renderObject.getRectForComposingRange(TextRange(start: selection.start, end: selection.start + firstSelectedGraphemeExtent));
endHandleRect = widget.renderObject.getRectForComposingRange(TextRange(start: selection.end - lastSelectedGraphemeExtent, end: selection.end));
}
final Offset handleAnchor = widget.selectionControls.getHandleAnchor( final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
type, widget.type,
widget.renderObject.preferredLineHeight, widget.preferredLineHeight,
startHandleRect?.height ?? widget.renderObject.preferredLineHeight, widget.glyphHeight,
endHandleRect?.height ?? widget.renderObject.preferredLineHeight, widget.glyphHeight,
); );
final Size handleSize = widget.selectionControls.getHandleSize( final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight, widget.preferredLineHeight,
); );
final Rect handleRect = Rect.fromLTWH( final Rect handleRect = Rect.fromLTWH(
...@@ -817,7 +843,7 @@ class _TextSelectionHandleOverlayState ...@@ -817,7 +843,7 @@ class _TextSelectionHandleOverlayState
); );
return CompositedTransformFollower( return CompositedTransformFollower(
link: layerLink, link: widget.handleLayerLink,
offset: interactiveRect.topLeft, offset: interactiveRect.topLeft,
showWhenUnlinked: false, showWhenUnlinked: false,
child: FadeTransition( child: FadeTransition(
...@@ -829,8 +855,8 @@ class _TextSelectionHandleOverlayState ...@@ -829,8 +855,8 @@ class _TextSelectionHandleOverlayState
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart, onPanStart: widget.onSelectionHandleDragStart,
onPanUpdate: _handleDragUpdate, onPanUpdate: widget.onSelectionHandleDragUpdate,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: padding.left, left: padding.left,
...@@ -840,11 +866,11 @@ class _TextSelectionHandleOverlayState ...@@ -840,11 +866,11 @@ class _TextSelectionHandleOverlayState
), ),
child: widget.selectionControls.buildHandle( child: widget.selectionControls.buildHandle(
context, context,
type, widget.type,
widget.renderObject.preferredLineHeight, widget.preferredLineHeight,
widget.onSelectionHandleTapped, widget.onSelectionHandleTapped,
startHandleRect?.height ?? widget.renderObject.preferredLineHeight, widget.glyphHeight,
endHandleRect?.height ?? widget.renderObject.preferredLineHeight, widget.glyphHeight,
), ),
), ),
), ),
...@@ -852,23 +878,6 @@ class _TextSelectionHandleOverlayState ...@@ -852,23 +878,6 @@ class _TextSelectionHandleOverlayState
), ),
); );
} }
TextSelectionHandleType _chooseType(
TextDirection textDirection,
TextSelectionHandleType ltrType,
TextSelectionHandleType rtlType,
) {
if (widget.selection.isCollapsed)
return TextSelectionHandleType.collapsed;
assert(textDirection != null);
switch (textDirection) {
case TextDirection.ltr:
return ltrType;
case TextDirection.rtl:
return rtlType;
}
}
} }
/// Delegate interface for the [TextSelectionGestureDetectorBuilder]. /// Delegate interface for the [TextSelectionGestureDetectorBuilder].
......
...@@ -9138,7 +9138,7 @@ void main() { ...@@ -9138,7 +9138,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final List<FadeTransition> transitions = find.descendant( final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'), of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition), matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2); expect(transitions.length, 2);
......
...@@ -4697,7 +4697,7 @@ void main() { ...@@ -4697,7 +4697,7 @@ void main() {
// direction. // direction.
final List<FadeTransition> transitions = find.descendant( final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'), of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition), matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2); expect(transitions.length, 2);
......
...@@ -4128,7 +4128,7 @@ void main() { ...@@ -4128,7 +4128,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final List<FadeTransition> transitions = find.descendant( final List<FadeTransition> transitions = find.descendant(
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionHandleOverlay'), of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'),
matching: find.byType(FadeTransition), matching: find.byType(FadeTransition),
).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList(); ).evaluate().map((Element e) => e.widget).cast<FadeTransition>().toList();
expect(transitions.length, 2); expect(transitions.length, 2);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment