Skip to content

Commit

Permalink
Merge pull request #10 from cb-cloud/feature_swipe_horizontal
Browse files Browse the repository at this point in the history
Feature: horizontal swipe gesture to dismiss notification.
  • Loading branch information
Kurogoma4D authored Jun 25, 2021
2 parents 22f7191 + cf80dac commit 643f13d
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 27 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.0.0
### FEAT
- Added horizontal swipe gesture to dismiss notifications.
- Now, using cache of `InAppNotification`'s state. This makes it possible to decrease overhead on showing notification.

## 0.3.0
### FEAT
- **BREAKING: Overall, changes API.**
Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,8 @@ A Flutter package to show custom in-app notification with any Widgets.
```

## 🗺 Roadmap / Known issue
- ~~Null-safety migration~~ ✅
- Implementation for more gesture
- Swipe horizontal
- Performance optimization
- ~~Currently `InAppNotification` is recommended to use in `builder` of `MaterialApp`, but it means create instance each time of routing.~~ ✅
- Animation improvement
- ~~So far, we have confirmed that using a Widget with a height higher than the `minAlertHeight ` specified for `InApp` will slightly break the animation.~~ ✅
See [Discussions](https://github.com/cb-cloud/flutter_in_app_notification/discussions).
If you have some idea or proposal, feel free to [create new one](https://github.com/cb-cloud/flutter_in_app_notification/discussions/new).

## 💭 Have a question?
If you have a question or found issue, feel free to [create an issue](https://github.com/cb-cloud/flutter_in_app_notification/issues/new).
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.3.0"
version: "1.0.0"
matcher:
dependency: transitive
description:
Expand Down
91 changes: 73 additions & 18 deletions lib/src/in_app_notification.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import 'package:in_app_notification/src/size_listenable_container.dart';
@visibleForTesting
const notificationShowingDuration = Duration(milliseconds: 350);

@visibleForTesting
const notificationHorizontalAnimationDuration = Duration(milliseconds: 350);

/// A widget for display foreground notification.
///
/// It is mainly intended to wrap whole your app Widgets.
Expand All @@ -33,6 +36,8 @@ class InAppNotification extends StatefulWidget {

final Widget child;

static _InAppNotificationState? _state;

/// Shows specified Widget as notification.
///
/// [child] is required, this will be displayed as notification body.
Expand All @@ -54,11 +59,11 @@ class InAppNotification extends StatefulWidget {
Curve curve = Curves.ease,
@visibleForTesting FutureOr Function()? notificationCreatedCallback,
}) async {
final state = context.findAncestorStateOfType<_InAppNotificationState>();
_state ??= context.findAncestorStateOfType<_InAppNotificationState>();

assert(state != null);
assert(_state != null);

await state!.create(
await _state!.create(
child: child,
context: context,
onTap: onTap,
Expand All @@ -67,29 +72,43 @@ class InAppNotification extends StatefulWidget {
if (kDebugMode) {
await notificationCreatedCallback?.call();
}
state.show(duration: duration);
_state!.show(duration: duration);
}

@visibleForTesting
static void clearStateCache() {
_state = null;
}

@override
_InAppNotificationState createState() => _InAppNotificationState();
}

class _InAppNotificationState extends State<InAppNotification>
with SingleTickerProviderStateMixin {
with TickerProviderStateMixin {
VoidCallback? _onTap;
Timer? _timer;
double _dragDistance = 0.0;
double _verticalDragDistance = 0.0;
double _horizontalDragDistance = 0.0;

OverlayEntry? _overlay;
late CurvedAnimation _animation;
Animation? _horizontalAnimation;

double get _currentPosition =>
_animation.value * _notificationSize.height + _dragDistance;
double get _currentVerticalPosition =>
_animation.value * _notificationSize.height + _verticalDragDistance;
double get _currentHorizontalPosition =>
(_horizontalAnimation?.value ?? 0.0) + _horizontalDragDistance;

late final AnimationController _controller =
AnimationController(vsync: this, duration: notificationShowingDuration)
..addListener(_updateNotification);

late final AnimationController _horizontalAnimationController =
AnimationController(
vsync: this, duration: notificationHorizontalAnimationDuration)
..addListener(_updateNotification);

Size _notificationSize = Size.zero;
Completer<Size> _notificationSizeCompleter = Completer();
Size _screenSize = Size.zero;
Expand All @@ -112,23 +131,24 @@ class _InAppNotificationState extends State<InAppNotification>
}) async {
await dismiss();

_dragDistance = 0.0;
_verticalDragDistance = 0.0;
_horizontalDragDistance = 0.0;
_onTap = onTap;
_animation = CurvedAnimation(parent: _controller, curve: curve);
_horizontalAnimation = null;

_overlay = OverlayEntry(
builder: (context) {
if (_screenSize == Size.zero) {
_screenSize = MediaQuery.of(context).size *
MediaQuery.of(context).devicePixelRatio;
_screenSize = MediaQuery.of(context).size;
}

return Positioned(
bottom: MediaQuery.of(context).size.height -
MediaQuery.of(context).viewPadding.top -
_currentPosition,
left: 0,
right: 0,
_currentVerticalPosition,
left: _currentHorizontalPosition,
width: MediaQuery.of(context).size.width,
child: SizeListenableContainer(
onSizeChanged: (size) => _notificationSizeCompleter.complete(size),
child: GestureDetector(
Expand All @@ -137,6 +157,8 @@ class _InAppNotificationState extends State<InAppNotification>
onTapDown: (_) => _onTapDown(),
onVerticalDragUpdate: _onVerticalDragUpdate,
onVerticalDragEnd: _onVerticalDragEnd,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onHorizontalDragEnd: _onHorizontalDragEnd,
child: Material(color: Colors.transparent, child: child),
),
),
Expand Down Expand Up @@ -182,28 +204,60 @@ class _InAppNotificationState extends State<InAppNotification>
}

void _onVerticalDragUpdate(DragUpdateDetails details) {
_dragDistance = (_dragDistance + details.delta.dy)
_verticalDragDistance = (_verticalDragDistance + details.delta.dy)
.clamp(-_notificationSize.height, 0.0);
_updateNotification();
}

void _onVerticalDragEnd(DragEndDetails details) async {
final percentage = _currentPosition.abs() / _notificationSize.height;
final percentage =
_currentVerticalPosition.abs() / _notificationSize.height;
final velocity = details.velocity.pixelsPerSecond.dy * _screenSize.height;
if (velocity <= -1.0) {
await dismiss(animationFrom: percentage);
return;
}

if (percentage >= 0.5) {
if (_dragDistance == 0.0) return;
_dragDistance = 0.0;
if (_verticalDragDistance == 0.0) return;
_verticalDragDistance = 0.0;
_controller.forward(from: percentage);
} else {
dismiss(animationFrom: percentage);
}
}

void _onHorizontalDragUpdate(DragUpdateDetails details) {
_horizontalDragDistance += details.delta.dx;
_updateNotification();
}

void _onHorizontalDragEnd(DragEndDetails details) async {
final velocity = details.velocity.pixelsPerSecond.dx / _screenSize.width;
final position = _horizontalDragDistance / _screenSize.width;

if (velocity.abs() >= 1.0 || position.abs() >= 0.2) {
final endValue = _horizontalDragDistance.sign * _screenSize.width;
_horizontalAnimation =
Tween(begin: _horizontalDragDistance, end: endValue)
.chain(CurveTween(curve: Curves.easeOutCubic))
.animate(_horizontalAnimationController);
_horizontalDragDistance = 0.0;

await _horizontalAnimationController.forward(from: 0.0);
await dismiss();
} else {
final endValue = 0.0;
_horizontalAnimation =
Tween(begin: _horizontalDragDistance, end: endValue)
.chain(CurveTween(curve: Curves.easeOutCubic))
.animate(_horizontalAnimationController);
_horizontalDragDistance = 0.0;

await _horizontalAnimationController.forward(from: 0.0);
}
}

@override
Widget build(BuildContext context) {
return widget.child;
Expand All @@ -212,6 +266,7 @@ class _InAppNotificationState extends State<InAppNotification>
@override
void dispose() {
_controller.dispose();
_horizontalAnimationController.dispose();
super.dispose();
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: in_app_notification
description: A Flutter package to show custom in-app notification with any Widgets.
version: 0.3.0
version: 1.0.0
repository: https://github.com/cb-cloud/flutter_in_app_notification

environment:
Expand Down
39 changes: 39 additions & 0 deletions test/in_app_notification_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ void main() {

setUp(() {
TestWidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance!.resetEpoch();
});

tearDown(() {
InAppNotification.clearStateCache();
});

testWidgets('SizeListenableContainer test.', (tester) async {
Expand Down Expand Up @@ -120,4 +125,38 @@ void main() {
});
},
);

testWidgets(
'InAppNotification should dismiss on swipe left to right.',
(tester) async {
await tester.runAsync(() async {
final key = GlobalKey();
await tester.pumpWidget(base(key));

final context = key.currentContext!;

await InAppNotification.show(
child: Container(
height: 300,
color: Colors.green,
child: Text('test'),
),
context: context,
onTap: () {},
duration: Duration.zero,
notificationCreatedCallback: () async => await tester.pumpAndSettle(),
);

expect(find.text('test'), findsOneWidget);

await tester.dragFrom(Offset(400, 250), Offset(480, 250));
await tester.pumpAndSettle();
expect(find.text('test'), findsOneWidget);

await tester.fling(find.text('test'), Offset(1, 0), 1.0);
await tester.pumpAndSettle();
expect(find.text('test'), findsNothing);
});
},
);
}

0 comments on commit 643f13d

Please sign in to comment.