Files
huangguo_android/lib/tools_base/banner/ads_banner_widget.dart
T
2026-09-15 15:44:13 +07:00

365 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AdsInfoModel>? models; // 广告数据列表
final double? width; // 整体宽度
final double? height; // 整体高度
final ValueChanged<int>? onItemClick; // 点击某页回调(传 index
final ValueChanged<int>? onIndexChanged; // 切换页回调(传 index
final int? autoPlayMs; // 自动轮播间隔(毫秒),默认 5000
final bool isIndicatorBottomCenter; // 指示器置于底部居中
final bool isIndicatorUnderCenter; // 指示器置于轮播下方居中(Column 布局)
final Color? color; // 指示器未选中色
final Color? selectColor; // 指示器选中色
final bool isCircle; // 透传给 CIndicator.isBarStyletrue=长条红色风格)
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<AdsBannerWidget> createState() => _AdsBannerWidgetState();
}
class _AdsBannerWidgetState extends State<AdsBannerWidget> {
// 当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper)
final ValueNotifier<int> _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<int>(
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<String>? 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<ImageCollectionWidget> createState() => _ImageCollectionWidgetState();
}
class _ImageCollectionWidgetState extends State<ImageCollectionWidget> {
// 当前页用 ValueNotifier 局部刷新页码/指示器,避免每次滑动都 setState 重建 Swiper
final ValueNotifier<int> _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<int>(
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<int>(
valueListenable: _selectIndex,
builder: (_, index, __) =>
CIndicator(itemCount: _count, selectIndex: index, space: 6),
),
),
],
),
);
}
}
/// AI 图生视频 banner:每页左原图、右生成图,自动轮播
class AIBannerWidget extends StatefulWidget {
final int index; // 初始选中页
final List<AdsInfoModel>? models;
const AIBannerWidget({super.key, this.index = 0, this.models});
@override
State<AIBannerWidget> createState() => _AIBannerWidgetState();
}
class _AIBannerWidgetState extends State<AIBannerWidget> {
// 当前页用 ValueNotifier 局部刷新指示器,避免滑动重建 Swiper
late final ValueNotifier<int> _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<int>(
valueListenable: _selectIndex,
builder: (_, index, __) =>
CIndicator(itemCount: _count, selectIndex: index),
)
: const SizedBox.shrink(),
),
]);
}
}