Skip to content

Commit d79b712

Browse files
button: Replace GestureDetector with Listener for AnimatedScaleOnTap
Fixes #1953. Latter provides instant scaleDown animation on tap, unlike GestureDetector which has a `kPressTimeOut` causing delay of 100ms https://main-api.flutter.dev/flutter/gestures/kPressTimeout-constant.html
1 parent 9296fb3 commit d79b712

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed

lib/widgets/button.dart

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/gestures.dart';
12
import 'package:flutter/material.dart';
23

34
import 'color.dart';
@@ -159,7 +160,7 @@ class ZulipWebUiKitButton extends StatelessWidget {
159160

160161
final labelColor = _labelColor(designVariables);
161162

162-
return AnimatedScaleOnTap(
163+
return AnimatedScaleOnPrimaryPointerDown(
163164
scaleEnd: 0.96,
164165
duration: Duration(milliseconds: 100),
165166
child: TextButton.icon(
@@ -270,10 +271,10 @@ class ZulipIconButton extends StatelessWidget {
270271
}
271272
}
272273

273-
/// Apply [Transform.scale] to the child widget when tapped, and reset its scale
274-
/// when released, while animating the transitions.
275-
class AnimatedScaleOnTap extends StatefulWidget {
276-
const AnimatedScaleOnTap({
274+
/// Apply [Transform.scale] to the child widget on primary pointer-down,
275+
/// and reset its scale on -up or -cancel, with animated transitions.
276+
class AnimatedScaleOnPrimaryPointerDown extends StatefulWidget {
277+
const AnimatedScaleOnPrimaryPointerDown({
277278
super.key,
278279
required this.scaleEnd,
279280
required this.duration,
@@ -289,11 +290,12 @@ class AnimatedScaleOnTap extends StatefulWidget {
289290
final Widget child;
290291

291292
@override
292-
State<AnimatedScaleOnTap> createState() => _AnimatedScaleOnTapState();
293+
State<AnimatedScaleOnPrimaryPointerDown> createState() => _AnimatedScaleOnPrimaryPointerDownState();
293294
}
294295

295-
class _AnimatedScaleOnTapState extends State<AnimatedScaleOnTap> {
296+
class _AnimatedScaleOnPrimaryPointerDownState extends State<AnimatedScaleOnPrimaryPointerDown> {
296297
double _scale = 1;
298+
int _pressedButton = 0;
297299

298300
void _changeScale(double scale) {
299301
setState(() {
@@ -303,11 +305,19 @@ class _AnimatedScaleOnTapState extends State<AnimatedScaleOnTap> {
303305

304306
@override
305307
Widget build(BuildContext context) {
306-
return GestureDetector(
308+
return Listener(
307309
behavior: HitTestBehavior.translucent,
308-
onTapDown: (_) => _changeScale(widget.scaleEnd),
309-
onTapUp: (_) => _changeScale(1),
310-
onTapCancel: () => _changeScale(1),
310+
onPointerDown: (PointerDownEvent pointerDownEvent) {
311+
_pressedButton = pointerDownEvent.buttons;
312+
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
313+
},
314+
onPointerUp: (_) {
315+
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
316+
},
317+
onPointerCancel: (_) {
318+
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
319+
_pressedButton = 0;
320+
},
311321
child: AnimatedScale(
312322
scale: _scale,
313323
duration: widget.duration,

lib/widgets/home.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ class _NavigationBarButton extends StatelessWidget {
287287
final designVariables = DesignVariables.of(context);
288288
final color = selected ? designVariables.iconSelected : designVariables.icon;
289289

290-
Widget result = AnimatedScaleOnTap(
290+
Widget result = AnimatedScaleOnPrimaryPointerDown(
291291
scaleEnd: 0.875,
292292
duration: const Duration(milliseconds: 100),
293293
child: Material(
@@ -389,7 +389,7 @@ void _showMainMenu(BuildContext context, {
389389
child: Column(children: menuItems)))),
390390
const Padding(
391391
padding: EdgeInsets.symmetric(horizontal: 16),
392-
child: AnimatedScaleOnTap(
392+
child: AnimatedScaleOnPrimaryPointerDown(
393393
scaleEnd: 0.95,
394394
duration: Duration(milliseconds: 100),
395395
child: BottomSheetDismissButton(
@@ -459,7 +459,7 @@ abstract class _MenuButton extends StatelessWidget {
459459
~WidgetState.pressed: selected ? borderSideSelected : null,
460460
}));
461461

462-
return AnimatedScaleOnTap(
462+
return AnimatedScaleOnPrimaryPointerDown(
463463
duration: const Duration(milliseconds: 100),
464464
scaleEnd: 0.95,
465465
child: ConstrainedBox(

test/widgets/button_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,39 @@ void main() {
114114
check(renderObject).size.equals(Size.square(40));
115115
});
116116

117+
group('AnimatedScaleOnTap', () {
118+
void checkScale(WidgetTester tester, Finder finder, double expectedScale) {
119+
final scale = tester.widget<AnimatedScale>(finder).scale;
120+
check(scale).equals(expectedScale);
121+
}
122+
123+
testWidgets('Animation happen instantly when tap down', (tester) async {
124+
addTearDown(testBinding.reset);
125+
126+
await tester.pumpWidget(TestZulipApp(
127+
child: AnimatedScaleOnPrimaryPointerDown(
128+
scaleEnd: 0.95,
129+
duration: Duration(milliseconds: 100),
130+
child: UnconstrainedBox(
131+
child: ZulipIconButton(
132+
icon: ZulipIcons.follow,
133+
onPressed: () {})))));
134+
await tester.pump();
135+
136+
final animatedScaleFinder = find.byType(AnimatedScale);
137+
138+
// Now that the widget is being held down, its scale should be at the target scaleEnd i.e 0.95.
139+
final gesture = await tester.startGesture(tester.getCenter(find.byType(ZulipIconButton)));
140+
await tester.pumpAndSettle(const Duration(milliseconds: 50));
141+
checkScale(tester, animatedScaleFinder, 0.95);
142+
143+
// After releasing, the scale must return to 1.0.
144+
await gesture.up();
145+
await tester.pumpAndSettle();
146+
checkScale(tester, animatedScaleFinder, 1.0);
147+
});
148+
});
149+
117150
// TODO test that the touch feedback fills the whole square
118151
});
119152
}

0 commit comments

Comments
 (0)