From 0aff6e83fc21a81ec35c43a4fcc444b7caf895d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:32:29 -0300 Subject: [PATCH 1/6] feature: initial stack test --- lib/src/core/toastification_manager.dart | 172 ++++++++--------------- 1 file changed, 61 insertions(+), 111 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index b00dc0f..eeec36b 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -20,31 +20,29 @@ class ToastificationManager { OverlayEntry? _overlayEntry; - /// this key is attached to [AnimatedList] so we can add or remove items using it. - final _listGlobalKey = GlobalKey(); - - /// this is the list of items that are currently shown + /// This is the list of items that are currently shown /// if the list is empty, the overlay entry will be removed - final List _notifications = []; + final ValueNotifier> _notifications = + ValueNotifier>([]); - /// this is the delay for showing the overlay entry + /// This is the delay for showing the overlay entry /// We need this delay because we want to show the item animation after - /// the overlay created + /// the overlay is created /// /// When we want to show first toast, we need to wait for the overlay to be created /// and then show the toast item. final _createOverlayDelay = const Duration(milliseconds: 100); - /// this is the delay for removing the overlay entry + /// This is the delay for removing the overlay entry /// - /// when we want to remove the last toast, we need to wait for the animation + /// When we want to remove the last toast, we need to wait for the animation /// to be completed and then remove the overlay. final _removeOverlayDelay = const Duration(milliseconds: 50); /// Shows a [ToastificationItem] with the given [builder] and [animationBuilder]. /// - /// if the [_notifications] list is empty, we will create the [_overlayEntry] - /// otherwise we will just add the [item] to the [_notifications] list. + /// If the [_notifications] list is empty, we will create the [_overlayEntry] + /// otherwise, we will just add the [item] to the [_notifications] list. ToastificationItem showCustom({ required OverlayState overlayState, required ToastificationBuilder builder, @@ -65,8 +63,8 @@ class ToastificationManager { }, ); - /// we need this delay because we want to show the item animation after - /// the overlay created + /// We need this delay because we want to show the item animation after + /// the overlay is created Duration delay = const Duration(milliseconds: 10); if (_overlayEntry == null) { @@ -78,90 +76,49 @@ class ToastificationManager { Future.delayed( delay, () { - _notifications.insert(0, item); - - _listGlobalKey.currentState?.insertItem( - 0, - duration: _createAnimationDuration(item), - ); + _notifications.value = [item, ..._notifications.value]; }, ); - // TODO(payam): add limit count feature - return item; } /// Finds the [ToastificationItem] with the given [id]. ToastificationItem? findToastificationItem(String id) { try { - return _notifications + return _notifications.value .firstWhereOrNull((notification) => notification.id == id); } catch (e) { return null; } } - /// using this method you can remove a notification item - /// if there is no notification in the notification list, + /// Using this method you can remove a notification item + /// If there is no notification in the notification list, /// we will remove the overlay entry /// - /// if the [showRemoveAnimation] is true, we will show the remove animation + /// If the [showRemoveAnimation] is true, we will show the remove animation /// of the [notification] item. - /// otherwise we will remove the notification without showing any animation. - /// this is useful when you want to remove the notification manually, + /// Otherwise, we will remove the notification without showing any animation. + /// This is useful when you want to remove the notification manually, /// like when you have some [Dismissible] widget void dismiss( ToastificationItem notification, { bool showRemoveAnimation = true, }) { - final index = _notifications.indexOf(notification); - // print("Toastification Manager Dismiss Notifications: $_notifications"); - if (index != -1) { - notification = _notifications[index]; - - if (notification.isRunning) { - notification.stop(); - } - - final removedItem = _notifications.removeAt(index); - - /// if the [showRemoveAnimation] is true, we will show the remove animation - /// of the notification. - if (showRemoveAnimation) { - _listGlobalKey.currentState?.removeItem( - index, - (BuildContext context, Animation animation) { - return ToastHolderWidget( - item: removedItem, - animation: animation, - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(removedItem), - ); - }, - duration: _createAnimationDuration(removedItem), - ); - - /// if the [showRemoveAnimation] is false, we will remove the notification - /// without showing the remove animation. - } else { - _listGlobalKey.currentState?.removeItem( - index, - (BuildContext context, Animation animation) { - return const SizedBox.shrink(); - }, - ); - } + final updatedList = List.from(_notifications.value); + if (updatedList.remove(notification)) { + _notifications.value = updatedList; - /// we will remove the [_overlayEntry] if there are no notifications + /// We will remove the [_overlayEntry] if there are no notifications /// We need to check if the _notifications list is empty twice. /// To make sure after the delay, there are no new notifications added. - if (_notifications.isEmpty) { + if (_notifications.value.isEmpty) { Future.delayed( - (removedItem.animationDuration ?? config.animationDuration) + + (notification.animationDuration ?? config.animationDuration) + _removeOverlayDelay, () { - if (_notifications.isEmpty) { + if (_notifications.value.isEmpty) { _overlayEntry?.remove(); _overlayEntry = null; } @@ -175,18 +132,12 @@ class ToastificationManager { /// The [delayForAnimation] parameter is optional and defaults to true. /// When it is true, it adds a delay for better animation. void dismissAll({bool delayForAnimation = true}) async { - // Creates a new list cloneList that has all the notifications from the _notifications list, but in reverse order. - final cloneList = _notifications.toList(growable: false).reversed; + final cloneList = _notifications.value.toList(growable: false).reversed; - // For each cloned "toastItem" notification in "cloneList", - // we will remove it and then pause for a duration if delayForAnimation is true. for (final toastItem in cloneList) { - /// If the item is still in the [_notification] list, we will remove it if (findToastificationItem(toastItem.id) != null) { - // Dismiss the current notification item dismiss(toastItem); - // If delayForAnimation is true, wait for 150ms before proceeding to the next item if (delayForAnimation) { await Future.delayed(const Duration(milliseconds: 150)); } @@ -194,14 +145,18 @@ class ToastificationManager { } } - /// remove the first notification in the list. + /// Remove the first notification in the list. void dismissFirst() { - dismiss(_notifications.first); + if (_notifications.value.isNotEmpty) { + dismiss(_notifications.value.first); + } } - /// remove the last notification in the list. + /// Remove the last notification in the list. void dismissLast() { - dismiss(_notifications.last); + if (_notifications.value.isNotEmpty) { + dismiss(_notifications.value.last); + } } void _createNotificationHolder(OverlayState overlay) { @@ -209,49 +164,46 @@ class ToastificationManager { overlay.insert(_overlayEntry!); } - /// create a [OverlayEntry] as holder of the notifications + /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { return OverlayEntry( opaque: false, builder: (context) { - Widget overlay = Align( + return Align( alignment: alignment, child: Container( margin: _marginBuilder(context, alignment, config), constraints: BoxConstraints.tightFor( width: config.itemWidth, ), - child: MediaQuery.removePadding( - context: context, - removeTop: true, - removeBottom: true, - child: AnimatedList( - key: _listGlobalKey, - clipBehavior: config.clipBehavior, - initialItemCount: _notifications.length, - reverse: alignment.y >= 0, - primary: true, - shrinkWrap: true, - itemBuilder: ( - BuildContext context, - int index, - Animation animation, - ) { - final item = _notifications[index]; - - return ToastHolderWidget( - item: item, - animation: animation, - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(item), - ); - }, - ), + child: ValueListenableBuilder>( + valueListenable: _notifications, + builder: (context, notifications, child) { + return Stack( + alignment: Alignment.topCenter, + children: notifications.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final offset = index * 10.0; + + return Positioned( + key: ValueKey(item.id), + top: offset, + left: 0, + right: 0, + child: ToastHolderWidget( + item: item, + animation: const AlwaysStoppedAnimation(1.0), + alignment: alignment, + transformerBuilder: _toastAnimationBuilder(item), + ), + ); + }).toList(), + ); + }, ), ), ); - - return overlay; }, ); } @@ -267,8 +219,6 @@ class ToastificationManager { marginValue = marginValue.add(MediaQuery.of(context).viewInsets); } - /// Add the MediaQuery viewPadding as margin so other widgets behind the toastification overlay - /// will be touchable and not covered by the toastification overlay. return marginValue.add(MediaQuery.of(context).viewPadding); } From 1ba7d15ba443634139ca6f21af013e34a02f7cca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:58:22 -0300 Subject: [PATCH 2/6] feat: enhance toast notifications with hover effects and opacity transitions --- lib/src/core/toastification_manager.dart | 125 +++++++++++++++++------ 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index eeec36b..c20c964 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -166,41 +166,106 @@ class ToastificationManager { /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { + ValueNotifier _isHovering = ValueNotifier(false); + return OverlayEntry( opaque: false, builder: (context) { return Align( alignment: alignment, - child: Container( - margin: _marginBuilder(context, alignment, config), - constraints: BoxConstraints.tightFor( - width: config.itemWidth, - ), - child: ValueListenableBuilder>( - valueListenable: _notifications, - builder: (context, notifications, child) { - return Stack( - alignment: Alignment.topCenter, - children: notifications.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final offset = index * 10.0; - - return Positioned( - key: ValueKey(item.id), - top: offset, - left: 0, - right: 0, - child: ToastHolderWidget( - item: item, - animation: const AlwaysStoppedAnimation(1.0), - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(item), - ), - ); - }).toList(), - ); - }, + child: MouseRegion( + onEnter: (_) => _isHovering.value = true, + onExit: (_) => _isHovering.value = false, + child: Container( + margin: _marginBuilder(context, alignment, config), + constraints: BoxConstraints.tightFor( + width: config.itemWidth, + ), + child: ValueListenableBuilder>( + valueListenable: _notifications, + builder: (context, notifications, child) { + return ValueListenableBuilder( + valueListenable: _isHovering, + builder: (context, isHovering, child) { + // Limita aos últimos 5 toasts (o último item é o mais novo) + final maxToasts = 5; + final visibleNotifications = + notifications.length <= maxToasts + ? notifications + : notifications + .sublist(notifications.length - maxToasts); + + // Reverte a lista para que o toast mais novo seja renderizado por último + final reverseVisibleNotifications = + visibleNotifications.reversed.toList(); + + final totalToasts = visibleNotifications.length; + final maxOpacity = 1.0; + final minOpacity = 0.5; // Ajuste conforme necessário + + return Stack( + alignment: Alignment.topCenter, + children: reverseVisibleNotifications + .asMap() + .entries + .map((entry) { + final index = entry.key; + final item = entry.value; + + // Calcula a opacidade: todos os toasts têm opacidade máxima ao hover + double opacity; + if (isHovering) { + opacity = maxOpacity; + } else { + final reverseListIndex = totalToasts - index - 1; + if (reverseListIndex != 0) { + opacity = maxOpacity - + (reverseListIndex / (totalToasts - 1)) * + (maxOpacity - minOpacity); + } else { + opacity = maxOpacity; + } + } + + // Expande a stack ao hover + final targetOffset = isHovering + ? (totalToasts - index - 1) * + 90.0 // Ajuste conforme necessário + : (totalToasts - index - 1) * + 10.0; // Ajuste conforme necessário + + return AnimatedPositioned( + key: ValueKey(item.id), + duration: _createAnimationDuration(item), + curve: Curves.easeInOut, + top: targetOffset, + left: 0, + right: 0, + child: AnimatedOpacity( + opacity: opacity, + duration: _createAnimationDuration(item), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: _createAnimationDuration(item), + builder: (context, animationValue, child) { + return ToastHolderWidget( + item: item, + animation: + AlwaysStoppedAnimation(animationValue), + alignment: alignment, + transformerBuilder: + _toastAnimationBuilder(item), + ); + }, + ), + ), + ); + }).toList(), + ); + }, + ); + }, + ), ), ), ); From 874097912bcc73df04f3932c2f07cb355b7e9309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:10:44 -0300 Subject: [PATCH 3/6] refactor: change function structure, add notification pause and resume timer function --- lib/src/core/toastification_manager.dart | 164 +++++++++++------------ 1 file changed, 78 insertions(+), 86 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index c20c964..0cdc6b9 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -166,7 +166,20 @@ class ToastificationManager { /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { - ValueNotifier _isHovering = ValueNotifier(false); + ValueNotifier isHovering = ValueNotifier(false); + + // Handle hover state changes to pause/start timers + isHovering.addListener(() { + for (var item in _notifications.value) { + if (item.hasTimer) { + if (isHovering.value) { + item.pause(); + } else { + item.start(); + } + } + } + }); return OverlayEntry( opaque: false, @@ -174,95 +187,74 @@ class ToastificationManager { return Align( alignment: alignment, child: MouseRegion( - onEnter: (_) => _isHovering.value = true, - onExit: (_) => _isHovering.value = false, + onEnter: (_) => isHovering.value = true, + onExit: (_) => isHovering.value = false, child: Container( margin: _marginBuilder(context, alignment, config), - constraints: BoxConstraints.tightFor( - width: config.itemWidth, - ), - child: ValueListenableBuilder>( - valueListenable: _notifications, - builder: (context, notifications, child) { - return ValueListenableBuilder( - valueListenable: _isHovering, - builder: (context, isHovering, child) { - // Limita aos últimos 5 toasts (o último item é o mais novo) - final maxToasts = 5; - final visibleNotifications = - notifications.length <= maxToasts - ? notifications - : notifications - .sublist(notifications.length - maxToasts); - - // Reverte a lista para que o toast mais novo seja renderizado por último - final reverseVisibleNotifications = - visibleNotifications.reversed.toList(); - - final totalToasts = visibleNotifications.length; - final maxOpacity = 1.0; - final minOpacity = 0.5; // Ajuste conforme necessário - - return Stack( - alignment: Alignment.topCenter, - children: reverseVisibleNotifications - .asMap() - .entries - .map((entry) { - final index = entry.key; - final item = entry.value; - - // Calcula a opacidade: todos os toasts têm opacidade máxima ao hover - double opacity; - if (isHovering) { - opacity = maxOpacity; - } else { - final reverseListIndex = totalToasts - index - 1; - if (reverseListIndex != 0) { - opacity = maxOpacity - - (reverseListIndex / (totalToasts - 1)) * - (maxOpacity - minOpacity); - } else { - opacity = maxOpacity; - } - } - - // Expande a stack ao hover - final targetOffset = isHovering - ? (totalToasts - index - 1) * - 90.0 // Ajuste conforme necessário - : (totalToasts - index - 1) * - 10.0; // Ajuste conforme necessário - - return AnimatedPositioned( - key: ValueKey(item.id), + constraints: BoxConstraints.tightFor(width: config.itemWidth), + child: AnimatedBuilder( + animation: Listenable.merge([_notifications, isHovering]), + builder: (context, child) { + const int maxToasts = 5; + const double maxOpacity = 1.0; + const double minOpacity = 0.5; + const double expandedOffset = 90.0; + const double collapsedOffset = 10.0; + + final notifications = _notifications.value; + final isHoveringValue = isHovering.value; + + final startIndex = notifications.length > maxToasts + ? notifications.length - maxToasts + : 0; + final visibleNotifications = + notifications.sublist(startIndex).reversed.toList(); + + return Stack( + alignment: Alignment.topCenter, + children: + List.generate(visibleNotifications.length, (index) { + final item = visibleNotifications[index]; + final reverseIndex = + visibleNotifications.length - index - 1; + + final opacity = isHoveringValue || reverseIndex == 0 + ? maxOpacity + : maxOpacity - + (reverseIndex / + (visibleNotifications.length - 1)) * + (maxOpacity - minOpacity); + + final targetOffset = reverseIndex * + (isHoveringValue ? expandedOffset : collapsedOffset); + + return AnimatedPositioned( + key: ValueKey(item.id), + duration: _createAnimationDuration(item), + curve: Curves.easeInOut, + top: targetOffset, + left: 0, + right: 0, + child: AnimatedOpacity( + opacity: opacity, + duration: _createAnimationDuration(item), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), duration: _createAnimationDuration(item), - curve: Curves.easeInOut, - top: targetOffset, - left: 0, - right: 0, - child: AnimatedOpacity( - opacity: opacity, - duration: _createAnimationDuration(item), - child: TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: _createAnimationDuration(item), - builder: (context, animationValue, child) { - return ToastHolderWidget( - item: item, - animation: - AlwaysStoppedAnimation(animationValue), - alignment: alignment, - transformerBuilder: - _toastAnimationBuilder(item), - ); - }, - ), - ), - ); - }).toList(), + builder: (context, animationValue, child) { + return ToastHolderWidget( + item: item, + animation: + AlwaysStoppedAnimation(animationValue), + alignment: alignment, + transformerBuilder: + _toastAnimationBuilder(item), + ); + }, + ), + ), ); - }, + }), ); }, ), From c590139eee4301771ae15e5057f932289d52ebb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:32:29 -0300 Subject: [PATCH 4/6] feature: initial stack test --- lib/src/core/toastification_manager.dart | 159 +++++++++-------------- 1 file changed, 58 insertions(+), 101 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index ac584a0..95b7225 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -20,31 +20,29 @@ class ToastificationManager { OverlayEntry? _overlayEntry; - /// this key is attached to [AnimatedList] so we can add or remove items using it. - final _listGlobalKey = GlobalKey(); - - /// this is the list of items that are currently shown + /// This is the list of items that are currently shown /// if the list is empty, the overlay entry will be removed - final List _notifications = []; + final ValueNotifier> _notifications = + ValueNotifier>([]); - /// this is the delay for showing the overlay entry + /// This is the delay for showing the overlay entry /// We need this delay because we want to show the item animation after - /// the overlay created + /// the overlay is created /// /// When we want to show first toast, we need to wait for the overlay to be created /// and then show the toast item. final _createOverlayDelay = const Duration(milliseconds: 100); - /// this is the delay for removing the overlay entry + /// This is the delay for removing the overlay entry /// - /// when we want to remove the last toast, we need to wait for the animation + /// When we want to remove the last toast, we need to wait for the animation /// to be completed and then remove the overlay. final _removeOverlayDelay = const Duration(milliseconds: 50); /// Shows a [ToastificationItem] with the given [builder] and [animationBuilder]. /// - /// if the [_notifications] list is empty, we will create the [_overlayEntry] - /// otherwise we will just add the [item] to the [_notifications] list. + /// If the [_notifications] list is empty, we will create the [_overlayEntry] + /// otherwise, we will just add the [item] to the [_notifications] list. ToastificationItem showCustom({ required OverlayState overlayState, required ToastificationBuilder builder, @@ -94,73 +92,39 @@ class ToastificationManager { /// Finds the [ToastificationItem] with the given [id]. ToastificationItem? findToastificationItem(String id) { try { - return _notifications + return _notifications.value .firstWhereOrNull((notification) => notification.id == id); } catch (e) { return null; } } - /// using this method you can remove a notification item - /// if there is no notification in the notification list, + /// Using this method you can remove a notification item + /// If there is no notification in the notification list, /// we will remove the overlay entry /// - /// if the [showRemoveAnimation] is true, we will show the remove animation + /// If the [showRemoveAnimation] is true, we will show the remove animation /// of the [notification] item. - /// otherwise we will remove the notification without showing any animation. - /// this is useful when you want to remove the notification manually, + /// Otherwise, we will remove the notification without showing any animation. + /// This is useful when you want to remove the notification manually, /// like when you have some [Dismissible] widget void dismiss( ToastificationItem notification, { bool showRemoveAnimation = true, }) { - final index = _notifications.indexOf(notification); - // print("Toastification Manager Dismiss Notifications: $_notifications"); - if (index != -1) { - notification = _notifications[index]; - - if (notification.isRunning) { - notification.stop(); - } - - final removedItem = _notifications.removeAt(index); + final updatedList = List.from(_notifications.value); + if (updatedList.remove(notification)) { + _notifications.value = updatedList; - /// if the [showRemoveAnimation] is true, we will show the remove animation - /// of the notification. - if (showRemoveAnimation) { - _listGlobalKey.currentState?.removeItem( - index, - (BuildContext context, Animation animation) { - return ToastHolderWidget( - item: removedItem, - animation: animation, - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(removedItem), - ); - }, - duration: _createAnimationDuration(removedItem), - ); - - /// if the [showRemoveAnimation] is false, we will remove the notification - /// without showing the remove animation. - } else { - _listGlobalKey.currentState?.removeItem( - index, - (BuildContext context, Animation animation) { - return const SizedBox.shrink(); - }, - ); - } - - /// we will remove the [_overlayEntry] if there are no notifications + /// We will remove the [_overlayEntry] if there are no notifications /// We need to check if the _notifications list is empty twice. /// To make sure after the delay, there are no new notifications added. - if (_notifications.isEmpty) { + if (_notifications.value.isEmpty) { Future.delayed( - (removedItem.animationDuration ?? config.animationDuration) + + (notification.animationDuration ?? config.animationDuration) + _removeOverlayDelay, () { - if (_notifications.isEmpty) { + if (_notifications.value.isEmpty) { _overlayEntry?.remove(); _overlayEntry = null; } @@ -174,18 +138,12 @@ class ToastificationManager { /// The [delayForAnimation] parameter is optional and defaults to true. /// When it is true, it adds a delay for better animation. void dismissAll({bool delayForAnimation = true}) async { - // Creates a new list cloneList that has all the notifications from the _notifications list, but in reverse order. - final cloneList = _notifications.toList(growable: false).reversed; + final cloneList = _notifications.value.toList(growable: false).reversed; - // For each cloned "toastItem" notification in "cloneList", - // we will remove it and then pause for a duration if delayForAnimation is true. for (final toastItem in cloneList) { - /// If the item is still in the [_notification] list, we will remove it if (findToastificationItem(toastItem.id) != null) { - // Dismiss the current notification item dismiss(toastItem); - // If delayForAnimation is true, wait for 150ms before proceeding to the next item if (delayForAnimation) { await Future.delayed(const Duration(milliseconds: 150)); } @@ -193,14 +151,18 @@ class ToastificationManager { } } - /// remove the first notification in the list. + /// Remove the first notification in the list. void dismissFirst() { - dismiss(_notifications.first); + if (_notifications.value.isNotEmpty) { + dismiss(_notifications.value.first); + } } - /// remove the last notification in the list. + /// Remove the last notification in the list. void dismissLast() { - dismiss(_notifications.last); + if (_notifications.value.isNotEmpty) { + dismiss(_notifications.value.last); + } } void _createNotificationHolder(OverlayState overlay) { @@ -208,49 +170,46 @@ class ToastificationManager { overlay.insert(_overlayEntry!); } - /// create a [OverlayEntry] as holder of the notifications + /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { return OverlayEntry( opaque: false, builder: (context) { - Widget overlay = Align( + return Align( alignment: alignment, child: Container( margin: _marginBuilder(context, alignment, config), constraints: BoxConstraints.tightFor( width: config.itemWidth, ), - child: MediaQuery.removePadding( - context: context, - removeTop: true, - removeBottom: true, - child: AnimatedList( - key: _listGlobalKey, - clipBehavior: config.clipBehavior, - initialItemCount: _notifications.length, - reverse: alignment.y >= 0, - primary: true, - shrinkWrap: true, - itemBuilder: ( - BuildContext context, - int index, - Animation animation, - ) { - final item = _notifications[index]; - - return ToastHolderWidget( - item: item, - animation: animation, - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(item), - ); - }, - ), + child: ValueListenableBuilder>( + valueListenable: _notifications, + builder: (context, notifications, child) { + return Stack( + alignment: Alignment.topCenter, + children: notifications.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final offset = index * 10.0; + + return Positioned( + key: ValueKey(item.id), + top: offset, + left: 0, + right: 0, + child: ToastHolderWidget( + item: item, + animation: const AlwaysStoppedAnimation(1.0), + alignment: alignment, + transformerBuilder: _toastAnimationBuilder(item), + ), + ); + }).toList(), + ); + }, ), ), ); - - return overlay; }, ); } @@ -266,8 +225,6 @@ class ToastificationManager { marginValue = marginValue.add(MediaQuery.of(context).viewInsets); } - /// Add the MediaQuery viewPadding as margin so other widgets behind the toastification overlay - /// will be touchable and not covered by the toastification overlay. return marginValue.add(MediaQuery.of(context).viewPadding); } From 1aa2ca15f4e272a9a55bae9e495b4bbf32932609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:58:22 -0300 Subject: [PATCH 5/6] feat: enhance toast notifications with hover effects and opacity transitions --- lib/src/core/toastification_manager.dart | 125 +++++++++++++++++------ 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index 95b7225..c02f02a 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -172,41 +172,106 @@ class ToastificationManager { /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { + ValueNotifier _isHovering = ValueNotifier(false); + return OverlayEntry( opaque: false, builder: (context) { return Align( alignment: alignment, - child: Container( - margin: _marginBuilder(context, alignment, config), - constraints: BoxConstraints.tightFor( - width: config.itemWidth, - ), - child: ValueListenableBuilder>( - valueListenable: _notifications, - builder: (context, notifications, child) { - return Stack( - alignment: Alignment.topCenter, - children: notifications.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final offset = index * 10.0; - - return Positioned( - key: ValueKey(item.id), - top: offset, - left: 0, - right: 0, - child: ToastHolderWidget( - item: item, - animation: const AlwaysStoppedAnimation(1.0), - alignment: alignment, - transformerBuilder: _toastAnimationBuilder(item), - ), - ); - }).toList(), - ); - }, + child: MouseRegion( + onEnter: (_) => _isHovering.value = true, + onExit: (_) => _isHovering.value = false, + child: Container( + margin: _marginBuilder(context, alignment, config), + constraints: BoxConstraints.tightFor( + width: config.itemWidth, + ), + child: ValueListenableBuilder>( + valueListenable: _notifications, + builder: (context, notifications, child) { + return ValueListenableBuilder( + valueListenable: _isHovering, + builder: (context, isHovering, child) { + // Limita aos últimos 5 toasts (o último item é o mais novo) + final maxToasts = 5; + final visibleNotifications = + notifications.length <= maxToasts + ? notifications + : notifications + .sublist(notifications.length - maxToasts); + + // Reverte a lista para que o toast mais novo seja renderizado por último + final reverseVisibleNotifications = + visibleNotifications.reversed.toList(); + + final totalToasts = visibleNotifications.length; + final maxOpacity = 1.0; + final minOpacity = 0.5; // Ajuste conforme necessário + + return Stack( + alignment: Alignment.topCenter, + children: reverseVisibleNotifications + .asMap() + .entries + .map((entry) { + final index = entry.key; + final item = entry.value; + + // Calcula a opacidade: todos os toasts têm opacidade máxima ao hover + double opacity; + if (isHovering) { + opacity = maxOpacity; + } else { + final reverseListIndex = totalToasts - index - 1; + if (reverseListIndex != 0) { + opacity = maxOpacity - + (reverseListIndex / (totalToasts - 1)) * + (maxOpacity - minOpacity); + } else { + opacity = maxOpacity; + } + } + + // Expande a stack ao hover + final targetOffset = isHovering + ? (totalToasts - index - 1) * + 90.0 // Ajuste conforme necessário + : (totalToasts - index - 1) * + 10.0; // Ajuste conforme necessário + + return AnimatedPositioned( + key: ValueKey(item.id), + duration: _createAnimationDuration(item), + curve: Curves.easeInOut, + top: targetOffset, + left: 0, + right: 0, + child: AnimatedOpacity( + opacity: opacity, + duration: _createAnimationDuration(item), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: _createAnimationDuration(item), + builder: (context, animationValue, child) { + return ToastHolderWidget( + item: item, + animation: + AlwaysStoppedAnimation(animationValue), + alignment: alignment, + transformerBuilder: + _toastAnimationBuilder(item), + ); + }, + ), + ), + ); + }).toList(), + ); + }, + ); + }, + ), ), ), ); From 04764f7d271d7bc8ac1e8cfef8521833c2308896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20A=20E=20Martins?= <81169982+AntonioAEMartins@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:10:44 -0300 Subject: [PATCH 6/6] refactor: change function structure, add notification pause and resume timer function --- lib/src/core/toastification_manager.dart | 164 +++++++++++------------ 1 file changed, 78 insertions(+), 86 deletions(-) diff --git a/lib/src/core/toastification_manager.dart b/lib/src/core/toastification_manager.dart index c02f02a..fb3a87e 100644 --- a/lib/src/core/toastification_manager.dart +++ b/lib/src/core/toastification_manager.dart @@ -172,7 +172,20 @@ class ToastificationManager { /// Create a [OverlayEntry] as holder of the notifications OverlayEntry _createOverlayEntry() { - ValueNotifier _isHovering = ValueNotifier(false); + ValueNotifier isHovering = ValueNotifier(false); + + // Handle hover state changes to pause/start timers + isHovering.addListener(() { + for (var item in _notifications.value) { + if (item.hasTimer) { + if (isHovering.value) { + item.pause(); + } else { + item.start(); + } + } + } + }); return OverlayEntry( opaque: false, @@ -180,95 +193,74 @@ class ToastificationManager { return Align( alignment: alignment, child: MouseRegion( - onEnter: (_) => _isHovering.value = true, - onExit: (_) => _isHovering.value = false, + onEnter: (_) => isHovering.value = true, + onExit: (_) => isHovering.value = false, child: Container( margin: _marginBuilder(context, alignment, config), - constraints: BoxConstraints.tightFor( - width: config.itemWidth, - ), - child: ValueListenableBuilder>( - valueListenable: _notifications, - builder: (context, notifications, child) { - return ValueListenableBuilder( - valueListenable: _isHovering, - builder: (context, isHovering, child) { - // Limita aos últimos 5 toasts (o último item é o mais novo) - final maxToasts = 5; - final visibleNotifications = - notifications.length <= maxToasts - ? notifications - : notifications - .sublist(notifications.length - maxToasts); - - // Reverte a lista para que o toast mais novo seja renderizado por último - final reverseVisibleNotifications = - visibleNotifications.reversed.toList(); - - final totalToasts = visibleNotifications.length; - final maxOpacity = 1.0; - final minOpacity = 0.5; // Ajuste conforme necessário - - return Stack( - alignment: Alignment.topCenter, - children: reverseVisibleNotifications - .asMap() - .entries - .map((entry) { - final index = entry.key; - final item = entry.value; - - // Calcula a opacidade: todos os toasts têm opacidade máxima ao hover - double opacity; - if (isHovering) { - opacity = maxOpacity; - } else { - final reverseListIndex = totalToasts - index - 1; - if (reverseListIndex != 0) { - opacity = maxOpacity - - (reverseListIndex / (totalToasts - 1)) * - (maxOpacity - minOpacity); - } else { - opacity = maxOpacity; - } - } - - // Expande a stack ao hover - final targetOffset = isHovering - ? (totalToasts - index - 1) * - 90.0 // Ajuste conforme necessário - : (totalToasts - index - 1) * - 10.0; // Ajuste conforme necessário - - return AnimatedPositioned( - key: ValueKey(item.id), + constraints: BoxConstraints.tightFor(width: config.itemWidth), + child: AnimatedBuilder( + animation: Listenable.merge([_notifications, isHovering]), + builder: (context, child) { + const int maxToasts = 5; + const double maxOpacity = 1.0; + const double minOpacity = 0.5; + const double expandedOffset = 90.0; + const double collapsedOffset = 10.0; + + final notifications = _notifications.value; + final isHoveringValue = isHovering.value; + + final startIndex = notifications.length > maxToasts + ? notifications.length - maxToasts + : 0; + final visibleNotifications = + notifications.sublist(startIndex).reversed.toList(); + + return Stack( + alignment: Alignment.topCenter, + children: + List.generate(visibleNotifications.length, (index) { + final item = visibleNotifications[index]; + final reverseIndex = + visibleNotifications.length - index - 1; + + final opacity = isHoveringValue || reverseIndex == 0 + ? maxOpacity + : maxOpacity - + (reverseIndex / + (visibleNotifications.length - 1)) * + (maxOpacity - minOpacity); + + final targetOffset = reverseIndex * + (isHoveringValue ? expandedOffset : collapsedOffset); + + return AnimatedPositioned( + key: ValueKey(item.id), + duration: _createAnimationDuration(item), + curve: Curves.easeInOut, + top: targetOffset, + left: 0, + right: 0, + child: AnimatedOpacity( + opacity: opacity, + duration: _createAnimationDuration(item), + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), duration: _createAnimationDuration(item), - curve: Curves.easeInOut, - top: targetOffset, - left: 0, - right: 0, - child: AnimatedOpacity( - opacity: opacity, - duration: _createAnimationDuration(item), - child: TweenAnimationBuilder( - tween: Tween(begin: 0.0, end: 1.0), - duration: _createAnimationDuration(item), - builder: (context, animationValue, child) { - return ToastHolderWidget( - item: item, - animation: - AlwaysStoppedAnimation(animationValue), - alignment: alignment, - transformerBuilder: - _toastAnimationBuilder(item), - ); - }, - ), - ), - ); - }).toList(), + builder: (context, animationValue, child) { + return ToastHolderWidget( + item: item, + animation: + AlwaysStoppedAnimation(animationValue), + alignment: alignment, + transformerBuilder: + _toastAnimationBuilder(item), + ); + }, + ), + ), ); - }, + }), ); }, ),