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

Flutter中实现类似Tinder的卡片滑动功能方案咨询

Flutter Tinder式卡片滑动功能实现方案

一、基于你现有代码的自定义改造实现

你现有代码已经搭建了UI框架,下面是添加滑动交互的改造方案,核心用GestureDetector监听手势,配合Transform实现卡片的位移与旋转效果,同时结合GetX管理卡片列表:

改造HomeController

添加状态变量跟踪卡片位置和滑动状态:

import 'package:get/get.dart';
import 'package:flutter/material.dart';

class HomeController extends GetxController {
  List swipeItems = [
    {
      'name': 'Michell',
      'age': '24',
      'imagePath': 'assets/background_images/szabo-viktor.jpg'
    },
    {
      'name': 'Ryan',
      'age': '26',
      'imagePath': 'assets/background_images/ryan-jacobson.jpg'
    },
    {
      'name': 'Daria',
      'age': '25',
      'imagePath': 'assets/background_images/the-paris.jpg'
    }
  ].obs;

  // 当前卡片的位移和旋转角度
  double _cardOffsetX = 0.0;
  double get cardOffsetX => _cardOffsetX;
  double _cardRotation = 0.0;
  double get cardRotation => _cardRotation;

  // 更新卡片位置和旋转
  void updateCardPosition(DragUpdateDetails details) {
    _cardOffsetX += details.delta.dx;
    // 滑动时根据位移计算旋转角度(最大15度)
    _cardRotation = (_cardOffsetX / Get.width) * 15;
    update();
  }

  // 处理滑动结束
  void onSwipeEnd() {
    const threshold = 100.0; // 触发滑动的最小位移
    if (_cardOffsetX.abs() > threshold) {
      // 触发对应操作:左滑拒绝,右滑喜欢
      if (_cardOffsetX > 0) {
        _likeCard();
      } else {
        _dislikeCard();
      }
    }
    // 重置卡片位置
    _cardOffsetX = 0.0;
    _cardRotation = 0.0;
    update();
  }

  void _likeCard() {
    if (swipeItems.isNotEmpty) {
      // 实现喜欢逻辑,比如添加到喜欢列表
      print("喜欢 ${swipeItems.first['name']}");
      swipeItems.removeAt(0);
    }
  }

  void _dislikeCard() {
    if (swipeItems.isNotEmpty) {
      // 实现拒绝逻辑
      print("拒绝 ${swipeItems.first['name']}");
      swipeItems.removeAt(0);
    }
  }

  // 手动触发喜欢/拒绝的方法,用于底部按钮
  void manualLike() {
    _likeCard();
    update();
  }

  void manualDislike() {
    _dislikeCard();
    update();
  }
}

改造HomeView的卡片区域

替换原来的单个卡片,用Stack堆叠所有卡片,并添加手势交互:

// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:swipe_card_demo/widget/colors.dart';
import 'package:swipe_card_demo/widget/text_widget.dart';
import '../controllers/home_controller.dart';

class HomeView extends GetView<HomeController> {
  const HomeView({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: AppColors.backgroundColor,
        body: Padding(
          padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
          child: Column(
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.end,
                children: [
                  InkWell(
                    onTap: () => Get.back(),
                    child: Container(
                      height: 50,
                      width: 50,
                      decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(10),
                          border: Border.all(width: 1, color: Colors.grey)),
                      child: Center(
                          child: Image.asset(
                              "assets/icon/notification-icon.png",
                              height: 30)),
                    ),
                  ),
                  SizedBox(width: 15),
                  InkWell(
                    onTap: () => Get.back(),
                    child: Container(
                      height: 50,
                      width: 50,
                      decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(10),
                          border: Border.all(width: 1, color: Colors.grey)),
                      child: Center(
                          child: Image.asset("assets/icon/home-icon.png",
                              height: 30)),
                    ),
                  ),
                ],
              ),
              SizedBox(height: 20),
              // 卡片堆叠区域
              Obx(() {
                if (controller.swipeItems.isEmpty) {
                  return Center(child: Text("没有更多卡片了"));
                }
                return SizedBox(
                  height: MediaQuery.of(context).size.height * 0.55,
                  child: Stack(
                    children: List.generate(controller.swipeItems.length, (index) {
                      // 只有最上面的卡片可以滑动
                      bool isTopCard = index == 0;
                      return Positioned.fill(
                        child: Transform(
                          transform: Matrix4.identity()
                            ..translate(isTopCard ? controller.cardOffsetX : 0, 0)
                            ..rotateZ(isTopCard ? controller.cardRotation * 0.0174533 : 0), // 转弧度
                          alignment: Alignment.center,
                          child: GestureDetector(
                            enabled: isTopCard,
                            onPanUpdate: isTopCard ? controller.updateCardPosition : null,
                            onPanEnd: isTopCard ? (_) => controller.onSwipeEnd() : null,
                            child: _buildCard(controller.swipeItems[index]),
                          ),
                        ),
                      );
                    }).reversed.toList(), // 反转列表,让第一个元素在最上层
                  ),
                );
              }),
              SizedBox(height: 20),
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  InkWell(
                    onTap: controller.manualDislike,
                    child: Image.asset("assets/icon/close-icon.png", height: 70),
                  ),
                  SizedBox(width: 15),
                  InkWell(
                    onTap: controller.manualLike,
                    child: Image.asset("assets/icon/heart-icon.png", height: 90),
                  ),
                  SizedBox(width: 15),
                  Image.asset("assets/icon/fevorite-icon.png", height: 70)
                ],
              )
            ],
          ),
        ),
      ),
    );
  }

  // 提取卡片Widget
  Widget _buildCard(Map item) {
    return Container(
      decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(15),
          color: Colors.amber),
      child: Stack(
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(15),
            child: Image.asset(
              item['imagePath'],
              fit: BoxFit.cover,
              width: double.infinity,
              height: double.infinity,
            ),
          ),
          Positioned(
              right: 0,
              child: IconButton(
                  onPressed: () {},
                  icon: Icon(
                    Icons.more_vert,
                    color: AppColors.whiteColor,
                    size: 40,
                  ))),
          Align(
            alignment: Alignment.bottomCenter,
            child: Container(
              height: MediaQuery.of(Get.context!).size.height * 0.095,
              width: double.infinity,
              decoration: BoxDecoration(
                  borderRadius: BorderRadius.only(
                      bottomRight: Radius.circular(15),
                      bottomLeft: Radius.circular(15)),
                  color: Colors.black.withOpacity(0.8)),
              child: Padding(
                padding: EdgeInsets.symmetric(
                    horizontal: 24, vertical: 8),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    CommonTextWidget(
                      item['name'],
                      color: AppColors.whiteColor,
                      fontSize: 26,
                      fontWeight: FontWeight.bold,
                    ),
                    Row(
                      children: [
                        CommonTextWidget(
                          "Professional Model",
                          color: AppColors.whiteColor,
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                        SizedBox(
                          height: 25,
                          child: VerticalDivider(
                            color: AppColors.whiteColor,
                            thickness: 2,
                          ),
                        ),
                        CommonTextWidget(
                          item['age'],
                          color: AppColors.whiteColor,
                          fontSize: 18,
                          fontWeight: FontWeight.bold,
                        ),
                      ],
                    )
                  ],
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

二、推荐的第三方包

如果自定义实现觉得繁琐,推荐两个稳定且定制性强的包:

  • card_swiper:轻量易用,支持左右滑动、自定义卡片布局,内置滑动动画,适合快速集成。
  • swipeable_stack:提供更精细的手势控制,支持自定义滑动阈值、动画曲线,还能监听滑动状态回调,适合需要复杂交互的场景。

使用这些包时,只需将卡片列表传入组件,配置回调函数处理喜欢/拒绝逻辑即可,比自定义实现更省心。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 06:08:18