Commit 92d0445a authored by Matt Perry's avatar Matt Perry Committed by GitHub

Add a fade-in animation for the text selection controls. (#5190)

BUG=https://github.com/flutter/flutter/issues/3938
parent 40d26f43
...@@ -389,7 +389,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> { ...@@ -389,7 +389,7 @@ class RawInputLineState extends ScrollableState<RawInputLine> {
if (_cursorTimer != null) if (_cursorTimer != null)
_stopCursorTimer(); _stopCursorTimer();
scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild scheduleMicrotask(() { // can't hide while disposing, since it triggers a rebuild
_selectionOverlay?.hide(); _selectionOverlay?.dispose();
}); });
super.dispose(); super.dispose();
} }
......
...@@ -11,6 +11,7 @@ import 'editable.dart'; ...@@ -11,6 +11,7 @@ import 'editable.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'transitions.dart';
// TODO(mpcomplete): Need one for [collapsed]. // TODO(mpcomplete): Need one for [collapsed].
/// Which type of selection handle to be displayed. /// Which type of selection handle to be displayed.
...@@ -114,6 +115,13 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -114,6 +115,13 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// The tool bar typically contains buttons for copying and pasting text. /// The tool bar typically contains buttons for copying and pasting text.
final TextSelectionToolbarBuilder toolbarBuilder; final TextSelectionToolbarBuilder toolbarBuilder;
/// Controls the fade-in animations.
static const Duration _kFadeDuration = const Duration(milliseconds: 150);
final AnimationController _handleController = new AnimationController(duration: _kFadeDuration);
final AnimationController _toolbarController = new AnimationController(duration: _kFadeDuration);
Animation<double> get _handleOpacity => _handleController.view;
Animation<double> get _toolbarOpacity => _toolbarController.view;
InputValue _input; InputValue _input;
/// A pair of handles. If this is non-null, there are always 2, though the /// A pair of handles. If this is non-null, there are always 2, though the
...@@ -129,10 +137,11 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -129,10 +137,11 @@ class TextSelectionOverlay implements TextSelectionDelegate {
void showHandles() { void showHandles() {
assert(_handles == null); assert(_handles == null);
_handles = <OverlayEntry>[ _handles = <OverlayEntry>[
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.start)), new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.start)),
new OverlayEntry(builder: (BuildContext c) => _buildOverlay(c, _TextSelectionHandlePosition.end)), new OverlayEntry(builder: (BuildContext c) => _buildHandle(c, _TextSelectionHandlePosition.end)),
]; ];
Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles); Overlay.of(context, debugRequiredFor: debugRequiredFor).insertAll(_handles);
_handleController.forward(from: 0.0);
} }
/// Shows the toolbar by inserting it into the [context]'s overlay. /// Shows the toolbar by inserting it into the [context]'s overlay.
...@@ -140,6 +149,7 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -140,6 +149,7 @@ class TextSelectionOverlay implements TextSelectionDelegate {
assert(_toolbar == null); assert(_toolbar == null);
_toolbar = new OverlayEntry(builder: _buildToolbar); _toolbar = new OverlayEntry(builder: _buildToolbar);
Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar); Overlay.of(context, debugRequiredFor: debugRequiredFor).insert(_toolbar);
_toolbarController.forward(from: 0.0);
} }
/// Updates the overlay after the [selection] has changed. /// Updates the overlay after the [selection] has changed.
...@@ -164,19 +174,33 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -164,19 +174,33 @@ class TextSelectionOverlay implements TextSelectionDelegate {
} }
_toolbar?.remove(); _toolbar?.remove();
_toolbar = null; _toolbar = null;
_handleController.stop();
_toolbarController.stop();
}
/// Final cleanup.
void dispose() {
hide();
_handleController.dispose();
_toolbarController.dispose();
} }
Widget _buildOverlay(BuildContext context, _TextSelectionHandlePosition position) { Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) || if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
handleBuilder == null) handleBuilder == null)
return new Container(); // hide the second handle when collapsed return new Container(); // hide the second handle when collapsed
return new _TextSelectionHandleOverlay(
return new FadeTransition(
opacity: _handleOpacity,
child: new _TextSelectionHandleOverlay(
onSelectionHandleChanged: _handleSelectionHandleChanged, onSelectionHandleChanged: _handleSelectionHandleChanged,
onSelectionHandleTapped: _handleSelectionHandleTapped, onSelectionHandleTapped: _handleSelectionHandleTapped,
renderObject: renderObject, renderObject: renderObject,
selection: _selection, selection: _selection,
builder: handleBuilder, builder: handleBuilder,
position: position position: position
)
); );
} }
...@@ -193,7 +217,10 @@ class TextSelectionOverlay implements TextSelectionDelegate { ...@@ -193,7 +217,10 @@ class TextSelectionOverlay implements TextSelectionDelegate {
endpoints[0].point.y - renderObject.size.height endpoints[0].point.y - renderObject.size.height
); );
return toolbarBuilder(context, midpoint, this); return new FadeTransition(
opacity: _toolbarOpacity,
child: toolbarBuilder(context, midpoint, this)
);
} }
void _handleSelectionHandleChanged(TextSelection newSelection) { void _handleSelectionHandleChanged(TextSelection newSelection) {
...@@ -252,6 +279,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { ...@@ -252,6 +279,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> { class _TextSelectionHandleOverlayState extends State<_TextSelectionHandleOverlay> {
Point _dragPosition; Point _dragPosition;
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
_dragPosition = details.globalPosition; _dragPosition = details.globalPosition;
} }
......
...@@ -382,4 +382,58 @@ void main() { ...@@ -382,4 +382,58 @@ void main() {
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
expect(inputValue.text, 'abc d${testValue}ef ghi'); expect(inputValue.text, 'abc d${testValue}ef ghi');
}); });
testWidgets('Selection toolbar fades in', (WidgetTester tester) async {
GlobalKey inputKey = new GlobalKey();
InputValue inputValue = InputValue.empty;
Widget builder() {
return new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
builder: (BuildContext context) {
return new Center(
child: new Material(
child: new Input(
value: inputValue,
key: inputKey,
onChanged: (InputValue value) { inputValue = value; }
)
)
);
}
)
]
);
}
await tester.pumpWidget(builder());
String testValue = 'abc def ghi';
enterText(testValue);
await tester.pumpWidget(builder());
// Tap the selection handle to bring up the "paste / select all" menu.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpWidget(builder());
RenderEditableLine renderLine = findRenderEditableLine(tester);
List<TextSelectionPoint> endpoints = renderLine.getEndpointsForSelection(
inputValue.selection);
await tester.tapAt(endpoints[0].point + new Offset(1.0, 1.0));
await tester.pumpWidget(builder());
// Toolbar should fade in. Starting at 0% opacity.
Element target = tester.element(find.text('SELECT ALL'));
Opacity opacity = target.ancestorWidgetOfExactType(Opacity);
expect(opacity, isNotNull);
expect(opacity.opacity, equals(0.0));
// Still fading in.
await tester.pump(const Duration(milliseconds: 50));
opacity = target.ancestorWidgetOfExactType(Opacity);
expect(opacity.opacity, greaterThan(0.0));
expect(opacity.opacity, lessThan(1.0));
// End the test here to ensure the animation is properly disposed of.
});
} }
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