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

相关推荐
程序员Ctrl喵16 小时前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难17 小时前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡18 小时前
flutter列表中实现置顶动画
flutter
始持19 小时前
第十二讲 风格与主题统一
前端·flutter
始持19 小时前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持19 小时前
第十三讲 异步操作与异步构建
前端·flutter
新镜19 小时前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴20 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Swift社区20 小时前
Flutter 应该按功能拆,还是按技术层拆?
flutter
肠胃炎21 小时前
树形选择器组件封装
前端·flutter