flutter 自定义走马灯,内部为Widget控件的走马灯效果二:横向无限匀速滚动+每个Item与屏幕左侧对齐时,停靠3秒再继续滚动

flutter走马灯横向无限匀速滚动,item左侧对齐停靠

dart 复制代码
import 'dart:async';
import 'package:card_swiper/card_swiper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

/*
* 促销速递、体验购轮播、选品广场 走马灯
* 每个item滚动到屏幕左侧对齐时停靠3秒再继续滚动
* */
class SmoothInfiniteScroll extends StatefulWidget {
  final RecommendModel recommendModel;
  const SmoothInfiniteScroll(this.recommendModel, {super.key});

  @override
  State<SmoothInfiniteScroll> createState() => _SmoothInfiniteScrollState();
}

class _SmoothInfiniteScrollState extends State<SmoothInfiniteScroll> with SingleTickerProviderStateMixin {
  late ScrollController _scrollController;
  late AnimationController _animationController;
  late Animation<double> _animation;

  // 原始数据
  List<Widget> _originalItems = [];
  List<Widget> _items = []; // 翻倍后的数据

  // 配置参数
  final double _itemWidth = 220;
  final double _itemSpacing = 10;
  final Duration _scrollDuration = const Duration(seconds: 35); // 滚动一轮时间
  final Duration _pauseDuration = const Duration(seconds: 3); // 停靠时间

  // 原始数据总宽度(用于判断过渡点)
  double _originalTotalWidth = 0.0;
  double _itemTotalWidth = 0.0; // 单个item占用的总宽度(包括间距)

  // 停靠控制
  Timer? _pauseTimer;
  bool _isPaused = false;
  int _currentItemIndex = 0; // 当前停靠的item索引

  @override
  void initState() {
    super.initState();

    _scrollController = ScrollController()
      ..addListener(_handleScroll);

    // 动画控制器(匀速滚动)
    _animationController = AnimationController(
      vsync: this,
      duration: _scrollDuration,
    );

    // 线性动画(确保速度均匀)
    _animation = CurvedAnimation(
      parent: _animationController,
      curve: Curves.linear,
    )..addListener(() {
      if (_scrollController.hasClients && !_isPaused) {
        /*
        * 实现匀速的自动滚动效果
        * 计算目标位置(基于动画进度)
        * */
        final maxExtent = _scrollController.position.maxScrollExtent;
        final target = _animation.value * maxExtent;
        _scrollController.jumpTo(target);
        
        // 检查是否需要停靠
        _checkAndPauseAtItem();
      }
    });

    _animationController.repeat();
  }

  /*
  * 检查并处理item停靠逻辑
  * */
  void _checkAndPauseAtItem() {
    if (!_scrollController.hasClients) return;
    
    final currentOffset = _scrollController.offset;
    final adjustedOffset = currentOffset % _originalTotalWidth;
    
    // 计算当前应该显示哪个item在左侧
    final itemIndex = (adjustedOffset / _itemTotalWidth).floor();
    final itemOffset = itemIndex * _itemTotalWidth;
    final diff = adjustedOffset - itemOffset;
    
    // 当滚动到item的左边缘时(允许5像素的误差范围)
    if (diff < 5 && itemIndex != _currentItemIndex && !_isPaused) {
      _currentItemIndex = itemIndex;
      _pauseScrolling();
    }
  }

  /*
  * 暂停滚动
  * */
  void _pauseScrolling() {
    if (_isPaused) return;
    
    _isPaused = true;
    _animationController.stop();
    
    // 1秒后恢复滚动
    _pauseTimer?.cancel();
    _pauseTimer = Timer(_pauseDuration, () {
      if (mounted) {
        setState(() {
          _isPaused = false;
          _animationController.repeat();
        });
      }
    });
  }

  /*
  * 无缝循环的衔接
  * 处理滚动:平滑过渡替代生硬跳转
  * */
  void _handleScroll() {
    if (!_scrollController.hasClients) return;

    final currentOffset = _scrollController.offset;

    // 当滚动超过原始数据总宽度时,计算偏移差并平滑过渡
    if (currentOffset >= _originalTotalWidth * (_items.length / _originalItems.length - 1)) {
      // 计算当前位置与原始数据末尾的差值(这部分是需要衔接的偏移量)
      final offsetDiff = currentOffset - _originalTotalWidth;
      // 平滑滚动到差值位置(视觉上相当于从末尾回到开头的偏移处)
      _scrollController.animateTo(
        offsetDiff,
        duration: const Duration(microseconds: 1), // 瞬间过渡(视觉无感知)
        curve: Curves.linear,
      );
    }
  }

  @override
  void dispose() {
    _pauseTimer?.cancel();
    _scrollController.dispose();
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _originalItems = [_promoBanner(), _aTuiExpBanner(), _productSelectionBanner()];
    // 数据翻倍,消除滚动到最后一条继续滚动出现空白区域跳回开头产品明显滚动断层问题,结合位置重置(jumpTo())实现无缝衔接
    _items = [..._originalItems, ..._originalItems, ..._originalItems, ..._originalItems];
    _originalTotalWidth = _originalItems.length * (_itemWidth + _itemSpacing);
    _itemTotalWidth = _itemWidth + _itemSpacing;
    
    return Container(
      height: 330.w,
      margin: EdgeInsets.only(left: 30.w, right: 30.w, top: 30.w),
      padding: EdgeInsets.only(left: 15.w, right: 15.w, top: 20.w, bottom: 20.w),
      decoration: BoxDecoration(
        color: Colors.white,
        image: DecorationImage(
          image: AssetImage(PathConfig.imageMarketPromoExpBg),
          fit: BoxFit.fill
        ),
        boxShadow: [BoxShadow(color: JadeColors.grey_6, blurRadius: 1.0, offset: const Offset(0.0, 0.0))]
      ),
      child: ListView.builder(
        controller: _scrollController,
        scrollDirection: Axis.horizontal,
        physics: const NeverScrollableScrollPhysics(),
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return GestureDetector(
            onTap: () {
              esLoadingToast('${_items[index]}');
            },
            child: Container(
              width: _itemWidth,
              margin: EdgeInsets.only(right: _itemSpacing),
              child: _items[index]
            ),
          );
        },
      ),
    );
  }

  // 促销速递轮播
  _promoBanner() {
    List<PromotionExpressItemBean>? _promotionExpressList = widget.recommendModel.promotionExpressList;
    return GestureDetector(
      child: SizedBox(
        child: Column(
          children: [
            Row(
              children: [
                Text(S.current.cuxiaosudi, style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold)),
                Container(
                  margin: EdgeInsets.only(left: 10.w),
                  padding: EdgeInsets.only(left: 6.w, right: 6.w, top: 4.w, bottom: 4.w),
                  decoration: BoxDecoration(
                    color: JadeColors.green_13,
                    border: Border.all(color: JadeColors.green_4, width: 2.w),
                    borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), bottomRight: Radius.circular(4))
                  ),
                  child: Text(
                    '促销 优惠 活动',
                    style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.w600, color: JadeColors.green_4),
                  ),
                )
              ],
            ),
            SizedBox(height: 20.w),
            Expanded(
              child: _promotionExpressList != null && _promotionExpressList.isNotEmpty
                ? Swiper(
                    key: UniqueKey(),
                    axisDirection: AxisDirection.right,
                    itemCount: _promotionExpressList.length,
                    autoplay: true,
                    autoplayDelay: 3000,
                    itemBuilder: (BuildContext context, int index) {
                      return MarketPromoExpBannerItemView(_promotionExpressList[index]);
                    },
                    onIndexChanged: (index) {},
                  )
                : Container(color: JadeColors.lightGrey)
            )
          ],
        ),
      ),
      onTap: () {
        NavigatorUtil.push(PromotionExpress());
      },
    );
  }

  // 体验购轮播
  _aTuiExpBanner() {
    return SizedBox(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(S.current.experienceStation, style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold)),
          SizedBox(height: 20.w),
          Expanded(
            child: widget.recommendModel.marketExpList != null
              && widget.recommendModel.marketExpList!.isNotEmpty
              ? GestureDetector(
                  child: Swiper(
                    key: UniqueKey(),
                    itemCount: widget.recommendModel.marketExpList!.length,
                    autoplay: true,
                    autoplayDelay: 3000,
                    itemBuilder: (BuildContext context, int index) {
                      return MarketExpBannerItemView(marketExp: widget.recommendModel.marketExpList![index]);
                    },
                    onIndexChanged: (index) {},
                  ),
                  onTap: () {
                    NavigatorUtil.push(MarketExpGoodsListPage());
                  },
                )
              : GestureDetector(
                  onTap: () {
                    isLogin().then((value) {
                      if (value == true) {
                        NavigatorUtil.push(ExperienceStationListTwo());
                      } else {
                        NavigatorUtil.push(LoginPage());
                      }
                    });
                  },
                  child: Container(
                    color: JadeColors.lightGrey,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text('点击查看更多体验购', style: TextStyle(color: JadeColors.grey_3, fontSize: 26.sp)),
                        Image.asset(PathConfig.iconDoubleNextGrey, width: 22.w, height: 18.w)
                      ],
                    ),
                  )
                )
          )
        ],
      )
    );
  }

  // 选品轮播
  _productSelectionBanner() {
    return SizedBox(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('样品广场', style: TextStyle(fontSize: 32.sp, fontWeight: FontWeight.bold)),
          SizedBox(height: 20.w),
          Expanded(
            child: widget.recommendModel.productSelectionGoodsList.isNotEmpty
              ? GestureDetector(
                  child: Swiper(
                    key: UniqueKey(),
                    itemCount: widget.recommendModel.productSelectionGoodsList.length,
                    autoplay: true,
                    autoplayDelay: 3000,
                    itemBuilder: (BuildContext context, int index) {
                      return MarketProductSelectionBannerItemView(marketProductSelectBean: widget.recommendModel.productSelectionGoodsList[index]);
                    },
                    onIndexChanged: (index) {},
                  ),
                  onTap: () {
                    Utils().inRootPageToHomeExpProductSelection();
                  },
                )
              : Container(color: JadeColors.lightGrey)
          )
        ],
      )
    );
  }
}
相关推荐
白日梦想家6812 小时前
博客二:递归实战避坑指南,从入门到熟练运用
开发语言·python
星晨雪海2 小时前
若依框架原有页面功能进行了点位管理模块完整改造(3)
开发语言·前端·javascript
AC赳赳老秦2 小时前
OpenClaw与系统环境冲突:Windows/Mac系统兼容问题解决指南
开发语言·python·产品经理·策略模式·pygame·deepseek·openclaw
曹牧2 小时前
Java:将XML字符串上传到FTP服务器
java·开发语言
小张同学8242 小时前
Python 封神技巧:1 行代码搞定 90% 日常数据处理,效率直接拉满
开发语言·人工智能·python
HoweChenya2 小时前
Gemma-4 实测:31B Dense 与 26B MoE 在 H20 上的性能分水岭
开发语言·php
Je1lyfish2 小时前
Haskell 初探
开发语言·笔记·算法·rust·lisp·抽象代数
景庆1972 小时前
vscode启动springBoot项目配置,激活环境
java·开发语言·vscode
幽络源小助理2 小时前
PHP网站统计系统源码下载_极简统计程序支持宝塔部署_幽络源
开发语言·php