Flutter与OpenHarmony打卡消息提示组件

前言

消息提示是移动应用中向用户传递反馈信息的重要组件。在打卡工具类应用中,消息提示用于显示打卡成功、操作确认、错误警告等各种反馈。本文将详细介绍如何在Flutter和OpenHarmony平台上实现美观且功能完善的消息提示组件。

消息提示的设计需要考虑显示位置、持续时间、视觉样式和交互方式。我们将实现支持多种类型和样式的消息提示组件,包括Toast、Snackbar和顶部通知等形式。

Flutter消息提示实现

首先创建自定义Toast组件:

dart 复制代码
enum ToastType { success, error, warning, info }

class CustomToast {
  static OverlayEntry? _overlayEntry;
  static Timer? _timer;

  static void show(
    BuildContext context, {
    required String message,
    ToastType type = ToastType.info,
    Duration duration = const Duration(seconds: 2),
  }) {
    _dismiss();
    
    _overlayEntry = OverlayEntry(
      builder: (context) => _ToastWidget(
        message: message,
        type: type,
        onDismiss: _dismiss,
      ),
    );
    
    Overlay.of(context).insert(_overlayEntry!);
    
    _timer = Timer(duration, _dismiss);
  }

  static void _dismiss() {
    _timer?.cancel();
    _overlayEntry?.remove();
    _overlayEntry = null;
  }
}

class _ToastWidget extends StatefulWidget {
  final String message;
  final ToastType type;
  final VoidCallback onDismiss;

  const _ToastWidget({
    required this.message,
    required this.type,
    required this.onDismiss,
  });

  @override
  State<_ToastWidget> createState() => _ToastWidgetState();
}

class _ToastWidgetState extends State<_ToastWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _fadeAnimation;
  late Animation<Offset> _slideAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      vsync: this,
    );
    _fadeAnimation = Tween<double>(begin: 0, end: 1).animate(_controller);
    _slideAnimation = Tween<Offset>(
      begin: const Offset(0, -0.5),
      end: Offset.zero,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
    _controller.forward();
  }

  Color get _backgroundColor {
    switch (widget.type) {
      case ToastType.success: return Colors.green;
      case ToastType.error: return Colors.red;
      case ToastType.warning: return Colors.orange;
      case ToastType.info: return Colors.blue;
    }
  }

  IconData get _icon {
    switch (widget.type) {
      case ToastType.success: return Icons.check_circle;
      case ToastType.error: return Icons.error;
      case ToastType.warning: return Icons.warning;
      case ToastType.info: return Icons.info;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: MediaQuery.of(context).padding.top + 16,
      left: 16,
      right: 16,
      child: FadeTransition(
        opacity: _fadeAnimation,
        child: SlideTransition(
          position: _slideAnimation,
          child: Material(
            color: Colors.transparent,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
              decoration: BoxDecoration(
                color: _backgroundColor,
                borderRadius: BorderRadius.circular(12),
                boxShadow: [
                  BoxShadow(
                    color: _backgroundColor.withOpacity(0.3),
                    blurRadius: 12,
                    offset: const Offset(0, 4),
                  ),
                ],
              ),
              child: Row(
                children: [
                  Icon(_icon, color: Colors.white),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      widget.message,
                      style: const TextStyle(color: Colors.white, fontSize: 14),
                    ),
                  ),
                  GestureDetector(
                    onTap: widget.onDismiss,
                    child: const Icon(Icons.close, color: Colors.white70, size: 20),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

CustomToast使用Overlay在应用顶层显示消息。四种类型(成功、错误、警告、信息)使用不同的颜色和图标。淡入和滑动动画让消息出现更加自然,关闭按钮允许用户手动关闭。Timer控制自动消失时间。

OpenHarmony消息提示实现

在鸿蒙系统中创建消息提示:

typescript 复制代码
@Component
struct ToastMessage {
  @Prop message: string = ''
  @Prop type: string = 'info'  // success, error, warning, info
  @State visible: boolean = true
  @State offsetY: number = -50
  private onDismiss: () => void = () => {}

  aboutToAppear() {
    animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
      this.offsetY = 0
    })
  }

  getBackgroundColor(): string {
    switch (this.type) {
      case 'success': return '#4CAF50'
      case 'error': return '#F44336'
      case 'warning': return '#FF9800'
      default: return '#2196F3'
    }
  }

  getIcon(): Resource {
    switch (this.type) {
      case 'success': return $r('app.media.check_circle')
      case 'error': return $r('app.media.error')
      case 'warning': return $r('app.media.warning')
      default: return $r('app.media.info')
    }
  }

  build() {
    if (this.visible) {
      Row() {
        Image(this.getIcon())
          .width(20)
          .height(20)
          .fillColor(Color.White)
        
        Text(this.message)
          .fontSize(14)
          .fontColor(Color.White)
          .margin({ left: 12 })
          .layoutWeight(1)
        
        Image($r('app.media.close'))
          .width(20)
          .height(20)
          .fillColor('rgba(255,255,255,0.7)')
          .onClick(() => {
            this.visible = false
            this.onDismiss()
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 12 })
      .backgroundColor(this.getBackgroundColor())
      .borderRadius(12)
      .shadow({ radius: 12, color: this.getBackgroundColor() + '4D', offsetY: 4 })
      .offset({ y: this.offsetY })
    }
  }
}

鸿蒙的消息提示使用offset属性配合animateTo实现滑入动画。visible状态控制消息的显示和隐藏,点击关闭按钮时设为false并调用onDismiss回调。颜色和图标根据type动态获取。

打卡成功提示

实现打卡成功的专用提示:

dart 复制代码
class CheckInSuccessToast {
  static void show(BuildContext context, {required int streak}) {
    final overlay = Overlay.of(context);
    late OverlayEntry entry;
    
    entry = OverlayEntry(
      builder: (context) => _CheckInSuccessWidget(
        streak: streak,
        onDismiss: () => entry.remove(),
      ),
    );
    
    overlay.insert(entry);
    
    Future.delayed(const Duration(seconds: 3), () {
      if (entry.mounted) entry.remove();
    });
  }
}

class _CheckInSuccessWidget extends StatefulWidget {
  final int streak;
  final VoidCallback onDismiss;

  const _CheckInSuccessWidget({required this.streak, required this.onDismiss});

  @override
  State<_CheckInSuccessWidget> createState() => _CheckInSuccessWidgetState();
}

class _CheckInSuccessWidgetState extends State<_CheckInSuccessWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    )..forward();
  }

  @override
  Widget build(BuildContext context) {
    return Positioned(
      top: MediaQuery.of(context).size.height * 0.3,
      left: 0,
      right: 0,
      child: ScaleTransition(
        scale: CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
        child: Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(20),
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.15),
                  blurRadius: 20,
                  offset: const Offset(0, 10),
                ),
              ],
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.celebration, color: Colors.orange, size: 48),
                const SizedBox(height: 12),
                const Text(
                  '打卡成功!',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 8),
                Text(
                  '已连续打卡 ${widget.streak} 天',
                  style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

打卡成功提示使用弹性缩放动画(elasticOut)增强成就感。居中显示的卡片包含庆祝图标、成功文字和连续天数,视觉效果更加丰富。3秒后自动消失,不打断用户操作。

Snackbar消息

实现底部Snackbar消息:

dart 复制代码
class AppSnackbar {
  static void show(
    BuildContext context, {
    required String message,
    String? actionLabel,
    VoidCallback? onAction,
    Duration duration = const Duration(seconds: 3),
  }) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: duration,
        behavior: SnackBarBehavior.floating,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        action: actionLabel != null
            ? SnackBarAction(
                label: actionLabel,
                onPressed: onAction ?? () {},
              )
            : null,
      ),
    );
  }

  static void showUndo(
    BuildContext context, {
    required String message,
    required VoidCallback onUndo,
  }) {
    show(
      context,
      message: message,
      actionLabel: '撤销',
      onAction: onUndo,
      duration: const Duration(seconds: 5),
    );
  }
}

AppSnackbar封装了Flutter的SnackBar,提供了简化的API。floating行为让Snackbar悬浮在内容上方,圆角设计更加美观。showUndo方法专门用于可撤销操作的提示,如删除习惯后提供撤销选项。

使用示例

dart 复制代码
// 显示成功提示
CustomToast.show(context, message: '设置已保存', type: ToastType.success);

// 显示错误提示
CustomToast.show(context, message: '网络连接失败', type: ToastType.error);

// 显示打卡成功
CheckInSuccessToast.show(context, streak: 7);

// 显示可撤销操作
AppSnackbar.showUndo(
  context,
  message: '习惯已删除',
  onUndo: () => restoreHabit(),
);

总结

本文详细介绍了在Flutter和OpenHarmony平台上实现消息提示组件的完整方案。消息提示通过不同的类型、位置和样式,为用户提供了清晰的操作反馈。打卡成功提示使用动画增强成就感,Snackbar支持撤销操作。两个平台的实现都注重动画效果和用户体验,确保消息提示既美观又实用。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
走在路上的菜鸟2 小时前
Android学Flutter学习笔记 第二节 Android视角认知Flutter(resource,生命周期,layout)
android·学习·flutter
2501_9462447816 小时前
Flutter & OpenHarmony OA系统设置页面组件开发指南
开发语言·javascript·flutter
l134062082351 天前
Flutter Geocoding 在鸿蒙上的使用指南
flutter·华为·harmonyos
AiFlutter1 天前
三、内容展示(02):图片
flutter·低代码·低代码平台·aiflutter·aiflutter低代码
l134062082351 天前
344.在鸿蒙上使用 animations Flutter 包的指南
flutter·华为·harmonyos
2501_946244781 天前
Flutter & OpenHarmony OA系统底部导航栏组件开发指南
android·javascript·flutter
2501_944446001 天前
Flutter&OpenHarmony字体与排版设计
android·javascript·flutter
消失的旧时光-19431 天前
mixin 写一个 Flutter 的“埋点 + 日志 + 性能监控”完整框架示例
android·flutter
消失的旧时光-19431 天前
Flutter 工程中 mixin 的正确打开方式:5 种高质量设计范式 + mixin vs 继承 vs 组合 + 为什么它比 BasePage 更优雅
前端·flutter·架构