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
相关产品推荐
相关产品推荐

