Skip to content

Commit 4b3744b

Browse files
committed
implement a press event
1 parent ec7e98d commit 4b3744b

File tree

5 files changed

+191
-64
lines changed

5 files changed

+191
-64
lines changed

packages/mix/lib/mix.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export 'src/core/spec.dart';
7070
export 'src/core/styled_widget.dart';
7171
export 'src/core/utility.dart';
7272
export 'src/core/variant.dart';
73+
export 'src/core/widget_state/press_event_mix_state.dart';
7374
export 'src/core/widget_state/widget_state_controller.dart';
7475
/// MODIFIERS
7576
export 'src/modifiers/align_widget_modifier.dart';

packages/mix/lib/src/core/widget_state/internal/gesture_mix_state.dart

Lines changed: 106 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@ import 'dart:async';
33
import 'package:flutter/material.dart';
44

55
import '../widget_state_controller.dart';
6+
import '../press_event_mix_state.dart';
7+
8+
abstract interface class WidgetStateHandler {
9+
@visibleForTesting
10+
late final MixWidgetStateController controller;
11+
}
12+
13+
mixin HandlePress on WidgetStateHandler {
14+
int _pressCount = 0;
15+
Timer? _timer;
16+
17+
void pressCallback();
18+
19+
void handlePress({required bool value, required Duration delay}) {
20+
controller.pressed = value;
21+
if (value) {
22+
_pressCount++;
23+
final initialPressCount = _pressCount;
24+
_unpressAfterDelay(initialPressCount, delay: delay);
25+
}
26+
}
27+
28+
void _unpressAfterDelay(int initialPressCount, {required Duration delay}) {
29+
void unpressCallback() {
30+
if (controller.pressed && _pressCount == initialPressCount) {
31+
controller.pressed = false;
32+
pressCallback();
33+
}
34+
}
35+
36+
_timer?.cancel();
37+
38+
if (delay != Duration.zero) {
39+
_timer = Timer(delay, unpressCallback);
40+
} else {
41+
unpressCallback();
42+
}
43+
}
44+
}
645

746
class GestureMixStateWidget extends StatefulWidget {
847
const GestureMixStateWidget({
@@ -83,15 +122,21 @@ class GestureMixStateWidget extends StatefulWidget {
83122
State createState() => _GestureMixStateWidgetState();
84123
}
85124

86-
class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
87-
int _pressCount = 0;
88-
Timer? _timer;
89-
late final MixWidgetStateController _controller;
125+
abstract class _GestureMixStateWidgetStateWithController
126+
extends State<GestureMixStateWidget> implements WidgetStateHandler {}
127+
128+
class _GestureMixStateWidgetState
129+
extends _GestureMixStateWidgetStateWithController with HandlePress {
130+
PressEvent _event = PressEvent.idle;
131+
132+
@override
133+
@protected
134+
late final MixWidgetStateController controller;
90135

91136
@override
92137
void initState() {
93138
super.initState();
94-
_controller = widget.controller ?? MixWidgetStateController();
139+
controller = widget.controller ?? MixWidgetStateController();
95140
}
96141

97142
void _onPanUpdate(DragUpdateDetails event) {
@@ -102,42 +147,27 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
102147
widget.onPanDown?.call(details);
103148
}
104149

105-
_handlePress(bool value) {
106-
_controller.pressed = value;
107-
if (value) {
108-
_pressCount++;
109-
final initialPressCount = _pressCount;
110-
_unpressAfterDelay(initialPressCount);
111-
}
112-
}
113-
114150
void _onPanEnd(DragEndDetails details) {
115-
_handlePress(true);
151+
handlePress(value: true, delay: widget.unpressDelay);
116152
widget.onPanEnd?.call(details);
117153
}
118154

119-
void _onTapUp(TapUpDetails details) {
120-
_controller.longPressed = false;
121-
widget.onTapUp?.call(details);
122-
}
123-
124-
void _onTapCancel() {
125-
_controller.longPressed = false;
126-
widget.onTapCancel?.call();
127-
}
128-
129155
void _onLongPressStart(LongPressStartDetails details) {
130-
_controller.longPressed = true;
156+
controller.longPressed = true;
131157
widget.onLongPressStart?.call(details);
132158
}
133159

134160
void _onLongPressEnd(LongPressEndDetails details) {
135-
_controller.longPressed = false;
161+
controller.longPressed = false;
162+
controller.pressed = false;
163+
setState(() {
164+
_event = PressEvent.idle;
165+
});
136166
widget.onLongPressEnd?.call(details);
137167
}
138168

139169
void _onLongPressCancel() {
140-
_controller.longPressed = false;
170+
controller.longPressed = false;
141171
widget.onLongPressCancel?.call();
142172
}
143173

@@ -149,28 +179,30 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
149179
widget.onPanStart?.call(details);
150180
}
151181

152-
void _unpressAfterDelay(int initialPressCount) {
153-
void unpressCallback() {
154-
if (_controller.pressed && _pressCount == initialPressCount) {
155-
_controller.pressed = false;
156-
}
157-
}
182+
void _onTap() {
183+
handlePress(value: true, delay: widget.unpressDelay);
158184

159-
_timer?.cancel();
185+
widget.onTap?.call();
186+
if (widget.enableFeedback) Feedback.forTap(context);
187+
}
160188

161-
final delay = widget.unpressDelay;
189+
void _onTapDown(TapDownDetails details) {
190+
setState(() {
191+
_event = PressEvent.onTapDown;
192+
});
193+
}
162194

163-
if (delay != Duration.zero) {
164-
_timer = Timer(delay, unpressCallback);
165-
} else {
166-
unpressCallback();
167-
}
195+
void _onTapUp(TapUpDetails details) {
196+
setState(() {
197+
_event = PressEvent.onTapUp;
198+
});
199+
controller.longPressed = false;
200+
widget.onTapUp?.call(details);
168201
}
169202

170-
void _onTap() {
171-
_handlePress(true);
172-
widget.onTap?.call();
173-
if (widget.enableFeedback) Feedback.forTap(context);
203+
void _onTapCancel() {
204+
controller.longPressed = false;
205+
widget.onTapCancel?.call();
174206
}
175207

176208
void _onLongPress() {
@@ -182,28 +214,40 @@ class _GestureMixStateWidgetState extends State<GestureMixStateWidget> {
182214
void dispose() {
183215
_timer?.cancel();
184216
// Dispose if being managed internally
185-
if (widget.controller == null) _controller.dispose();
217+
if (widget.controller == null) controller.dispose();
186218
super.dispose();
187219
}
188220

221+
@override
222+
void pressCallback() {
223+
setState(() {
224+
_event = PressEvent.idle;
225+
});
226+
}
227+
189228
@override
190229
Widget build(BuildContext context) {
191-
return GestureDetector(
192-
onTapUp: widget.onTap != null ? _onTapUp : null,
193-
onTap: widget.onTap != null ? _onTap : null,
194-
onTapCancel: widget.onTap != null ? _onTapCancel : null,
195-
onLongPressCancel: widget.onLongPress != null ? _onLongPressCancel : null,
196-
onLongPress: widget.onLongPress != null ? _onLongPress : null,
197-
onLongPressStart: widget.onLongPress != null ? _onLongPressStart : null,
198-
onLongPressEnd: widget.onLongPress != null ? _onLongPressEnd : null,
199-
onPanDown: widget.onPanDown != null ? _onPanDown : null,
200-
onPanStart: widget.onPanStart != null ? _onPanStart : null,
201-
onPanUpdate: widget.onPanUpdate != null ? _onPanUpdate : null,
202-
onPanEnd: widget.onPanEnd != null ? _onPanEnd : null,
203-
onPanCancel: widget.onPanCancel != null ? _onPanCancel : null,
204-
behavior: widget.hitTestBehavior,
205-
excludeFromSemantics: widget.excludeFromSemantics,
206-
child: widget.child,
230+
return PressEventMixWidgetState(
231+
_event,
232+
child: GestureDetector(
233+
onTapDown: widget.onTap != null ? _onTapDown : null,
234+
onTapUp: widget.onTap != null ? _onTapUp : null,
235+
onTap: widget.onTap != null ? _onTap : null,
236+
onTapCancel: widget.onTap != null ? _onTapCancel : null,
237+
onLongPressCancel:
238+
widget.onLongPress != null ? _onLongPressCancel : null,
239+
onLongPress: widget.onLongPress != null ? _onLongPress : null,
240+
onLongPressStart: widget.onLongPress != null ? _onLongPressStart : null,
241+
onLongPressEnd: widget.onLongPress != null ? _onLongPressEnd : null,
242+
onPanDown: widget.onPanDown != null ? _onPanDown : null,
243+
onPanStart: widget.onPanStart != null ? _onPanStart : null,
244+
onPanUpdate: widget.onPanUpdate != null ? _onPanUpdate : null,
245+
onPanEnd: widget.onPanEnd != null ? _onPanEnd : null,
246+
onPanCancel: widget.onPanCancel != null ? _onPanCancel : null,
247+
behavior: widget.hitTestBehavior,
248+
excludeFromSemantics: widget.excludeFromSemantics,
249+
child: widget.child,
250+
),
207251
);
208252
}
209253
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
enum PressEvent {
4+
idle,
5+
onTapUp,
6+
onTapDown;
7+
}
8+
9+
class PressEventMixWidgetState extends InheritedWidget {
10+
const PressEventMixWidgetState(this.event, {super.key, required super.child});
11+
12+
static PressEventMixWidgetState? of(BuildContext context) {
13+
return context.dependOnInheritedWidgetOfExactType();
14+
}
15+
16+
final PressEvent event;
17+
18+
@override
19+
bool updateShouldNotify(PressEventMixWidgetState oldWidget) {
20+
return event != oldWidget.event;
21+
}
22+
}

packages/mix/lib/src/variants/widget_state_variant.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import 'package:flutter/widgets.dart';
22

33
import '../core/factory/style_mix.dart';
44
import '../core/variant.dart';
5+
import '../core/widget_state/internal/gesture_mix_state.dart';
56
import '../core/widget_state/internal/mouse_region_mix_state.dart';
7+
import '../core/widget_state/press_event_mix_state.dart';
68
import '../core/widget_state/widget_state_controller.dart';
79
import 'context_variant.dart';
810

@@ -60,8 +62,19 @@ class OnHoverVariant extends MixWidgetStateVariant<PointerPosition?> {
6062
}
6163

6264
/// Applies styles when the widget is pressed.
63-
class OnPressVariant extends _ToggleMixStateVariant {
64-
const OnPressVariant() : super(MixWidgetState.pressed);
65+
class OnPressVariant extends MixWidgetStateVariant<PressEvent> {
66+
const OnPressVariant();
67+
68+
@override
69+
PressEvent builder(BuildContext context) {
70+
final event = PressEventMixWidgetState.of(context)?.event;
71+
72+
return event ?? PressEvent.idle;
73+
}
74+
75+
@override
76+
bool when(BuildContext context) =>
77+
MixWidgetState.hasStateOf(context, MixWidgetState.pressed);
6578
}
6679

6780
/// Applies styles when the widget is long pressed.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:mix/mix.dart';
4+
import 'package:mix/src/core/widget_state/internal/gesture_mix_state.dart';
5+
6+
import '../../../helpers/context_finder.dart';
7+
import '../../../helpers/testing_utils.dart';
8+
9+
extension _WidgetTesterX on WidgetTester {
10+
PressEventMixWidgetState findPressEventWidget() {
11+
return findWidgetOfType<PressEventMixWidgetState>();
12+
}
13+
}
14+
15+
void main() {
16+
testWidgets(
17+
'Pressable should transition through (onTapDown and onTapUp) states correctly',
18+
(tester) async {
19+
int counter = 0;
20+
await tester.pumpMaterialApp(
21+
PressableBox(
22+
onPress: () {
23+
counter++;
24+
},
25+
child: Text('$counter'),
26+
),
27+
);
28+
29+
// Initial state should be idle
30+
expect(tester.findPressEventWidget().event, PressEvent.idle);
31+
32+
// Press down on the PressableBox
33+
final gesture = await tester.press(find.byType(PressableBox));
34+
await tester.pumpAndSettle();
35+
expect(tester.findPressEventWidget().event, PressEvent.onTapDown);
36+
37+
// Release the press
38+
await gesture.up();
39+
await tester.pumpAndSettle();
40+
expect(tester.findPressEventWidget().event, PressEvent.onTapUp);
41+
42+
expect(counter, equals(1));
43+
44+
await tester.pump(const Duration(milliseconds: 300));
45+
expect(tester.findPressEventWidget().event, PressEvent.idle);
46+
});
47+
}

0 commit comments

Comments
 (0)