import 'package:flutter/material.dart'; import 'package:jydj/hj_utils/screen.dart'; import 'package:jydj/tools_base/widget/card_swiper/src/swiper.dart'; import 'package:jydj/tools_base/widget/image_browser_page.dart'; import 'package:jydj/tools_base/widget/net_image_widget.dart'; import '../../hj_model/splash/ads_model.dart'; import '../../track_event_manager/track_manager.dart'; import 'ads_item.dart'; /// 广告轮播的指示器位置 enum IndicatorAlign { bottomRight, //右下角(默认) bottomCenter, //底部居中,小号主题黄点(视频广告 cell 用) underCenter, //轮播下方居中(Column 布局,轮播固定高 256) } /// 广告轮播:多张时自动轮播 + 指示器 class AdsBanner extends StatefulWidget { final List? models; //广告数据 final double? width; final double? height; final int autoPlayMs; //自动轮播间隔(毫秒) final double borderRadius; //图片圆角 final ValueChanged? onTap; //点击某页,传 index final ValueChanged? onIndexChanged; //切换页,传 index // ===== 指示器 ===== final IndicatorAlign indicatorAlign; final IndicatorStyle indicatorStyle; final Color? color; //未选中色,不传按位置/样式取默认 final Color? selectColor; //选中色,不传按位置/样式取默认 final AdExposureModel? exposureModel; //曝光埋点模型 const AdsBanner( this.models, { super.key, this.width, this.height, this.autoPlayMs = 5000, this.borderRadius = 12, this.onTap, this.onIndexChanged, this.indicatorAlign = IndicatorAlign.bottomRight, this.indicatorStyle = IndicatorStyle.bar, this.color, this.selectColor, this.exposureModel, }); @override State createState() => _AdsBannerState(); } class _AdsBannerState extends State { //当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper) final _index = ValueNotifier(0); int get _count => widget.models?.length ?? 0; @override void initState() { super.initState(); //adType 兜底设一次(不放 itemBuilder 里,避免每次构建都 mutate) widget.exposureModel?.adType ??= AdType.bigBanner; } @override void dispose() { _index.dispose(); super.dispose(); } @override Widget build(BuildContext context) { //指示器放轮播下方:轮播固定高 256(唯一调用方不传 height,靠它撑开 Swiper) if (widget.indicatorAlign == IndicatorAlign.underCenter) { return SizedBox( width: widget.width, height: widget.height, child: Column( children: [ SizedBox(height: 256, child: _swiper()), 12.sizeBoxH, if (_count > 1) Center(child: _indicator(space: 4, dotSize: 4, selectWidth: 8, color: widget.color, selectColor: widget.selectColor)), ], ), ); } return SizedBox( width: widget.width, height: widget.height, child: Stack( clipBehavior: Clip.none, children: [ _swiper(), //指示器:只有一张时不出 if (_count > 1) widget.indicatorAlign == IndicatorAlign.bottomCenter ? Positioned( bottom: 10, left: 0, right: 0, child: Center( child: _indicator( space: 2, dotSize: 2, selectWidth: 5, //视频广告 cell:主题黄(未选中半透明,选中实心) color: widget.color ?? const Color(0xffFFD900).withValues(alpha: 0.4), selectColor: widget.selectColor ?? const Color(0xffFFD900), ), ), ) : Positioned( bottom: 7, right: 7, child: _indicator(space: 4, dotSize: 4, selectWidth: 8, color: widget.color, selectColor: widget.selectColor), ), ], ), ); } //轮播主体;两种指示器布局共用 Widget _swiper() { return Swiper( autoplay: _count > 1, autoplayDelay: widget.autoPlayMs, loop: _count > 1, itemBuilder: (_, index) => AdsItem( adInfo: widget.models![index], showType: AdShowType.img, borderRadius: widget.borderRadius, exposureModel: widget.exposureModel, ), onTap: widget.onTap, onIndexChanged: (index) { _index.value = index; widget.onIndexChanged?.call(index); }, itemCount: _count, ); } //指示器随当前页局部刷新 Widget _indicator({ required double space, required double dotSize, required double selectWidth, Color? color, Color? selectColor, }) { return ValueListenableBuilder( valueListenable: _index, builder: (_, index, __) => DotIndicator( count: _count, index: index, space: space, dotSize: dotSize, selectWidth: selectWidth, style: widget.indicatorStyle, color: color, selectColor: selectColor, ), ); } } /// 指示器样式 enum IndicatorStyle { bar, //选中项拉成长条,默认红色系 dot, //全是圆点,默认白色系 } /// 分页指示器:选中项宽度与颜色平滑过渡(类似 Android 系统页面指示器) class DotIndicator extends StatelessWidget { final int count; //圆点总数 final int index; //当前选中页 final double space; //每个点左侧的间距 final double dotSize; //圆点直径 final double selectWidth; //选中长条宽度,仅 bar 样式生效 final IndicatorStyle style; final Color? color; //未选中色,不传按样式取默认 final Color? selectColor; //选中色,不传按样式取默认 const DotIndicator({ super.key, required this.count, required this.index, this.space = 5, this.dotSize = 6, this.selectWidth = 12, this.style = IndicatorStyle.bar, this.color, this.selectColor, }); @override Widget build(BuildContext context) { final isBar = style == IndicatorStyle.bar; final activeColor = selectColor ?? (isBar ? const Color(0xffE1351F) : Colors.white); final inactiveColor = color ?? (isBar ? const Color(0xffE1351F).withValues(alpha: 0.3) : Colors.white.withValues(alpha: 0.5)); return Row( mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < count; i++) Padding( padding: EdgeInsets.only(left: space), child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, width: i == index && isBar ? selectWidth : dotSize, height: dotSize, decoration: BoxDecoration( borderRadius: BorderRadius.circular(dotSize / 2), color: i == index ? activeColor : inactiveColor, ), ), ), ], ); } } /// 图片轮播:左右滑动浏览,点图看大图,多张时右下角出指示器 class ImageSwiper extends StatefulWidget { final List? imgs; final double aspectRatio; const ImageSwiper({super.key, this.imgs, required this.aspectRatio}); @override State createState() => _ImageSwiperState(); } class _ImageSwiperState extends State { //当前页用 ValueNotifier 局部刷新指示器,避免每次滑动都 setState 重建 Swiper final _index = ValueNotifier(0); int get _count => widget.imgs?.length ?? 0; @override void dispose() { _index.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: (_, index) => InkWell( enableFeedback: false, onTap: () => ImageBrowserPage.open(widget.imgs ?? [], index: index), child: NetworkImageLoader(imageUrl: widget.imgs![index]), ), onIndexChanged: (index) => _index.value = index, itemCount: _count, ), if (_count > 1) Positioned( bottom: 10, right: 12, child: ValueListenableBuilder( valueListenable: _index, builder: (_, index, __) => DotIndicator(count: _count, index: index, space: 6), ), ), ], ), ); } } /// AI 图生视频 banner:每页左原图、右生成图,多张时自动轮播 class AiBanner extends StatefulWidget { final List? models; const AiBanner({super.key, this.models}); @override State createState() => _AiBannerState(); } class _AiBannerState extends State { //当前页用 ValueNotifier 局部刷新指示器,避免滑动重建 Swiper final _index = ValueNotifier(0); int get _count => widget.models?.length ?? 0; @override void dispose() { _index.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Stack( children: [ Swiper( autoplay: _count > 1, loop: _count > 1, autoplayDelay: 5000, itemBuilder: (_, 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) => _index.value = index, itemCount: _count, ), if (_count > 1) Positioned( bottom: 10, right: 10, child: ValueListenableBuilder( valueListenable: _index, builder: (_, index, __) => DotIndicator(count: _count, index: index), ), ), ], ); } }