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

Flutter实现Tinder式左滑及卡片上滑覆盖动画求助

实现Tinder风格的卡片切换动画

要实现你需要的左滑(Pass按钮)和上滑覆盖(Back按钮)动画,我们可以通过AnimationController结合SlideTransition、FadeTransition和Stack组件来完成,以下是具体实现方案:

核心思路

  1. 使用Stack叠放两张卡片,底层显示待切换的卡片,顶层显示当前卡片
  2. 通过AnimationController控制顶层卡片的位移和透明度动画
  3. 根据按钮类型(Pass/Back)切换动画的方向和参数

完整代码实现

1. 状态与动画初始化

替换你原来的状态定义,添加动画相关变量:

import 'package:flutter/material.dart';

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

  @override
  State<TinderCardScreen> createState() => _TinderCardScreenState();
}

class _TinderCardScreenState extends State<TinderCardScreen> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<Offset> _slideAnimation;
  late Animation<double> _fadeAnimation;
  int _currentIndex = 0;
  bool _isHidden = true; // 保留你原来的可见性控制变量

  // 定义动画类型
  enum _AnimationType { leftSlide, topSlide }
  _AnimationType? _currentAnimation;

  // 图片列表,可扩展更多图片
  final List<String> _imagePaths = [
    "assets/images/image.jpeg",
    "assets/images/imageQ.jpeg",
  ];

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器,时长800ms
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 800),
    );
    // 默认左滑动画参数
    _slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(-1.5, 0),
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
    _fadeAnimation = Tween<double>(
      begin: 1.0,
      end: 0.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeIn,
    ));
  }

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

2. 按钮点击与动画逻辑

替换原来的_onBackButtonTap和_onPassButtonTap方法:

void _onBackButtonTap() {
    if (_currentIndex == 0) return; // 已经是第一张,无需切换

    // 设置上滑覆盖动画参数:从屏幕上方滑入到当前位置,透明度从0到1
    _currentAnimation = _AnimationType.topSlide;
    _slideAnimation = Tween<Offset>(
      begin: const Offset(0, -1.5),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
    _fadeAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeIn,
    ));

    // 执行动画,结束后切换索引并重置控制器
    _animationController.forward().then((_) {
      setState(() {
        _currentIndex--;
      });
      _animationController.reset();
    });
  }

  void _onPassButtonTap() {
    if (_currentIndex == _imagePaths.length - 1) return; // 已经是最后一张,无需切换

    // 设置左滑动画参数:从当前位置滑出到屏幕左侧,透明度从1到0
    _currentAnimation = _AnimationType.leftSlide;
    _slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(-1.5, 0),
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeInOut,
    ));
    _fadeAnimation = Tween<double>(
      begin: 1.0,
      end: 0.0,
    ).animate(CurvedAnimation(
      parent: _animationController,
      curve: Curves.easeIn,
    ));

    // 执行动画,结束后切换索引并重置控制器
    _animationController.forward().then((_) {
      setState(() {
        _currentIndex++;
      });
      _animationController.reset();
    });
  }

  // 保留你原来的Shoot按钮逻辑
  void _onBasketballIconTap() {
    // 你的原有逻辑
  }

3. 重构UI部分

将原来的单个卡片容器替换为Stack结构,提取卡片为单独方法:

// 构建单个卡片组件
  Widget _buildCard(String imagePath) {
    return Container(
      height: context.height * 0.68,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        image: DecorationImage(
          image: AssetImage(imagePath),
          fit: BoxFit.cover,
        ),
      ),
      child: Align(
        alignment: Alignment.bottomLeft,
        child: Container(
          padding: const EdgeInsets.only(left: 16, bottom: 16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              // User name
              Visibility(
                visible: _isHidden,
                child: const Text("Jessica, 24", style: TextStyle(
                    fontSize: 30,
                    color: Colors.white
                ),),
              ),
              SizedBox(height: context.height * 0.02,),
              // Location
              Visibility(
                visible: _isHidden,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Material(
                    elevation: 4,
                    child: Container(
                      padding: const EdgeInsets.all(8),
                      width: context.width * 0.37,
                      decoration: BoxDecoration(
                        color: const Color(0xFF485354),
                        borderRadius: BorderRadius.circular(12),
                      ),
                      child: const Row(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        children: [
                          Icon(
                            Icons.location_on,
                            color: Colors.white,
                            size: 30,
                          ),
                          Text(
                            'New York',
                            style: TextStyle(
                              color: Colors.white,
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ),
                ),
              ),
              // Action buttons row
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  // Pass button
                  InkWell(
                    onTap: _onPassButtonTap,
                    child: const CircleAvatar(
                      radius: 50,
                      backgroundColor: Colors.white,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text("👋", style: TextStyle(fontSize: 29)),
                          Text("Pass", style: TextStyle(color: Colors.black))
                        ],
                      ),
                    ),
                  ),
                  // Shoot button
                  InkWell(
                      onTap: _onBasketballIconTap,
                      child: Container(
                        decoration: const BoxDecoration(
                            shape: BoxShape.circle,
                            gradient: LinearGradient(
                              colors: [
                                Color(0xFF2A64E5),
                                Color(0xFF3264E4),
                                Color(0xFF453CCA),
                                Color(0xFF3A43AF),
                                Color(0xFF4634B1),
                              ],
                            )
                        ),
                        child: const CircleAvatar(
                          radius: 60,
                          backgroundColor: Color(0xFF6814CE),
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Text("🏀", style: TextStyle(fontSize: 42, fontFamily: "Roboto")),
                              Text("Shoot")
                            ],
                          ),
                        ),
                      )
                  ),
                  // Back button
                  InkWell(
                    onTap: _onBackButtonTap,
                    child: const CircleAvatar(
                      radius: 50,
                      backgroundColor: Colors.white,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text("👈", style: TextStyle(fontSize: 29)),
                          Text("Back", style: TextStyle(color: Colors.black))
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: SizedBox(
          height: context.height * 0.68,
          child: Stack(
            children: [
              // 底层卡片:显示待切换的卡片(当前索引的前/后一张)
              _buildCard(_currentIndex == 0 ? _imagePaths[1] : _imagePaths[0]),
              // 顶层卡片:当前显示的卡片,应用动画
              SlideTransition(
                position: _slideAnimation,
                child: FadeTransition(
                  opacity: _fadeAnimation,
                  child: _buildCard(_imagePaths[_currentIndex]),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

关键说明

  • SingleTickerProviderStateMixin:提供动画所需的帧同步
  • SlideTransition:控制卡片的位移动画,Offset参数控制方向(左滑用x轴负方向,上滑用y轴负方向)
  • FadeTransition:配合位移添加透明度变化,增强动画层次感
  • Stack:实现卡片叠放效果,确保切换时底层卡片可见

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 08:55:56