Unverified Commit 988bfc16 authored by xster's avatar xster Committed by GitHub

iOS tap handling on CupertinoTextField (#24034)

parent 0155ee71
// Copyright 2018 The Chromium Authors. All rights reserved. // Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -418,6 +420,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -418,6 +420,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
FocusNode _focusNode; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
// Is shortly after a previous single tap when not null.
Timer _doubleTapTimer;
Offset _lastTapOffset;
// True if second tap down of a double tap is detected. Used to discard
// subsequent tap up / tap hold of the same tap.
bool _isDoubleTap = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -447,6 +456,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -447,6 +456,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
void dispose() { void dispose() {
_focusNode?.dispose(); _focusNode?.dispose();
_controller?.removeListener(updateKeepAlive); _controller?.removeListener(updateKeepAlive);
_doubleTapTimer?.cancel();
super.dispose(); super.dispose();
} }
...@@ -456,17 +466,54 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -456,17 +466,54 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable; RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
// The down handler is force-run on success of a single tap and optimistically
// run before a long press success.
void _handleTapDown(TapDownDetails details) { void _handleTapDown(TapDownDetails details) {
_renderEditable.handleTapDown(details); _renderEditable.handleTapDown(details);
// This isn't detected as a double tap gesture in the gesture recognizer
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) {
// If there was already a previous tap, the second down hold/tap is a
// double tap.
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
_doubleTapTimer.cancel();
_doubleTapTimeout();
_isDoubleTap = true;
}
} }
void _handleTap() { void _handleTapUp(TapUpDetails details) {
_renderEditable.handleTap(); if (!_isDoubleTap) {
_renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
_lastTapOffset = details.globalPosition;
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
_requestKeyboard(); _requestKeyboard();
} }
_isDoubleTap = false;
}
void _handleLongPress() { void _handleLongPress() {
_renderEditable.handleLongPress(); if (!_isDoubleTap) {
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
}
_isDoubleTap = false;
}
void _doubleTapTimeout() {
_doubleTapTimer = null;
_lastTapOffset = null;
}
bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
assert(secondTapOffset != null);
if (_lastTapOffset == null) {
return false;
}
final Offset difference = secondTapOffset - _lastTapOffset;
return difference.distance <= kDoubleTapSlop;
} }
@override @override
...@@ -648,7 +695,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -648,7 +695,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTapDown: _handleTapDown, onTapDown: _handleTapDown,
onTap: _handleTap, onTapUp: _handleTapUp,
onLongPress: _handleLongPress, onLongPress: _handleLongPress,
excludeFromSemantics: true, excludeFromSemantics: true,
child: _addTextDependentAttachments(paddedEditable), child: _addTextDependentAttachments(paddedEditable),
......
...@@ -32,6 +32,10 @@ enum SelectionChangedCause { ...@@ -32,6 +32,10 @@ enum SelectionChangedCause {
/// of the cursor) to change. /// of the cursor) to change.
tap, tap,
/// The user tapped twice in quick succession on the text and that caused
/// the selection (or the location of the cursor) to change.
doubleTap,
/// The user long-pressed the text and that caused the selection (or the /// The user long-pressed the text and that caused the selection (or the
/// location of the cursor) to change. /// location of the cursor) to change.
longPress, longPress,
...@@ -190,7 +194,7 @@ class RenderEditable extends RenderBox { ...@@ -190,7 +194,7 @@ class RenderEditable extends RenderBox {
/// If true [handleEvent] does nothing and it's assumed that this /// If true [handleEvent] does nothing and it's assumed that this
/// renderer will be notified of input gestures via [handleTapDown], /// renderer will be notified of input gestures via [handleTapDown],
/// [handleTap], and [handleLongPress]. /// [handleTap], [handleDoubleTap], and [handleLongPress].
/// ///
/// The default value of this property is false. /// The default value of this property is false.
bool ignorePointer; bool ignorePointer;
...@@ -1081,18 +1085,23 @@ class RenderEditable extends RenderBox { ...@@ -1081,18 +1085,23 @@ class RenderEditable extends RenderBox {
/// When [ignorePointer] is true, an ancestor widget must respond to tap /// When [ignorePointer] is true, an ancestor widget must respond to tap
/// events by calling this method. /// events by calling this method.
void handleTap() { void handleTap() {
_layoutText(constraints.maxWidth); selectPosition(cause: SelectionChangedCause.tap);
assert(_lastTapDownPosition != null);
if (onSelectionChanged != null) {
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
onSelectionChanged(TextSelection.fromPosition(position), this, SelectionChangedCause.tap);
}
} }
void _handleTap() { void _handleTap() {
assert(!ignorePointer); assert(!ignorePointer);
handleTap(); handleTap();
} }
/// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [DoubleTapGestureRecognizer.onDoubleTap]
/// callback.
///
/// When [ignorePointer] is true, an ancestor widget must respond to double
/// tap events by calling this method.
void handleDoubleTap() {
selectWord(cause: SelectionChangedCause.doubleTap);
}
/// If [ignorePointer] is false (the default) then this method is called by /// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [LongPressRecognizer.onLongPress] /// the internal gesture recognizer's [LongPressRecognizer.onLongPress]
/// callback. /// callback.
...@@ -1100,16 +1109,57 @@ class RenderEditable extends RenderBox { ...@@ -1100,16 +1109,57 @@ class RenderEditable extends RenderBox {
/// When [ignorePointer] is true, an ancestor widget must respond to long /// When [ignorePointer] is true, an ancestor widget must respond to long
/// press events by calling this method. /// press events by calling this method.
void handleLongPress() { void handleLongPress() {
selectWord(cause: SelectionChangedCause.longPress);
}
void _handleLongPress() {
assert(!ignorePointer);
handleLongPress();
}
/// Move selection to the location of the last tap down.
void selectPosition({@required SelectionChangedCause cause}) {
assert(cause != null);
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
assert(_lastTapDownPosition != null); assert(_lastTapDownPosition != null);
if (onSelectionChanged != null) { if (onSelectionChanged != null) {
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition)); final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
onSelectionChanged(_selectWordAtOffset(position), this, SelectionChangedCause.longPress); onSelectionChanged(TextSelection.fromPosition(position), this, cause);
}
}
/// Select a word around the location of the last tap down.
void selectWord({@required SelectionChangedCause cause}) {
assert(cause != null);
_layoutText(constraints.maxWidth);
assert(_lastTapDownPosition != null);
if (onSelectionChanged != null) {
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
onSelectionChanged(_selectWordAtOffset(position), this, cause);
}
}
/// Move the selection to the beginning or end of a word.
void selectWordEdge({@required SelectionChangedCause cause}) {
assert(cause != null);
_layoutText(constraints.maxWidth);
assert(_lastTapDownPosition != null);
if (onSelectionChanged != null) {
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
final TextRange word = _textPainter.getWordBoundary(position);
if (position.offset - word.start <= 1) {
onSelectionChanged(
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
this,
cause,
);
} else {
onSelectionChanged(
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
this,
cause,
);
} }
} }
void _handleLongPress() {
assert(!ignorePointer);
handleLongPress();
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
......
...@@ -740,7 +740,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -740,7 +740,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final bool longPress = cause == SelectionChangedCause.longPress; final bool longPress = cause == SelectionChangedCause.longPress;
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress)) if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
_selectionOverlay.showHandles(); _selectionOverlay.showHandles();
if (longPress) if (longPress || cause == SelectionChangedCause.doubleTap)
_selectionOverlay.showToolbar(); _selectionOverlay.showToolbar();
if (widget.onSelectionChanged != null) if (widget.onSelectionChanged != null)
widget.onSelectionChanged(selection, cause); widget.onSelectionChanged(selection, cause);
......
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