如何高效管理fg-data-profiling版本控制:Git工作流完整指南 [特殊字符]
2026/5/15 16:49:20
在 Flutter 开发中,轮播图(Banner)是首页广告、商品推荐、活动展示的核心组件。原生PageView需手动实现自动播放、指示器联动、图片加载等逻辑,重复开发易导致体验不一致。本文封装的BannerWidget整合 “自动播放 + 循环滚动 + 指示器自定义 + 图片加载优化” 四大核心能力,适配本地 / 网络图片、支持手势控制,一行代码即可集成。
| 配置分类 | 核心参数 | 核心作用 |
|---|---|---|
| 必选配置 | images: List<String>、onTap: Function(int) | 图片列表(本地路径 / 网络 URL)、点击回调(返回索引) |
| 播放配置 | autoPlay、autoPlayDuration、loop、animationCurve | 自动播放、播放时长、循环滚动、动画曲线 |
| 指示器配置 | indicatorType、indicatorColor、indicatorSelectedColor、indicatorPosition | 指示器类型、默认颜色、选中颜色、显示位置 |
| 图片样式配置 | imageRadius、imageFit、placeholder、errorWidget | 图片圆角、缩放模式、占位组件、失败组件 |
| 适配配置 | adaptDarkMode、height、padding | 深色模式适配、轮播图高度、内边距 |
dart
import 'package:flutter/material.dart'; import 'dart:async'; /// 指示器类型枚举 enum BannerIndicatorType { dot, // 圆点指示器(默认) number, // 数字指示器(如“1/5”) progress, // 进度条指示器 } /// 指示器位置枚举 enum BannerIndicatorPosition { bottom, // 底部(默认) top, // 顶部 } /// 通用轮播图组件 class BannerWidget extends StatefulWidget { // 必选参数 final List<String> images; // 图片列表(本地路径以"asset://"开头,否则为网络图片) final Function(int) onTap; // 图片点击回调(参数:当前图片索引) // 播放配置 final bool autoPlay; // 是否自动播放(默认true) final Duration autoPlayDuration; // 自动播放时长(默认3秒) final bool loop; // 是否循环滚动(默认true) final Curve animationCurve; // 滑动动画曲线(默认线性) final bool pauseOnTouch; // 触摸时暂停播放(默认true) // 指示器配置 final bool showIndicator; // 是否显示指示器(默认true) final BannerIndicatorType indicatorType; // 指示器类型 final Color indicatorColor; // 指示器默认颜色 final Color indicatorSelectedColor; // 指示器选中颜色 final double indicatorSize; // 指示器大小(圆点直径/数字字号/进度条高度) final double indicatorSpacing; // 指示器间距(仅圆点类型) final BannerIndicatorPosition indicatorPosition; // 指示器位置 final EdgeInsetsGeometry indicatorPadding; // 指示器内边距 // 图片样式配置 final double height; // 轮播图高度(默认200px) final double imageRadius; // 图片圆角(默认0) final BoxFit imageFit; // 图片缩放模式(默认cover) final Widget? placeholder; // 图片加载占位组件 final Widget? errorWidget; // 图片加载失败组件 // 适配配置 final bool adaptDarkMode; // 适配深色模式(默认true) final EdgeInsetsGeometry padding; // 轮播图内边距(默认无) const BannerWidget({ super.key, required this.images, required this.onTap, // 播放配置 this.autoPlay = true, this.autoPlayDuration = const Duration(seconds: 3), this.loop = true, this.animationCurve = Curves.linear, this.pauseOnTouch = true, // 指示器配置 this.showIndicator = true, this.indicatorType = BannerIndicatorType.dot, this.indicatorColor = Colors.white38, this.indicatorSelectedColor = Colors.white, this.indicatorSize = 8.0, this.indicatorSpacing = 6.0, this.indicatorPosition = BannerIndicatorPosition.bottom, this.indicatorPadding = const EdgeInsets.symmetric(vertical: 12, horizontal: 16), // 图片样式配置 this.height = 200.0, this.imageRadius = 0.0, this.imageFit = BoxFit.cover, this.placeholder, this.errorWidget, // 适配配置 this.adaptDarkMode = true, this.padding = EdgeInsets.zero, }) : assert(images.isNotEmpty, "图片列表不可为空"), assert(autoPlayDuration.inMilliseconds > 500, "自动播放时长需大于500ms"); @override State<BannerWidget> createState() => _BannerWidgetState(); } class _BannerWidgetState extends State<BannerWidget> { late PageController _pageController; late Timer? _autoPlayTimer; int _currentIndex = 0; bool _isTouching = false; // 实际数据源(循环模式下前后添加哨兵元素) List<String> get _actualImages => widget.loop ? [...widget.images, widget.images.first] : widget.images; @override void initState() { super.initState(); _pageController = PageController( initialPage: widget.loop ? 1 : 0, viewportFraction: 1.0, ); _initAutoPlayTimer(); } @override void dispose() { _autoPlayTimer?.cancel(); _pageController.dispose(); super.dispose(); } @override void didUpdateWidget(covariant BannerWidget oldWidget) { super.didUpdateWidget(oldWidget); if (widget.images != oldWidget.images || widget.autoPlay != oldWidget.autoPlay || widget.autoPlayDuration != oldWidget.autoPlayDuration) { _autoPlayTimer?.cancel(); _initAutoPlayTimer(); } } /// 初始化自动播放计时器 void _initAutoPlayTimer() { if (!widget.autoPlay) return; _autoPlayTimer = Timer.periodic(widget.autoPlayDuration, (_) { if (_isTouching) return; _pageController.nextPage( duration: const Duration(milliseconds: 300), curve: widget.animationCurve, ); }); } /// 处理页面滚动回调 void _onPageChanged(int index) { if (!widget.loop) { setState(() => _currentIndex = index); return; } // 循环模式下处理边界 if (index == 0) { // 滚动到最左侧哨兵元素,切换到最后一张 _currentIndex = widget.images.length - 1; _pageController.jumpToPage(widget.images.length); } else if (index == widget.images.length + 1) { // 滚动到最右侧哨兵元素,切换到第一张 _currentIndex = 0; _pageController.jumpToPage(1); } else { setState(() => _currentIndex = index - 1); } } /// 深色模式颜色适配 Color _adaptDarkMode(Color lightColor, Color darkColor) { if (!widget.adaptDarkMode) return lightColor; return MediaQuery.platformBrightnessOf(context) == Brightness.dark ? darkColor : lightColor; } /// 构建图片组件(支持本地/网络图片) Widget _buildImage(String imageUrl, int index) { final isAsset = imageUrl.startsWith("asset://"); final actualUrl = isAsset ? imageUrl.replaceFirst("asset://", "") : imageUrl; // 占位组件 final placeholderWidget = widget.placeholder ?? Container( color: _adaptDarkMode(const Color(0xFFF5F5F5), const Color(0xFF3D3D3D)), child: const Center(child: Icon(Icons.image_outlined, size: 40, color: Colors.grey)), ); // 失败组件 final errorWidget = widget.errorWidget ?? Container( color: _adaptDarkMode(const Color(0xFFF5F5F5), const Color(0xFF3D3D3D)), child: const Center(child: Icon(Icons.error_outlined, size: 40, color: Colors.redAccent)), ); // 实际图片组件 Widget imageWidget; if (isAsset) { imageWidget = Image.asset( actualUrl, fit: widget.imageFit, errorBuilder: (_, __, ___) => errorWidget, ); } else { imageWidget = Image.network( actualUrl, fit: widget.imageFit, placeholder: (_, __) => placeholderWidget, errorBuilder: (_, __, ___) => errorWidget, ); } // 图片容器(添加圆角、点击事件) return GestureDetector( onTap: () => widget.onTap(_currentIndex), onTapDown: (_) => _isTouching = widget.pauseOnTouch, onTapUp: (_) => _isTouching = false, onTapCancel: () => _isTouching = false, child: ClipRRect( borderRadius: BorderRadius.circular(widget.imageRadius), child: imageWidget, ), ); } /// 构建指示器组件 Widget _buildIndicator() { if (!widget.showIndicator || widget.images.length <= 1) return const SizedBox.shrink(); final adaptedIndicatorColor = _adaptDarkMode( widget.indicatorColor, const Color(0xFF666666), ); final adaptedSelectedColor = _adaptDarkMode( widget.indicatorSelectedColor, Colors.white70, ); switch (widget.indicatorType) { case BannerIndicatorType.dot: return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.images.length, (index) { final isSelected = index == _currentIndex; return Container( width: isSelected ? widget.indicatorSize * 2 : widget.indicatorSize, height: widget.indicatorSize, margin: EdgeInsets.symmetric(horizontal: widget.indicatorSpacing / 2), decoration: BoxDecoration( color: isSelected ? adaptedSelectedColor : adaptedIndicatorColor, borderRadius: BorderRadius.circular(widget.indicatorSize / 2), ), ); }), ); case BannerIndicatorType.number: return Text( "${_currentIndex + 1}/${widget.images.length}", style: TextStyle( color: adaptedSelectedColor, fontSize: widget.indicatorSize, fontWeight: FontWeight.w500, ), ); case BannerIndicatorType.progress: return Container( height: widget.indicatorSize, width: 80, decoration: BoxDecoration( color: adaptedIndicatorColor, borderRadius: BorderRadius.circular(widget.indicatorSize / 2), ), child: FractionallySizedBox( widthFactor: (_currentIndex + 1) / widget.images.length, child: Container( color: adaptedSelectedColor, borderRadius: BorderRadius.circular(widget.indicatorSize / 2), ), ), ); } } @override Widget build(BuildContext context) { return Padding( padding: widget.padding, child: Container( height: widget.height, width: double.infinity, child: Stack( alignment: widget.indicatorPosition == BannerIndicatorPosition.bottom ? Alignment.bottomCenter : Alignment.topCenter, children: [ // 轮播图主体 PageView.builder( controller: _pageController, itemCount: _actualImages.length, onPageChanged: _onPageChanged, physics: const BouncingScrollPhysics(), itemBuilder: (context, index) => _buildImage(_actualImages[index], index), ), // 指示器(带背景遮罩) Padding( padding: widget.indicatorPadding, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _adaptDarkMode(Colors.black26, Colors.black45), borderRadius: BorderRadius.circular(12), ), child: _buildIndicator(), ), ), ], ), ), ); } }适用场景:APP 首页广告、活动宣传、Banner 图展示
dart
// 首页顶部广告轮播 BannerWidget( images: [ "https://example.com/banner1.jpg", "https://example.com/banner2.jpg", "https://example.com/banner3.jpg", ], onTap: (index) { debugPrint("点击第${index+1}张广告"); // 跳转至广告详情页 Navigator.push( context, MaterialPageRoute(builder: (context) => BannerDetailPage(index: index)), ); }, height: 220, imageRadius: 12, autoPlayDuration: const Duration(seconds: 4), indicatorColor: Colors.white54, indicatorSelectedColor: Colors.white, indicatorSize: 6, indicatorSpacing: 8, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), adaptDarkMode: true, );适用场景:商品详情页推荐、本地资源轮播展示
dart
// 商品详情页推荐轮播 BannerWidget( images: [ "asset://assets/images/product1.jpg", "asset://assets/images/product2.jpg", "asset://assets/images/product3.jpg", ], onTap: (index) { debugPrint("点击第${index+1}个推荐商品"); // 切换商品详情图 setState(() => currentProductImageIndex = index); }, height: 180, imageFit: BoxFit.contain, autoPlay: false, // 手动滑动,不自动播放 loop: false, // 不循环 showIndicator: true, indicatorType: BannerIndicatorType.number, indicatorSize: 14, indicatorPosition: BannerIndicatorPosition.top, indicatorPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), placeholder: Container(color: const Color(0xFFF8F8F8)), );适用场景:限时活动展示、带有进度提示的轮播场景
dart
// 限时活动轮播 BannerWidget( images: [ "https://example.com/event1.jpg", "https://example.com/event2.jpg", ], onTap: (index) { debugPrint("点击第${index+1}个活动"); Navigator.push(context, MaterialPageRoute(builder: (context) => EventPage(index: index))); }, height: 160, autoPlayDuration: const Duration(seconds: 5), showIndicator: true, indicatorType: BannerIndicatorType.progress, indicatorSize: 3, indicatorColor: Colors.white30, indicatorSelectedColor: Colors.orangeAccent, imageRadius: 8, padding: const EdgeInsets.symmetric(horizontal: 16), adaptDarkMode: true, );PageController.jumpToPage实现无缝循环,避免原生PageView边界卡顿Timer.periodic实现自动播放,触摸时暂停、松开恢复,提升交互体验pubspec.yaml中配置资源路径;网络图片需确保 URL 有效,建议配置placeholder_actualImages长度比原列表多 2(前后哨兵),PageController初始页码需设为 1,避免首次加载显示哨兵元素_adaptDarkMode方法适配,避免深色模式下颜色冲突https://openharmonycrossplatform.csdn.net/content