import 'package:flutter/material.dart'; import 'package:hgdj/hj_utils/screen.dart'; import 'package:hgdj/tools_base/widget/card_swiper/src/swiper.dart'; import 'package:hgdj/tools_base/widget/image_browser_page.dart'; import 'package:hgdj/tools_base/widget/net_image_widget.dart'; import '../../hj_model/splash/ads_model.dart'; import 'ads_item.dart'; /// 广告轮播 class AdsBannerWidget extends StatefulWidget { final List? models; // 广告数据列表 final double? width; // 整体宽度 final double? height; // 整体高度 final ValueChanged? onItemClick; // 点击某页回调(传 index) final ValueChanged? onIndexChanged; // 切换页回调(传 index) final int? autoPlayMs; // 自动轮播间隔(毫秒),默认 5000 final bool isIndicatorBottomCenter; // 指示器置于底部居中 final bool isIndicatorUnderCenter; // 指示器置于轮播下方居中(Column 布局) final Color? color; // 指示器未选中色 final Color? selectColor; // 指示器选中色 final bool isCircle; // 透传给 CIndicator.isBarStyle(true=长条红色风格) final double borderRadius; // 图片圆角,默认 12 AdsBannerWidget( this.models, { super.key, this.width, this.height, this.autoPlayMs = 5000, this.onItemClick, this.onIndexChanged, this.isIndicatorBottomCenter = false, this.isIndicatorUnderCenter = false, this.color, this.selectColor, this.isCircle = true, this.borderRadius = 12, }); @override State createState() => _AdsBannerWidgetState(); } class _AdsBannerWidgetState extends State { // 当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper) final ValueNotifier _selectIndex = ValueNotifier(0); int get _count => widget.models?.length ?? 0; @override void dispose() { _selectIndex.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SizedBox( width: widget.width, height: widget.height, child: widget.isIndicatorUnderCenter ? Column( children: [ // 该分支高度固定 256(唯一调用方不传 height,靠此撑开 Swiper) SizedBox(height: 256, child: _buildSwiper()), 12.sizeBoxH, if (_count > 1) Container( alignment: Alignment.center, child: _buildIndicator(space: 4, dotSize: 4, selectWidth: 8), ), ], ) : Stack( clipBehavior: Clip.none, children: [ _buildSwiper(), if (widget.isIndicatorBottomCenter) Positioned( bottom: 10, left: 0, right: 0, child: _count > 1 ? Container( alignment: Alignment.center, child: _buildIndicator( space: 2, dotSize: 2, selectWidth: 5, // 视频广告 cell:主题黄(未选中半透明,选中实心) color: widget.color ?? const Color(0xffFFD900) .withValues(alpha: 0.4), selectColor: widget.selectColor ?? const Color(0xffFFD900), ), ) : const SizedBox.shrink(), ) else Positioned( bottom: 7, right: 7, child: _count > 1 ? _buildIndicator(space: 4, dotSize: 4, selectWidth: 8) : const SizedBox.shrink(), ), ], ), ); } /// 轮播主体;两种指示器布局共用 Widget _buildSwiper() { return Swiper( autoplay: _count > 1, autoplayDelay: widget.autoPlayMs ?? 5000, loop: _count > 1, itemBuilder: (context, index) => AdsItem( adInfo: widget.models![index], showType: AdShowType.img, borderRadius: widget.borderRadius, ), onTap: widget.onItemClick, onIndexChanged: (index) { _selectIndex.value = index; widget.onIndexChanged?.call(index); }, itemCount: _count, ); } /// 指示器随当前页局部刷新 Widget _buildIndicator({ required double space, required double dotSize, required double selectWidth, Color? color, Color? selectColor, }) { return ValueListenableBuilder( valueListenable: _selectIndex, builder: (_, index, __) => CIndicator( itemCount: _count, selectIndex: index, space: space, dotSize: dotSize, selectWidth: selectWidth, color: color ?? widget.color, selectColor: selectColor ?? widget.selectColor, isBarStyle: widget.isCircle, ), ); } } /// 分页指示器 class CIndicator extends StatelessWidget { const CIndicator({ super.key, this.itemCount, this.selectIndex, this.space = 5.0, this.isBarStyle = true, this.color, this.selectColor, this.dotSize = 6, this.selectWidth = 12, }); final int? itemCount; // 圆点总数 final int? selectIndex; // 当前选中页 final double? space; // 圆点间距 final bool isBarStyle; // true=选中态长条+红色系;false=圆点+白色系 final Color? color; // 未选中色(覆盖默认) final Color? selectColor; // 选中色(覆盖默认) final double dotSize; // 圆点直径 final double selectWidth; // 选中态(长条)宽度,isBarStyle 时生效 Widget _buildDot(int index) { final isSelected = selectIndex == index; final activeColor = selectColor ?? (isBarStyle ? const Color(0xffE1351F) : Colors.white); final inactiveColor = color ?? (isBarStyle ? const Color(0xffE1351F).withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.5)); // 选中态长条、未选中态圆点;宽度与颜色平滑过渡(类似 Android 系统页面指示器) return Padding( padding: EdgeInsets.only(left: space ?? 0), child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, width: isSelected && isBarStyle ? selectWidth : dotSize, height: dotSize, decoration: BoxDecoration( borderRadius: BorderRadius.circular(dotSize / 2), color: isSelected ? activeColor : inactiveColor, ), ), ); } @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [for (int i = 0; i < (itemCount ?? 0); i++) _buildDot(i)], ); } } /// 图集:左右滑动浏览,可选右上角页码 + 底部圆点指示器 class ImageCollectionWidget extends StatefulWidget { final List? imgs; final double aspectRatio; final bool showCIndicator; // 是否显示底部圆点指示器 final bool showIndex; // 是否显示右上角页码 ImageCollectionWidget({ super.key, this.imgs, this.aspectRatio = 375 / 467, this.showCIndicator = true, this.showIndex = true, }); @override State createState() => _ImageCollectionWidgetState(); } class _ImageCollectionWidgetState extends State { // 当前页用 ValueNotifier 局部刷新页码/指示器,避免每次滑动都 setState 重建 Swiper final ValueNotifier _selectIndex = ValueNotifier(0); int get _count => widget.imgs?.length ?? 0; @override void dispose() { _selectIndex.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AspectRatio( aspectRatio: widget.aspectRatio, child: Stack( alignment: Alignment.center, children: [ Swiper( autoplay: _count > 1, loop: _count > 1, itemBuilder: (context, index) { final cover = widget.imgs![index]; return InkWell( enableFeedback: false, onTap: () => ImageBrowserPage.open(widget.imgs ?? [], index: index), child: NetworkImageLoader(imageUrl: cover), ); }, onIndexChanged: (index) => _selectIndex.value = index, itemCount: _count, ), // 右上角页码 1/N if (widget.showIndex && _count > 1) Positioned( top: 24, right: 16, child: ValueListenableBuilder( valueListenable: _selectIndex, builder: (_, index, __) => Container( padding: const EdgeInsets.symmetric(horizontal: 14), alignment: Alignment.center, height: 28, decoration: BoxDecoration( borderRadius: BorderRadius.circular(999), color: Colors.black.withValues(alpha: .6), ), child: Text('${index + 1}/$_count', style: const TextStyle(fontSize: 14, color: Colors.white)), ), ), ), // 底部圆点指示器 if (widget.showCIndicator && _count > 1) Positioned( bottom: 10, right: 12, child: ValueListenableBuilder( valueListenable: _selectIndex, builder: (_, index, __) => CIndicator(itemCount: _count, selectIndex: index, space: 6), ), ), ], ), ); } } /// AI 图生视频 banner:每页左原图、右生成图,自动轮播 class AIBannerWidget extends StatefulWidget { final int index; // 初始选中页 final List? models; const AIBannerWidget({super.key, this.index = 0, this.models}); @override State createState() => _AIBannerWidgetState(); } class _AIBannerWidgetState extends State { // 当前页用 ValueNotifier 局部刷新指示器,避免滑动重建 Swiper late final ValueNotifier _selectIndex = ValueNotifier(widget.index); int get _count => widget.models?.length ?? 0; @override void dispose() { _selectIndex.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Stack(children: [ Swiper( autoplay: _count > 1, loop: _count > 1, autoplayDelay: 5000, itemBuilder: (context, index) { final item = widget.models?[index]; // 左:原图 右:AI 生成图 return Row(children: [ Expanded( child: NetworkImageLoader( imageUrl: item?.cover ?? '', borderRadius: 0)), Expanded( child: NetworkImageLoader( imageUrl: item?.newUrl ?? '', borderRadius: 0)), ]); }, onIndexChanged: (index) => _selectIndex.value = index, itemCount: _count, ), Positioned( bottom: 10, right: 10, child: _count > 1 ? ValueListenableBuilder( valueListenable: _selectIndex, builder: (_, index, __) => CIndicator(itemCount: _count, selectIndex: index), ) : const SizedBox.shrink(), ), ]); } }