初始化
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
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.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<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(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_infinite_marquee/flutter_infinite_marquee.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/ad_manager.dart';
|
||||
|
||||
import '../../hj_model/splash/ads_model.dart';
|
||||
import 'ads_banner_widget.dart';
|
||||
import 'ads_item.dart';
|
||||
|
||||
enum AdStyle {
|
||||
normal, //默认为10宫格
|
||||
banner, //大banner
|
||||
oneScroll, //单排左右手动滚动
|
||||
oneAutoScroll, //单排左右自动滚动
|
||||
twoAutoScroll, //双排展示,上排固定,下排自动滚动
|
||||
oneScrollBig, //一排滚动,大样式
|
||||
}
|
||||
|
||||
/// 广告位通用容器:按 [AdStyle] 切换九宫格/banner/单双排滚动等展示形态
|
||||
class AdsGridViewWidget extends StatefulWidget {
|
||||
final int position; // 广告位标识,adsArr 为空时按此拉本地缓存广告
|
||||
final AdStyle? style; // 写死的展示样式(accordingAdsType=false 时生效)
|
||||
final double? aspectRatio; // banner 样式的宽高比
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final List<AdsInfoModel>? adsArr; // 外部直接传入的广告数据,优先于 position
|
||||
//是否根据后台广告coverImgSize判断,
|
||||
//false,写死类型不根据广告返回字段判断,比如直播广告,左右手动滑动等
|
||||
//yes,根据position数组的第一个广告类型判断
|
||||
final bool accordingAdsType;
|
||||
|
||||
AdsGridViewWidget(
|
||||
this.position, {
|
||||
super.key,
|
||||
this.style = AdStyle.normal,
|
||||
this.padding,
|
||||
this.adsArr,
|
||||
this.aspectRatio,
|
||||
this.accordingAdsType = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdsGridViewWidget> createState() => _AdsGridViewWidgetState();
|
||||
}
|
||||
|
||||
class _AdsGridViewWidgetState extends State<AdsGridViewWidget> {
|
||||
// 优先用外部传入的 adsArr,没有则按 position 取本地缓存广告
|
||||
// 缓存一次,避免一次 build / 自动滚动 itemBuilder 反复全表 filter
|
||||
List<AdsInfoModel>? _adsCache;
|
||||
List<AdsInfoModel> get adsList =>
|
||||
_adsCache ??= (widget.adsArr ?? AdManager().adsByType(widget.position));
|
||||
bool get accordingAdsType => widget.accordingAdsType;
|
||||
EdgeInsetsGeometry? get padding => widget.padding;
|
||||
double? get aspectRatio => widget.aspectRatio;
|
||||
|
||||
final double itemScale = 60 / 82; // 广告项宽高比(普通样式通用)
|
||||
final double horCount = 5; // 横向一屏显示个数(普通样式通用)
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_adsCache = null; // 每次 build 重新取一次,build 内(含滚动 itemBuilder)复用
|
||||
// 无数据或 ab 测未命中广告,直接占位不渲染
|
||||
if (adsList.isEmpty || !AdManager().showAbTestAd) return const SizedBox();
|
||||
// accordingAdsType=true 时按后台返回的广告类型决定样式,否则用写死的 style
|
||||
final adStyle = accordingAdsType
|
||||
? (adsList.firstOrNull?.adStyle ?? AdStyle.twoAutoScroll)
|
||||
: widget.style;
|
||||
switch (adStyle) {
|
||||
case AdStyle.normal:
|
||||
return _buildNormalStyle();
|
||||
case AdStyle.banner:
|
||||
return _buildBannerStyle();
|
||||
case AdStyle.oneScroll:
|
||||
return _buildOneScrollStyle();
|
||||
case AdStyle.oneAutoScroll:
|
||||
return _buildOneAutoScrollStyle();
|
||||
case AdStyle.twoAutoScroll:
|
||||
return _buildTwoAutoScrollStyle();
|
||||
case AdStyle.oneScrollBig:
|
||||
return _buildOneScrollBigStyle();
|
||||
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
}
|
||||
|
||||
// 统一构造广告项
|
||||
Widget _adsItem(AdsInfoModel model, {double? aspectRatio}) {
|
||||
return AdsItem(adInfo: model, aspectRatio: aspectRatio);
|
||||
}
|
||||
|
||||
//两排共10个
|
||||
Widget _buildNormalStyle() {
|
||||
return GridView.builder(
|
||||
padding: padding,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: itemScale,
|
||||
),
|
||||
itemCount: min(10, adsList.length),
|
||||
itemBuilder: (context, index) => _adsItem(adsList[index]),
|
||||
);
|
||||
}
|
||||
|
||||
// 大 banner 样式:单张轮播
|
||||
Widget _buildBannerStyle() {
|
||||
return Container(
|
||||
padding: padding,
|
||||
alignment: Alignment.center,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio ?? 720 / 150,
|
||||
child: AdsBannerWidget(
|
||||
adsList.length > 20 ? adsList.take(20).toList() : adsList,
|
||||
autoPlayMs: 2000,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//单排手动滚动
|
||||
Widget _buildOneScrollStyle() {
|
||||
return Container(
|
||||
margin: padding,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final width = (constraints.maxWidth - (horCount - 1) * 10) / horCount;
|
||||
final height = width / itemScale;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: adsList.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (context, index) => 10.sizeBoxW,
|
||||
itemBuilder: (context, index) => SizedBox(
|
||||
width: width,
|
||||
child: _adsItem(adsList[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//单排自动滚动
|
||||
Widget _buildOneAutoScrollStyle() {
|
||||
if (adsList.length <= 5) {
|
||||
return Container(
|
||||
padding: padding,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final width =
|
||||
(constraints.maxWidth - (horCount - 1) * 10) / horCount;
|
||||
final height = width / itemScale;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: adsList.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
separatorBuilder: (context, index) => 10.sizeBoxW,
|
||||
itemBuilder: (context, index) => SizedBox(
|
||||
width: width,
|
||||
child: _adsItem(adsList[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
double itemGap = 12;
|
||||
final itemWidth =
|
||||
(constraints.maxWidth - (horCount - 1) * itemGap - 32) /
|
||||
horCount;
|
||||
final height = itemWidth / itemScale;
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: InfiniteMarquee(
|
||||
frequency: const Duration(milliseconds: 20),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
AdsInfoModel model = adsList[index % adsList.length];
|
||||
return Container(
|
||||
width: itemWidth,
|
||||
margin: const EdgeInsets.only(right: 7),
|
||||
child: _adsItem(model),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
//双排展示,上排固定,下排自动滚动
|
||||
Widget _buildTwoAutoScrollStyle() {
|
||||
double hPadding = 16;
|
||||
if (adsList.length < 11) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.fromLTRB(hPadding, 12, hPadding, 12),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 60 / 82,
|
||||
),
|
||||
itemCount: adsList.length,
|
||||
itemBuilder: (context, index) => _adsItem(adsList[index]),
|
||||
);
|
||||
} else {
|
||||
final headList = adsList.take(5).toList(); //头数组
|
||||
final footList = adsList.sublist(5); //尾部数组
|
||||
//拆分数组
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final itemWidth = ((constraints.maxWidth - 40 - hPadding * 2) / 5);
|
||||
return Column(
|
||||
children: [
|
||||
GridView.builder(
|
||||
padding: EdgeInsets.fromLTRB(hPadding, 12, hPadding, 0),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 5,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 60 / 82,
|
||||
),
|
||||
itemCount: headList.length,
|
||||
itemBuilder: (context, index) => _adsItem(headList[index]),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
SizedBox(
|
||||
height: itemWidth / itemScale,
|
||||
child: InfiniteMarquee(
|
||||
frequency: const Duration(milliseconds: 20),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
AdsInfoModel model = footList[index % footList.length];
|
||||
return Container(
|
||||
width: itemWidth,
|
||||
margin: const EdgeInsets.only(right: 7),
|
||||
child: _adsItem(model),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//自动滚动的大图广告,最多显示20个
|
||||
Widget _buildOneScrollBigStyle() {
|
||||
// 大图样式:宽高比和一屏个数(3.2 个露出下一张引导滑动)
|
||||
const double itemScale = 104 / 176;
|
||||
const double horCount = 3.2;
|
||||
double itemMargin = 7;
|
||||
double hPadding = 16;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final dataSource =
|
||||
adsList.length > 20 ? adsList.take(20).toList() : adsList;
|
||||
final itemWidth =
|
||||
((constraints.maxWidth - itemMargin * 2 - hPadding * 2) / horCount);
|
||||
return Container(
|
||||
margin: padding,
|
||||
height: itemWidth / itemScale,
|
||||
child: InfiniteMarquee(
|
||||
frequency: const Duration(milliseconds: 20),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
AdsInfoModel model = dataSource[index % dataSource.length];
|
||||
return Container(
|
||||
width: itemWidth,
|
||||
margin: EdgeInsets.only(right: itemMargin),
|
||||
child: _adsItem(model, aspectRatio: 104 / 152),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
//广告item
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../hj_model/splash/ads_model.dart';
|
||||
import '../../routers/jump_router.dart';
|
||||
import '../widget/net_image_widget.dart';
|
||||
|
||||
enum AdShowType {
|
||||
img, // 纯图片
|
||||
hor, // 水平:左图标题 + 右下载按钮
|
||||
vImgText, // 图文:上图下文
|
||||
splash, // 启动全屏图
|
||||
}
|
||||
|
||||
//所有广告判断
|
||||
class AdsItem extends StatefulWidget {
|
||||
final AdsInfoModel adInfo;
|
||||
final double? aspectRatio;
|
||||
final AdShowType? showType;
|
||||
final double borderRadius;
|
||||
final int clickType; //0:应用 1:广告
|
||||
final Widget? child; //自定义子视图(IM 广告用)
|
||||
final VoidCallback? onTap; //点击附加回调(IM 广告已读上报用)
|
||||
|
||||
const AdsItem({
|
||||
super.key,
|
||||
required this.adInfo,
|
||||
this.aspectRatio,
|
||||
this.showType,
|
||||
this.borderRadius = 12,
|
||||
this.clickType = 1,
|
||||
this.child,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AdsItem> createState() => _AdsItemState();
|
||||
}
|
||||
|
||||
class _AdsItemState extends State<AdsItem> {
|
||||
AdsInfoModel get adInfo => widget.adInfo;
|
||||
|
||||
void _onTap() {
|
||||
pushToPageByLink(adInfo.href);
|
||||
CommonService.adsClick(widget.clickType);
|
||||
widget.onTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _onTap,
|
||||
child: widget.child ??
|
||||
switch (widget.showType) {
|
||||
AdShowType.img => _buildImageStyle(),
|
||||
AdShowType.hor => _buildHorStyle(),
|
||||
AdShowType.vImgText => _buildImageTextStyle(),
|
||||
AdShowType.splash => _buildSplash(),
|
||||
_ => _buildNormalStyle(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 启动全屏图:封面铺满 + 兜底背景图
|
||||
Widget _buildSplash() {
|
||||
return NetworkImageLoader(
|
||||
imageUrl: adInfo.cover ?? '',
|
||||
width: Get.width,
|
||||
height: Get.height,
|
||||
fit: BoxFit.cover,
|
||||
placeHolderWidget: Image.asset(
|
||||
'ic_splash_bg.webp'.launchPath,
|
||||
fit: BoxFit.cover,
|
||||
width: Get.width,
|
||||
height: Get.height,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 默认样式:上图下标题(九宫格项)
|
||||
Widget _buildNormalStyle() {
|
||||
return Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: widget.aspectRatio ?? 1,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: adInfo.cover ?? '', borderRadius: 12),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
adInfo.title?.trim() ?? '',
|
||||
maxLines: 1,
|
||||
style: const TextStyle(color: Color(0xffFAFAFA), fontSize: 11),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 图文样式:上图下文
|
||||
Widget _buildImageTextStyle() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 72 / 20,
|
||||
child: NetworkImageLoader(
|
||||
borderRadius: 4,
|
||||
imageUrl: adInfo.cover ?? '',
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
10.sizeBoxH,
|
||||
Text(
|
||||
adInfo.title ?? '',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffdddfdf),
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 纯图样式:可选宽高比
|
||||
Widget _buildImageStyle() {
|
||||
final image = NetworkImageLoader(
|
||||
imageUrl: adInfo.cover ?? '',
|
||||
borderRadius: widget.borderRadius,
|
||||
fit: BoxFit.fill,
|
||||
);
|
||||
if (widget.aspectRatio == null) return image;
|
||||
return AspectRatio(aspectRatio: widget.aspectRatio!, child: image);
|
||||
}
|
||||
|
||||
// 水平样式:左图+标题,右「立即下载」按钮
|
||||
Widget _buildHorStyle() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: adInfo.cover ?? '',
|
||||
borderRadius: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
Text(
|
||||
adInfo.title ?? '',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 72,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff3476FF),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'立即下载',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user