Flutter---轮播图

效果图

数据流
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]}');
  }
}
相关推荐
武玄天宗2 小时前
第五章、flutter怎么创建底部底部导航栏界面
前端·flutter
子榆.2 小时前
Flutter 与开源鸿蒙(OpenHarmony)生物识别实战:人脸 + 指纹双模认证,筑牢信创应用安全防线
flutter·开源·harmonyos
子榆.3 小时前
Flutter 与开源鸿蒙(OpenHarmony)离线地图与定位实战:无网络也能精准导航
flutter·开源·harmonyos
音浪豆豆_Rachel4 小时前
Flutter鸿蒙文件选择器进阶解析:多图选择的实现
flutter·华为·harmonyos
AiFlutter4 小时前
二、页面布局(01):容器
flutter·低代码·教程·低代码平台·aiflutter·aiflutter 低代码
武玄天宗14 小时前
第四章、用flutter创建登录界面
flutter
搬砖的kk14 小时前
基于Flutter开发应用如何快速适配HarmonyOS
flutter·华为·harmonyos
昼-枕15 小时前
Flutter深度解析:如何构建高性能、跨平台的移动应用
flutter
音浪豆豆_Rachel16 小时前
Flutter 与原生通信的桥梁:深入解析 Pigeon 与后台线程通信
flutter·harmonyos