Space Ad Mobile 9.5.1
Space Ad Mobile makes ads easy to add into your mobile game: one unified workflow for LevelPlay, AdMob, Yandex.Ads across banners, interstitials and rewarded.
Loading...
Searching...
No Matches
BannerToolkitOffsetHelper.cs
1using System.Collections.Generic;
2using System.Linq;
3using System.Runtime.CompilerServices;
4using UnityEngine;
5using UnityEngine.UIElements;
6
8{
15 {
17 [Tooltip("Applies banner offset to the target element margin.")]
18 Margin = 0,
19
21 [Tooltip("Applies banner offset to the target element padding.")]
23 }
24
30 [AddComponentMenu("SCG/SpaceAdMobile/" + nameof(BannerToolkitOffsetHelper))]
31 public class BannerToolkitOffsetHelper : MonoBehaviour
32 {
33 #region Constants
34
35 private const string SubRootName = "sub-root";
36 private const int MillisecondsPerSecond = 1000;
37 private const int PanelReadyRefreshDelayMs = 50;
38 private const int MinOffsetApplyAttempts = 1;
39 private const int MaxOffsetApplyAttempts = 10;
40 private const float MinApplyOffsetDelaySeconds = 0f;
41 private const float MaxApplyOffsetDelaySeconds = 1f;
42
43#if UNITY_6000_4_OR_NEWER
44 private static readonly Dictionary<int, Dictionary<BannerAnchor, EntityId>> sideLocksByContainer = new();
45#else
46 private static readonly Dictionary<int, Dictionary<BannerAnchor, int>> sideLocksByContainer = new();
47#endif
48
49 #endregion
50
51 #region Serialized fields
52
54 [Tooltip("UIDocument that hosts UI Toolkit content. If null, it is searched on this GameObject.")]
55#if UNITY_EDITOR && UNITY_6000_5_OR_NEWER
56 [Editor.ShowIf(nameof(HasPanelRenderer), invert: true)]
57#endif
58 [SerializeField] private UIDocument uiDocument;
59
60#if UNITY_6000_5_OR_NEWER
62 [Tooltip("PanelRenderer that hosts UI Toolkit content when no UIDocument is available.")]
63#if UNITY_EDITOR
64 [Editor.ShowIf(nameof(HasUIDocument), invert: true)]
65#endif
66 [SerializeField] private PanelRenderer panelRenderer;
67#endif
68
70 [Tooltip("Banner ad component implementing IBannerAd.")]
71 [SerializeField] private AdUnitBehaviourBase bannerAdComponent;
72
74 [Tooltip("Optional banner ad controller that keeps a Banner reference available, including when the Banner persists between scenes.")]
75 [SerializeField] private AdControllerBase bannerAdController;
76
78 [Tooltip("Optional name of the target container in the visual tree.")]
79 [SerializeField] private string explicitContainerName;
80
82 [Tooltip("Controls whether banner-side offsets are written as margin or padding.")]
83 [SerializeField] private BannerToolkitLayoutOffsetMode layoutOffsetMode = BannerToolkitLayoutOffsetMode.Margin;
84
86 [Tooltip("Delay in seconds before applying offset after a visible banner refresh.")]
87 [Range(MinApplyOffsetDelaySeconds, MaxApplyOffsetDelaySeconds)]
88 [SerializeField] private float applyOffsetDelaySeconds = MinApplyOffsetDelaySeconds;
89
91 [Tooltip("Number of offset applications performed after a banner becomes visible.")]
92 [Range(MinOffsetApplyAttempts, MaxOffsetApplyAttempts)]
93 [SerializeField] private int offsetApplyAttempts = 3;
94
96 [Tooltip("Extra margin applied when the banner is anchored to the top.")]
97 [SerializeField] private float marginTop;
98
100 [Tooltip("Extra margin applied when the banner is anchored to the bottom.")]
101 [SerializeField] private float marginBottom;
102
104 [Tooltip("Extra margin applied when the banner is anchored to the left.")]
105 [SerializeField] private float marginLeft;
106
108 [Tooltip("Extra margin applied when the banner is anchored to the right.")]
109 [SerializeField] private float marginRight;
110
112 [Tooltip("When true, only the banner side is inset. When false, full shift is allowed if the opposite side is free.")]
113 [SerializeField] private bool shiftOnlyBannerSide = true;
114
116 [Tooltip("When true, safe-area inset on the banner side is subtracted from the applied banner offset.")]
117 [SerializeField] private bool accountForSafeArea = true;
118
119 #endregion
120
121 #region Runtime state
122
123 // This flag is read by callbacks that may be invoked from an ad SDK thread.
124 private volatile bool isComponentActive;
125
126 private IBannerAd bannerAd;
127 private VisualElement targetContainer;
128 private BannerAnchor appliedAnchor;
129 private BannerToolkitLayoutOffsetMode appliedLayoutOffsetMode;
130 private (float top, float bottom, float left, float right) appliedInsets;
131 private bool hasAppliedInsets;
132 private Vector2 appliedTranslateOffset;
133 private bool hasAppliedTranslateOffset;
134 private bool hasInitialState;
135 private bool isSubscribed;
136 private int applyOffsetRequestVersion;
137#if UNITY_6000_5_OR_NEWER
138 private VisualElement currentRoot;
139 private PanelRenderer registeredPanelRenderer;
140 private int lastPanelReloadVersion = -1;
141#endif
142
143 #endregion
144
145#if UNITY_EDITOR && UNITY_6000_5_OR_NEWER
146 #region Editor state
147
148 private bool HasUIDocument => uiDocument != null;
149
150 private bool HasPanelRenderer => panelRenderer != null;
151
152 #endregion
153#endif
154
155 #region Unity lifecycle
156
157#if UNITY_EDITOR
163 protected virtual void OnValidate()
164 {
165 if (Application.isPlaying) return;
166
167 if (uiDocument == null)
168 TryGetComponent(out uiDocument);
169#if UNITY_6000_5_OR_NEWER
170 if (uiDocument == null && panelRenderer == null)
171 TryGetComponent(out panelRenderer);
172#endif
173
174 bannerAdComponent = bannerAdComponent ? bannerAdComponent : FindBannerAdCandidate();
175
176 if (bannerAdComponent == null || bannerAdComponent is IBannerAd) return;
177
178 Debug.LogWarning(
179 $"{nameof(BannerToolkitOffsetHelper)}: Assigned banner component must implement {nameof(IBannerAd)}.",
180 this);
181 bannerAdComponent = null;
182 }
183
190 private AdUnitBehaviourBase FindBannerAdCandidate()
191 {
192 if (bannerAdController != null && bannerAdController.Ad is IBannerAd)
193 return bannerAdController.Ad;
194
195#if UNITY_6000_3_OR_NEWER
196 var behaviours = FindObjectsByType<AdUnitBehaviourBase>(FindObjectsInactive.Exclude);
197#elif UNITY_2023_1_OR_NEWER
198 var behaviours = FindObjectsByType<AdUnitBehaviourBase>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
199#else
200 var behaviours = FindObjectsOfType<AdUnitBehaviourBase>();
201#endif
202 return behaviours.OfType<IBannerAd>().FirstOrDefault() as AdUnitBehaviourBase;
203 }
204
210 protected virtual void Reset()
211 {
212 if (Application.isPlaying) return;
213
214 if (uiDocument == null)
215 TryGetComponent(out uiDocument);
216
217#if UNITY_6000_5_OR_NEWER
218 if (uiDocument == null && panelRenderer == null)
219 TryGetComponent(out panelRenderer);
220#endif
221
222 if (uiDocument != null)
223 return;
224
225#if UNITY_6000_5_OR_NEWER
226 if (panelRenderer != null)
227 return;
228#endif
229
230#if UNITY_6000_3_OR_NEWER
231 uiDocument = FindAnyObjectByType<UIDocument>();
232#elif UNITY_2023_1_OR_NEWER
233 uiDocument = FindFirstObjectByType<UIDocument>();
234#else
235 uiDocument = FindObjectOfType<UIDocument>();
236#endif
237 }
238#endif
239
243 protected virtual void Awake() => isComponentActive = isActiveAndEnabled;
244
250 protected virtual void OnEnable()
251 {
252 isComponentActive = true;
253 InvalidatePendingApplyOffset();
254 ClearAppliedOffsetState();
255
256#if UNITY_6000_5_OR_NEWER
257 SynchronizeSource();
258#else
259 ResetOffset();
260 ClearRuntimeReferences();
261#endif
262#if UNITY_6000_5_OR_NEWER
263 if (uiDocument != null || currentRoot != null)
264 {
265 if (ResolveRuntimeReferences())
266 Subscribe();
267 }
268 else if (registeredPanelRenderer == null)
269 {
270 ResolveRuntimeReferences();
271 }
272#else
273 if (ResolveRuntimeReferences())
274 Subscribe();
275#endif
276
277 SchedulePanelReadyRefresh();
278 }
279
285 protected virtual void Start() => RefreshOffset();
286
292 protected virtual void OnDisable()
293 {
294 isComponentActive = false;
295 InvalidatePendingApplyOffset();
296 Unsubscribe();
297#if UNITY_6000_5_OR_NEWER
298 UnregisterPanelRenderer();
299#endif
300 ResetOffset();
301 ClearRuntimeReferences();
302 }
303
304 #endregion
305
306 #region Public API
307
312 public void RefreshSource()
313 {
314 if (!isComponentActive)
315 return;
316
317#if UNITY_6000_5_OR_NEWER
318 SynchronizeSource();
319#else
320 ResetOffset();
321 ClearRuntimeReferences();
322#endif
323#if UNITY_6000_5_OR_NEWER
324 if (uiDocument == null && registeredPanelRenderer != null && currentRoot == null)
325 {
326 RequestPanelRendererUpdate();
327 SchedulePanelReadyRefresh();
328 return;
329 }
330#endif
331 if (!ResolveRuntimeReferences())
332 return;
333
334 Subscribe();
336 }
337
343 public void RefreshOffset()
344 {
345 if (!isComponentActive)
346 return;
347
348 if (!EnsureRuntimeReferences())
349 return;
350
351 switch (bannerAd)
352 {
353 case { IsVisible: true }:
354 ApplyOffset();
355 return;
356 case { IsVisible: false }:
357 ResetOffset();
358 break;
359 }
360 }
361
362 #endregion
363
364 #region Event handlers
365
371 private void OnBannerShown() => RefreshOffset();
372
378 private void OnBannerHidden() => RefreshOffset();
379
380 #endregion
381
382 #region Resolution
383
389 private void SchedulePanelReadyRefresh()
390 {
391 var root = ResolveCurrentRoot();
392 root?.schedule.Execute(RefreshAfterPanelReady).StartingIn(PanelReadyRefreshDelayMs);
393 }
394
400 private void RefreshAfterPanelReady()
401 {
402 if (!isComponentActive)
403 return;
404
406 }
407
414 private bool EnsureRuntimeReferences()
415 {
416#if UNITY_6000_5_OR_NEWER
417 if (uiDocument == null && currentRoot == null)
418 return false;
419#endif
420 if (!hasInitialState || bannerAd == null || targetContainer == null)
421 {
422 if (!ResolveRuntimeReferences())
423 return false;
424 }
425
426 Subscribe();
427 return true;
428 }
429
437 private bool ResolveRuntimeReferences()
438 {
439 if (uiDocument == null)
440 TryGetComponent(out uiDocument);
441
442#if UNITY_6000_5_OR_NEWER
443 if (uiDocument == null && registeredPanelRenderer == null)
444 {
445 Debug.LogWarning($"{nameof(BannerToolkitOffsetHelper)}: UIDocument or PanelRenderer not found.", this);
446 return false;
447 }
448#else
449 if (uiDocument == null)
450 {
451 Debug.LogWarning($"{nameof(BannerToolkitOffsetHelper)}: UIDocument not found.", this);
452 return false;
453 }
454#endif
455
456 var root = ResolveCurrentRoot();
457 if (root == null)
458 {
459 Debug.LogWarning($"{nameof(BannerToolkitOffsetHelper)}: UI Toolkit root is not available.", this);
460 return false;
461 }
462
463 bannerAd = ResolveBannerAd();
464 if (bannerAd == null)
465 {
466 Debug.LogWarning(
467 $"{nameof(BannerToolkitOffsetHelper)}: Banner component implementing {nameof(IBannerAd)} not found.",
468 this);
469 return false;
470 }
471
472 targetContainer = ResolveTargetContainer(root);
473 if (targetContainer == null)
474 return false;
475
476 hasInitialState = true;
477 return true;
478 }
479
485 private VisualElement ResolveCurrentRoot() =>
486 uiDocument != null ? uiDocument.rootVisualElement :
487#if UNITY_6000_5_OR_NEWER
488 registeredPanelRenderer != null ? currentRoot :
489#endif
490 null;
491
492#if UNITY_6000_5_OR_NEWER
493 private void SynchronizeSource()
494 {
495 var resolvedDocument = uiDocument;
496 if (resolvedDocument == null)
497 TryGetComponent(out resolvedDocument);
498
499 var resolvedRenderer = resolvedDocument == null ? panelRenderer : null;
500 if (resolvedDocument == null && resolvedRenderer == null)
501 TryGetComponent(out resolvedRenderer);
502
503 var sourceChanged = uiDocument != resolvedDocument || registeredPanelRenderer != resolvedRenderer;
504 if (sourceChanged)
505 {
506 Unsubscribe();
507 ResetOffset();
508 ClearAppliedOffsetState();
509 targetContainer = null;
510 currentRoot = null;
511 hasInitialState = false;
512 lastPanelReloadVersion = -1;
513 }
514
515 uiDocument = resolvedDocument;
516
517 if (uiDocument != null)
518 {
519 UnregisterPanelRenderer();
520 currentRoot = uiDocument.rootVisualElement;
521 return;
522 }
523
524 if (registeredPanelRenderer == resolvedRenderer)
525 return;
526
527 UnregisterPanelRenderer();
528 registeredPanelRenderer = resolvedRenderer;
529 if (registeredPanelRenderer == null) return;
530 registeredPanelRenderer.RegisterUIReloadCallback(OnUIReload);
531 ((IPanelComponent)registeredPanelRenderer).PerformUpdate();
532 }
533
534 private void UnregisterPanelRenderer()
535 {
536 if (registeredPanelRenderer == null)
537 return;
538
539 registeredPanelRenderer.UnregisterUIReloadCallback(OnUIReload);
540 registeredPanelRenderer = null;
541 currentRoot = null;
542 }
543
544 private void RequestPanelRendererUpdate()
545 {
546 var renderer = registeredPanelRenderer;
547 if (renderer == null)
548 return;
549
550 ((IPanelComponent)renderer).PerformUpdate();
551 renderer.UnregisterUIReloadCallback(OnUIReload);
552 renderer.RegisterUIReloadCallback(OnUIReload);
553 }
554
561 private void OnUIReload(PanelRenderer renderer, VisualElement root, int version)
562 {
563 if (!isComponentActive)
564 return;
565
566 SynchronizeSource();
567 if (uiDocument != null)
568 {
569 if (!ResolveRuntimeReferences()) return;
570 Subscribe();
572
573 return;
574 }
575
576 if (renderer != registeredPanelRenderer)
577 return;
578
579 if (root != null && version == lastPanelReloadVersion && currentRoot != null)
580 return;
581
582 ResetOffset();
583 currentRoot = root;
584 targetContainer = null;
585 hasInitialState = false;
586
587 if (root == null)
588 return;
589
590 lastPanelReloadVersion = version;
591
592 if (!ResolveRuntimeReferences())
593 return;
594
595 Subscribe();
597 }
598#endif
599
606 private IBannerAd ResolveBannerAd()
607 {
608 if (bannerAdComponent != null)
609 {
610 if (bannerAdComponent is IBannerAd directBanner)
611 return directBanner;
612
613 Debug.LogWarning(
614 $"{nameof(BannerToolkitOffsetHelper)}: Assigned banner component must implement {nameof(IBannerAd)}.",
615 this);
616 return null;
617 }
618
619 if (bannerAdController == null || bannerAdController.Ad is not IBannerAd controllerBanner)
620 return null;
621
622 bannerAdComponent = bannerAdController.Ad;
623 return controllerBanner;
624 }
625
633 private VisualElement ResolveTargetContainer(VisualElement root)
634 {
635 if (string.IsNullOrWhiteSpace(explicitContainerName))
636 return EnsureSubRoot(root);
637
638 var explicitContainer = root.Q<VisualElement>(explicitContainerName);
639 if (explicitContainer != null)
640 return explicitContainer;
641
642 Debug.LogWarning(
643 $"{nameof(BannerToolkitOffsetHelper)}: Container '{explicitContainerName}' not found. " +
644 $"Falling back to '{SubRootName}'.",
645 this);
646
647 return EnsureSubRoot(root);
648 }
649
657 private static VisualElement EnsureSubRoot(VisualElement root)
658 {
659 VisualElement subRoot = null;
660 for (var index = 0; index < root.childCount; index++)
661 {
662 var child = root[index];
663 if (child.name != SubRootName)
664 continue;
665
666 subRoot = child;
667 break;
668 }
669
670 if (subRoot == null)
671 {
672 subRoot = new VisualElement { name = SubRootName };
673 ConfigureSubRoot(subRoot);
674 root.Add(subRoot);
675 }
676
677 var children = new List<VisualElement>(root.childCount);
678 for (var index = 0; index < root.childCount; index++)
679 {
680 var child = root[index];
681 if (ReferenceEquals(child, subRoot))
682 continue;
683
684 children.Add(child);
685 }
686
687 foreach (var child in children)
688 {
689 child.RemoveFromHierarchy();
690 subRoot.Add(child);
691 }
692
693 return subRoot;
694 }
695
702 private static void ConfigureSubRoot(VisualElement subRoot)
703 {
704 subRoot.style.flexGrow = 1f;
705 subRoot.style.width = new Length(100f, LengthUnit.Percent);
706 subRoot.style.height = new Length(100f, LengthUnit.Percent);
707 }
708
714 private void ClearRuntimeReferences()
715 {
716 bannerAd = null;
717 targetContainer = null;
718#if UNITY_6000_5_OR_NEWER
719 currentRoot = null;
720#endif
721 ClearAppliedOffsetState();
722 hasInitialState = false;
723 }
724
725 #endregion
726
727 #region Event subscription
728
734 private void Subscribe()
735 {
736 if (isSubscribed || bannerAd == null)
737 return;
738
739 bannerAd.OnShown += OnBannerShown;
740 bannerAd.OnHide += OnBannerHidden;
741 isSubscribed = true;
742 }
743
749 private void Unsubscribe()
750 {
751 if (!isSubscribed)
752 return;
753
754 if (bannerAd != null)
755 {
756 bannerAd.OnShown -= OnBannerShown;
757 bannerAd.OnHide -= OnBannerHidden;
758 }
759
760 isSubscribed = false;
761 }
762
763 #endregion
764
765 #region Offset logic
766
772 private void ApplyOffset()
773 {
774 var requestVersion = ++applyOffsetRequestVersion;
775 var delayMilliseconds = GetApplyOffsetDelayMilliseconds();
776 if (delayMilliseconds <= 0)
777 {
778 ApplyOffsetAttempt(requestVersion, 1);
779 return;
780 }
781
782 ScheduleOffsetAttempt(requestVersion, 1, delayMilliseconds);
783 }
784
791 private void ScheduleOffsetAttempt(int requestVersion, int attempt, long delayMilliseconds)
792 {
793 var root = ResolveCurrentRoot();
794 if (root == null)
795 {
796 ApplyOffsetAttempt(requestVersion, attempt);
797 return;
798 }
799
800 root.schedule.Execute(() => ApplyOffsetAttempt(requestVersion, attempt)).StartingIn(delayMilliseconds);
801 }
802
808 private void ApplyOffsetAttempt(int requestVersion, int attempt)
809 {
810 ApplyOffsetNow(requestVersion);
811
812 if (requestVersion != applyOffsetRequestVersion || !isComponentActive || bannerAd is not { IsVisible: true } ||
813 attempt >= Mathf.Clamp(offsetApplyAttempts, MinOffsetApplyAttempts, MaxOffsetApplyAttempts))
814 return;
815
816 ScheduleOffsetAttempt(requestVersion, attempt + 1, 0);
817 }
818
825 private void ApplyOffsetNow(int requestVersion)
826 {
827 if (requestVersion != applyOffsetRequestVersion)
828 return;
829
830 if (!isComponentActive || !hasInitialState || targetContainer == null || bannerAd == null)
831 return;
832
833 if (!bannerAd.IsVisible)
834 return;
835
836 if (shiftOnlyBannerSide)
837 {
838 ApplySingleSideInset();
839 return;
840 }
841
842 ApplyFullShiftOffset();
843 }
844
851 private long GetApplyOffsetDelayMilliseconds() =>
852 Mathf.RoundToInt(Mathf.Clamp(
853 applyOffsetDelaySeconds,
854 MinApplyOffsetDelaySeconds,
855 MaxApplyOffsetDelaySeconds) * MillisecondsPerSecond);
856
862 private void InvalidatePendingApplyOffset() => applyOffsetRequestVersion++;
863
869 private void ResetOffset()
870 {
871 InvalidatePendingApplyOffset();
872
873 if (!hasInitialState || targetContainer == null)
874 return;
875
876 RemoveAppliedLayoutOffset();
877 RemoveAppliedTranslateOffset();
878 ReleaseSideLock(appliedAnchor);
879 ClearAppliedOffsetState();
880 }
881
887 private void ApplySingleSideInset()
888 {
889 RemoveAppliedLayoutOffset();
890 RemoveAppliedTranslateOffset();
891 ReleaseSideLock(appliedAnchor);
892
893 var insets = GetInsetValues();
894 if (IsZeroInsets(insets))
895 {
896 ClearAppliedOffsetState();
897 return;
898 }
899
900 var side = GetInsetSide(insets);
901 if (!TryAcquireSideLock(side))
902 {
903 ClearAppliedOffsetState();
904 return;
905 }
906
907 var currentOffsets = GetCurrentLayoutOffsetValues(layoutOffsetMode);
908 ApplyLayoutOffsetValues(AddInsets(currentOffsets, insets), layoutOffsetMode);
909
910 appliedAnchor = side;
911 appliedLayoutOffsetMode = layoutOffsetMode;
912 appliedInsets = insets;
913 hasAppliedInsets = true;
914 appliedTranslateOffset = Vector2.zero;
915 hasAppliedTranslateOffset = false;
916 }
917
923 private void ApplyFullShiftOffset()
924 {
925 RemoveAppliedLayoutOffset();
926 RemoveAppliedTranslateOffset();
927 ReleaseSideLock(appliedAnchor);
928
929 var offset = GetShiftOffsetVector();
930 if (IsZeroVector(offset))
931 {
932 ClearAppliedOffsetState();
933 return;
934 }
935
936 var side = GetShiftLockSide();
937 if (!TryAcquireSideLock(side))
938 {
939 ClearAppliedOffsetState();
940 return;
941 }
942
943 var currentTranslate = GetCurrentTranslateValues();
944 ApplyTranslateValues(currentTranslate + offset);
945
946 appliedAnchor = side;
947 appliedInsets = (0f, 0f, 0f, 0f);
948 hasAppliedInsets = false;
949 appliedTranslateOffset = offset;
950 hasAppliedTranslateOffset = true;
951 }
952
958 private void RemoveAppliedLayoutOffset()
959 {
960 if (!hasAppliedInsets || targetContainer == null)
961 return;
962
963 var currentOffsets = GetCurrentLayoutOffsetValues(appliedLayoutOffsetMode);
964 ApplyLayoutOffsetValues(SubtractInsets(currentOffsets, appliedInsets), appliedLayoutOffsetMode);
965
966 appliedInsets = (0f, 0f, 0f, 0f);
967 hasAppliedInsets = false;
968 }
969
975 private void RemoveAppliedTranslateOffset()
976 {
977 if (!hasAppliedTranslateOffset || targetContainer == null)
978 return;
979
980 var currentTranslate = GetCurrentTranslateValues();
981 ApplyTranslateValues(currentTranslate - appliedTranslateOffset);
982
983 appliedTranslateOffset = Vector2.zero;
984 hasAppliedTranslateOffset = false;
985 }
986
993 private (float top, float bottom, float left, float right) GetInsetValues()
994 {
995 if (bannerAd == null)
996 return (0f, 0f, 0f, 0f);
997
998 var anchor = bannerAd.GetBannerAnchor();
999 var effectiveExtent = GetEffectiveBannerExtent(anchor);
1000
1001 return anchor switch
1002 {
1003 BannerAnchor.Top => (effectiveExtent, 0f, 0f, 0f),
1004 BannerAnchor.Bottom => (0f, effectiveExtent, 0f, 0f),
1005 BannerAnchor.Left => (0f, 0f, effectiveExtent, 0f),
1006 BannerAnchor.Right => (0f, 0f, 0f, effectiveExtent),
1007 _ => (0f, 0f, 0f, 0f)
1008 };
1009 }
1010
1017 private Vector2 GetShiftOffsetVector()
1018 {
1019 if (bannerAd == null)
1020 return Vector2.zero;
1021
1022 var anchor = bannerAd.GetBannerAnchor();
1023 var effectiveExtent = GetEffectiveBannerExtent(anchor);
1024
1025 return anchor switch
1026 {
1027 BannerAnchor.Top => new Vector2(0f, effectiveExtent),
1028 BannerAnchor.Bottom => new Vector2(0f, -effectiveExtent),
1029 BannerAnchor.Left => new Vector2(effectiveExtent, 0f),
1030 BannerAnchor.Right => new Vector2(-effectiveExtent, 0f),
1031 _ => Vector2.zero
1032 };
1033 }
1034
1042 private float GetEffectiveBannerExtent(BannerAnchor anchor)
1043 {
1044 var resolvedBannerExtent = GetResolvedBannerExtent(anchor);
1045 var margin = GetMarginForAnchor(anchor);
1046 if (!accountForSafeArea)
1047 return resolvedBannerExtent + margin;
1048
1049 return Mathf.Max(0f, resolvedBannerExtent - GetSafeAreaInsetForAnchor(anchor)) + margin;
1050 }
1051
1059 private float GetResolvedBannerExtent(BannerAnchor anchor)
1060 {
1061 if (bannerAd == null)
1062 return 0f;
1063
1064 var resolvedHeight = Mathf.Max(0f, bannerAd.GetBannerHeightPixels());
1065 var resolvedWidth = Mathf.Max(0f, bannerAd.GetBannerWidthPixels());
1066 if (resolvedWidth <= 0f)
1067 resolvedWidth = Screen.width;
1068
1069 return anchor switch
1070 {
1071 BannerAnchor.Top or BannerAnchor.Bottom => resolvedHeight,
1072 BannerAnchor.Left or BannerAnchor.Right => resolvedWidth,
1073 _ => 0f
1074 };
1075 }
1076
1084 private float GetMarginForAnchor(BannerAnchor anchor) =>
1085 anchor switch
1086 {
1087 BannerAnchor.Top => marginTop,
1088 BannerAnchor.Bottom => marginBottom,
1089 BannerAnchor.Left => marginLeft,
1090 BannerAnchor.Right => marginRight,
1091 _ => 0f
1092 };
1093
1101 private static float GetSafeAreaInsetForAnchor(BannerAnchor anchor)
1102 {
1103 var (top, bottom, left, right) = GetSafeAreaInsets();
1104 return anchor switch
1105 {
1106 BannerAnchor.Top => top,
1107 BannerAnchor.Bottom => bottom,
1108 BannerAnchor.Left => left,
1109 BannerAnchor.Right => right,
1110 _ => 0f
1111 };
1112 }
1113
1120 private static (float top, float bottom, float left, float right) GetSafeAreaInsets()
1121 {
1122 var safeArea = Screen.safeArea;
1123 return (
1124 top: Mathf.Max(0f, Screen.height - (safeArea.y + safeArea.height)),
1125 bottom: Mathf.Max(0f, safeArea.y),
1126 left: Mathf.Max(0f, safeArea.x),
1127 right: Mathf.Max(0f, Screen.width - (safeArea.x + safeArea.width)));
1128 }
1129
1136 private BannerAnchor GetShiftLockSide()
1137 {
1138 if (bannerAd == null)
1139 return BannerAnchor.Custom;
1140
1141 return bannerAd.GetBannerAnchor() switch
1142 {
1143 BannerAnchor.Top => BannerAnchor.Bottom,
1144 BannerAnchor.Bottom => BannerAnchor.Top,
1145 BannerAnchor.Left => BannerAnchor.Right,
1146 BannerAnchor.Right => BannerAnchor.Left,
1147 _ => BannerAnchor.Custom
1148 };
1149 }
1150
1158 private (float top, float bottom, float left, float right) GetCurrentLayoutOffsetValues(BannerToolkitLayoutOffsetMode mode) =>
1159 mode == BannerToolkitLayoutOffsetMode.Margin
1160 ? GetCurrentMarginValues()
1161 : GetCurrentPaddingValues();
1162
1170 private void ApplyLayoutOffsetValues(
1171 (float top, float bottom, float left, float right) offsetValues,
1173 {
1174 if (mode == BannerToolkitLayoutOffsetMode.Margin)
1175 {
1176 ApplyMarginValues(offsetValues);
1177 return;
1178 }
1179
1180 ApplyPaddingValues(offsetValues);
1181 }
1182
1189 private (float top, float bottom, float left, float right) GetCurrentPaddingValues()
1190 {
1191 var resolvedStyle = targetContainer.resolvedStyle;
1192 return (
1193 top: ResolveStyleLengthValue(targetContainer.style.paddingTop, resolvedStyle.paddingTop),
1194 bottom: ResolveStyleLengthValue(targetContainer.style.paddingBottom, resolvedStyle.paddingBottom),
1195 left: ResolveStyleLengthValue(targetContainer.style.paddingLeft, resolvedStyle.paddingLeft),
1196 right: ResolveStyleLengthValue(targetContainer.style.paddingRight, resolvedStyle.paddingRight));
1197 }
1198
1205 private void ApplyPaddingValues((float top, float bottom, float left, float right) paddingValues)
1206 {
1207 targetContainer.style.paddingTop = paddingValues.top;
1208 targetContainer.style.paddingBottom = paddingValues.bottom;
1209 targetContainer.style.paddingLeft = paddingValues.left;
1210 targetContainer.style.paddingRight = paddingValues.right;
1211 }
1212
1219 private (float top, float bottom, float left, float right) GetCurrentMarginValues()
1220 {
1221 var resolvedStyle = targetContainer.resolvedStyle;
1222 return (
1223 top: ResolveStyleLengthValue(targetContainer.style.marginTop, resolvedStyle.marginTop),
1224 bottom: ResolveStyleLengthValue(targetContainer.style.marginBottom, resolvedStyle.marginBottom),
1225 left: ResolveStyleLengthValue(targetContainer.style.marginLeft, resolvedStyle.marginLeft),
1226 right: ResolveStyleLengthValue(targetContainer.style.marginRight, resolvedStyle.marginRight));
1227 }
1228
1235 private void ApplyMarginValues((float top, float bottom, float left, float right) marginValues)
1236 {
1237 targetContainer.style.marginTop = marginValues.top;
1238 targetContainer.style.marginBottom = marginValues.bottom;
1239 targetContainer.style.marginLeft = marginValues.left;
1240 targetContainer.style.marginRight = marginValues.right;
1241 }
1242
1249 private Vector2 GetCurrentTranslateValues()
1250 {
1251 var resolvedTranslate = targetContainer.resolvedStyle.translate;
1252 var resolvedValues = (Vector2)resolvedTranslate;
1253
1254 var styleTranslate = targetContainer.style.translate;
1255 if (styleTranslate.keyword is StyleKeyword.Null or StyleKeyword.Auto)
1256 return resolvedValues;
1257
1258 var value = styleTranslate.value;
1259 var x = ResolveTranslateLength(value.x, resolvedValues.x);
1260 var y = ResolveTranslateLength(value.y, resolvedValues.y);
1261 return new Vector2(x, y);
1262 }
1263
1270 private void ApplyTranslateValues(Vector2 translateValues) =>
1271 targetContainer.style.translate = new Translate(
1272 new Length(translateValues.x, LengthUnit.Pixel),
1273 new Length(translateValues.y, LengthUnit.Pixel),
1274 0f);
1275
1284 private static float ResolveStyleLengthValue(StyleLength styleLength, float fallback) =>
1285 styleLength.keyword is StyleKeyword.Null or StyleKeyword.Auto
1286 ? fallback
1287 : styleLength.value.unit == LengthUnit.Pixel
1288 ? styleLength.value.value
1289 : fallback;
1290
1299 private static float ResolveTranslateLength(Length length, float fallback) =>
1300 length.unit == LengthUnit.Pixel ? length.value : fallback;
1301
1310 private static (float top, float bottom, float left, float right) AddInsets(
1311 (float top, float bottom, float left, float right) left,
1312 (float top, float bottom, float left, float right) right) =>
1313 (
1314 top: left.top + right.top,
1315 bottom: left.bottom + right.bottom,
1316 left: left.left + right.left,
1317 right: left.right + right.right);
1318
1327 private static (float top, float bottom, float left, float right) SubtractInsets(
1328 (float top, float bottom, float left, float right) left,
1329 (float top, float bottom, float left, float right) right) =>
1330 (
1331 top: left.top - right.top,
1332 bottom: left.bottom - right.bottom,
1333 left: left.left - right.left,
1334 right: left.right - right.right);
1335
1343 private static bool IsZeroInsets((float top, float bottom, float left, float right) insets) =>
1344 Mathf.Approximately(insets.top, 0f) &&
1345 Mathf.Approximately(insets.bottom, 0f) &&
1346 Mathf.Approximately(insets.left, 0f) &&
1347 Mathf.Approximately(insets.right, 0f);
1348
1356 private static bool IsZeroVector(Vector2 shiftOffset) =>
1357 Mathf.Approximately(shiftOffset.x, 0f) &&
1358 Mathf.Approximately(shiftOffset.y, 0f);
1359
1365 private void ClearAppliedOffsetState()
1366 {
1367 appliedAnchor = BannerAnchor.Custom;
1368 appliedLayoutOffsetMode = layoutOffsetMode;
1369 appliedInsets = (0f, 0f, 0f, 0f);
1370 hasAppliedInsets = false;
1371 appliedTranslateOffset = Vector2.zero;
1372 hasAppliedTranslateOffset = false;
1373 }
1374
1382 private static BannerAnchor GetInsetSide((float top, float bottom, float left, float right) insets) =>
1383 !Mathf.Approximately(insets.top, 0f) ? BannerAnchor.Top :
1384 !Mathf.Approximately(insets.bottom, 0f) ? BannerAnchor.Bottom :
1385 !Mathf.Approximately(insets.left, 0f) ? BannerAnchor.Left :
1386 !Mathf.Approximately(insets.right, 0f) ? BannerAnchor.Right : BannerAnchor.Custom;
1387
1395 private bool TryAcquireSideLock(BannerAnchor side)
1396 {
1397 side = NormalizeLockAnchor(side);
1398 if (side == BannerAnchor.Custom || targetContainer == null)
1399 return true;
1400
1401 var containerId = RuntimeHelpers.GetHashCode(targetContainer);
1402 if (!sideLocksByContainer.TryGetValue(containerId, out var sideLocks))
1403 {
1404#if UNITY_6000_4_OR_NEWER
1405 sideLocks = new Dictionary<BannerAnchor, EntityId>();
1406#else
1407 sideLocks = new Dictionary<BannerAnchor, int>();
1408#endif
1409 sideLocksByContainer[containerId] = sideLocks;
1410 }
1411
1412#if UNITY_6000_4_OR_NEWER
1413 var ownerId = GetEntityId();
1414#else
1415 var ownerId = GetInstanceID();
1416#endif
1417
1418 if (sideLocks.TryGetValue(side, out var lockedBy) && lockedBy != ownerId)
1419 return false;
1420
1421 sideLocks[side] = ownerId;
1422 return true;
1423 }
1424
1431 private void ReleaseSideLock(BannerAnchor side)
1432 {
1433 side = NormalizeLockAnchor(side);
1434 if (side == BannerAnchor.Custom || targetContainer == null)
1435 return;
1436
1437 var containerId = RuntimeHelpers.GetHashCode(targetContainer);
1438 if (!sideLocksByContainer.TryGetValue(containerId, out var sideLocks))
1439 return;
1440
1441#if UNITY_6000_4_OR_NEWER
1442 var ownerId = GetEntityId();
1443#else
1444 var ownerId = GetInstanceID();
1445#endif
1446
1447 if (!sideLocks.TryGetValue(side, out var lockedBy) || lockedBy != ownerId)
1448 return;
1449
1450 sideLocks.Remove(side);
1451 if (sideLocks.Count == 0)
1452 sideLocksByContainer.Remove(containerId);
1453 }
1454
1462 private static BannerAnchor NormalizeLockAnchor(BannerAnchor anchor) =>
1463 anchor is BannerAnchor.Center or BannerAnchor.Custom
1464 ? BannerAnchor.Custom
1465 : anchor;
1466
1467 #endregion
1468 }
1469}
Base class for controlling ad units within Unity, providing the foundation for specific ad functional...
Base MonoBehaviour class for ad units. This class defines the abstract and virtual properties and met...
virtual void OnDisable()
Restores original state and unsubscribes from events. The method removes only offsets applied by this...
void RefreshSource()
Re-resolves the preferred UI Toolkit source and reapplies the banner offset. This method is intended ...
void RefreshOffset()
Refreshes the target container offset according to the current banner visibility. Visible banners req...
virtual void OnEnable()
Resolves dependencies and subscribes to banner events. A delayed refresh is scheduled because custom ...
virtual void Start()
Applies or resets offset based on the current banner state. This covers banners that were shown befor...
virtual void Awake()
Captures the initial active state on Unity's main thread.
Offsets a UI Toolkit container to avoid overlap with an active banner ad. The helper can use an expli...
Provides banner-specific layout data needed by UI offset helpers. Implementations expose current visi...
BannerToolkitLayoutOffsetMode
Defines how banner-side layout offsets are applied to the target UI Toolkit element....
@ Margin
Applies banner offset to the target element margin.
@ Padding
Applies banner offset to the target element padding.
BannerAnchor
Describes screen anchors used by banner ads. The value defines from which edge of the screen UI conte...