Flutter实现滑动至1/3屏幕宽度后停止的Dismissible列表项及CTA
实现限制滑动距离的列表项及CTA交互
因为Dismissible组件的设计初衷是支持滑动删除/完成这类一次性操作,无法限制滑动终止位置,所以我们需要用手势监听+动画控制的方式来实现需求,核心思路是:用GestureDetector捕获滑动手势,配合AnimationController和SlideTransition控制列表项的滑动范围,同时通过Stack布局在列表项下方放置CTA区域。
具体实现步骤
- 计算屏幕宽度的1/3作为最大滑动偏移量,确保列表项滑动后刚好露出右侧1/3区域
- 用
AnimationController管理滑动动画,限制动画值在0-1之间,对应列表项从初始位置滑动到最大偏移位置 - 通过
GestureDetector监听滑动手势,实时更新动画进度;滑动结束时根据当前位置和滑动速度,决定是停在最大偏移位置还是回弹到初始状态 - 使用
Stack布局,底层放置可点击的CTA区域,上层放置可滑动的列表项内容
完整代码示例
import 'package:flutter/material.dart'; class SwipeListItem extends StatefulWidget { final String title; final VoidCallback onCTATap; const SwipeListItem({ super.key, required this.title, required this.onCTATap, }); @override State<SwipeListItem> createState() => _SwipeListItemState(); } class _SwipeListItemState extends State<SwipeListItem> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation<double> _slideAnimation; double _maxSlideOffset = 0; @override void didChangeDependencies() { super.didChangeDependencies(); // 计算屏幕1/3宽度作为最大滑动偏移量 _maxSlideOffset = MediaQuery.of(context).size.width / 3; _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), upperBound: 1, lowerBound: 0, ); // 关联动画值与实际滑动距离 _slideAnimation = Tween<double>(begin: 0, end: _maxSlideOffset).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeOut), ); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _handlePanUpdate(DragUpdateDetails details) { // 处理左滑/右滑手势,计算并限制动画进度在0-1之间 double deltaRatio = details.delta.dx / _maxSlideOffset; double newPosition = _animationController.value - deltaRatio; newPosition = newPosition.clamp(0, 1); _animationController.value = newPosition; } void _handlePanEnd(DragEndDetails details) { // 根据滑动进度和速度,决定最终状态 if (_animationController.value > 0.5 || details.velocity.pixelsPerSecond.dx < -200) { // 滑动超过一半或速度足够快,停在最大偏移位置 _animationController.animateTo(1); } else { // 否则回弹到初始位置 _animationController.animateTo(0); } } @override Widget build(BuildContext context) { return Stack( children: [ // 底层CTA区域:占屏幕右侧1/3,点击触发BottomSheet Positioned( right: 0, top: 0, bottom: 0, width: _maxSlideOffset, child: GestureDetector( onTap: widget.onCTATap, child: Container( color: Colors.blueAccent, alignment: Alignment.center, child: const Text( '操作', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), ), ), ), ), // 上层可滑动的列表项内容 GestureDetector( onPanUpdate: _handlePanUpdate, onPanEnd: _handlePanEnd, child: SlideTransition( position: _slideAnimation.drive( Tween<Offset>( begin: Offset.zero, end: const Offset(-1, 0), // 向左滑动1个单位,对应_maxSlideOffset的距离 ), ), child: Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: Text(widget.title, style: const TextStyle(fontSize: 16)), ), ), ), ], ); } } // 页面使用示例 class SwipeListDemo extends StatelessWidget { const SwipeListDemo({super.key}); void _openBottomSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (context) => const SizedBox( height: 220, child: Center( child: Text( 'BottomSheet 内容', style: TextStyle(fontSize: 18), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('滑动列表示例')), body: ListView.builder( itemCount: 10, itemBuilder: (context, index) => SwipeListItem( title: '列表项 ${index + 1}', onCTATap: () => _openBottomSheet(context), ), ), ); } }
可自定义调整的细节
- 修改
_animationController的duration参数,调整滑动动画的快慢 - 更改CTA区域的
color、文字内容,适配项目UI风格 - 调整
_handlePanEnd中的阈值(0.5、-200),改变滑动触发停住的灵敏度 - 若需要支持右滑操作,可扩展手势逻辑,添加对应方向的偏移和底层按钮
内容的提问来源于stack exchange,提问作者Dev
相关产品推荐
相关产品推荐

