效果图

数据流
Dart
原始数据 (carouselItems)
↓ 包装
无限循环数据 (infiniteItems)
↓ 传递
PageView.builder → 渲染图片
核心组件
Dart
1. PageController
核心控制器,管理页面滚动
viewportFraction: 1.0:每页占满屏幕
initialPage: 1:从真实第一张开始
2. PageView.builder
懒加载构建页面
支持无限滚动
与PageController配合
3. AnimatedContainer
实现指示器动画效果
自动处理属性变化的动画
无限循环轮播图的核心技巧
Dart
1.假设原始数据有3张图片:carouselItems = ['A', 'B', 'C']
2.扩展数据
infiniteItems = [
carouselItems.last, // 'C' ← 最后一张放在最前面
...carouselItems, // 'A', 'B', 'C' ← 原始数据
carouselItems.first, // 'A' ← 第一张放在最后面
]
得到
索引: 0 1 2 3 4
数据: [C] [A] [B] [C] [A]
3.初始状态
用户看到的: [A] [B] [C]
实际数据: [C] [A] [B] [C] [A]
↑ ↑ ↑ ↑ ↑
索引: 0 1 2 3 4
当前显示: 👆 (索引1,显示A)
4.非边界滑动情况
滑动前: [C] [A] [B] [C] [A]
👆 显示A (索引1)
滑动后: [C] [A] [B] [C] [A]
👆 显示B (索引2)
5.滑动到最左边边界:当用户从A(索引1)向左滑到C(索引0)时:
滑动后: [C] [A] [B] [C] [A]
👆 显示C (索引0)
此时程序检测到:索引0 == 0(边界条件)
立即跳转到:索引3(也是C)
跳转后: [C] [A] [B] [C] [A]
👆 显示C (索引3)
6.滑动到最右边边界:当用户从C(索引3)向右滑到A(索引4)时
滑动后: [C] [A] [B] [C] [A]
👆 显示A (索引4)
此时程序检测到:索引4 == 数组长度-1
立即跳转到:索引1(也是A)
跳转后: [C] [A] [B] [C] [A]
👆 显示A (索引1)
代码逻辑对应
Dart
void _onPageChanged(int index) {
if (index == 0) {
// 滑到了最左边的"假"C
_pageController.jumpToPage(3); // 跳转到最右边的"真"C
_currentIndex = 2; // 显示原始数据的最后一项(索引2)
} else if (index == 4) {
// 滑到了最右边的"假"A
_pageController.jumpToPage(1); // 跳转到最左边的"真"A
_currentIndex = 0; // 显示原始数据的第一项(索引0)
} else {
// 正常滑动
_currentIndex = index - 1; // 减去前面的"假"C
}
}
实现步骤
1.准备图片
Dart
//轮播图数据
final List<String> carouselItems = [
'assets/images/apple.png',
'assets/images/banana.png',
'assets/images/cherry.png',
];
2.定义一些必要的变量
Dart
late PageController _pageController; //核心控制器
int _currentIndex = 1; // 从原始数据的第一个开始(注意:索引1对应原始数据的第一个)
Timer? _timer;//定时器
3.为了实现无缝循环,扩展图片数据
Dart
//实现无缝循环
List<String> get infiniteItems {
return [
carouselItems.last, // 最后一项放在最前面
...carouselItems, // 原始数据
carouselItems.first, // 第一项放在最后面
];
}
4.初始化
Dart
@override
void initState() {
super.initState();
//初始胡控制器
_pageController = PageController(
viewportFraction: 1.0, //每个页面占视口的比例(0.0~1.0)
initialPage: 1, //初始显示第几页
);
_startAutoPlay(); //开始自动播放
}
5.注销控制器
Dart
@override
void dispose() {
_timer?.cancel();
_pageController.dispose();
super.dispose();
}
6.开始自动播放的方法
Dart
//==============================开始自动播放================================
void _startAutoPlay() {
//Timer.periodic:创建一个周期性定时器
_timer = Timer.periodic(const Duration(seconds: 3), (timer) { //每3秒执行一次回调函数
//页面安全判断
if (!mounted) return;
//页面切换
_pageController.nextPage( //切换下一页的方法
duration: const Duration(milliseconds: 800), //动画持续时间
curve: Curves.fastOutSlowIn,//动画曲线为 先加速后减速
);
});
}
7.页面切换的回调函数
Dart
//==============================页面切换的回调函数================================
void _onPageChanged(int index) {
// 处理边界情况,实现无缝循环
if (index == 0) {
// 如果滚动到虚拟的第一页(实际是原始数据的最后一页)
// 无动画跳转到真实数据的最后一页
_pageController.jumpToPage(carouselItems.length);
setState(() {
_currentIndex = carouselItems.length - 1; // 显示指示器为最后一页
});
} else if (index == infiniteItems.length - 1) {
// 如果滚动到虚拟的最后一页(实际是原始数据的第一页)
// 无动画跳转到真实数据的第一页
_pageController.jumpToPage(1);
setState(() {
_currentIndex = 0; // 显示指示器为第一页
});
} else {
// 正常页面变化
setState(() {
_currentIndex = index - 1; // 转换为原始数据的索引
});
}
}
8.暂停播放和继续播放
Dart
//================================暂停自动播放==============================
void _pauseAutoPlay() {
_timer?.cancel();
}
//=============================继续自动播放===================================
void _resumeAutoPlay() {
_timer?.cancel();
_startAutoPlay();
}
9.轮播图的核心*******
Dart
// 轮播图区域
SizedBox(
height: 142, // 固定高度
child: PageView.builder(
controller: _pageController, //控制器
onPageChanged: _onPageChanged,//页面切换回调
itemCount: infiniteItems.length, //总页数
physics: const ClampingScrollPhysics(), //滚动物理效果
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 计算真实数据的索引
int realIndex = index - 1;
if (realIndex < 0) realIndex = carouselItems.length - 1;
if (realIndex >= carouselItems.length) realIndex = 0;
_handleCarouselTap(realIndex);
},
onTapDown: (_) => _pauseAutoPlay(), //按下时暂停自动播放
onTapCancel: () => _resumeAutoPlay(), //取消点击时恢复
onTapUp: (_) => _resumeAutoPlay(), //抬起时恢复
child: Container(
margin: EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(infiniteItems[index]),
fit: BoxFit.contain,
),
),
),
);
},
),
),
10.分页指示器的实现
Dart
// 分页指示器
Container(
margin: const EdgeInsets.only(top: 10), // 距离上方10像素的外边距
height: 20, // 容器高度20像素
child: Row( // 水平排列子组件
mainAxisAlignment: MainAxisAlignment.center, // 子组件水平居中
children: List.generate( // 动态生成指示点列表
carouselItems.length, // 根据轮播图数量生成
(index) => AnimatedContainer( // 每个指示点是一个动画容器
duration: const Duration(milliseconds: 300), // 动画持续时间300ms
width: _currentIndex == index ? 20 : 8, // 当前激活点宽20,其他宽8
height: 8, // 所有点高度固定为8
margin: const EdgeInsets.symmetric(horizontal: 4), // 左右间距4像素
decoration: BoxDecoration(
shape: BoxShape.circle, // 圆形形状
color: _currentIndex == index // 颜色:激活点蓝色,其他灰色
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
),
),
),
11.单击的方法
Dart
void _handleCarouselTap(int index) {
print('点击了: ${carouselItems[index]}');
}
代码实例
Dart
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class DialMain extends StatefulWidget {
const DialMain({super.key});
@override
State<StatefulWidget> createState() => _DialMainState();
}
class _DialMainState extends State<DialMain> with SingleTickerProviderStateMixin {
//轮播图数据
final List<String> carouselItems = [
'assets/images/apple.png',
'assets/images/banana.png',
'assets/images/cherry.png',
];
late PageController _pageController; //核心控制器
int _currentIndex = 1; // 从原始数据的第一个开始(注意:索引1对应原始数据的第一个)
Timer? _timer;//定时器
//实现无缝循环
List<String> get infiniteItems {
return [
carouselItems.last, // 最后一项放在最前面
...carouselItems, // 原始数据
carouselItems.first, // 第一项放在最后面
];
}
@override
void initState() {
super.initState();
//初始胡控制器
_pageController = PageController(
viewportFraction: 1.0, //每个页面占视口的比例(0.0~1.0)
initialPage: 1, //初始显示第几页
);
_startAutoPlay(); //开始自动播放
}
@override
void dispose() {
_timer?.cancel();
_pageController.dispose();
super.dispose();
}
//==============================开始自动播放================================
void _startAutoPlay() {
//Timer.periodic:创建一个周期性定时器
_timer = Timer.periodic(const Duration(seconds: 3), (timer) { //每3秒执行一次回调函数
//页面安全判断
if (!mounted) return;
//页面切换
_pageController.nextPage( //切换下一页的方法
duration: const Duration(milliseconds: 800), //动画持续时间
curve: Curves.fastOutSlowIn,//动画曲线为 先加速后减速
);
});
}
//==============================页面切换的回调函数================================
void _onPageChanged(int index) {
// 处理边界情况,实现无缝循环
if (index == 0) {
// 如果滚动到虚拟的第一页(实际是原始数据的最后一页)
// 无动画跳转到真实数据的最后一页
_pageController.jumpToPage(carouselItems.length);
setState(() {
_currentIndex = carouselItems.length - 1; // 显示指示器为最后一页
});
} else if (index == infiniteItems.length - 1) {
// 如果滚动到虚拟的最后一页(实际是原始数据的第一页)
// 无动画跳转到真实数据的第一页
_pageController.jumpToPage(1);
setState(() {
_currentIndex = 0; // 显示指示器为第一页
});
} else {
// 正常页面变化
setState(() {
_currentIndex = index - 1; // 转换为原始数据的索引
});
}
}
//================================暂停自动播放==============================
void _pauseAutoPlay() {
_timer?.cancel();
}
//=============================继续自动播放===================================
void _resumeAutoPlay() {
_timer?.cancel();
_startAutoPlay();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF7F7F9),
appBar: AppBar(
backgroundColor: const Color(0xFFF7F7F9),
title: const Text("轮播图"),
centerTitle: true,
leading: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
),
),
body: Column(
children: [
// 轮播图区域
SizedBox(
height: 142, // 固定高度
child: PageView.builder(
controller: _pageController, //控制器
onPageChanged: _onPageChanged,//页面切换回调
itemCount: infiniteItems.length, //总页数
physics: const ClampingScrollPhysics(), //滚动物理效果
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 计算真实数据的索引
int realIndex = index - 1;
if (realIndex < 0) realIndex = carouselItems.length - 1;
if (realIndex >= carouselItems.length) realIndex = 0;
_handleCarouselTap(realIndex);
},
onTapDown: (_) => _pauseAutoPlay(), //按下时暂停自动播放
onTapCancel: () => _resumeAutoPlay(), //取消点击时恢复
onTapUp: (_) => _resumeAutoPlay(), //抬起时恢复
child: Container(
margin: EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(infiniteItems[index]),
fit: BoxFit.contain,
),
),
),
);
},
),
),
// 分页指示器
Container(
margin: const EdgeInsets.only(top: 10), // 距离上方10像素的外边距
height: 20, // 容器高度20像素
child: Row( // 水平排列子组件
mainAxisAlignment: MainAxisAlignment.center, // 子组件水平居中
children: List.generate( // 动态生成指示点列表
carouselItems.length, // 根据轮播图数量生成
(index) => AnimatedContainer( // 每个指示点是一个动画容器
duration: const Duration(milliseconds: 300), // 动画持续时间300ms
width: _currentIndex == index ? 20 : 8, // 当前激活点宽20,其他宽8
height: 8, // 所有点高度固定为8
margin: const EdgeInsets.symmetric(horizontal: 4), // 左右间距4像素
decoration: BoxDecoration(
shape: BoxShape.circle, // 圆形形状
color: _currentIndex == index // 颜色:激活点蓝色,其他灰色
? Colors.blue
: Colors.grey.withOpacity(0.3),
),
),
),
),
),
const SizedBox(height: 20),
],
),
);
}
void _handleCarouselTap(int index) {
print('点击了: ${carouselItems[index]}');
}
}