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)
)
],
)
);
}
}