Skip to content

Commit c1a4c33

Browse files
committed
example app: add drag details example
1 parent 323ad5c commit c1a4c33

File tree

7 files changed

+150
-47
lines changed

7 files changed

+150
-47
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
- Exposed a *PositionedIndicatorContainer* widget that allows easy positioning of the indicator.
1919
- **Example app**:
2020
- Updated custom material indicator example.
21+
- Updated application design.
22+
- Added tooltips.
23+
- Added *ball indicator* example, which is an overview of the drag details based indicator.
2124
## 3.1.1
2225
- Fix:
2326
- Fixed *durations* parameter of *CustomRefreshIndicator* widget. Reported by [@jccd1996](https://github.com/jccd1996) in [#58](https://github.com/gonuit/flutter-custom-refresh-indicator/issues/58).

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,11 @@ Almost all of these examples are available in the example application.
8282

8383
| With complete state [[SOURCE](example/lib/indicators/check_mark_indicator.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/check-mark)] | Pull to fetch more [[SOURCE](example/lib/indicators/swipe_action.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/fetch-more)] | Envelope [[SOURCE](example/lib/indicators/envelope_indicator.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/envelope)] |
8484
| :----------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------: |
85-
| ![complete_state](readme/complete_state.gif) | ![fetch_more](readme/fetch_more.gif) | ![Envelope indicator](readme/envelope_indicator.gif) |
85+
| ![complete_state](readme/complete_state.gif) | ![fetch_more](readme/fetch_more.gif) | ![Envelope indicator](readme/envelope_indicator.gif) |
8686

87-
| Programmatically controlled [[SOURCE](example/lib/screens/programmatically_controlled_indicator_screen.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/programmatically-controlled)] | Your indicator | Your indicator |
88-
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------: |
89-
| ![programmatically_controlled](readme/programmatically_controlled.gif) | Have you created a fancy refresh indicator? This place is for you. [Open PR](https://github.com/gonuit/flutter-custom-refresh-indicator/pulls). | Have you created a fancy refresh indicator? This place is for you. [Open PR](https://github.com/gonuit/flutter-custom-refresh-indicator/pulls). |
87+
| Programmatically controlled [[SOURCE](example/lib/screens/programmatically_controlled_indicator_screen.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/programmatically-controlled)] | Based on drag details [[SOURCE](example/lib/indicators/ball_indicator.dart)][[DEMO](https://custom-refresh-indicator.klyta.it/#/drag-details)] | Your indicator |
88+
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------: |
89+
| ![programmatically_controlled](readme/programmatically_controlled.gif) | ![drag_details](readme/drag_details.gif) | Have you created a fancy refresh indicator? This place is for you. [Open PR](https://github.com/gonuit/flutter-custom-refresh-indicator/pulls). |
9090

9191
---
9292

example/lib/indicators/ball_indicator.dart

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import 'package:flutter/material.dart';
66
import 'package:flutter/scheduler.dart';
77
import 'package:flutter/services.dart';
88

9+
extension _GetRandomFromList<T> on List<T> {
10+
T get random {
11+
return this[math.Random().nextInt(length)];
12+
}
13+
}
14+
915
class ShakeState {
1016
final bool isInitial;
1117
final Offset shift;
@@ -22,28 +28,40 @@ class BallIndicator extends StatefulWidget {
2228
final double acceleration;
2329
final double ballRadius;
2430
final double shakeOffset;
31+
final List<Color> ballColors;
32+
final double strokeWidth;
33+
final IndicatorController? controller;
2534

2635
const BallIndicator({
2736
super.key,
2837
required this.child,
2938
required this.onRefresh,
3039
this.acceleration = 1.2,
3140
this.ballRadius = 25.0,
41+
this.strokeWidth = 3.0,
3242
this.shakeOffset = 4.0,
33-
});
43+
this.ballColors = const [Colors.blue],
44+
this.controller,
45+
}) : assert(ballColors.length > 0, 'ballColors cannot be empty.');
3446

3547
@override
3648
State<BallIndicator> createState() => _BallIndicatorState();
3749
}
3850

39-
class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateMixin {
51+
class _BallIndicatorState extends State<BallIndicator>
52+
with TickerProviderStateMixin {
53+
IndicatorController? _internalIndicatorController;
54+
IndicatorController get controller =>
55+
widget.controller ??
56+
(_internalIndicatorController ??= IndicatorController());
57+
4058
late final Ticker _ticker;
41-
final _controller = IndicatorController();
4259
final _ballPosition = ValueNotifier<Offset>(Offset.zero);
4360

4461
Offset _direction = Offset.zero;
4562
double _lastAngle = 0;
4663
Size _rectSize = Size.zero;
64+
late Color _ballColor;
4765

4866
late final _shakeController = AnimationController(
4967
vsync: this,
@@ -62,12 +80,32 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
6280

6381
@override
6482
void initState() {
83+
_ballColor =
84+
widget.ballColors.length > 1 ? widget.ballColors.random : Colors.blue;
85+
6586
_ticker = createTicker(_onTick);
6687
_shakeController.addStatusListener(_onShakeStatusChanged);
6788
_centerController.addListener(_onCenterChanged);
6889
super.initState();
6990
}
7091

92+
@override
93+
void didUpdateWidget(covariant BallIndicator oldWidget) {
94+
super.didUpdateWidget(oldWidget);
95+
96+
if (oldWidget.controller != widget.controller &&
97+
widget.controller != null) {
98+
_internalIndicatorController?.dispose();
99+
_internalIndicatorController = null;
100+
}
101+
102+
assert(
103+
widget.controller == null ||
104+
(widget.controller != null && _internalIndicatorController == null),
105+
'An internal indicator should not exist when an external indicator is provided.',
106+
);
107+
}
108+
71109
void _onShakeStatusChanged(AnimationStatus status) {
72110
if (status == AnimationStatus.completed && _shakeState.isInitial) {
73111
_shakeState = ShakeState(shift: _shakeState.shift, isInitial: false);
@@ -79,35 +117,35 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
79117
_ballPosition.value = _centerTween.transform(_centerController.value);
80118
}
81119

82-
@override
83-
void dispose() {
84-
_ticker.dispose();
85-
_controller.dispose();
86-
_ballPosition.dispose();
87-
_shakeController.dispose();
88-
_arrowOpacityController.dispose();
89-
_centerController.dispose();
90-
super.dispose();
91-
}
92-
93120
void _onHitBorder(Offset direction) {
94-
_shakeState = ShakeState(
95-
shift: direction * widget.shakeOffset,
96-
isInitial: true,
97-
);
98-
_shakeController.forward(from: 0.0);
99-
HapticFeedback.lightImpact().ignore();
100-
setState(() {});
121+
setState(() {
122+
_shakeState = ShakeState(
123+
shift: direction * widget.shakeOffset,
124+
isInitial: true,
125+
);
126+
127+
// Update ball color
128+
if (widget.ballColors.length > 1) {
129+
final colors =
130+
widget.ballColors.where((color) => color != _ballColor).toList();
131+
_ballColor = colors.random;
132+
}
133+
134+
_shakeController.forward(from: 0.0);
135+
HapticFeedback.lightImpact().ignore();
136+
});
101137
}
102138

103139
Duration _prevTickerDuration = Duration.zero;
104140
void _onTick(Duration time) {
105141
final delta = time - _prevTickerDuration;
106142
_prevTickerDuration = time;
107143

108-
Offset ballPosition = _ballPosition.value + _direction * delta.inMilliseconds.toDouble() * widget.acceleration;
144+
Offset ballPosition = _ballPosition.value +
145+
_direction * delta.inMilliseconds.toDouble() * widget.acceleration;
109146

110-
final Size ballSafeSpace = Size(_rectSize.width - widget.ballRadius * 2, _rectSize.height - widget.ballRadius * 2);
147+
final Size ballSafeSpace = Size(_rectSize.width - widget.ballRadius * 2,
148+
_rectSize.height - widget.ballRadius * 2);
111149

112150
if (ballPosition.dx < 0) {
113151
_onHitBorder(_direction);
@@ -146,7 +184,8 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
146184
required Offset origin,
147185
required Offset point,
148186
}) {
149-
final double angle = math.atan2(point.dy - origin.dy, point.dx - origin.dx) - (math.pi / 2);
187+
final double angle =
188+
math.atan2(point.dy - origin.dy, point.dx - origin.dx) - (math.pi / 2);
150189

151190
final normalizedAngle = (angle + math.pi) % (2 * math.pi) - math.pi;
152191
return normalizedAngle;
@@ -173,7 +212,7 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
173212
);
174213

175214
return CustomRefreshIndicator(
176-
controller: _controller,
215+
controller: controller,
177216
trailingScrollIndicatorVisible: false,
178217
leadingScrollIndicatorVisible: false,
179218
onRefresh: widget.onRefresh,
@@ -222,12 +261,12 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
222261
duration: const Duration(milliseconds: 60),
223262
curve: Curves.easeIn,
224263
decoration: BoxDecoration(
225-
color: Colors.blue,
264+
color: _ballColor,
226265
shape: BoxShape.circle,
227266
border: controller.isArmed
228267
? Border.all(
229268
color: Colors.white,
230-
width: 4,
269+
width: widget.strokeWidth,
231270
)
232271
: null,
233272
),
@@ -282,15 +321,17 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
282321
child: Arrow(
283322
radius: arrowRadius,
284323
angle: angle,
285-
strokeWidth: 4,
324+
strokeWidth: widget.strokeWidth,
286325
distanceFromCenter: ballSize * 0.75,
287326
color: Colors.white,
288327
),
289328
),
290329
);
291330
},
292331
),
293-
if (controller.isLoading || controller.isSettling || controller.isComplete)
332+
if (controller.isLoading ||
333+
controller.isSettling ||
334+
controller.isComplete)
294335
Align(
295336
alignment: Alignment.topLeft,
296337
child: AnimatedBuilder(
@@ -303,7 +344,10 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
303344
},
304345
),
305346
),
306-
if (controller.isDragging || controller.isArmed)
347+
if (controller.isDragging ||
348+
controller.isArmed ||
349+
controller.isCanceling ||
350+
controller.isFinalizing)
307351
PositionedTransition(
308352
rect: RelativeRectTween(
309353
begin: RelativeRect.fromRect(
@@ -334,6 +378,17 @@ class _BallIndicatorState extends State<BallIndicator> with TickerProviderStateM
334378
);
335379
});
336380
}
381+
382+
@override
383+
void dispose() {
384+
_ticker.dispose();
385+
_ballPosition.dispose();
386+
_shakeController.dispose();
387+
_arrowOpacityController.dispose();
388+
_centerController.dispose();
389+
_internalIndicatorController?.dispose();
390+
super.dispose();
391+
}
337392
}
338393

339394
class Arrow extends StatelessWidget {
@@ -414,10 +469,12 @@ class ArrowPainter extends CustomPainter {
414469
/// Calculate the vector from A to B
415470
Offset direction = end - start;
416471
// Calculate the magnitude of the direction vector
417-
double magnitude = math.sqrt(direction.dx * direction.dx + direction.dy * direction.dy);
472+
double magnitude =
473+
math.sqrt(direction.dx * direction.dx + direction.dy * direction.dy);
418474

419475
/// Normalize the vector
420-
Offset unitVector = Offset(direction.dx / magnitude, direction.dy / magnitude);
476+
Offset unitVector =
477+
Offset(direction.dx / magnitude, direction.dy / magnitude);
421478

422479
/// Scale the unit vector by 50 to get the new point offset
423480
Offset startFrom = start + unitVector * distanceFromCenter;
@@ -426,13 +483,16 @@ class ArrowPainter extends CustomPainter {
426483

427484
/// Arrowhead settings
428485
double arrowLength = 20; // Length of the arrow lines
429-
double arrowAngle = math.pi / 6; // Angle of the arrow lines from the main line
486+
double arrowAngle =
487+
math.pi / 6; // Angle of the arrow lines from the main line
430488

431489
/// Calculating arrowhead lines
432490
final arrowEnd1 = Offset(
433-
end.dx - arrowLength * math.sin(angle - arrowAngle), end.dy + arrowLength * math.cos(angle - arrowAngle));
491+
end.dx - arrowLength * math.sin(angle - arrowAngle),
492+
end.dy + arrowLength * math.cos(angle - arrowAngle));
434493
final arrowEnd2 = Offset(
435-
end.dx - arrowLength * math.sin(angle + arrowAngle), end.dy + arrowLength * math.cos(angle + arrowAngle));
494+
end.dx - arrowLength * math.sin(angle + arrowAngle),
495+
end.dy + arrowLength * math.cos(angle + arrowAngle));
436496

437497
final path = Path()
438498
..moveTo(arrowEnd1.dx, arrowEnd1.dy)

example/lib/main.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class MyApp extends StatelessWidget {
4040
'/example': (context) => const CustomMaterialIndicatorScreen(),
4141
'/plane': (context) => const PlaneIndicatorScreen(),
4242
'/ice-cream': (context) => const IceCreamIndicatorScreen(),
43-
'/ball': (context) => const BallIndicatorScreen(),
43+
'/drag-details': (context) => const BallIndicatorScreen(),
4444
'/check-mark': (context) => const CheckMarkIndicatorScreen(),
4545
'/warp': (context) => const WarpIndicatorScreen(),
4646
'/envelope': (context) => const EnvelopIndicatorScreen(),
@@ -118,19 +118,19 @@ class MainScreen extends StatelessWidget {
118118
child: Container(
119119
height: 50,
120120
alignment: Alignment.center,
121-
child: const Text("Event based"),
121+
child: const Text("Drag details (Ball)"),
122122
),
123123
onPressed: () => Navigator.pushNamed(
124124
context,
125-
'/ball',
125+
'/drag-details',
126126
),
127127
),
128128
const SizedBox(height: 16),
129129
ElevatedButton(
130130
child: Container(
131131
height: 50,
132132
alignment: Alignment.center,
133-
child: const Text("Programmatically-controlled"),
133+
child: const Text("Programmatically-controlled (Warp)"),
134134
),
135135
onPressed: () => Navigator.pushNamed(
136136
context,

example/lib/screens/ball_indicator_screen.dart

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:custom_refresh_indicator/custom_refresh_indicator.dart';
12
import 'package:example/indicators/ball_indicator.dart';
23
import 'package:example/widgets/example_app_bar.dart';
34
import 'package:example/widgets/example_list.dart';
@@ -11,25 +12,62 @@ class BallIndicatorScreen extends StatefulWidget {
1112
}
1213

1314
class _BallIndicatorScreenState extends State<BallIndicatorScreen> {
15+
final _controller = IndicatorController();
16+
1417
@override
1518
Widget build(BuildContext context) {
1619
return Scaffold(
1720
appBar: const ExampleAppBar(
18-
title: "Event based",
21+
title: "Drag details",
1922
),
2023
body: SafeArea(
2124
child: BallIndicator(
25+
controller: _controller,
26+
ballColors: const [
27+
Colors.blue,
28+
Colors.red,
29+
Colors.green,
30+
Colors.amber,
31+
Colors.pink,
32+
Colors.purple,
33+
Colors.cyan,
34+
Colors.orange,
35+
Colors.yellow,
36+
],
2237
onRefresh: () async {
2338
await Future.delayed(const Duration(seconds: 5));
2439
},
25-
child: const ExampleList(
40+
child: ExampleList(
2641
leading: Column(
2742
children: [
28-
ListHelpBox(
43+
const ListHelpBox(
2944
child: Text(
3045
r"You can create indicators based on user drag details.",
3146
),
3247
),
48+
ListHelpBox(
49+
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
50+
child: ListenableBuilder(
51+
listenable: _controller,
52+
builder: (_, __) => Column(
53+
children: [
54+
const Expanded(
55+
child: Text(
56+
"This indicator is based on the local position of the drag details, "
57+
"which is currently: ",
58+
),
59+
),
60+
const SizedBox(height: 8),
61+
ListHelpBox(
62+
icon: Icons.data_object_sharp,
63+
child: Text(
64+
"${_controller.dragDetails?.localPosition}",
65+
),
66+
)
67+
],
68+
),
69+
),
70+
),
3371
],
3472
),
3573
),

0 commit comments

Comments
 (0)