Commit 210774f3 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Make cursor blinking more efficient (#9142)

Rather than rebuilding to blink the cursor, we now pass a
ValueNotifier<bool> to the RenderEditable so that it can simply repaint.

This patch also contains some refactoring towards being able to do the
same thing with the text being edited, but I didn't quite get it
working.
parent 0b31c699
...@@ -49,7 +49,7 @@ class RenderEditable extends RenderBox { ...@@ -49,7 +49,7 @@ class RenderEditable extends RenderBox {
TextSpan text, TextSpan text,
TextAlign textAlign, TextAlign textAlign,
Color cursorColor, Color cursorColor,
bool showCursor: false, ValueNotifier<bool> showCursor,
int maxLines: 1, int maxLines: 1,
Color selectionColor, Color selectionColor,
double textScaleFactor: 1.0, double textScaleFactor: 1.0,
...@@ -58,15 +58,15 @@ class RenderEditable extends RenderBox { ...@@ -58,15 +58,15 @@ class RenderEditable extends RenderBox {
this.onSelectionChanged, this.onSelectionChanged,
}) : _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor), }) : _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor),
_cursorColor = cursorColor, _cursorColor = cursorColor,
_showCursor = showCursor, _showCursor = showCursor ?? new ValueNotifier<bool>(false),
_maxLines = maxLines, _maxLines = maxLines,
_selection = selection, _selection = selection,
_offset = offset { _offset = offset {
assert(showCursor != null); assert(_showCursor != null);
assert(maxLines != null); assert(maxLines != null);
assert(textScaleFactor != null); assert(textScaleFactor != null);
assert(offset != null); assert(offset != null);
assert(!showCursor || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
_tap = new TapGestureRecognizer() _tap = new TapGestureRecognizer()
..onTapDown = _handleTapDown ..onTapDown = _handleTapDown
..onTap = _handleTap ..onTap = _handleTap
...@@ -108,13 +108,17 @@ class RenderEditable extends RenderBox { ...@@ -108,13 +108,17 @@ class RenderEditable extends RenderBox {
} }
/// Whether to paint the cursor. /// Whether to paint the cursor.
bool get showCursor => _showCursor; ValueNotifier<bool> get showCursor => _showCursor;
bool _showCursor; ValueNotifier<bool> _showCursor;
set showCursor(bool value) { set showCursor(ValueNotifier<bool> value) {
assert(value != null); assert(value != null);
if (_showCursor == value) if (_showCursor == value)
return; return;
if (attached)
_showCursor.removeListener(markNeedsPaint);
_showCursor = value; _showCursor = value;
if (attached)
_showCursor.addListener(markNeedsPaint);
markNeedsPaint(); markNeedsPaint();
} }
...@@ -186,6 +190,20 @@ class RenderEditable extends RenderBox { ...@@ -186,6 +190,20 @@ class RenderEditable extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(markNeedsPaint);
_showCursor.addListener(markNeedsPaint);
}
@override
void detach() {
_offset.removeListener(markNeedsPaint);
_showCursor.removeListener(markNeedsPaint);
super.detach();
}
bool get _isMultiline => maxLines > 1; bool get _isMultiline => maxLines > 1;
Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal; Axis get _viewportAxis => _isMultiline ? Axis.vertical : Axis.horizontal;
...@@ -377,7 +395,7 @@ class RenderEditable extends RenderBox { ...@@ -377,7 +395,7 @@ class RenderEditable extends RenderBox {
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
if (_selection != null) { if (_selection != null) {
if (_selection.isCollapsed && _showCursor && cursorColor != null) { if (_selection.isCollapsed && _showCursor.value && cursorColor != null) {
_paintCaret(context.canvas, effectiveOffset); _paintCaret(context.canvas, effectiveOffset);
} else if (!_selection.isCollapsed && _selectionColor != null) { } else if (!_selection.isCollapsed && _selectionColor != null) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection); _selectionRects ??= _textPainter.getBoxesForSelection(_selection);
......
...@@ -23,29 +23,19 @@ export 'package:flutter/services.dart' show TextEditingValue, TextSelection, Tex ...@@ -23,29 +23,19 @@ export 'package:flutter/services.dart' show TextEditingValue, TextSelection, Tex
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500); const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
class TextEditingController extends ChangeNotifier { class TextEditingController extends ValueNotifier<TextEditingValue> {
TextEditingController({ String text }) TextEditingController({ String text })
: _value = text == null ? TextEditingValue.empty : new TextEditingValue(text: text); : super(text == null ? TextEditingValue.empty : new TextEditingValue(text: text));
TextEditingController.fromValue(TextEditingValue value) TextEditingController.fromValue(TextEditingValue value)
: _value = value ?? TextEditingValue.empty; : super(value ?? TextEditingValue.empty);
TextEditingValue get value => _value; String get text => value.text;
TextEditingValue _value;
set value(TextEditingValue newValue) {
assert(newValue != null);
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
String get text => _value.text;
set text(String newText) { set text(String newText) {
value = value.copyWith(text: newText, composing: TextRange.empty); value = value.copyWith(text: newText, composing: TextRange.empty);
} }
TextSelection get selection => _value.selection; TextSelection get selection => value.selection;
set selection(TextSelection newSelection) { set selection(TextSelection newSelection) {
value = value.copyWith(selection: newSelection, composing: TextRange.empty); value = value.copyWith(selection: newSelection, composing: TextRange.empty);
} }
...@@ -174,7 +164,7 @@ class EditableText extends StatefulWidget { ...@@ -174,7 +164,7 @@ class EditableText extends StatefulWidget {
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> implements TextInputClient { class EditableTextState extends State<EditableText> implements TextInputClient {
Timer _cursorTimer; Timer _cursorTimer;
bool _showCursor = false; final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
TextInputConnection _textInputConnection; TextInputConnection _textInputConnection;
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
...@@ -206,7 +196,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -206,7 +196,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (config.controller != oldConfig.controller) { if (config.controller != oldConfig.controller) {
oldConfig.controller.removeListener(_didChangeTextEditingValue); oldConfig.controller.removeListener(_didChangeTextEditingValue);
config.controller.addListener(_didChangeTextEditingValue); config.controller.addListener(_didChangeTextEditingValue);
if (_isAttachedToKeyboard && config.controller.value != oldConfig.controller.value) if (_hasInputConnection && config.controller.value != oldConfig.controller.value)
_textInputConnection.setEditingState(config.controller.value); _textInputConnection.setEditingState(config.controller.value);
} }
if (config.focusNode != oldConfig.focusNode) { if (config.focusNode != oldConfig.focusNode) {
...@@ -218,13 +208,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -218,13 +208,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
@override @override
void dispose() { void dispose() {
config.controller.removeListener(_didChangeTextEditingValue); config.controller.removeListener(_didChangeTextEditingValue);
if (_isAttachedToKeyboard) { _closeInputConnectionIfNeeded();
_textInputConnection.close(); assert(!_hasInputConnection);
_textInputConnection = null; _stopCursorTimer();
}
assert(!_isAttachedToKeyboard);
if (_cursorTimer != null)
_stopCursorTimer();
assert(_cursorTimer == null); assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
...@@ -256,12 +242,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -256,12 +242,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
config.controller.value = value; config.controller.value = value;
} }
void _didChangeTextEditingValue() { bool get _hasFocus => config.focusNode.hasFocus;
setState(() { /* We use config.controller.value in build(). */ });
}
bool get _isAttachedToKeyboard => _textInputConnection != null && _textInputConnection.attached;
bool get _isMultiline => config.maxLines > 1; bool get _isMultiline => config.maxLines > 1;
// Calculate the new scroll offset so the cursor remains visible. // Calculate the new scroll offset so the cursor remains visible.
...@@ -278,17 +259,28 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -278,17 +259,28 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
} }
bool _didRequestKeyboard = false; bool _didRequestKeyboard = false;
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
void _attachOrDetachKeyboard(bool focused) { void _openInputConnectionIfNeeded() {
if (focused && !_isAttachedToKeyboard && _didRequestKeyboard) { if (!_hasInputConnection) {
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType)) _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: config.keyboardType))
..setEditingState(_value) ..setEditingState(_value)
..show(); ..show();
} else if (!focused) { }
if (_isAttachedToKeyboard) { }
_textInputConnection.close();
_textInputConnection = null; void _closeInputConnectionIfNeeded() {
} if (_hasInputConnection) {
_textInputConnection.close();
_textInputConnection = null;
}
}
void _openOrCloseInputConnectionIfNeeded() {
if (_hasFocus && _didRequestKeyboard) {
_openInputConnectionIfNeeded();
} else if (!_hasFocus) {
_closeInputConnectionIfNeeded();
config.controller.clearComposing(); config.controller.clearComposing();
} }
_didRequestKeyboard = false; _didRequestKeyboard = false;
...@@ -302,14 +294,15 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -302,14 +294,15 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
/// focus, the control will then attach to the keyboard and request that the /// focus, the control will then attach to the keyboard and request that the
/// keyboard become visible. /// keyboard become visible.
void requestKeyboard() { void requestKeyboard() {
if (_isAttachedToKeyboard) { if (_hasInputConnection) {
_textInputConnection.show(); _textInputConnection.show();
} else { } else {
_didRequestKeyboard = true; if (_hasFocus) {
if (config.focusNode.hasFocus) _openInputConnectionIfNeeded();
_attachOrDetachKeyboard(true); } else {
else _didRequestKeyboard = true;
FocusScope.of(context).requestFocus(config.focusNode); FocusScope.of(context).requestFocus(config.focusNode);
}
} }
} }
...@@ -318,6 +311,17 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -318,6 +311,17 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
_selectionOverlay = null; _selectionOverlay = null;
} }
void _updateOrDisposeSelectionOverlayIfNeeded() {
if (_selectionOverlay != null) {
if (_hasFocus) {
_selectionOverlay.update(_value);
} else {
_selectionOverlay.dispose();
_selectionOverlay = null;
}
}
}
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) { void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, bool longPress) {
// Note that this will show the keyboard for all selection changes on the // Note that this will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures. // EditableWidget, not just changes triggered by user gestures.
...@@ -351,7 +355,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -351,7 +355,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
/// Whether the blinking cursor is actually visible at this precise moment /// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks). /// (it's hidden half the time, since it blinks).
@visibleForTesting @visibleForTesting
bool get cursorCurrentlyVisible => _showCursor; bool get cursorCurrentlyVisible => _showCursor.value;
/// The cursor blink interval (the amount of time the cursor is in the "on" /// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this /// state or the "off" state). A complete cursor blink period is twice this
...@@ -360,39 +364,39 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -360,39 +364,39 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod; Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
setState(() { _showCursor.value = !_showCursor.value;
_showCursor = !_showCursor;
});
} }
void _startCursorTimer() { void _startCursorTimer() {
_showCursor = true; _showCursor.value = true;
_cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); _cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
} }
void _handleFocusChanged() { void _stopCursorTimer() {
final bool focused = config.focusNode.hasFocus; _cursorTimer?.cancel();
_attachOrDetachKeyboard(focused); _cursorTimer = null;
_showCursor.value = false;
}
if (_cursorTimer == null && focused && _value.selection.isCollapsed) void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed)
_startCursorTimer(); _startCursorTimer();
else if (_cursorTimer != null && (!focused || !_value.selection.isCollapsed)) else if (_cursorTimer != null && (!_hasFocus || !_value.selection.isCollapsed))
_stopCursorTimer(); _stopCursorTimer();
}
if (_selectionOverlay != null) { void _didChangeTextEditingValue() {
if (focused) { _startOrStopCursorTimerIfNeeded();
_selectionOverlay.update(_value); _updateOrDisposeSelectionOverlayIfNeeded();
} else { // TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
_selectionOverlay.dispose(); // to avoid this setState().
_selectionOverlay = null; setState(() { /* We use config.controller.value in build(). */ });
}
}
} }
void _stopCursorTimer() { void _handleFocusChanged() {
_cursorTimer.cancel(); _openOrCloseInputConnectionIfNeeded();
_cursorTimer = null; _startOrStopCursorTimerIfNeeded();
_showCursor = false; _updateOrDisposeSelectionOverlayIfNeeded();
} }
@override @override
...@@ -440,7 +444,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -440,7 +444,7 @@ class _Editable extends LeafRenderObjectWidget {
final TextEditingValue value; final TextEditingValue value;
final TextStyle style; final TextStyle style;
final Color cursorColor; final Color cursorColor;
final bool showCursor; final ValueNotifier<bool> showCursor;
final int maxLines; final int maxLines;
final Color selectionColor; final Color selectionColor;
final double textScaleFactor; final double textScaleFactor;
......
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