350 lines
10 KiB
Dart
350 lines
10 KiB
Dart
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),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|