diff --git a/Assets/Demo/Core/DemoEntryPoint.unity b/Assets/Demo/Core/DemoEntryPoint.unity index cca0d450..3c836e32 100644 --- a/Assets/Demo/Core/DemoEntryPoint.unity +++ b/Assets/Demo/Core/DemoEntryPoint.unity @@ -368,7 +368,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _name: MainModalContainer - _backdropStrategy: 1 + _backdropStrategy: 0 _overrideBackdropPrefab: {fileID: 4672728316167856403, guid: 30228440436e2a248b41c61d678f615e, type: 3} --- !u!114 &779631005 diff --git a/Assets/Demo/Core/Scripts/Presentation/Loading/LoadingPagePresenter.cs b/Assets/Demo/Core/Scripts/Presentation/Loading/LoadingPagePresenter.cs index b1b7ebf1..2451ea92 100644 --- a/Assets/Demo/Core/Scripts/Presentation/Loading/LoadingPagePresenter.cs +++ b/Assets/Demo/Core/Scripts/Presentation/Loading/LoadingPagePresenter.cs @@ -1,3 +1,4 @@ +using System.Collections; using Demo.Core.Scripts.Presentation.Shared; using Demo.Core.Scripts.View.Loading; @@ -12,6 +13,12 @@ public LoadingPagePresenter(LoadingPage view, ITransitionService transitionServi protected override void ViewDidPushEnter(LoadingPage view, LoadingViewState viewState) { + view.StartCoroutine(WaitAndCallHomeLoadingPageShown()); + } + + private IEnumerator WaitAndCallHomeLoadingPageShown() + { + yield return null; TransitionService.HomeLoadingPageShown(); } } diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalContainer.cs b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalContainer.cs index c8190fdf..225a8050 100644 --- a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalContainer.cs +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalContainer.cs @@ -1,13 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UI; -using UnityScreenNavigator.Runtime.Core.Page; using UnityScreenNavigator.Runtime.Core.Shared; -using UnityScreenNavigator.Runtime.Core.Sheet; using UnityScreenNavigator.Runtime.Foundation; using UnityScreenNavigator.Runtime.Foundation.AssetLoader; using UnityScreenNavigator.Runtime.Foundation.Coroutine; @@ -15,40 +12,37 @@ namespace UnityScreenNavigator.Runtime.Core.Modal { [RequireComponent(typeof(RectMask2D))] - public sealed class ModalContainer : MonoBehaviour + public sealed class ModalContainer : MonoBehaviour, IScreenContainer { - private static readonly Dictionary InstanceCacheByTransform = - new Dictionary(); + private static readonly Dictionary InstanceCacheByTransform = new(); - private static readonly Dictionary InstanceCacheByName = - new Dictionary(); + private static readonly Dictionary InstanceCacheByName = new(); [SerializeField] private string _name; [SerializeField] private ModalBackdropStrategy _backdropStrategy = ModalBackdropStrategy.GeneratePerModal; - + [SerializeField] private ModalBackdrop _overrideBackdropPrefab; - private readonly Dictionary> _assetLoadHandles - = new Dictionary>(); + private readonly Dictionary> _assetLoadHandles = new(); - private readonly List _callbackReceivers = - new List(); + private readonly List _callbackReceivers = new(); - private readonly Dictionary _modals = new Dictionary(); + private readonly Dictionary _modals = new(); - private readonly List _orderedModalIds = new List(); + private readonly List _orderedModalIds = new(); - private readonly Dictionary> _preloadedResourceHandles = - new Dictionary>(); + private readonly Dictionary> _preloadedResourceHandles = new(); private IAssetLoader _assetLoader; + private IModalBackdropHandler _backdropHandler; + private ModalBackdrop _backdropPrefab; private CanvasGroup _canvasGroup; - public static List Instances { get; } = new List(); - - private IModalBackdropHandler _backdropHandler; + private ModalLifecycleHandler _lifecycleHandler; + private ScreenContainerTransitionHandler _transitionHandler; + public static List Instances { get; } = new(); /// /// By default, in is used. @@ -60,11 +54,6 @@ public IAssetLoader AssetLoader set => _assetLoader = value; } - /// - /// True if in transition. - /// - public bool IsInTransition { get; private set; } - /// /// List of ModalIds sorted in the order they are stacked. /// @@ -75,12 +64,6 @@ public IAssetLoader AssetLoader /// public IReadOnlyDictionary Modals => _modals; - public bool Interactable - { - get => _canvasGroup.interactable; - set => _canvasGroup.interactable = value; - } - private void Awake() { Instances.Add(this); @@ -94,6 +77,11 @@ private void Awake() _canvasGroup = gameObject.GetOrAddComponent(); _backdropHandler = ModalBackdropHandlerFactory.Create(_backdropStrategy, _backdropPrefab); + _transitionHandler = new ScreenContainerTransitionHandler(this); + _lifecycleHandler = new ModalLifecycleHandler( + (RectTransform)transform, + _callbackReceivers, + _backdropHandler); } private void OnDestroy() @@ -123,6 +111,17 @@ private void OnDestroy() Instances.Remove(this); } + /// + /// True if in transition. + /// + public bool IsInTransition => _transitionHandler.IsInTransition; + + public bool Interactable + { + get => _canvasGroup.interactable; + set => _canvasGroup.interactable = value; + } + /// /// Get the that manages the modal to which belongs. /// @@ -194,11 +193,20 @@ public void RemoveCallbackReceiver(IModalContainerCallbackReceiver callbackRecei /// /// /// - public AsyncProcessHandle Push(string resourceKey, bool playAnimation, string modalId = null, - bool loadAsync = true, Action<(string modalId, Modal modal)> onLoad = null) + public AsyncProcessHandle Push( + string resourceKey, + bool playAnimation, + string modalId = null, + bool loadAsync = true, + Action<(string modalId, Modal modal)> onLoad = null + ) { - return CoroutineManager.Instance.Run(PushRoutine(typeof(Modal), resourceKey, playAnimation, onLoad, - loadAsync, modalId)); + return CoroutineManager.Instance.Run(PushRoutine(typeof(Modal), + resourceKey, + playAnimation, + onLoad, + loadAsync, + modalId)); } /// @@ -211,10 +219,20 @@ public AsyncProcessHandle Push(string resourceKey, bool playAnimation, string mo /// /// /// - public AsyncProcessHandle Push(Type modalType, string resourceKey, bool playAnimation, string modalId = null, - bool loadAsync = true, Action<(string modalId, Modal modal)> onLoad = null) + public AsyncProcessHandle Push( + Type modalType, + string resourceKey, + bool playAnimation, + string modalId = null, + bool loadAsync = true, + Action<(string modalId, Modal modal)> onLoad = null + ) { - return CoroutineManager.Instance.Run(PushRoutine(modalType, resourceKey, playAnimation, onLoad, loadAsync, + return CoroutineManager.Instance.Run(PushRoutine(modalType, + resourceKey, + playAnimation, + onLoad, + loadAsync, modalId)); } @@ -228,12 +246,21 @@ public AsyncProcessHandle Push(Type modalType, string resourceKey, bool playAnim /// /// /// - public AsyncProcessHandle Push(string resourceKey, bool playAnimation, string modalId = null, - bool loadAsync = true, Action<(string modalId, TModal modal)> onLoad = null) + public AsyncProcessHandle Push( + string resourceKey, + bool playAnimation, + string modalId = null, + bool loadAsync = true, + Action<(string modalId, TModal modal)> onLoad = null + ) where TModal : Modal { - return CoroutineManager.Instance.Run(PushRoutine(typeof(TModal), resourceKey, playAnimation, - x => onLoad?.Invoke((x.modalId, (TModal)x.modal)), loadAsync, modalId)); + return CoroutineManager.Instance.Run(PushRoutine(typeof(TModal), + resourceKey, + playAnimation, + x => onLoad?.Invoke((x.modalId, (TModal)x.modal)), + loadAsync, + modalId)); } /// @@ -261,7 +288,7 @@ public AsyncProcessHandle Pop(bool playAnimation, string destinationModalId) var modalId = _orderedModalIds[i]; if (modalId == destinationModalId) break; - + popCount++; } @@ -271,256 +298,100 @@ public AsyncProcessHandle Pop(bool playAnimation, string destinationModalId) return CoroutineManager.Instance.Run(PopRoutine(playAnimation, popCount)); } - private IEnumerator PushRoutine(Type modalType, string resourceKey, bool playAnimation, - Action<(string modalId, Modal modal)> onLoad = null, bool loadAsync = true, string modalId = null) + private IEnumerator PushRoutine( + Type modalType, + string resourceKey, + bool playAnimation, + Action<(string modalId, Modal modal)> onLoad = null, + bool loadAsync = true, + string modalId = null + ) { - if (resourceKey == null) - throw new ArgumentNullException(nameof(resourceKey)); + Assert.IsNotNull(resourceKey); + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); + _transitionHandler.Begin(); - IsInTransition = true; + // If the user explicitly specifies modalId, use it, otherwise generate it + modalId ??= Guid.NewGuid().ToString(); - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) + Modal modal = null; + yield return LoadModal(modalType, + resourceKey, + loadAsync, + (m, lh) => { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = false; - } - else - { - Interactable = false; - } - } - - var assetLoadHandle = loadAsync - ? AssetLoader.LoadAsync(resourceKey) - : AssetLoader.Load(resourceKey); - if (!assetLoadHandle.IsDone) yield return new WaitUntil(() => assetLoadHandle.IsDone); - - if (assetLoadHandle.Status == AssetLoadStatus.Failed) throw assetLoadHandle.OperationException; - - var instance = Instantiate(assetLoadHandle.Result); - if (!instance.TryGetComponent(modalType, out var c)) - c = instance.AddComponent(modalType); - var enterModal = (Modal)c; - - if (modalId == null) - modalId = Guid.NewGuid().ToString(); - _assetLoadHandles.Add(modalId, assetLoadHandle); - onLoad?.Invoke((modalId, enterModal)); - var afterLoadHandle = enterModal.AfterLoad((RectTransform)transform); - while (!afterLoadHandle.IsTerminated) - yield return null; - - var exitModalId = _orderedModalIds.Count == 0 ? null : _orderedModalIds[_orderedModalIds.Count - 1]; - var exitModal = exitModalId == null ? null : _modals[exitModalId]; - - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforePush(enterModal, exitModal); - - var preprocessHandles = new List(); - if (exitModal != null) preprocessHandles.Add(exitModal.BeforeExit(true, enterModal)); - - preprocessHandles.Add(enterModal.BeforeEnter(true, exitModal)); + modal = m; + _assetLoadHandles.Add(modalId, lh); + onLoad?.Invoke((modalId, m)); + }); - foreach (var coroutineHandle in preprocessHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; + var context = ModalPushContext.Create(modalId, modal, _orderedModalIds, _modals); - // Play Animation - var enterModalIndex = _modals.Count; - var animationHandles = new List(); - animationHandles.Add(_backdropHandler.BeforeModalEnter(enterModal, enterModalIndex, playAnimation)); + yield return _lifecycleHandler.AfterLoad(context); - if (exitModal != null) animationHandles.Add(exitModal.Exit(true, playAnimation, enterModal)); + yield return _lifecycleHandler.BeforePush(context); - animationHandles.Add(enterModal.Enter(true, playAnimation, exitModal)); + yield return _lifecycleHandler.Push(context, playAnimation); - foreach (var coroutineHandle in animationHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; + _lifecycleHandler.AfterPush(context); - _backdropHandler.AfterModalEnter(enterModal, enterModalIndex, true); + _modals.Add(context.ModalId, context.EnterModal); + _orderedModalIds.Add(context.ModalId); - // End Transition - _modals.Add(modalId, enterModal); - _orderedModalIds.Add(modalId); - IsInTransition = false; - - // Postprocess - if (exitModal != null) exitModal.AfterExit(true, enterModal); - - enterModal.AfterEnter(true, exitModal); - - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.AfterPush(enterModal, exitModal); - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (PageContainer.Instances.All(x => !x.IsInTransition) - && Instances.All(x => !x.IsInTransition) - && SheetContainer.Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } - } + _transitionHandler.End(); } private IEnumerator PopRoutine(bool playAnimation, int popCount = 1) { Assert.IsTrue(popCount >= 1); + Assert.IsTrue(_orderedModalIds.Count >= popCount, + "Cannot transition because the modal count is less than the pop count."); + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); - if (_orderedModalIds.Count < popCount) - throw new InvalidOperationException( - "Cannot transition because the modal count is less than the pop count."); - - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); - - IsInTransition = true; + _transitionHandler.Begin(); - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = false; - } - else - { - Interactable = false; - } - } - - var exitModalId = _orderedModalIds[_orderedModalIds.Count - 1]; - var exitModal = _modals[exitModalId]; + var context = ModalPopContext.Create(_orderedModalIds, _modals, popCount); - var unusedModalIds = new List(); - var unusedModals = new List(); - var unusedModalIndices = new List(); - for (var i = _orderedModalIds.Count - 1; i >= _orderedModalIds.Count - popCount; i--) - { - var unusedModalId = _orderedModalIds[i]; - unusedModalIds.Add(unusedModalId); - unusedModals.Add(_modals[unusedModalId]); - unusedModalIndices.Add(i); - } + yield return _lifecycleHandler.BeforePop(context); - var enterModalIndex = _orderedModalIds.Count - popCount - 1; - var enterModalId = enterModalIndex < 0 ? null : _orderedModalIds[enterModalIndex]; - var enterModal = enterModalId == null ? null : _modals[enterModalId]; + yield return _lifecycleHandler.Pop(context, playAnimation); - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforePop(enterModal, exitModal); + _lifecycleHandler.AfterPop(context); - var preprocessHandles = new List + for (var i = 0; i < context.ExitModalIds.Count; i++) { - exitModal.BeforeExit(false, enterModal) - }; - if (enterModal != null) preprocessHandles.Add(enterModal.BeforeEnter(false, exitModal)); - - foreach (var coroutineHandle in preprocessHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; + var exitModal = context.ExitModals[i]; + var exitModalId = context.ExitModalIds[i]; - // Play Animation - var animationHandles = new List(); - for (var i = unusedModalIds.Count - 1; i >= 0; i--) - { - var unusedModalId = unusedModalIds[i]; - var unusedModal = _modals[unusedModalId]; - var unusedModalIndex = unusedModalIndices[i]; - var partnerModalId = i == 0 ? enterModalId : unusedModalIds[i - 1]; - var partnerModal = partnerModalId == null ? null : _modals[partnerModalId]; - animationHandles.Add(_backdropHandler.BeforeModalExit(unusedModal, unusedModalIndex, playAnimation)); - animationHandles.Add(unusedModal.Exit(false, playAnimation, partnerModal)); + exitModal.gameObject.SetActive(false); + _modals.Remove(exitModalId); + _orderedModalIds.Remove(exitModalId); } - if (enterModal != null) animationHandles.Add(enterModal.Enter(false, playAnimation, exitModal)); + _transitionHandler.End(); - foreach (var coroutineHandle in animationHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; - - // End Transition - for (var i = 0; i < unusedModalIds.Count; i++) - { - var unusedModalId = unusedModalIds[i]; - _modals.Remove(unusedModalId); - _orderedModalIds.RemoveAt(_orderedModalIds.Count - 1); - } - IsInTransition = false; - - // Postprocess - exitModal.AfterExit(false, enterModal); - if (enterModal != null) enterModal.AfterEnter(false, exitModal); - - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.AfterPop(enterModal, exitModal); + // Resource cleanup isn't tied to the transition itself, so it should be handled asynchronously in the background. + StartCoroutine(AfterPopRoutine(context)); + } - // Unload Unused Page - var beforeReleaseHandle = exitModal.BeforeRelease(); - while (!beforeReleaseHandle.IsTerminated) yield return null; + private IEnumerator AfterPopRoutine( + ModalPopContext context + ) + { + yield return _lifecycleHandler.AfterPopRoutine(context); - for (var i = 0; i < unusedModalIds.Count; i++) + for (var i = 0; i < context.ExitModalIds.Count; i++) { - var unusedModalId = unusedModalIds[i]; - var unusedModal = unusedModals[i]; - var unusedModalIndex = unusedModalIndices[i]; - var loadHandle = _assetLoadHandles[unusedModalId]; - Destroy(unusedModal.gameObject); - AssetLoader.Release(loadHandle); - _assetLoadHandles.Remove(unusedModalId); - _backdropHandler.AfterModalExit(exitModal, unusedModalIndex, playAnimation); - } + var exitModalId = context.ExitModalIds[i]; + var exitModal = context.ExitModals[i]; + var loadHandle = _assetLoadHandles[exitModalId]; - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (PageContainer.Instances.All(x => !x.IsInTransition) - && Instances.All(x => !x.IsInTransition) - && SheetContainer.Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } + Destroy(exitModal.gameObject); + AssetLoader.Release(loadHandle); + _assetLoadHandles.Remove(exitModalId); } } @@ -565,5 +436,30 @@ public void ReleasePreloaded(string resourceKey) var handle = _preloadedResourceHandles[resourceKey]; AssetLoader.Release(handle); } + + private IEnumerator LoadModal( + Type modalType, + string resourceKey, + bool loadAsync, + Action> onLoaded + ) + { + var assetLoadHandle = loadAsync + ? AssetLoader.LoadAsync(resourceKey) + : AssetLoader.Load(resourceKey); + + if (!assetLoadHandle.IsDone) + yield return new WaitUntil(() => assetLoadHandle.IsDone); + + if (assetLoadHandle.Status == AssetLoadStatus.Failed) + throw assetLoadHandle.OperationException; + + var instance = Instantiate(assetLoadHandle.Result); + if (!instance.TryGetComponent(modalType, out var c)) + c = instance.AddComponent(modalType); + + var modal = (Modal)c; + onLoaded?.Invoke(modal, assetLoadHandle); + } } -} +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs new file mode 100644 index 00000000..9f604bee --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs @@ -0,0 +1,136 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityScreenNavigator.Runtime.Foundation.Coroutine; + +namespace UnityScreenNavigator.Runtime.Core.Modal +{ + internal sealed class ModalLifecycleHandler + { + private readonly IModalBackdropHandler _backdropHandler; + private readonly IEnumerable _callbackReceivers; + private readonly RectTransform _containerTransform; + + public ModalLifecycleHandler( + RectTransform containerTransform, + IEnumerable callbackReceivers, + IModalBackdropHandler backdropHandler + ) + { + _containerTransform = containerTransform; + _callbackReceivers = callbackReceivers; + _backdropHandler = backdropHandler; + } + + public IEnumerator AfterLoad(ModalPushContext context) + { + yield return context.EnterModal.AfterLoad(_containerTransform); + } + + public IEnumerator BeforePush(ModalPushContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforePush(context.EnterModal, context.ExitModal); + + var handles = new List(); + if (context.ExitModal != null) + handles.Add(context.ExitModal.BeforeExit(true, context.EnterModal)); + + handles.Add(context.EnterModal.BeforeEnter(true, context.ExitModal)); + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator Push(ModalPushContext context, bool playAnimation) + { + var handles = new List + { + _backdropHandler.BeforeModalEnter(context.EnterModal, context.EnterModalIndex, playAnimation) + }; + + if (context.ExitModal != null) + handles.Add(context.ExitModal.Exit(true, playAnimation, context.EnterModal)); + + handles.Add(context.EnterModal.Enter(true, playAnimation, context.ExitModal)); + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + + _backdropHandler.AfterModalEnter(context.EnterModal, context.EnterModalIndex, true); + } + + public void AfterPush(ModalPushContext context) + { + if (context.ExitModal != null) + context.ExitModal.AfterExit(true, context.EnterModal); + + context.EnterModal.AfterEnter(true, context.ExitModal); + + foreach (var receiver in _callbackReceivers) + receiver.AfterPush(context.EnterModal, context.ExitModal); + } + + public IEnumerator BeforePop(ModalPopContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforePop(context.EnterModal, context.FirstExitModal); + + var handles = new List(); + foreach (var exitModal in context.ExitModals) + handles.Add(exitModal.BeforeExit(false, context.EnterModal)); + + if (context.EnterModal != null) + handles.Add(context.EnterModal.BeforeEnter(false, context.FirstExitModal)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator Pop(ModalPopContext context, bool playAnimation) + { + var handles = new List(); + + for (var i = 0; i < context.ExitModals.Count; i++) + { + var exitModal = context.ExitModals[i]; + var exitModalIndex = context.ExitModalIndices[i]; + var partner = i == context.ExitModals.Count - 1 ? context.EnterModal : context.ExitModals[i + 1]; + + handles.Add(_backdropHandler.BeforeModalExit(exitModal, exitModalIndex, playAnimation)); + handles.Add(exitModal.Exit(false, playAnimation, partner)); + } + + if (context.EnterModal != null) + handles.Add(context.EnterModal.Enter(false, playAnimation, context.FirstExitModal)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + + _backdropHandler.AfterModalExit(context.FirstExitModal, context.FirstExitModalIndex, playAnimation); + } + + public void AfterPop(ModalPopContext context) + { + foreach (var exitModal in context.ExitModals) + exitModal.AfterExit(false, context.EnterModal); + if (context.EnterModal != null) + context.EnterModal.AfterEnter(false, context.FirstExitModal); + + foreach (var receiver in _callbackReceivers) + receiver.AfterPop(context.EnterModal, context.FirstExitModal); + } + + public IEnumerator AfterPopRoutine(ModalPopContext context) + { + var handles = context.ExitModals.Select(exitModal => exitModal.BeforeRelease()); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs.meta new file mode 100644 index 00000000..20544955 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalLifecycleHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ab757eca3efd460cb6a7e876c8b5fd69 +timeCreated: 1746287127 \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs new file mode 100644 index 00000000..c7cad898 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; + +namespace UnityScreenNavigator.Runtime.Core.Modal +{ + internal readonly struct ModalPopContext + { + public IReadOnlyList ExitModalIds { get; } + public IReadOnlyList ExitModals { get; } + public IReadOnlyList ExitModalIndices { get; } + + public string EnterModalId { get; } + public Modal EnterModal { get; } + + public Modal FirstExitModal => ExitModals[0]; + + public int FirstExitModalIndex => ExitModalIndices[0]; + + private ModalPopContext( + List exitModalIds, + List exitModals, + List exitModalIndices, + string enterModalId, + Modal enterModal + ) + { + ExitModalIds = exitModalIds; + ExitModals = exitModals; + ExitModalIndices = exitModalIndices; + EnterModalId = enterModalId; + EnterModal = enterModal; + } + + public static ModalPopContext Create( + List orderedModalIds, + Dictionary modals, + int popCount + ) + { + var exitIds = new List(); + var exitModals = new List(); + var indices = new List(); + + for (var i = orderedModalIds.Count - 1; i >= orderedModalIds.Count - popCount; i--) + { + var id = orderedModalIds[i]; + exitIds.Add(id); + exitModals.Add(modals[id]); + indices.Add(i); + } + + var enterIndex = orderedModalIds.Count - popCount - 1; + var enterId = enterIndex >= 0 ? orderedModalIds[enterIndex] : null; + var enterModal = enterId != null ? modals[enterId] : null; + + return new ModalPopContext(exitIds, exitModals, indices, enterId, enterModal); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs.meta new file mode 100644 index 00000000..2c8a7aaf --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPopContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 25267d6e7e9d415780116b79a30acb74 +timeCreated: 1746284749 \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs new file mode 100644 index 00000000..cc4d1453 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace UnityScreenNavigator.Runtime.Core.Modal +{ + internal readonly struct ModalPushContext + { + public string ModalId { get; } + public Modal EnterModal { get; } + + public string ExitModalId { get; } + public Modal ExitModal { get; } + + public int EnterModalIndex { get; } + + private ModalPushContext( + string modalId, + Modal enterModal, + string exitModalId, + Modal exitModal, + int enterModalIndex + ) + { + ModalId = modalId; + EnterModal = enterModal; + ExitModalId = exitModalId; + ExitModal = exitModal; + EnterModalIndex = enterModalIndex; + } + + public static ModalPushContext Create( + string modalId, + Modal enterModal, + List orderedModalIds, + Dictionary modals + ) + { + var hasExit = orderedModalIds.Count > 0; + var exitId = hasExit ? orderedModalIds[orderedModalIds.Count - 1] : null; + var exitModal = hasExit ? modals[exitId] : null; + + var enterIndex = modals.Count; + + return new ModalPushContext(modalId, enterModal, exitId, exitModal, enterIndex); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs.meta new file mode 100644 index 00000000..e7432af0 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Modal/ModalPushContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 97863090c46a4f62954f7f4dba934c3f +timeCreated: 1746284400 \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PageContainer.cs b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageContainer.cs index cd37a841..700d86a2 100644 --- a/Assets/UnityScreenNavigator/Runtime/Core/Page/PageContainer.cs +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageContainer.cs @@ -1,13 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.UI; -using UnityScreenNavigator.Runtime.Core.Modal; using UnityScreenNavigator.Runtime.Core.Shared; -using UnityScreenNavigator.Runtime.Core.Sheet; using UnityScreenNavigator.Runtime.Foundation; using UnityScreenNavigator.Runtime.Foundation.AssetLoader; using UnityScreenNavigator.Runtime.Foundation.Coroutine; @@ -15,35 +12,33 @@ namespace UnityScreenNavigator.Runtime.Core.Page { [RequireComponent(typeof(RectMask2D))] - public sealed class PageContainer : MonoBehaviour + public sealed class PageContainer : MonoBehaviour, IScreenContainer { - private static readonly Dictionary InstanceCacheByTransform = - new Dictionary(); + private static readonly Dictionary InstanceCacheByTransform = new(); - private static readonly Dictionary InstanceCacheByName = - new Dictionary(); + private static readonly Dictionary InstanceCacheByName = new(); [SerializeField] private string _name; - private readonly Dictionary> _assetLoadHandles - = new Dictionary>(); + private readonly Dictionary> _assetLoadHandles = new(); - private readonly List _callbackReceivers = - new List(); + private readonly List _callbackReceivers = new(); - private readonly List _orderedPageIds = new List(); + private readonly List _orderedPageIds = new(); - private readonly Dictionary _pages = new Dictionary(); + private readonly Dictionary _pages = new(); - private readonly Dictionary> _preloadedResourceHandles = - new Dictionary>(); + private readonly Dictionary> _preloadedResourceHandles = new(); private IAssetLoader _assetLoader; private CanvasGroup _canvasGroup; private bool _isActivePageStacked; - public static List Instances { get; } = new List(); + private PageLifecycleHandler _lifecycleHandler; + + private ScreenContainerTransitionHandler _transitionHandler; + public static List Instances { get; } = new(); /// /// By default, in is used. @@ -56,12 +51,7 @@ public IAssetLoader AssetLoader } /// - /// True if in transition. - /// - public bool IsInTransition { get; private set; } - - /// - /// List of PageIds sorted in the order they are stacked. + /// List of PageIds sorted in the order they are stacked. /// public IReadOnlyList OrderedPagesIds => _orderedPageIds; @@ -70,12 +60,6 @@ public IAssetLoader AssetLoader /// public IReadOnlyDictionary Pages => _pages; - public bool Interactable - { - get => _canvasGroup.interactable; - set => _canvasGroup.interactable = value; - } - private void Awake() { Instances.Add(this); @@ -84,6 +68,8 @@ private void Awake() if (!string.IsNullOrWhiteSpace(_name)) InstanceCacheByName.Add(_name, this); _canvasGroup = gameObject.GetOrAddComponent(); + _transitionHandler = new ScreenContainerTransitionHandler(this); + _lifecycleHandler = new PageLifecycleHandler((RectTransform)transform, _callbackReceivers); } private void OnDestroy() @@ -114,6 +100,17 @@ private void OnDestroy() Instances.Remove(this); } + /// + /// True if in transition. + /// + public bool IsInTransition => _transitionHandler.IsInTransition; + + public bool Interactable + { + get => _canvasGroup.interactable; + set => _canvasGroup.interactable = value; + } + /// /// Get the that manages the page to which belongs. /// @@ -186,11 +183,22 @@ public void RemoveCallbackReceiver(IPageContainerCallbackReceiver callbackReceiv /// /// /// - public AsyncProcessHandle Push(string resourceKey, bool playAnimation, bool stack = true, string pageId = null, - bool loadAsync = true, Action<(string pageId, Page page)> onLoad = null) + public AsyncProcessHandle Push( + string resourceKey, + bool playAnimation, + bool stack = true, + string pageId = null, + bool loadAsync = true, + Action<(string pageId, Page page)> onLoad = null + ) { - return CoroutineManager.Instance.Run(PushRoutine(typeof(Page), resourceKey, playAnimation, stack, onLoad, - loadAsync, pageId)); + return CoroutineManager.Instance.Run(PushRoutine(typeof(Page), + resourceKey, + playAnimation, + stack, + onLoad, + loadAsync, + pageId)); } /// @@ -204,11 +212,23 @@ public AsyncProcessHandle Push(string resourceKey, bool playAnimation, bool stac /// /// /// - public AsyncProcessHandle Push(Type pageType, string resourceKey, bool playAnimation, bool stack = true, - string pageId = null, bool loadAsync = true, Action<(string pageId, Page page)> onLoad = null) + public AsyncProcessHandle Push( + Type pageType, + string resourceKey, + bool playAnimation, + bool stack = true, + string pageId = null, + bool loadAsync = true, + Action<(string pageId, Page page)> onLoad = null + ) { - return CoroutineManager.Instance.Run(PushRoutine(pageType, resourceKey, playAnimation, stack, onLoad, - loadAsync, pageId)); + return CoroutineManager.Instance.Run(PushRoutine(pageType, + resourceKey, + playAnimation, + stack, + onLoad, + loadAsync, + pageId)); } /// @@ -222,12 +242,23 @@ public AsyncProcessHandle Push(Type pageType, string resourceKey, bool playAnima /// /// /// - public AsyncProcessHandle Push(string resourceKey, bool playAnimation, bool stack = true, - string pageId = null, bool loadAsync = true, Action<(string pageId, TPage page)> onLoad = null) + public AsyncProcessHandle Push( + string resourceKey, + bool playAnimation, + bool stack = true, + string pageId = null, + bool loadAsync = true, + Action<(string pageId, TPage page)> onLoad = null + ) where TPage : Page { - return CoroutineManager.Instance.Run(PushRoutine(typeof(TPage), resourceKey, playAnimation, stack, - x => onLoad?.Invoke((x.pageId, (TPage)x.page)), loadAsync, pageId)); + return CoroutineManager.Instance.Run(PushRoutine(typeof(TPage), + resourceKey, + playAnimation, + stack, + x => onLoad?.Invoke((x.pageId, (TPage)x.page)), + loadAsync, + pageId)); } /// @@ -255,7 +286,7 @@ public AsyncProcessHandle Pop(bool playAnimation, string destinationPageId) var pageId = _orderedPageIds[i]; if (pageId == destinationPageId) break; - + popCount++; } @@ -265,267 +296,123 @@ public AsyncProcessHandle Pop(bool playAnimation, string destinationPageId) return CoroutineManager.Instance.Run(PopRoutine(playAnimation, popCount)); } - private IEnumerator PushRoutine(Type pageType, string resourceKey, bool playAnimation, bool stack = true, - Action<(string pageId, Page page)> onLoad = null, bool loadAsync = true, string pageId = null) + private IEnumerator PushRoutine( + Type pageType, + string resourceKey, + bool playAnimation, + bool stack = true, + Action<(string pageId, Page page)> onLoad = null, + bool loadAsync = true, + string pageId = null + ) { - if (resourceKey == null) - throw new ArgumentNullException(nameof(resourceKey)); + Assert.IsNotNull(resourceKey); + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); + _transitionHandler.Begin(); - IsInTransition = true; + pageId ??= Guid.NewGuid().ToString(); - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - foreach (var pageContainer in Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = false; - } - else + Page page = null; + yield return LoadPage(pageType, + resourceKey, + loadAsync, + (p, lh) => { - Interactable = false; - } - } - - // Setup - var assetLoadHandle = loadAsync - ? AssetLoader.LoadAsync(resourceKey) - : AssetLoader.Load(resourceKey); - if (!assetLoadHandle.IsDone) yield return new WaitUntil(() => assetLoadHandle.IsDone); - - if (assetLoadHandle.Status == AssetLoadStatus.Failed) throw assetLoadHandle.OperationException; - - var instance = Instantiate(assetLoadHandle.Result); - if (!instance.TryGetComponent(pageType, out var c)) - c = instance.AddComponent(pageType); - var enterPage = (Page)c; + page = p; + _assetLoadHandles.Add(pageId, lh); + onLoad?.Invoke((pageId, p)); + }); - if (pageId == null) - pageId = Guid.NewGuid().ToString(); - _assetLoadHandles.Add(pageId, assetLoadHandle); - onLoad?.Invoke((pageId, enterPage)); - var afterLoadHandle = enterPage.AfterLoad((RectTransform)transform); - while (!afterLoadHandle.IsTerminated) - yield return null; + var context = PagePushContext.Create(pageId, page, _orderedPageIds, _pages, stack, _isActivePageStacked); - var exitPageId = _orderedPageIds.Count == 0 ? null : _orderedPageIds[_pages.Count - 1]; - var exitPage = exitPageId == null ? null : _pages[exitPageId]; + yield return _lifecycleHandler.AfterLoad(context); - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforePush(enterPage, exitPage); + yield return _lifecycleHandler.BeforePush(context); - var preprocessHandles = new List(); - if (exitPage != null) preprocessHandles.Add(exitPage.BeforeExit(true, enterPage)); + yield return _lifecycleHandler.Push(context, playAnimation); - preprocessHandles.Add(enterPage.BeforeEnter(true, exitPage)); + _lifecycleHandler.AfterPush(context); - foreach (var coroutineHandle in preprocessHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; - - // Play Animations - var animationHandles = new List(); - if (exitPage != null) - animationHandles.Add(exitPage.Exit(true, playAnimation, enterPage)); - - animationHandles.Add(enterPage.Enter(true, playAnimation, exitPage)); - - foreach (var coroutineHandle in animationHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; - - // End Transition - if (!_isActivePageStacked && exitPage != null) + if (context.ShouldRemoveExitPage) { - _pages.Remove(exitPageId); - _orderedPageIds.Remove(exitPageId); + context.ExitPage.gameObject.SetActive(false); + _pages.Remove(context.ExitPageId); + _orderedPageIds.Remove(context.ExitPageId); } - _pages.Add(pageId, enterPage); - _orderedPageIds.Add(pageId); - IsInTransition = false; - - // Postprocess - if (exitPage != null) - exitPage.AfterExit(true, enterPage); + _pages.Add(context.EnterPageId, context.EnterPage); + _orderedPageIds.Add(context.EnterPageId); + _isActivePageStacked = stack; - enterPage.AfterEnter(true, exitPage); + _transitionHandler.End(); - foreach (var callbackReceiver in _callbackReceivers) - callbackReceiver.AfterPush(enterPage, exitPage); + // Resource cleanup isn't tied to the transition itself, so it should be handled asynchronously in the background. + StartCoroutine(AfterPushRoutine(context)); + } - // Unload Unused Page - if (!_isActivePageStacked && exitPage != null) + private IEnumerator AfterPushRoutine( + PagePushContext context + ) + { + yield return _lifecycleHandler.AfterPushRoutine(context); + if (context.ShouldRemoveExitPage) { - var beforeReleaseHandle = exitPage.BeforeRelease(); - while (!beforeReleaseHandle.IsTerminated) yield return null; - - var handle = _assetLoadHandles[exitPageId]; + Destroy(context.ExitPage.gameObject); + var handle = _assetLoadHandles[context.ExitPageId]; AssetLoader.Release(handle); - - Destroy(exitPage.gameObject); - _assetLoadHandles.Remove(exitPageId); - } - - _isActivePageStacked = stack; - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (Instances.All(x => !x.IsInTransition) - && ModalContainer.Instances.All(x => !x.IsInTransition) - && SheetContainer.Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } + _assetLoadHandles.Remove(context.ExitPageId); } } private IEnumerator PopRoutine(bool playAnimation, int popCount = 1) { Assert.IsTrue(popCount >= 1); + Assert.IsTrue(_pages.Count >= popCount, + "Cannot transition because the page count is less than the pop count."); + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); - if (_pages.Count < popCount) - throw new InvalidOperationException( - "Cannot transition because the page count is less than the pop count."); + _transitionHandler.Begin(); - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); + var context = PagePopContext.Create(_orderedPageIds, _pages, popCount); - IsInTransition = true; + yield return _lifecycleHandler.BeforePop(context); - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - foreach (var pageContainer in Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = false; - } - else - { - Interactable = false; - } - } + yield return _lifecycleHandler.Pop(context, playAnimation); - var exitPageId = _orderedPageIds[_orderedPageIds.Count - 1]; - var exitPage = _pages[exitPageId]; + _lifecycleHandler.AfterPop(context); - var unusedPageIds = new List(); - var unusedPages = new List(); - for (var i = _orderedPageIds.Count - 1; i >= _orderedPageIds.Count - popCount; i--) + for (var i = 0; i < context.ExitPageIds.Count; i++) { - var unusedPageId = _orderedPageIds[i]; - unusedPageIds.Add(unusedPageId); - unusedPages.Add(_pages[unusedPageId]); - } - - var enterPageIndex = _orderedPageIds.Count - popCount - 1; - var enterPageId = enterPageIndex < 0 ? null : _orderedPageIds[enterPageIndex]; - var enterPage = enterPageId == null ? null : _pages[enterPageId]; - - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforePop(enterPage, exitPage); - - var preprocessHandles = new List - { - exitPage.BeforeExit(false, enterPage) - }; - if (enterPage != null) preprocessHandles.Add(enterPage.BeforeEnter(false, exitPage)); - - foreach (var coroutineHandle in preprocessHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; - - // Play Animations - var animationHandles = new List - { - exitPage.Exit(false, playAnimation, enterPage) - }; - if (enterPage != null) animationHandles.Add(enterPage.Enter(false, playAnimation, exitPage)); - - foreach (var coroutineHandle in animationHandles) - while (!coroutineHandle.IsTerminated) - yield return coroutineHandle; - - // End Transition - for (var i = 0; i < unusedPageIds.Count; i++) - { - var unusedPageId = unusedPageIds[i]; - _pages.Remove(unusedPageId); + var exitPage = context.ExitPages[i]; + var exitPageId = context.ExitPageIds[i]; + exitPage.gameObject.SetActive(false); + _pages.Remove(exitPageId); _orderedPageIds.RemoveAt(_orderedPageIds.Count - 1); } - IsInTransition = false; - // Postprocess - exitPage.AfterExit(false, enterPage); - if (enterPage != null) enterPage.AfterEnter(false, exitPage); + _isActivePageStacked = true; - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.AfterPop(enterPage, exitPage); + _transitionHandler.End(); - // Unload Unused Page - var beforeReleaseHandle = exitPage.BeforeRelease(); - while (!beforeReleaseHandle.IsTerminated) yield return null; + // Resource cleanup isn't tied to the transition itself, so it should be handled asynchronously in the background. + StartCoroutine(AfterPopRoutine(context)); + } - for (var i = 0; i < unusedPageIds.Count; i++) + private IEnumerator AfterPopRoutine(PagePopContext context) + { + yield return _lifecycleHandler.AfterPopRoutine(context); + for (var i = 0; i < context.ExitPageIds.Count; i++) { - var unusedPageId = unusedPageIds[i]; - var unusedPage = unusedPages[i]; - var loadHandle = _assetLoadHandles[unusedPageId]; + var unusedPageId = context.ExitPageIds[i]; + var unusedPage = context.ExitPages[i]; Destroy(unusedPage.gameObject); + var loadHandle = _assetLoadHandles[unusedPageId]; AssetLoader.Release(loadHandle); _assetLoadHandles.Remove(unusedPageId); } - - _isActivePageStacked = true; - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (Instances.All(x => !x.IsInTransition) - && ModalContainer.Instances.All(x => !x.IsInTransition) - && SheetContainer.Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in SheetContainer.Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } - } } public AsyncProcessHandle Preload(string resourceKey, bool loadAsync = true) @@ -570,5 +457,30 @@ public void ReleasePreloaded(string resourceKey) _preloadedResourceHandles.Remove(resourceKey); AssetLoader.Release(handle); } + + private IEnumerator LoadPage( + Type pageType, + string resourceKey, + bool loadAsync, + Action> onLoaded + ) + { + var assetLoadHandle = loadAsync + ? AssetLoader.LoadAsync(resourceKey) + : AssetLoader.Load(resourceKey); + + if (!assetLoadHandle.IsDone) + yield return new WaitUntil(() => assetLoadHandle.IsDone); + + if (assetLoadHandle.Status == AssetLoadStatus.Failed) + throw assetLoadHandle.OperationException; + + var instance = Instantiate(assetLoadHandle.Result); + if (!instance.TryGetComponent(pageType, out var c)) + c = instance.AddComponent(pageType); + + var page = (Page)c; + onLoaded?.Invoke(page, assetLoadHandle); + } } -} +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs new file mode 100644 index 00000000..fea3b945 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs @@ -0,0 +1,133 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityScreenNavigator.Runtime.Foundation.Coroutine; + +namespace UnityScreenNavigator.Runtime.Core.Page +{ + internal sealed class PageLifecycleHandler + { + private readonly IEnumerable _callbackReceivers; + private readonly RectTransform _containerTransform; + + public PageLifecycleHandler( + RectTransform containerTransform, + IEnumerable callbackReceivers + ) + { + _containerTransform = containerTransform; + _callbackReceivers = callbackReceivers; + } + + public IEnumerator AfterLoad(PagePushContext context) + { + yield return context.EnterPage.AfterLoad(_containerTransform); + } + + public IEnumerator BeforePush(PagePushContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforePush(context.EnterPage, context.ExitPage); + + var handles = new List(); + if (context.ExitPage != null) + handles.Add(context.ExitPage.BeforeExit(true, context.EnterPage)); + + handles.Add(context.EnterPage.BeforeEnter(true, context.ExitPage)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator Push(PagePushContext context, bool playAnimation) + { + var handles = new List(); + + if (context.ExitPage != null) + handles.Add(context.ExitPage.Exit(true, playAnimation, context.EnterPage)); + + handles.Add(context.EnterPage.Enter(true, playAnimation, context.ExitPage)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public void AfterPush(PagePushContext context) + { + if (context.ExitPage != null) + context.ExitPage.AfterExit(true, context.EnterPage); + + context.EnterPage.AfterEnter(true, context.ExitPage); + + foreach (var receiver in _callbackReceivers) + receiver.AfterPush(context.EnterPage, context.ExitPage); + } + + public IEnumerator AfterPushRoutine(PagePushContext context) + { + if (context.IsExitPageStacked || context.ExitPage == null) + yield break; + + var handle = context.ExitPage.BeforeRelease(); + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator BeforePop(PagePopContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforePop(context.EnterPage, context.ExitPage); + + var handles = new List(); + foreach (var exitModal in context.ExitPages) + handles.Add(exitModal.BeforeExit(false, context.EnterPage)); + + if (context.EnterPage != null) + handles.Add(context.EnterPage.BeforeEnter(false, context.ExitPage)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator Pop(PagePopContext context, bool playAnimation) + { + // When popping multiple pages at once, only play the animation for + // the first page exiting (currently visible) and the page that is coming in. + var handles = new List + { + context.ExitPage.Exit(false, playAnimation, context.EnterPage) + }; + + if (context.EnterPage != null) + handles.Add(context.EnterPage.Enter(false, playAnimation, context.ExitPage)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public void AfterPop(PagePopContext context) + { + foreach (var exitModal in context.ExitPages) + exitModal.AfterExit(false, context.EnterPage); + if (context.EnterPage != null) + context.EnterPage.AfterEnter(false, context.ExitPage); + + foreach (var receiver in _callbackReceivers) + receiver.AfterPop(context.EnterPage, context.ExitPage); + } + + public IEnumerator AfterPopRoutine(PagePopContext context) + { + var handles = context.ExitPages.Select(exitModal => exitModal.BeforeRelease()); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs.meta new file mode 100644 index 00000000..cf6b06af --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PageLifecycleHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16b0520c7143940679200c4983b85097 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs new file mode 100644 index 00000000..0d033eeb --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace UnityScreenNavigator.Runtime.Core.Page +{ + internal readonly struct PagePopContext + { + public IReadOnlyList OrderedPageIds { get; } + public IReadOnlyDictionary Pages { get; } + + public IReadOnlyList ExitPageIds { get; } + public IReadOnlyList ExitPages { get; } + + public string ExitPageId => ExitPageIds[0]; + public Page ExitPage => ExitPages[0]; + + public string EnterPageId { get; } + public Page EnterPage { get; } + + private PagePopContext( + IReadOnlyList orderedPageIds, + IReadOnlyDictionary pages, + IReadOnlyList exitPageIds, + IReadOnlyList exitPages, + string enterPageId, + Page enterPage + ) + { + OrderedPageIds = orderedPageIds; + Pages = pages; + ExitPageIds = exitPageIds; + ExitPages = exitPages; + EnterPageId = enterPageId; + EnterPage = enterPage; + } + + public static PagePopContext Create( + IReadOnlyList orderedPageIds, + IReadOnlyDictionary pages, + int popCount + ) + { + var exitPageIds = new List(); + var exitPages = new List(); + + for (var i = orderedPageIds.Count - 1; i >= orderedPageIds.Count - popCount; i--) + { + var id = orderedPageIds[i]; + exitPageIds.Add(id); + exitPages.Add(pages[id]); + } + + var enterIndex = orderedPageIds.Count - popCount - 1; + var enterId = enterIndex >= 0 ? orderedPageIds[enterIndex] : null; + var enter = enterId != null ? pages[enterId] : null; + + return new PagePopContext(orderedPageIds, pages, exitPageIds, exitPages, enterId, enter); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs.meta new file mode 100644 index 00000000..85263dd3 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePopContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f71bc70776544b5adac863185df7e0e +timeCreated: 1746283660 \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs new file mode 100644 index 00000000..274275db --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace UnityScreenNavigator.Runtime.Core.Page +{ + internal readonly struct PagePushContext + { + public string EnterPageId { get; } + public Page EnterPage { get; } + + public string ExitPageId { get; } + public Page ExitPage { get; } + + public bool Stack { get; } + public bool IsExitPageStacked { get; } + public bool ShouldRemoveExitPage => ExitPage != null && !IsExitPageStacked; + + private PagePushContext( + string enterPageId, + Page enterPage, + string exitPageId, + Page exitPage, + bool stack, + bool isExitPageStacked + ) + { + EnterPageId = enterPageId; + EnterPage = enterPage; + ExitPageId = exitPageId; + ExitPage = exitPage; + Stack = stack; + IsExitPageStacked = isExitPageStacked; + } + + public static PagePushContext Create( + string pageId, + Page enterPage, + List orderedPageIds, + Dictionary pages, + bool stack, + bool isExitPageStacked + ) + { + var hasExit = orderedPageIds.Count > 0; + var exitPageId = hasExit ? orderedPageIds[orderedPageIds.Count - 1] : null; + + var exitPage = hasExit ? pages[exitPageId] : null; + + return new PagePushContext(pageId, enterPage, exitPageId, exitPage, stack, isExitPageStacked); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs.meta new file mode 100644 index 00000000..c546e93b --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Page/PagePushContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4fc775663e3d45aa885f09c7e9554718 +timeCreated: 1746284083 \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs b/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs new file mode 100644 index 00000000..3a8b83da --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs @@ -0,0 +1,9 @@ +namespace UnityScreenNavigator.Runtime.Core.Shared +{ + public interface IScreenContainer + { + bool IsInTransition { get; } + + bool Interactable { get; set; } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs.meta new file mode 100644 index 00000000..5c5e5b69 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Shared/IScreenContainer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 036e8d8e1309e472b8c9fea94a6cb545 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs b/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs new file mode 100644 index 00000000..cb639edb --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using UnityScreenNavigator.Runtime.Core.Modal; +using UnityScreenNavigator.Runtime.Core.Page; +using UnityScreenNavigator.Runtime.Core.Sheet; + +namespace UnityScreenNavigator.Runtime.Core.Shared +{ + public sealed class ScreenContainerTransitionHandler + { + private readonly IScreenContainer _container; + public bool IsInTransition { get; private set; } + + public ScreenContainerTransitionHandler(IScreenContainer container) + { + _container = container; + } + + public void Begin() + { + if (IsInTransition) + throw new InvalidOperationException("Transition already in progress."); + + IsInTransition = true; + + if (UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) + return; + + if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) + { + if (!AllContainersFinishedTransition()) + return; + + SetAllContainersInteractable(false); + } + else + { + _container.Interactable = false; + } + } + + public void End() + { + if (!IsInTransition) + throw new InvalidOperationException("Transition has not started."); + + IsInTransition = false; + + if (UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) + return; + + if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) + { + if (!AllContainersFinishedTransition()) + return; + + SetAllContainersInteractable(true); + } + else + { + _container.Interactable = true; + } + } + + private bool AllContainersFinishedTransition() + { + return PageContainer.Instances.All(x => !x.IsInTransition) + && ModalContainer.Instances.All(x => !x.IsInTransition) + && SheetContainer.Instances.All(x => !x.IsInTransition); + } + + private void SetAllContainersInteractable(bool value) + { + foreach (var container in PageContainer.Instances) + container.Interactable = value; + foreach (var container in ModalContainer.Instances) + container.Interactable = value; + foreach (var container in SheetContainer.Instances) + container.Interactable = value; + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs.meta new file mode 100644 index 00000000..b036e38d --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Shared/ScreenContainerTransitionHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b51f185da636d47268b1438b9f9a4f2c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetContainer.cs b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetContainer.cs index 994e4962..e3f0ba55 100644 --- a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetContainer.cs +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetContainer.cs @@ -1,11 +1,9 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; +using UnityEngine.Assertions; using UnityEngine.UI; -using UnityScreenNavigator.Runtime.Core.Modal; -using UnityScreenNavigator.Runtime.Core.Page; using UnityScreenNavigator.Runtime.Core.Shared; using UnityScreenNavigator.Runtime.Foundation; using UnityScreenNavigator.Runtime.Foundation.AssetLoader; @@ -14,7 +12,7 @@ namespace UnityScreenNavigator.Runtime.Core.Sheet { [RequireComponent(typeof(RectMask2D))] - public sealed class SheetContainer : MonoBehaviour + public sealed class SheetContainer : MonoBehaviour, IScreenContainer { private static readonly Dictionary InstanceCacheByTransform = new Dictionary(); @@ -37,6 +35,9 @@ private readonly Dictionary> _assetLoadHandl private CanvasGroup _canvasGroup; public static List Instances { get; } = new List(); + private ScreenContainerTransitionHandler _transitionHandler; + private SheetLifecycleHandler _lifecycleHandler; + /// /// By default, in is used. /// If this property is set, it is used instead. @@ -63,7 +64,7 @@ public Sheet ActiveSheet /// /// True if in transition. /// - public bool IsInTransition { get; private set; } + public bool IsInTransition => _transitionHandler.IsInTransition; /// /// Registered sheets. @@ -84,6 +85,8 @@ private void Awake() if (!string.IsNullOrWhiteSpace(_name)) InstanceCacheByName.Add(_name, this); _canvasGroup = gameObject.GetOrAddComponent(); + _transitionHandler = new ScreenContainerTransitionHandler(this); + _lifecycleHandler = new SheetLifecycleHandler((RectTransform)transform, _callbackReceivers); } private void OnDestroy() @@ -247,30 +250,28 @@ public AsyncProcessHandle Register(string resourceKey, private IEnumerator RegisterRoutine(Type sheetType, string resourceKey, Action<(string sheetId, Sheet sheet)> onLoad = null, bool loadAsync = true, string sheetId = null) { - if (resourceKey == null) throw new ArgumentNullException(nameof(resourceKey)); + Assert.IsNotNull(resourceKey); - var assetLoadHandle = loadAsync - ? AssetLoader.LoadAsync(resourceKey) - : AssetLoader.Load(resourceKey); - while (!assetLoadHandle.IsDone) yield return null; + sheetId ??= Guid.NewGuid().ToString(); - if (assetLoadHandle.Status == AssetLoadStatus.Failed) throw assetLoadHandle.OperationException; + Sheet sheet = null; + yield return LoadSheet(sheetType, + resourceKey, + loadAsync, + (s, lh) => + { + sheet = s; + _sheets.Add(sheetId, sheet); + _sheetNameToId[resourceKey] = sheetId; + _assetLoadHandles.Add(sheetId, lh); + onLoad?.Invoke((sheetId, s)); + }); - var instance = Instantiate(assetLoadHandle.Result); - if (!instance.TryGetComponent(sheetType, out var c)) - c = instance.AddComponent(sheetType); - var sheet = (Sheet)c; + var context = new SheetRegisterContext(sheetId, sheet); - if (sheetId == null) - sheetId = Guid.NewGuid().ToString(); - _sheets.Add(sheetId, sheet); - _sheetNameToId[resourceKey] = sheetId; - _assetLoadHandles.Add(sheetId, assetLoadHandle); - onLoad?.Invoke((sheetId, sheet)); - var afterLoadHandle = sheet.AfterLoad((RectTransform)transform); - while (!afterLoadHandle.IsTerminated) yield return null; + yield return _lifecycleHandler.AfterLoad(context); - yield return sheetId; + yield return context.SheetId; } private IEnumerator ShowByResourceKeyRoutine(string resourceKey, bool playAnimation) @@ -281,165 +282,46 @@ private IEnumerator ShowByResourceKeyRoutine(string resourceKey, bool playAnimat private IEnumerator ShowRoutine(string sheetId, bool playAnimation) { - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); - - if (ActiveSheetId != null && ActiveSheetId == sheetId) - throw new InvalidOperationException( - "Cannot transition because the sheet is already active."); - - IsInTransition = true; - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in Instances) - sheetContainer.Interactable = false; - } - else - { - Interactable = false; - } - } - - var enterSheet = _sheets[sheetId]; - var exitSheet = ActiveSheetId != null ? _sheets[ActiveSheetId] : null; + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); + Assert.IsFalse(ActiveSheetId != null && ActiveSheetId == sheetId, + "Cannot transition because the sheet is already active."); - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforeShow(enterSheet, exitSheet); + var context = SheetShowContext.Create(sheetId, ActiveSheetId, _sheets); - var preprocessHandles = new List(); - if (exitSheet != null) preprocessHandles.Add(exitSheet.BeforeExit(enterSheet)); + _transitionHandler.Begin(); - preprocessHandles.Add(enterSheet.BeforeEnter(exitSheet)); - foreach (var coroutineHandle in preprocessHandles) - while (!coroutineHandle.IsTerminated) - yield return null; + yield return _lifecycleHandler.BeforeShow(context); - // Play Animation - var animationHandles = new List(); - if (exitSheet != null) animationHandles.Add(exitSheet.Exit(playAnimation, enterSheet)); + yield return _lifecycleHandler.Show(context, playAnimation); - animationHandles.Add(enterSheet.Enter(playAnimation, exitSheet)); + _lifecycleHandler.AfterShow(context); - foreach (var handle in animationHandles) - while (!handle.IsTerminated) - yield return null; - - // End Transition ActiveSheetId = sheetId; - IsInTransition = false; - - // Postprocess - if (exitSheet != null) exitSheet.AfterExit(enterSheet); - - enterSheet.AfterEnter(exitSheet); - - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.AfterShow(enterSheet, exitSheet); - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (PageContainer.Instances.All(x => !x.IsInTransition) - && ModalContainer.Instances.All(x => !x.IsInTransition) - && Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } - } + + _transitionHandler.End(); } private IEnumerator HideRoutine(bool playAnimation) { - if (IsInTransition) - throw new InvalidOperationException( - "Cannot transition because the screen is already in transition."); + Assert.IsFalse(IsInTransition, + "Cannot transition because the screen is already in transition."); + Assert.IsNotNull(ActiveSheetId, + "Cannot transition because there is no active sheets."); - if (ActiveSheetId == null) - throw new InvalidOperationException( - "Cannot transition because there is no active sheets."); + _transitionHandler.Begin(); - IsInTransition = true; + var context = SheetHideContext.Create(_sheets[ActiveSheetId]); - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = false; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = false; - foreach (var sheetContainer in Instances) - sheetContainer.Interactable = false; - } - else - { - Interactable = false; - } - } + yield return _lifecycleHandler.BeforeHide(context); - var exitSheet = _sheets[ActiveSheetId]; + yield return _lifecycleHandler.Hide(context, playAnimation); - // Preprocess - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.BeforeHide(exitSheet); + _lifecycleHandler.AfterHide(context); - var preprocessHandle = exitSheet.BeforeExit(null); - while (!preprocessHandle.IsTerminated) yield return preprocessHandle; - - // Play Animation - var animationHandle = exitSheet.Exit(playAnimation, null); - while (!animationHandle.IsTerminated) yield return null; - - // End Transition ActiveSheetId = null; - IsInTransition = false; - - // Postprocess - exitSheet.AfterExit(null); - foreach (var callbackReceiver in _callbackReceivers) callbackReceiver.AfterHide(exitSheet); - - if (!UnityScreenNavigatorSettings.Instance.EnableInteractionInTransition) - { - if (UnityScreenNavigatorSettings.Instance.ControlInteractionsOfAllContainers) - { - // If there's a container in transition, it should restore Interactive to true when the transition is finished. - // So, do nothing here if there's a transitioning container. - if (PageContainer.Instances.All(x => !x.IsInTransition) - && ModalContainer.Instances.All(x => !x.IsInTransition) - && Instances.All(x => !x.IsInTransition)) - { - foreach (var pageContainer in PageContainer.Instances) - pageContainer.Interactable = true; - foreach (var modalContainer in ModalContainer.Instances) - modalContainer.Interactable = true; - foreach (var sheetContainer in Instances) - sheetContainer.Interactable = true; - } - } - else - { - Interactable = true; - } - } + + _transitionHandler.End(); } /// @@ -459,5 +341,30 @@ public void UnregisterAll() _assetLoadHandles.Clear(); } + + private IEnumerator LoadSheet( + Type sheetType, + string resourceKey, + bool loadAsync, + Action> onLoaded + ) + { + var assetLoadHandle = loadAsync + ? AssetLoader.LoadAsync(resourceKey) + : AssetLoader.Load(resourceKey); + + if (!assetLoadHandle.IsDone) + yield return new WaitUntil(() => assetLoadHandle.IsDone); + + if (assetLoadHandle.Status == AssetLoadStatus.Failed) + throw assetLoadHandle.OperationException; + + var instance = Instantiate(assetLoadHandle.Result); + if (!instance.TryGetComponent(sheetType, out var c)) + c = instance.AddComponent(sheetType); + + var sheet = (Sheet)c; + onLoaded?.Invoke(sheet, assetLoadHandle); + } } } diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs new file mode 100644 index 00000000..917ec395 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs @@ -0,0 +1,17 @@ +namespace UnityScreenNavigator.Runtime.Core.Sheet +{ + public sealed class SheetHideContext + { + private SheetHideContext(Sheet exitSheet) + { + ExitSheet = exitSheet; + } + + public Sheet ExitSheet { get; } + + public static SheetHideContext Create(Sheet exitSheet) + { + return new SheetHideContext(exitSheet); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs.meta new file mode 100644 index 00000000..e5aaf1e6 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetHideContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3993e7639afe4e9e9de1f5c62ef11e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs new file mode 100644 index 00000000..5c61d118 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs @@ -0,0 +1,98 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityScreenNavigator.Runtime.Foundation.Coroutine; + +namespace UnityScreenNavigator.Runtime.Core.Sheet +{ + internal sealed class SheetLifecycleHandler + { + private readonly RectTransform _containerTransform; + private readonly IEnumerable _callbackReceivers; + + public SheetLifecycleHandler( + RectTransform containerTransform, + IEnumerable callbackReceivers + ) + { + _containerTransform = containerTransform; + _callbackReceivers = callbackReceivers; + } + + public IEnumerator AfterLoad(SheetRegisterContext context) + { + yield return context.Sheet.AfterLoad(_containerTransform); + } + + public IEnumerator BeforeShow(SheetShowContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforeShow(context.EnterSheet, context.ExitSheet); + + var handles = new List(); + if (context.ExitSheet != null) + handles.Add(context.ExitSheet.BeforeExit(context.EnterSheet)); + + handles.Add(context.EnterSheet.BeforeEnter(context.ExitSheet)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return null; + } + + public IEnumerator Show(SheetShowContext context, bool playAnimation) + { + var handles = new List(); + + if (context.ExitSheet != null) + handles.Add(context.ExitSheet.Exit(playAnimation, context.EnterSheet)); + + handles.Add(context.EnterSheet.Enter(playAnimation, context.ExitSheet)); + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public void AfterShow(SheetShowContext context) + { + if (context.ExitSheet != null) + context.ExitSheet.AfterExit(context.EnterSheet); + + context.EnterSheet.AfterEnter(context.ExitSheet); + + foreach (var receiver in _callbackReceivers) + receiver.AfterShow(context.EnterSheet, context.ExitSheet); + } + + public IEnumerator BeforeHide(SheetHideContext context) + { + foreach (var receiver in _callbackReceivers) + receiver.BeforeHide(context.ExitSheet); + + var handles = new List + { + context.ExitSheet.BeforeExit(null) + }; + + foreach (var handle in handles) + while (!handle.IsTerminated) + yield return handle; + } + + public IEnumerator Hide(SheetHideContext context, bool playAnimation) + { + var handle = context.ExitSheet.Exit(playAnimation, null); + while (!handle.IsTerminated) + yield return null; + } + + public void AfterHide(SheetHideContext context) + { + context.ExitSheet.AfterExit(null); + + foreach (var receiver in _callbackReceivers) + receiver.AfterHide(context.ExitSheet); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs.meta new file mode 100644 index 00000000..8524d092 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetLifecycleHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e04331e9ae77471ebee0ac96e27654a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs new file mode 100644 index 00000000..852e6291 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs @@ -0,0 +1,14 @@ +namespace UnityScreenNavigator.Runtime.Core.Sheet +{ + public sealed class SheetRegisterContext + { + public string SheetId { get; } + public Sheet Sheet { get; private set; } + + public SheetRegisterContext(string sheetId, Sheet sheet) + { + SheetId = sheetId; + Sheet = sheet; + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs.meta new file mode 100644 index 00000000..17e4e7d1 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetRegisterContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 51566455142bf44b8b0b656bd6660211 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs new file mode 100644 index 00000000..a16bb01f --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace UnityScreenNavigator.Runtime.Core.Sheet +{ + internal readonly struct SheetShowContext + { + public string SheetId { get; } + public Sheet EnterSheet { get; } + + public string ExitSheetId { get; } + public Sheet ExitSheet { get; } + + private SheetShowContext(string sheetId, Sheet enterSheet, string exitSheetId, Sheet exitSheet) + { + SheetId = sheetId; + EnterSheet = enterSheet; + ExitSheetId = exitSheetId; + ExitSheet = exitSheet; + } + + public static SheetShowContext Create( + string sheetId, + string currentSheetId, + Dictionary sheets + ) + { + var enterSheet = sheets[sheetId]; + var exitSheetId = currentSheetId; + var exitSheet = exitSheetId != null ? sheets[exitSheetId] : null; + + return new SheetShowContext(sheetId, enterSheet, exitSheetId, exitSheet); + } + } +} \ No newline at end of file diff --git a/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs.meta b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs.meta new file mode 100644 index 00000000..7e2f0208 --- /dev/null +++ b/Assets/UnityScreenNavigator/Runtime/Core/Sheet/SheetShowContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4e2b9c8ecf5441cb9e6fcf3a065bfde6 +timeCreated: 1746286045 \ No newline at end of file