mirror of
https://github.com/smmarty/friflex_flutter_starter.git
synced 2025-12-22 01:20:46 +00:00
feat(app): Добавить снекбар
This commit is contained in:
17
lib/app/ui_kit/app_box.dart
Normal file
17
lib/app/ui_kit/app_box.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// {@template h_box}
|
||||
/// HBox виджет для вертикального отступа (Надстройка над SizedBox)
|
||||
/// {@endtemplate}
|
||||
class HBox extends SizedBox {
|
||||
/// {@macro h_box}
|
||||
const HBox(double height, {super.key}) : super(height: height);
|
||||
}
|
||||
|
||||
/// {@template w_box}
|
||||
/// WBox виджет для вертикального отступа (Надстройка над SizedBox)
|
||||
/// {@endtemplate}
|
||||
class WBox extends SizedBox {
|
||||
/// {@macro w_box}
|
||||
const WBox(double width, {super.key}) : super(width: width);
|
||||
}
|
||||
278
lib/app/ui_kit/app_snackbar.dart
Normal file
278
lib/app/ui_kit/app_snackbar.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:friflex_starter/app/app_context_ext.dart';
|
||||
import 'package:friflex_starter/app/theme/app_colors_scheme.dart';
|
||||
import 'package:friflex_starter/app/ui_kit/app_box.dart';
|
||||
|
||||
/// {@template app_snackbar}
|
||||
/// Перечисление для типов снекбаров
|
||||
/// Используется для определения цвета и назначения снекбара
|
||||
/// {@endtemplate}
|
||||
enum TypeSnackBar {
|
||||
/// Снекбар с успехом
|
||||
success,
|
||||
|
||||
/// Снекбар с ошибкой
|
||||
error,
|
||||
}
|
||||
|
||||
/// {@template app_snackbar}
|
||||
/// Менеджер для управления снекбарами
|
||||
/// Используется для показа снекбаров в верхней части экрана
|
||||
/// Имеет статические методы для показа снекбаров с разными типами
|
||||
/// [showError] - для показа снекбара с ошибкой,
|
||||
/// [showSuccess] - для показа снекбара с успехом
|
||||
/// {@endtemplate}
|
||||
class AppSnackBar extends StatefulWidget {
|
||||
/// {@macro app_snackbar}
|
||||
const AppSnackBar._({
|
||||
required this.message,
|
||||
required this.type,
|
||||
required this.displayDuration,
|
||||
this.onDismiss,
|
||||
});
|
||||
|
||||
/// Сообщение, которое будет отображаться в снекбаре
|
||||
final String message;
|
||||
|
||||
/// Тип снекбара, определяющий его цвет и назначение
|
||||
final TypeSnackBar type;
|
||||
|
||||
/// Продолжительность отображения снекбара
|
||||
final Duration displayDuration;
|
||||
|
||||
/// Функция, вызываемая при закрытии снекбара
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
@override
|
||||
State<AppSnackBar> createState() => _AppSnackBarState();
|
||||
|
||||
/// Показать снекбар с ошибкой
|
||||
/// [context] - контекст, в котором будет показан снекбар
|
||||
/// [message] - сообщение, которое будет отображаться в снекбаре
|
||||
/// [displayDuration] - продолжительность отображения снекбара
|
||||
/// По умолчанию 3 секунды
|
||||
static void showError(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration displayDuration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
type: TypeSnackBar.error,
|
||||
displayDuration: displayDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Показать снекбар с успехом
|
||||
/// [context] - контекст, в котором будет показан снекбар
|
||||
/// [message] - сообщение, которое будет отображаться в снекбаре
|
||||
/// [displayDuration] - продолжительность отображения снекбара
|
||||
/// По умолчанию 3 секунды
|
||||
static void showSuccess({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
Duration displayDuration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_show(
|
||||
context: context,
|
||||
message: message,
|
||||
type: TypeSnackBar.success,
|
||||
displayDuration: displayDuration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Приватный метод для показа снекбара
|
||||
/// Используется статическими методами [showError] и [showSuccess]
|
||||
static void _show({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
required TypeSnackBar type,
|
||||
required Duration displayDuration,
|
||||
}) {
|
||||
// Удаляем предыдущий снекбар
|
||||
_removeCurrentSnackBar();
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
final overlayEntry = OverlayEntry(
|
||||
builder: (context) => AppSnackBar._(
|
||||
message: message,
|
||||
type: type,
|
||||
displayDuration: displayDuration,
|
||||
onDismiss: _removeCurrentSnackBar,
|
||||
),
|
||||
);
|
||||
|
||||
_currentSnackBar = overlayEntry;
|
||||
overlay.insert(overlayEntry);
|
||||
}
|
||||
|
||||
static OverlayEntry? _currentSnackBar;
|
||||
|
||||
static void _removeCurrentSnackBar() {
|
||||
_currentSnackBar?.remove();
|
||||
_currentSnackBar = null;
|
||||
}
|
||||
}
|
||||
|
||||
class _AppSnackBarState extends State<AppSnackBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _slideAnimation;
|
||||
Timer? _dismissTimer;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_isInitialized) {
|
||||
_initAnimation();
|
||||
_startDismissTimer();
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Инициализация анимации для снекбара
|
||||
/// Используется для определения начальной и конечной позиции снекбара
|
||||
/// Начальная позиция - за пределами экрана, конечная - 15 пикселей ниже верхнего отступа
|
||||
void _initAnimation() {
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
// Начальная позиция снекбара - за пределами экрана
|
||||
final startPosition = -200.0;
|
||||
// Конечная позиция снекбара - 15 пикселей ниже верхнего отступа
|
||||
final endPosition = topPadding + 15;
|
||||
// Создание анимации с использованием Tween
|
||||
_slideAnimation = Tween<double>(begin: startPosition, end: endPosition)
|
||||
.animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
/// Запуск таймера для автоматического закрытия снекбара
|
||||
/// Таймер срабатывает по истечении [widget.displayDuration]
|
||||
/// и вызывает метод [_dismiss] для закрытия снекбара
|
||||
void _startDismissTimer() {
|
||||
_dismissTimer = Timer(widget.displayDuration, _dismiss);
|
||||
}
|
||||
|
||||
/// Закрытие снекбара
|
||||
/// Отменяет таймер, если он существует, и запускает обратную анимацию
|
||||
/// После завершения анимации вызывает функцию [widget.onDismiss], если она задана
|
||||
/// Если виджет не смонтирован, ничего не делает
|
||||
void _dismiss() {
|
||||
if (!mounted) return;
|
||||
|
||||
_dismissTimer?.cancel();
|
||||
_animationController.reverse().then((_) {
|
||||
if (mounted) {
|
||||
widget.onDismiss?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dismissTimer?.cancel();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _slideAnimation,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
top: _slideAnimation.value,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: GestureDetector(
|
||||
onTap: _dismiss,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 350),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(widget.type),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_Icon(type: widget.type),
|
||||
const WBox(10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.message,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Получение цвета фона снекбара в зависимости от его типа
|
||||
/// Возвращает [Color] в зависимости от типа снекбара
|
||||
/// [TypeSnackBar.success] - цвет успеха
|
||||
/// [TypeSnackBar.error] - цвет ошибки
|
||||
Color _getBackgroundColor(TypeSnackBar type) {
|
||||
return switch (type) {
|
||||
TypeSnackBar.success => context.colors.successSnackbarBackground,
|
||||
TypeSnackBar.error => context.colors.errorSnackbarBackground,
|
||||
};
|
||||
}
|
||||
}
|
||||
/// {@template _Icon}
|
||||
/// Виджет для отображения иконки в снекбаре
|
||||
/// Используется для отображения иконки в зависимости от типа снекбара
|
||||
/// {@endtemplate}
|
||||
class _Icon extends StatelessWidget {
|
||||
/// {@macro _Icon}
|
||||
/// Создает экземпляр иконки для снекбара
|
||||
/// Принимает [type] - тип снекбара, определяющий иконку
|
||||
/// Используется для отображения иконки успеха или ошибки
|
||||
const _Icon({required this.type});
|
||||
|
||||
/// Тип снекбара, определяющий иконку
|
||||
final TypeSnackBar type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return switch (type) {
|
||||
TypeSnackBar.success => Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
TypeSnackBar.error => Icon(Icons.error, color: Colors.white, size: 32),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user