-
Notifications
You must be signed in to change notification settings - Fork 28.8k
Provide haptic/acoustic feedback for tap & long-press on Android #10920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
} | ||
|
||
static TargetPlatform _platform(BuildContext context) => Theme.of(context).platform; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extraneous blank line
|
||
/// Provides platform-specific feedback for a tap. | ||
static Future<Null> forTap(BuildContext context) async { | ||
switch(_platform(context)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: space after switch
|
||
/// Provides platform-specific feedback for a long press. | ||
static Future<Null> forLongPress(BuildContext context) { | ||
switch(_platform(context)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
FeedbackWrapper._(); | ||
|
||
/// Provides platform-specific feedback for a tap. | ||
static GestureTapCallback forTap(GestureTapCallback callback, BuildContext context) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really quite clever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks 😄
return new Future<Null>.value(); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we merge these classes and have Feedback.forTap(context)
and Feedback.wrapForTap(callback, context)
?
Having two classes with two static methods seems like it will lead to confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Done.
import 'package:flutter/services.dart'; | ||
import 'package:flutter/widgets.dart'; | ||
|
||
/// Provides platform-specific acoustic and/or haptic feedback for certain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These docs need to be more elaborate.
Things to add at each place there are docs in this file (it's perfectly ok to have duplication in the docs):
- What does this do on Android?
- What does this do on iOS?
- How do you handle things like
onPressed: enabled ? callback : null
? - Sample code? (with the
## Sample code
pattern, see other files)
@@ -93,7 +94,8 @@ class InkResponse extends StatefulWidget { | |||
this.borderRadius: BorderRadius.zero, | |||
this.highlightColor, | |||
this.splashColor, | |||
}) : super(key: key); | |||
bool enableFeedback, | |||
}) : enableFeedback = enableFeedback ?? true, super(key: key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just make true the default value for the argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and then assert that it's not null, and mention in the docs for the constructor that it must be non-null)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea was that this way I don't have to specify the default value in two places (here and below for InkWell). Changed it nonetheless as I didn't have strong feelings.
_currentSplash?.confirm(); | ||
_currentSplash = null; | ||
if (widget.onLongPress != null) | ||
if (widget.enableFeedback) | ||
Feedback.forLongPress(context); | ||
widget.onLongPress(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you missed the braces here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please add a test that would have failed here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! Good catch!
I still wish if
s would always come with braces... goto fail; 😉
As to writing a test: there is actually another onLongPress != null
check that guards the entire execution of this method (see line 359). So, it's actually impossible to exploit the missing braces in a test here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case maybe we should just assert(widget.onLongPress != null)
and remove the test?
@@ -392,6 +405,7 @@ class InkWell extends InkResponse { | |||
Color highlightColor, | |||
Color splashColor, | |||
BorderRadius borderRadius, | |||
bool enableFeedback, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constructor docs update
if (_entry != null) { | ||
_timer?.cancel(); | ||
_timer = null; | ||
_controller.forward(); | ||
return; // Already visible. | ||
return false; // Already visible. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: extraneous space before comment
@@ -650,3 +656,6 @@ class _Editable extends LeafRenderObjectWidget { | |||
return new TextSpan(style: style, text: text); | |||
} | |||
} | |||
|
|||
/// Signature for the callback that reports when the user changes the selection. | |||
typedef void SelectionChangedCallback(TextSelection selection, bool longPress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
put typedefs near the top of the file, before first use if possible
@@ -243,6 +249,7 @@ class _TextFieldState extends State<TextField> { | |||
selectionControls: materialTextSelectionControls, | |||
onChanged: widget.onChanged, | |||
onSubmitted: widget.onSubmitted, | |||
onSelectionChanged: (TextSelection _, bool longPress) => _onSelectionChanged(context, longPress), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting, i would have expected this to belong to the selection controls delegate. This works too.
This is fantastic. |
/// tapped, call [forTap]. For the Android-specific vibration when long pressing | ||
/// an element, call [forLongPress]. Alternatively, you can also wrap your | ||
/// [onTap] or [onLongPress] callback in [wrapForTap] or [wrapForLongPress] to | ||
/// achive the same (see example code below). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
achieve
/// typically don't provide haptic or acoustic feedback. | ||
/// | ||
/// All methods in this class are usually called from within a [build] method | ||
/// as you have to provide a [BuildContext]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or from a State's methods
/// To trigger platform-specific feedback before executing the actual callback: | ||
/// | ||
/// ```dart | ||
/// @override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally each example would be something you can copy and paste into code and have work right away, so here we would usually include the StatefulWidget and State boilerplate, as in:
https://master-docs-flutter-io.firebaseapp.com/flutter/painting/TextSpan/recognizer.html
@@ -93,7 +94,8 @@ class InkResponse extends StatefulWidget { | |||
this.borderRadius: BorderRadius.zero, | |||
this.highlightColor, | |||
this.splashColor, | |||
}) : super(key: key); | |||
this.enableFeedback: true, | |||
}) : assert(enableFeedback != null), super(key: key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the doc for the constructor should mention that containedInkWell, borderRadius, and enableFeedback must not be null, and the asserts should include the other two as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Asserts are throwing left and right when I assert on containedInkWell and borderRadius. If the assert is really true we should do that (and the associated clean-up) in a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Odd.
/// Whether detected gestures should provide acoustic and/or haptic feedback. | ||
/// | ||
/// For example, on Android a tap will produce a clicking sound and a | ||
/// long-press will produce a short vibration, when feedback is enabled. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe point to Feedback?
@@ -110,12 +111,13 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { | |||
_removeEntry(); | |||
} | |||
|
|||
void ensureTooltipVisible() { | |||
/// Returns `false` when the tooltip was already visible. | |||
bool ensureTooltipVisible() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this doesn't say the most important part, that it shows a tooltip! :-)
@@ -226,6 +230,9 @@ class EditableText extends StatefulWidget { | |||
/// Called when the user indicates that they are done editing the text in the field. | |||
final ValueChanged<String> onSubmitted; | |||
|
|||
/// Called when the selection of text changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or when the cursor has moved? or not?
|
||
import 'package:flutter/services.dart'; | ||
|
||
/// Tracks how often feedback has been requested since its instantiation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably mention that only one of these can be used at a time.
|
||
/// Number of times the click sound was requested to play. | ||
int get clickSoundCount => _clickSoundCount; | ||
int _clickSoundCount = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should add a dispose() method than unhooks the mock handler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or don't bother, it's only used in tests, so...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it for completeness.
…tter#10920) * Provide haptic/acoustic feedback for tap & long-press on Android * review comments * fixed example code * review comments * comment fix
Fixes #6574.