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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 07:18:15