2 * Copyright(c) 2021 Samsung Electronics Co., Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 using System.Collections.Generic;
20 using System.ComponentModel;
21 using Tizen.NUI.BaseComponents;
23 namespace Tizen.NUI.Components
26 /// PoppedEventArgs is a class to record <see cref="Navigator.Popped"/> event arguments which will be sent to user.
28 /// <since_tizen> 9 </since_tizen>
29 public class PoppedEventArgs : EventArgs
32 /// Page popped by Navigator.
34 /// <since_tizen> 9 </since_tizen>
35 public Page Page { get; internal set; }
39 /// The Navigator is a class which navigates pages with stack methods such as Push and Pop.
42 /// With Transition class, Navigator supports smooth transition of View pair between two Pages
43 /// by using <see cref="PushWithTransition(Page)"/> and <see cref="PopWithTransition()"/> methods.
44 /// If current top Page and next top Page have <see cref="View"/>s those have same TransitionTag,
45 /// Navigator creates smooth transition motion for them.
46 /// Navigator.Transition property can be used to set properties of the Transition such as TimePeriod and AlphaFunction.
47 /// When all transitions are finished, Navigator calls a callback methods those connected on the "TransitionFinished" event.
51 /// Navigator navigator = new Navigator()
53 /// TimePeriod = new TimePeriod(500),
54 /// AlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.EaseInOutSine)
57 /// View view = new View()
59 /// TransitionOptions = new TransitionOptions()
61 /// /* Set properties for the transition of this View */
65 /// ContentPage newPage = new ContentPage()
70 /// Navigator.PushWithTransition(newPage);
73 /// <since_tizen> 9 </since_tizen>
74 public class Navigator : Control
76 private const int DefaultTransitionDuration = 500;
78 //This will be replaced with view transition class instance.
79 private Animation curAnimation = null;
81 //This will be replaced with view transition class instance.
82 private Animation newAnimation = null;
84 private TransitionSet transitionSet = null;
86 private Transition transition = new Transition()
88 TimePeriod = new TimePeriod(DefaultTransitionDuration),
89 AlphaFunction = new AlphaFunction(AlphaFunction.BuiltinFunctions.Default),
92 private bool transitionFinished = true;
94 //TODO: Needs to consider how to remove disposed window from dictionary.
95 //Two dictionaries are required to remove disposed navigator from dictionary.
96 private static Dictionary<Window, Navigator> windowNavigator = new Dictionary<Window, Navigator>();
97 private static Dictionary<Navigator, Window> navigatorWindow = new Dictionary<Navigator, Window>();
99 private List<Page> navigationPages = new List<Page>();
102 /// Creates a new instance of a Navigator.
104 /// <since_tizen> 9 </since_tizen>
105 public Navigator() : base()
107 Layout = new AbsoluteLayout();
111 [EditorBrowsable(EditorBrowsableState.Never)]
112 public override void OnInitialize()
116 SetAccessibilityConstructor(Role.PageTabList);
120 /// An event fired when Transition has been finished.
122 /// <since_tizen> 9 </since_tizen>
123 public event EventHandler<EventArgs> TransitionFinished;
126 /// An event fired when Pop of a page has been finished.
129 /// When you free resources in the Popped event handler, please make sure if the popped page is the page you find.
131 /// <since_tizen> 9 </since_tizen>
132 public event EventHandler<PoppedEventArgs> Popped;
135 /// Returns the count of pages in Navigator.
137 /// <since_tizen> 9 </since_tizen>
138 public int PageCount => navigationPages.Count;
141 /// Transition properties for the transition of View pair having same transition tag.
143 /// <since_tizen> 9 </since_tizen>
144 public Transition Transition
157 /// Pushes a page to Navigator.
158 /// If the page is already in Navigator, then it is not pushed.
160 /// <param name="page">The page to push to Navigator.</param>
161 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
162 /// <since_tizen> 9 </since_tizen>
163 public void PushWithTransition(Page page)
165 if (!transitionFinished)
167 Tizen.Log.Error("NUI", "Transition is still not finished.\n");
173 throw new ArgumentNullException(nameof(page), "page should not be null.");
176 //Duplicate page is not pushed.
177 if (navigationPages.Contains(page)) return;
179 var topPage = Peek();
187 navigationPages.Add(page);
189 page.Navigator = this;
192 page.InvokeAppearing();
193 topPage.InvokeDisappearing();
195 transitionSet = CreateTransitions(topPage, page, true);
196 transitionSet.Finished += (object sender, EventArgs e) =>
198 if (page is DialogPage == false)
200 topPage.SetVisible(false);
204 page.InvokeAppeared();
205 topPage.InvokeDisappeared();
207 transitionFinished = false;
211 /// Pops the top page from Navigator.
213 /// <returns>The popped page.</returns>
214 /// <exception cref="InvalidOperationException">Thrown when there is no page in Navigator.</exception>
215 /// <since_tizen> 9 </since_tizen>
216 public Page PopWithTransition()
218 if (!transitionFinished)
220 Tizen.Log.Error("NUI", "Transition is still not finished.\n");
224 if (navigationPages.Count == 0)
226 throw new InvalidOperationException("There is no page in Navigator.");
229 var topPage = Peek();
231 if (navigationPages.Count == 1)
235 //Invoke Popped event
236 Popped?.Invoke(this, new PoppedEventArgs() { Page = topPage });
240 var newTopPage = navigationPages[navigationPages.Count - 2];
243 newTopPage.InvokeAppearing();
244 topPage.InvokeDisappearing();
246 transitionSet = CreateTransitions(topPage, newTopPage, false);
247 transitionSet.Finished += (object sender, EventArgs e) =>
250 topPage.SetVisible(true);
253 newTopPage.InvokeAppeared();
254 topPage.InvokeDisappeared();
256 //Invoke Popped event
257 Popped?.Invoke(this, new PoppedEventArgs() { Page = topPage });
259 transitionFinished = false;
265 /// Pushes a page to Navigator.
266 /// If the page is already in Navigator, then it is not pushed.
268 /// <param name="page">The page to push to Navigator.</param>
269 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
270 /// <since_tizen> 9 </since_tizen>
271 public void Push(Page page)
273 if (!transitionFinished)
275 Tizen.Log.Error("NUI", "Transition is still not finished.\n");
281 throw new ArgumentNullException(nameof(page), "page should not be null.");
284 //Duplicate page is not pushed.
285 if (navigationPages.Contains(page)) return;
295 navigationPages.Add(page);
297 page.Navigator = this;
300 page.InvokeAppearing();
301 curTop.InvokeDisappearing();
303 //TODO: The following transition codes will be replaced with view transition.
304 InitializeAnimation();
306 if (page is DialogPage == false)
308 curAnimation = new Animation(1000);
309 curAnimation.AnimateTo(curTop, "Opacity", 1.0f, 0, 1000);
310 curAnimation.EndAction = Animation.EndActions.StopFinal;
311 curAnimation.Finished += (object sender, EventArgs args) =>
313 curTop.SetVisible(false);
316 curTop.InvokeDisappeared();
321 page.SetVisible(true);
322 newAnimation = new Animation(1000);
323 newAnimation.AnimateTo(page, "Opacity", 1.0f, 0, 1000);
324 newAnimation.EndAction = Animation.EndActions.StopFinal;
325 newAnimation.Finished += (object sender, EventArgs e) =>
328 page.InvokeAppeared();
335 /// Pops the top page from Navigator.
337 /// <returns>The popped page.</returns>
338 /// <exception cref="InvalidOperationException">Thrown when there is no page in Navigator.</exception>
339 /// <since_tizen> 9 </since_tizen>
342 if (!transitionFinished)
344 Tizen.Log.Error("NUI", "Transition is still not finished.\n");
348 if (navigationPages.Count == 0)
350 throw new InvalidOperationException("There is no page in Navigator.");
355 if (navigationPages.Count == 1)
359 //Invoke Popped event
360 Popped?.Invoke(this, new PoppedEventArgs() { Page = curTop });
365 var newTop = navigationPages[navigationPages.Count - 2];
368 newTop.InvokeAppearing();
369 curTop.InvokeDisappearing();
371 //TODO: The following transition codes will be replaced with view transition.
372 InitializeAnimation();
374 if (curTop is DialogPage == false)
376 curAnimation = new Animation(1000);
377 curAnimation.AnimateTo(curTop, "Opacity", 0.0f, 0, 1000);
378 curAnimation.EndAction = Animation.EndActions.StopFinal;
379 curAnimation.Finished += (object sender, EventArgs e) =>
381 //Removes the current top page after transition is finished.
383 curTop.Opacity = 1.0f;
386 curTop.InvokeDisappeared();
388 //Invoke Popped event
389 Popped?.Invoke(this, new PoppedEventArgs() { Page = curTop });
393 newTop.Opacity = 1.0f;
394 newTop.SetVisible(true);
395 newAnimation = new Animation(1000);
396 newAnimation.AnimateTo(newTop, "Opacity", 1.0f, 0, 1000);
397 newAnimation.EndAction = Animation.EndActions.StopFinal;
398 newAnimation.Finished += (object sender, EventArgs e) =>
401 newTop.InvokeAppeared();
414 /// Returns the page of the given index in Navigator.
415 /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator.
416 /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs.
418 /// <param name="index">The index of a page in Navigator.</param>
419 /// <returns>The page of the given index in Navigator.</returns>
420 /// <exception cref="ArgumentOutOfRangeException">Thrown when the argument index is less than 0, or greater than the number of pages.</exception>
421 public Page GetPage(int index)
423 if ((index < 0) || (index > navigationPages.Count))
425 throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than or equal to the number of pages.");
428 return navigationPages[index];
432 /// Returns the current index of the given page in Navigator.
433 /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator.
434 /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs.
436 /// <param name="page">The page in Navigator.</param>
437 /// <returns>The index of the given page in Navigator. If the given page is not in the Navigator, then -1 is returned.</returns>
438 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
439 /// <since_tizen> 9 </since_tizen>
440 public int IndexOf(Page page)
444 throw new ArgumentNullException(nameof(page), "page should not be null.");
447 for (int i = 0; i < navigationPages.Count; i++)
449 if (navigationPages[i] == page)
459 /// Inserts a page at the specified index of Navigator.
460 /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator.
461 /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs.
462 /// To find the current index of a page in Navigator, please use IndexOf(page).
463 /// If the page is already in Navigator, then it is not inserted.
465 /// <param name="index">The index of a page in Navigator where the page will be inserted.</param>
466 /// <param name="page">The page to insert to Navigator.</param>
467 /// <exception cref="ArgumentOutOfRangeException">Thrown when the argument index is less than 0, or greater than the number of pages.</exception>
468 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
469 /// <since_tizen> 9 </since_tizen>
470 public void Insert(int index, Page page)
472 if ((index < 0) || (index > navigationPages.Count))
474 throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than or equal to the number of pages.");
479 throw new ArgumentNullException(nameof(page), "page should not be null.");
482 //Duplicate page is not pushed.
483 if (navigationPages.Contains(page)) return;
485 //TODO: The following transition codes will be replaced with view transition.
486 InitializeAnimation();
488 if (index == PageCount)
491 page.SetVisible(true);
495 page.SetVisible(false);
499 navigationPages.Insert(index, page);
501 page.Navigator = this;
505 /// Inserts a page to Navigator before an existing page.
506 /// If the page is already in Navigator, then it is not inserted.
508 /// <param name="before">The existing page, before which a page will be inserted.</param>
509 /// <param name="page">The page to insert to Navigator.</param>
510 /// <exception cref="ArgumentNullException">Thrown when the argument before is null.</exception>
511 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
512 /// <exception cref="ArgumentException">Thrown when the argument before does not exist in Navigator.</exception>
513 /// <since_tizen> 9 </since_tizen>
514 public void InsertBefore(Page before, Page page)
518 throw new ArgumentNullException(nameof(before), "before should not be null.");
523 throw new ArgumentNullException(nameof(page), "page should not be null.");
526 //Find the index of before page.
527 int beforeIndex = navigationPages.FindIndex(x => x == before);
529 //before does not exist in Navigator.
530 if (beforeIndex == -1)
532 throw new ArgumentException("before does not exist in Navigator.", nameof(before));
535 Insert(beforeIndex, page);
539 /// Removes a page from Navigator.
541 /// <param name="page">The page to remove from Navigator.</param>
542 /// <exception cref="ArgumentNullException">Thrown when the argument page is null.</exception>
543 /// <since_tizen> 9 </since_tizen>
544 public void Remove(Page page)
548 throw new ArgumentNullException(nameof(page), "page should not be null.");
551 //TODO: The following transition codes will be replaced with view transition.
552 InitializeAnimation();
554 if ((page == Peek()) && (PageCount >= 2))
556 navigationPages[PageCount - 2].Opacity = 1.0f;
557 navigationPages[PageCount - 2].SetVisible(true);
560 page.Navigator = null;
561 navigationPages.Remove(page);
566 /// Removes a page at the specified index of Navigator.
567 /// The indices of pages in Navigator are basically the order of pushing or inserting to Navigator.
568 /// So a page's index in Navigator can be changed whenever push/insert or pop/remove occurs.
569 /// To find the current index of a page in Navigator, please use IndexOf(page).
571 /// <param name="index">The index of a page in Navigator where the page will be removed.</param>
572 /// <exception cref="ArgumentOutOfRangeException">Thrown when the index is less than 0, or greater than or equal to the number of pages.</exception>
573 /// <since_tizen> 9 </since_tizen>
574 public void RemoveAt(int index)
576 if ((index < 0) || (index >= navigationPages.Count))
578 throw new ArgumentOutOfRangeException(nameof(index), "index should be greater than or equal to 0, and less than the number of pages.");
581 Remove(navigationPages[index]);
585 /// Returns the page at the top of Navigator.
587 /// <returns>The page at the top of Navigator.</returns>
588 /// <since_tizen> 9 </since_tizen>
591 if (navigationPages.Count == 0) return null;
593 return navigationPages[navigationPages.Count - 1];
597 /// Disposes Navigator and all children on it.
599 /// <param name="type">Dispose type.</param>
600 [EditorBrowsable(EditorBrowsableState.Never)]
601 protected override void Dispose(DisposeTypes type)
608 if (type == DisposeTypes.Explicit)
610 foreach (Page page in navigationPages)
612 Utility.Dispose(page);
614 navigationPages.Clear();
618 if (navigatorWindow.TryGetValue(this, out window) == true)
620 navigatorWindow.Remove(this);
621 windowNavigator.Remove(window);
629 /// Returns the default navigator of the given window.
631 /// <returns>The default navigator of the given window.</returns>
632 /// <exception cref="ArgumentNullException">Thrown when the argument window is null.</exception>
633 /// <since_tizen> 9 </since_tizen>
634 public static Navigator GetDefaultNavigator(Window window)
638 throw new ArgumentNullException(nameof(window), "window should not be null.");
641 if (windowNavigator.ContainsKey(window) == true)
643 return windowNavigator[window];
646 var defaultNavigator = new Navigator();
647 defaultNavigator.WidthResizePolicy = ResizePolicyType.FillToParent;
648 defaultNavigator.HeightResizePolicy = ResizePolicyType.FillToParent;
649 window.Add(defaultNavigator);
650 windowNavigator.Add(window, defaultNavigator);
651 navigatorWindow.Add(defaultNavigator, window);
653 return defaultNavigator;
657 /// Create Transitions between currentTopPage and newTopPage
659 /// <param name="currentTopPage">The top page of Navigator.</param>
660 /// <param name="newTopPage">The new top page after transition.</param>
661 /// <param name="pushTransition">True if this transition is for push new page</param>
662 private TransitionSet CreateTransitions(Page currentTopPage, Page newTopPage, bool pushTransition)
664 currentTopPage.SetVisible(true);
665 newTopPage.SetVisible(true);
667 List<View> taggedViewsInNewTopPage = new List<View>();
668 RetrieveTaggedViews(taggedViewsInNewTopPage, newTopPage, true);
669 List<View> taggedViewsInCurrentTopPage = new List<View>();
670 RetrieveTaggedViews(taggedViewsInCurrentTopPage, currentTopPage, true);
672 List<KeyValuePair<View, View>> sameTaggedViewPair = new List<KeyValuePair<View, View>>();
673 foreach(View currentTopPageView in taggedViewsInCurrentTopPage)
675 bool findPair = false;
676 foreach(View newTopPageView in taggedViewsInNewTopPage)
678 if((currentTopPageView.TransitionOptions != null) && (newTopPageView.TransitionOptions != null) &&
679 currentTopPageView.TransitionOptions?.TransitionTag == newTopPageView.TransitionOptions?.TransitionTag)
681 sameTaggedViewPair.Add(new KeyValuePair<View, View>(currentTopPageView, newTopPageView));
688 taggedViewsInNewTopPage.Remove(sameTaggedViewPair[sameTaggedViewPair.Count - 1].Value);
691 foreach(KeyValuePair<View, View> pair in sameTaggedViewPair)
693 taggedViewsInCurrentTopPage.Remove(pair.Key);
696 TransitionSet newTransitionSet = new TransitionSet();
697 foreach(KeyValuePair<View, View> pair in sameTaggedViewPair)
699 TransitionItem pairTransition = transition.CreateTransition(pair.Key, pair.Value, pushTransition);
700 if(pair.Value.TransitionOptions?.TransitionWithChild ?? false)
702 pairTransition.TransitionWithChild = true;
704 newTransitionSet.AddTransition(pairTransition);
707 newTransitionSet.Finished += (object sender, EventArgs e) =>
709 if(newTopPage.Layout != null)
711 newTopPage.Layout.RequestLayout();
713 if(currentTopPage.Layout != null)
715 currentTopPage.Layout.RequestLayout();
717 transitionFinished = true;
718 InvokeTransitionFinished();
719 transitionSet.Dispose();
720 currentTopPage.Opacity = 1.0f;
723 if (!pushTransition || newTopPage is DialogPage == false)
725 View transitionView = (currentTopPage is ContentPage) ? (currentTopPage as ContentPage).Content : (currentTopPage as DialogPage).Content;
726 if (currentTopPage.DisappearingTransition != null && transitionView != null)
728 TransitionItemBase disappearingTransition = currentTopPage.DisappearingTransition.CreateTransition(transitionView, false);
729 disappearingTransition.TransitionWithChild = true;
730 newTransitionSet.AddTransition(disappearingTransition);
734 currentTopPage.SetVisible(false);
737 if (pushTransition || currentTopPage is DialogPage == false)
739 View transitionView = (newTopPage is ContentPage) ? (newTopPage as ContentPage).Content : (newTopPage as DialogPage).Content;
740 if (newTopPage.AppearingTransition != null && transitionView != null)
742 TransitionItemBase appearingTransition = newTopPage.AppearingTransition.CreateTransition(transitionView, true);
743 appearingTransition.TransitionWithChild = true;
744 newTransitionSet.AddTransition(appearingTransition);
748 newTransitionSet.Play();
750 return newTransitionSet;
754 /// Retrieve Tagged Views in the view tree.
756 /// <param name="taggedViews">Returned tagged view list..</param>
757 /// <param name="view">Root View to get tagged child View.</param>
758 /// <param name="isPage">Flag to check current View is page or not</param>
759 private void RetrieveTaggedViews(List<View> taggedViews, View view, bool isPage)
763 if (!string.IsNullOrEmpty(view.TransitionOptions?.TransitionTag))
765 taggedViews.Add((view as View));
768 if (view.ChildCount == 0)
773 if (view.TransitionOptions?.TransitionWithChild ?? false)
778 foreach (View child in view.Children)
780 RetrieveTaggedViews(taggedViews, child, false);
784 internal void InvokeTransitionFinished()
786 TransitionFinished?.Invoke(this, new EventArgs());
789 //TODO: The following transition codes will be replaced with view transition.
790 private void InitializeAnimation()
792 if (curAnimation != null)
795 curAnimation.Clear();
799 if (newAnimation != null)
802 newAnimation.Clear();