You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否实现从屏幕点击位置触发、类似animations包Container transform的AlertDialog动画?

实现从点击位置触发的Container Transform风格AlertDialog动画

完全可以实现这个效果,我们可以通过自定义动画组合(位移+缩放)来模拟类似Container Transform的过渡效果,不用依赖第三方包也能搞定。核心思路是监听点击位置,让弹窗从该位置逐步缩放、位移到屏幕中心,模拟展开的动画;关闭时反向执行动画回到点击位置再隐藏。

具体实现代码

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const HomePage(),
      theme: ThemeData(primarySwatch: Colors.blue),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<Offset> _positionAnimation;
  late Animation<double> _scaleAnimation;
  Offset? _tapOffset;
  bool _isDialogVisible = false;

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

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

  void _showAnimatedDialog(TapDownDetails details) {
    setState(() {
      _tapOffset = details.globalPosition;
      _isDialogVisible = true;
    });

    // 计算从点击位置到屏幕中心的偏移量
    final screenSize = MediaQuery.of(context).size;
    final centerOffset = Offset(screenSize.width / 2, screenSize.height / 2);
    final startOffset = _tapOffset! - centerOffset;

    // 初始化动画
    _positionAnimation = Tween<Offset>(
      begin: startOffset / screenSize.width, // 归一化处理偏移
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOutCubic,
    ));

    _scaleAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeOutCubic,
    ));

    _animationController.forward();
  }

  void _hideAnimatedDialog() {
    _animationController.reverse().then((_) {
      setState(() {
        _isDialogVisible = false;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 空白区域的点击触发
          GestureDetector(
            onTapDown: _showAnimatedDialog,
            child: Container(
              color: Colors.grey[100],
              width: double.infinity,
              height: double.infinity,
            ),
          ),
          // 带动画的自定义弹窗
          if (_isDialogVisible)
            AnimatedBuilder(
              animation: _animationController,
              builder: (context, child) {
                return Transform.translate(
                  offset: _positionAnimation.value * MediaQuery.of(context).size.width,
                  child: Transform.scale(
                    scale: _scaleAnimation.value,
                    child: child,
                  ),
                );
              },
              child: Center(
                child: Container(
                  width: 300,
                  padding: const EdgeInsets.all(20),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black.withOpacity(0.2),
                        blurRadius: 10,
                        spreadRadius: 2,
                      ),
                    ],
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Text(
                        "自定义弹窗",
                        style: TextStyle(
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      const SizedBox(height: 16),
                      const Text("这是从点击位置展开的弹窗,和Container Transform效果类似"),
                      const SizedBox(height: 20),
                      ElevatedButton(
                        onPressed: _hideAnimatedDialog,
                        child: const Text("关闭"),
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

关键细节说明

  • 点击位置获取:通过onTapDown的TapDownDetails拿到全局点击坐标,避免onTap延迟导致的位置偏差。
  • 动画组合:同时使用Transform.translate和Transform.scale实现从点击点到屏幕中心的位移+缩放过渡,曲线用Curves.easeOutCubic让动画更自然。
  • 自定义弹窗:因为系统AlertDialog的动画逻辑封装较深,这里用自定义容器模拟其样式,方便完全控制动画过程。
  • 动画生命周期:弹窗关闭时先执行反向动画,动画结束后再隐藏弹窗,保证过渡的完整性。

内容的提问来源于stack exchange,提问作者sub

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 14:50:30