ads_state.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:fluttertoast/fluttertoast.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:provider/provider.dart';
  6. import 'package:puzzleweave/ads/applovin_ads_controller.dart';
  7. import 'package:puzzleweave/l10n/app_localizations.dart';
  8. import 'package:puzzleweave/models/data.dart';
  9. import 'package:puzzleweave/persistence/persistence.dart';
  10. import 'package:puzzleweave/skin/skin.dart';
  11. import '../remote_config/remote_config.dart';
  12. import 'ad_helper.dart';
  13. final Logger _log = Logger('AdsState');
  14. /// Add by guoziyi
  15. /// 自带广告的 StatefulWidget State
  16. /// 从该类继承的 State 自动带有广告功能
  17. abstract class AdsState<T extends StatefulWidget> extends State<T> {
  18. ///这个值用于数据统计,子类应该在initState初始化的时候正确设置skuId
  19. String skuId = "";
  20. VoidCallback? onEnterBackground; // app 进入后台回调, 外部如关心, 可设置此回调
  21. Function(Duration duration)? onEnterForeground; // app 回到前台回调, 外部如关心, 可设置此回调
  22. Function(AdState state)? onRewardAdState; // reward广告状态回调,外部如关心,可设置此回调
  23. Function(AdState state)? onInterstitialAdState; // reward广告状态回调,外部如关心,可设置此回调
  24. late Data data;
  25. late ApplovinAdsController _applovinAdsController;
  26. late ValueNotifier<AppLifecycleState> lifecycleNotifier;
  27. Completer<bool>? _intersCompleter;
  28. Completer<bool>? _rewardCompleter;
  29. bool earnedReward = false; //是否确定获得激励
  30. //插屏广告是否准备好
  31. late ValueNotifier<bool> intersReadyNotifier;
  32. bool get isIntersReady => intersReadyNotifier.value;
  33. //激励广告是否准备好
  34. late ValueNotifier<bool> rewardReadyNotifier;
  35. bool get isRewardReady => rewardReadyNotifier.value;
  36. //插屏广告当前状态
  37. AdState get intersState => _applovinAdsController.interstitialAdState.value;
  38. //激励广告当前状态
  39. AdState get rewardState => _applovinAdsController.rewardedAdState.value;
  40. // 新增:用于控制 Banner 实际显示的变量
  41. bool _isBannerVisible = false;
  42. bool get isBannerVisible => _isBannerVisible;
  43. @override
  44. void initState() {
  45. super.initState();
  46. data = context.read<Data>();
  47. // 1. 监听 ValueNotifier 的变化
  48. // 当 completedWorks.value 被重新赋值时(见 data.dart 的 workDone 方法),触发刷新
  49. data.completedWorks.addListener(_onLevelChanged);
  50. _applovinAdsController = context.read<ApplovinAdsController>();
  51. _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
  52. _applovinAdsController.interstitialAdState.addListener(_onInterstitialAdState);
  53. _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
  54. _applovinAdsController.rewardedAdState.addListener(_onRewardAdState);
  55. intersReadyNotifier = ValueNotifier(intersState == AdState.ready);
  56. rewardReadyNotifier = ValueNotifier(rewardState == AdState.ready);
  57. lifecycleNotifier = context.read<ValueNotifier<AppLifecycleState>>();
  58. lifecycleNotifier.removeListener(_handleAppLifecycle);
  59. lifecycleNotifier.addListener(_handleAppLifecycle);
  60. _applovinAdsController.loadInterstitialAd();
  61. _applovinAdsController.loadRewardedAd();
  62. // 初始检查一次 Banner
  63. _updateBannerVisibility();
  64. }
  65. // 关卡变化的回调
  66. void _onLevelChanged() {
  67. _log.info("Level changed detected via ValueNotifier, updating banner...");
  68. _updateBannerVisibility();
  69. }
  70. // 核心改进:主动更新 Banner 状态
  71. Future<void> _updateBannerVisibility() async {
  72. if (!mounted) return;
  73. // 1. 确保 SDK 已初始化
  74. bool ready = await _applovinAdsController.completer.future;
  75. // 2. 获取当前关卡数
  76. int doneLevels = data.currentLevel;
  77. // 3. 综合判断:SDK就绪 + 逻辑允许 + 应用在前台
  78. bool shouldShow = ready && shouldShowBannerAd(doneLevels) && lifecycleNotifier.value == AppLifecycleState.resumed;
  79. if (_isBannerVisible != shouldShow) {
  80. setState(() {
  81. _isBannerVisible = shouldShow;
  82. });
  83. _log.info("Banner visibility updated: $_isBannerVisible");
  84. }
  85. }
  86. @override
  87. void dispose() {
  88. data.completedWorks.removeListener(_onLevelChanged);
  89. intersReadyNotifier.dispose();
  90. rewardReadyNotifier.dispose();
  91. lifecycleNotifier.removeListener(_handleAppLifecycle);
  92. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  93. _intersCompleter!.complete(false);
  94. }
  95. _intersCompleter = null;
  96. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  97. _rewardCompleter!.complete(false);
  98. }
  99. _rewardCompleter = null;
  100. _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
  101. _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
  102. super.dispose();
  103. }
  104. /// 广告 sdk 是否初始化完成
  105. Future<bool> adSDKReady() async {
  106. return _applovinAdsController.completer.future;
  107. }
  108. /// 检查是否应该展示banner广告
  109. bool shouldShowBannerAd(int doneLevels) {
  110. if (doneLevels == 0) {
  111. _log.info("首关不展示banner广告");
  112. return false; // 首关是引导关卡,一定不要显示广告
  113. }
  114. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  115. DateTime now = DateTime.now();
  116. int bannerFreeDuration = RemoteConfig().bannerFreeDuration;
  117. DateTime firstRunTime = Persistence().firstRunTime;
  118. bool timeShould = now.difference(firstRunTime).inMinutes > bannerFreeDuration;
  119. return levelShould || timeShould;
  120. }
  121. /// banner 广告
  122. Widget getBanner(String positionKey) {
  123. return _applovinAdsController.getBannerWidget(positionKey);
  124. }
  125. ///显示激励广告
  126. ///返回奖励获取结果。true:成功获取到奖励;false;未获得奖励
  127. Future<bool> showRewardAd(String src, String skuId) async {
  128. if (!(await _applovinAdsController.isRewardedAdReady())) {
  129. _applovinAdsController.loadRewardedAd();
  130. if (mounted) {
  131. Fluttertoast.showToast(
  132. msg: AppLocalizations.of(context)!.adNotReady,
  133. toastLength: Toast.LENGTH_SHORT,
  134. gravity: ToastGravity.CENTER,
  135. timeInSecForIosWeb: 1,
  136. backgroundColor: SkinHelper.slotBorderColor,
  137. textColor: Colors.white,
  138. fontSize: 16.0,
  139. );
  140. }
  141. return false;
  142. }
  143. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  144. _rewardCompleter!.complete(earnedReward);
  145. }
  146. _rewardCompleter = Completer<bool>();
  147. this.skuId = skuId;
  148. earnedReward = false;
  149. _applovinAdsController.showRewardedlAd(
  150. adSrc: src,
  151. skuId: skuId,
  152. onUserEarnedReward: (ad, reward) {
  153. _log.info("................earn reward..........");
  154. earnedReward = true;
  155. },
  156. );
  157. return _rewardCompleter!.future;
  158. }
  159. bool shouldShowInterstitialAd(String src, int doneLevels) {
  160. bool shouldShowAd = false;
  161. if (src == 'level_enter') {
  162. shouldShowAd = _shouldShowEnterIntersAd(doneLevels);
  163. } else {
  164. shouldShowAd = _shouldShowExitIntersAd(doneLevels);
  165. }
  166. return shouldShowAd;
  167. }
  168. ///显示插屏广告
  169. ///返回广告播放结果。true:播放成功;false;播放失败
  170. Future<bool> showInterstitialAd(String src, String skuId, int doneLevels) async {
  171. bool shouldShowAd = shouldShowInterstitialAd(src, doneLevels);
  172. if (!shouldShowAd) return false;
  173. // 插屏广告没有预备好或者当前不符合播放插屏广告条件,直接返回false
  174. if (!(await _applovinAdsController.isInterstitialAdReady())) {
  175. _applovinAdsController.loadInterstitialAd();
  176. return false;
  177. }
  178. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  179. _intersCompleter!.complete(false);
  180. }
  181. _intersCompleter = Completer<bool>();
  182. this.skuId = skuId;
  183. _applovinAdsController.showInterstitialAd(adSrc: src, skuId: skuId);
  184. return _intersCompleter!.future;
  185. }
  186. // 检查是否应该展示进入插屏广告
  187. bool _shouldShowEnterIntersAd(int doneLevels) {
  188. if (doneLevels == 0) return false; // 首关是引导关卡,一定不要显示广告
  189. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  190. DateTime now = DateTime.now();
  191. int enterIntersDuration = RemoteConfig().enterIntersFreeDuration; //新手保护时长,单位:分钟
  192. DateTime firstRunTime = Persistence().firstRunTime;
  193. bool timeShould = now.difference(firstRunTime).inMinutes > enterIntersDuration; //判断当前时间是否已超过新手保护时长
  194. return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏
  195. }
  196. // 检查是否应该展示退出插屏广告
  197. bool _shouldShowExitIntersAd(int doneLevels) {
  198. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  199. DateTime now = DateTime.now();
  200. int quitIntersDuration = RemoteConfig().quitIntersFreeDuration; //新手保护时长,单位:分钟
  201. DateTime firstRunTime = Persistence().firstRunTime;
  202. bool timeShould = now.difference(firstRunTime).inMinutes > quitIntersDuration;
  203. return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏
  204. }
  205. // 插屏广告回调处理
  206. _onInterstitialAdState() {
  207. if (intersState == AdState.dismissed) {
  208. _log.info("onInterstitialAdState AdState.dismissed");
  209. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  210. _intersCompleter!.complete(true);
  211. }
  212. _intersCompleter = null;
  213. }
  214. intersReadyNotifier.value = (intersState == AdState.ready);
  215. onInterstitialAdState?.call(intersState);
  216. }
  217. // 激励广告回调处理
  218. _onRewardAdState() {
  219. if (_applovinAdsController.rewardedAdState.value == AdState.dismissed) {
  220. _log.info("onRewardAdState AdState.dismissed");
  221. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  222. _rewardCompleter!.complete(earnedReward);
  223. }
  224. _rewardCompleter = null;
  225. }
  226. rewardReadyNotifier.value = (rewardState == AdState.ready);
  227. onRewardAdState?.call(rewardState);
  228. }
  229. ///-----------[Lifecycle]-------------
  230. ///需要处理可重写状态处理函数
  231. AppLifecycleState lifeState = AppLifecycleState.resumed;
  232. bool get isActive => lifeState == AppLifecycleState.resumed;
  233. void _handleAppLifecycle() {
  234. _log.info('AppLifecycleState changed: ${lifecycleNotifier.value}');
  235. lifeState = lifecycleNotifier.value;
  236. // 当回到前台时,重新检查 Banner;切到后台时,理论上 MaxAdView 会自动处理,
  237. // 但我们可以通过 setState 强制隐藏它来确保安全。
  238. _updateBannerVisibility();
  239. if (lifeState == AppLifecycleState.inactive) {
  240. //前台可见,但是无法交互
  241. onInactive();
  242. } else if (lifeState == AppLifecycleState.paused) {
  243. //前台不可见
  244. onPause();
  245. } else if (lifeState == AppLifecycleState.resumed) {
  246. //前台可见,可交互
  247. onResume();
  248. }
  249. }
  250. void onInactive() {}
  251. DateTime? _leaveTime;
  252. void onPause() {
  253. if (intersState == AdState.showing || rewardState == AdState.showing) {
  254. return;
  255. }
  256. _leaveTime = DateTime.now(); // 记录切换到后台的时间
  257. onEnterBackground?.call();
  258. _log.info("App enter background, leaveTime: $_leaveTime");
  259. }
  260. void onResume() {
  261. if (_leaveTime != null) {
  262. var duration = DateTime.now().difference(_leaveTime!);
  263. _log.info("App enter foreground, leaveTime: $_leaveTime, duration=$duration");
  264. onEnterForeground?.call(duration);
  265. }
  266. _leaveTime = null;
  267. }
  268. }