Files
2026-10-14 11:23:36 +07:00

350 lines
10 KiB
Dart
Raw Permalink 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:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/widget/card_swiper/src/swiper.dart';
import 'package:wydj/tools_base/widget/image_browser_page.dart';
import 'package:wydj/tools_base/widget/net_image_widget.dart';
import '../../hj_model/splash/ads_model.dart';
import 'ads_item.dart';
/// 广告轮播的指示器位置
enum IndicatorAlign {
bottomRight, //右下角(默认)
bottomCenter, //底部居中,小号主题黄点(视频广告 cell 用)
underCenter, //轮播下方居中(Column 布局,轮播固定高 256)
}
/// 广告轮播:多张时自动轮播 + 指示器
class AdsBanner extends StatefulWidget {
final List<AdsInfoModel>? models; //广告数据
final double? width;
final double? height;
final int autoPlayMs; //自动轮播间隔(毫秒)
final double borderRadius; //图片圆角
final ValueChanged<int>? onTap; //点击某页,传 index
final ValueChanged<int>? onIndexChanged; //切换页,传 index
// ===== 指示器 =====
final IndicatorAlign indicatorAlign;
final IndicatorStyle indicatorStyle;
final Color? color; //未选中色,不传按位置/样式取默认
final Color? selectColor; //选中色,不传按位置/样式取默认
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,
});
@override
State<AdsBanner> createState() => _AdsBannerState();
}
class _AdsBannerState extends State<AdsBanner> {
//当前页随轮播变化,用 ValueNotifier 局部刷新指示器,避免每次都 setState 重建整棵树(含 Swiper)
final _index = ValueNotifier(0);
int get _count => widget.models?.length ?? 0;
@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,
),
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<int>(
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<String>? imgs;
final double aspectRatio;
const ImageSwiper({super.key, this.imgs, required this.aspectRatio});
@override
State<ImageSwiper> createState() => _ImageSwiperState();
}
class _ImageSwiperState extends State<ImageSwiper> {
//当前页用 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<int>(
valueListenable: _index,
builder: (_, index, __) =>
DotIndicator(count: _count, index: index, space: 6),
),
),
],
),
);
}
}
/// AI 图生视频 banner:每页左原图、右生成图,多张时自动轮播
class AiBanner extends StatefulWidget {
final List<AdsInfoModel>? models;
const AiBanner({super.key, this.models});
@override
State<AiBanner> createState() => _AiBannerState();
}
class _AiBannerState extends State<AiBanner> {
//当前页用 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<int>(
valueListenable: _index,
builder: (_, index, __) =>
DotIndicator(count: _count, index: index),
),
),
],
);
}
}