From 740ad3c326f29f51205b8f0fb046ff0658076925 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 24 Jan 2019 13:36:50 +0200 Subject: [PATCH] Bottom tabs attach mode (#4633) This pr adds support for changing tab initialisation mode. Currently bottom tabs are attached together and consequently, their corresponding react root views are created and rendered. This adds lots of stress on the js thread and hiders app start time. To mitigate this issue, this pr adds support for three modes * `together` (default, current behaviour) - all tabs are loaded together, adding unnecessary load on the js thread as we're loading invisible views, which leads to increased app start time. * `afterInitialTab` - Initial tab is loaded first. After it is rendered, other tabs are loaded as well. This should shave a few hunderdish ms from app start time. Since other tabs are loaded after the initial tab is visible, the ui might be unresponsive for a few hunderdish ms as multiple root views (which are typically complex) are being created and attached to hierarchy at once. * `onSwitchToTab` - initial tab is loaded. Other tabs are loaded when switching to them (tab click or programmatically). While this won't stress js thread after initial tab is rendered, there will be a flicker when entering a tab for the first time as it's ui isn't ready. --- .../parse/BottomTabsOptions.java | 4 + .../parse/LayoutFactory.java | 5 +- .../parse/TabsAttachMode.java | 25 ++++++ .../presentation/OverlayManager.java | 2 +- .../utils/CollectionUtils.java | 15 ++++ .../viewcontrollers/ViewController.java | 21 +++-- .../bottomtabs/AfterInitialTab.java | 36 +++++++++ .../bottomtabs/AttachMode.java | 62 ++++++++++++++ .../bottomtabs/BottomTabsAttacher.java | 38 +++++++++ .../bottomtabs/BottomTabsController.java | 25 +++--- .../bottomtabs/OnSwitchToTab.java | 34 ++++++++ .../viewcontrollers/bottomtabs/Together.java | 22 +++++ .../viewcontrollers/modal/ModalPresenter.java | 4 +- .../navigator/RootPresenter.java | 2 +- .../stack/StackController.java | 2 +- .../topbar/TopBarController.java | 4 +- .../com/reactnativenavigation/BaseTest.java | 11 +++ .../viewcontrollers/TopBarControllerTest.java | 12 +++ .../bottomtabs/BottomTabsAttacherTest.java | 45 +++++++++++ .../BottomTabsControllerTest.java | 24 +++++- .../attachmode/AfterInitialTabTest.java | 39 +++++++++ .../bottomtabs/attachmode/AttachModeTest.java | 80 +++++++++++++++++++ .../attachmode/OnSwitchToTabTest.java | 33 ++++++++ .../bottomtabs/attachmode/TogetherTest.java | 21 +++++ .../modal/ModalPresenterTest.java | 2 +- .../navigator/NavigatorTest.java | 4 +- .../navigator/RootPresenterTest.java | 2 +- .../stack/StackControllerTest.java | 3 +- lib/src/interfaces/Options.ts | 5 ++ 29 files changed, 547 insertions(+), 35 deletions(-) create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java create mode 100644 lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java rename lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/{ => bottomtabs}/BottomTabsControllerTest.java (95%) create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java create mode 100644 lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java index 2bfafef22d8..d250e89bcd5 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java @@ -34,6 +34,7 @@ public static BottomTabsOptions parse(JSONObject json) { options.elevation = FractionParser.parse(json, "elevation"); options.testId = TextParser.parse(json, "testID"); options.titleDisplayMode = TitleDisplayMode.fromString(json.optString("titleDisplayMode")); + options.tabsAttachMode = TabsAttachMode.fromString(json.optString("tabsAttachMode")); return options; } @@ -47,6 +48,7 @@ public static BottomTabsOptions parse(JSONObject json) { public Text currentTabId = new NullText(); public Text testId = new NullText(); public TitleDisplayMode titleDisplayMode = TitleDisplayMode.UNDEFINED; + public TabsAttachMode tabsAttachMode = TabsAttachMode.UNDEFINED; void mergeWith(final BottomTabsOptions other) { if (other.currentTabId.hasValue()) currentTabId = other.currentTabId; @@ -58,6 +60,7 @@ void mergeWith(final BottomTabsOptions other) { if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor; if (other.testId.hasValue()) testId = other.testId; if (other.titleDisplayMode.hasValue()) titleDisplayMode = other.titleDisplayMode; + if (other.tabsAttachMode.hasValue()) tabsAttachMode = other.tabsAttachMode; } void mergeWithDefault(final BottomTabsOptions defaultOptions) { @@ -69,6 +72,7 @@ void mergeWithDefault(final BottomTabsOptions defaultOptions) { if (!elevation.hasValue()) elevation = defaultOptions.elevation; if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor; if (!titleDisplayMode.hasValue()) titleDisplayMode = defaultOptions.titleDisplayMode; + if (!tabsAttachMode.hasValue()) tabsAttachMode = defaultOptions.tabsAttachMode; } public void clearOneTimeOptions() { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java index bcaf2ce93c3..a01f16aa706 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java @@ -16,6 +16,7 @@ import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry; import com.reactnativenavigation.viewcontrollers.ComponentViewController; import com.reactnativenavigation.viewcontrollers.ViewController; +import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsAttacher; import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController; import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator; import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentViewController; @@ -193,6 +194,7 @@ private ViewController createBottomTabs(LayoutNode node) { for (int i = 0; i < node.children.size(); i++) { tabs.add(create(node.children.get(i))); } + BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, defaultOptions); return new BottomTabsController(activity, tabs, childRegistry, @@ -201,7 +203,8 @@ private ViewController createBottomTabs(LayoutNode node) { node.id, parse(typefaceManager, node.getOptions()), new Presenter(activity, defaultOptions), - new BottomTabsPresenter(tabs, defaultOptions), + new BottomTabsAttacher(tabs, bottomTabsPresenter), + bottomTabsPresenter, new BottomTabPresenter(activity, tabs, new ImageLoader(), defaultOptions)); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java b/lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java new file mode 100644 index 00000000000..d41ad408405 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java @@ -0,0 +1,25 @@ +package com.reactnativenavigation.parse; + +public enum TabsAttachMode { + TOGETHER, + AFTER_INITIAL_TAB, + ON_SWITCH_TO_TAB, + UNDEFINED; + + public static TabsAttachMode fromString(String mode) { + switch (mode) { + case "together": + return TOGETHER; + case "afterInitialTab": + return AFTER_INITIAL_TAB; + case "onSwitchToTab": + return ON_SWITCH_TO_TAB; + default: + return UNDEFINED; + } + } + + public boolean hasValue() { + return this != UNDEFINED; + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java b/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java index cedafd3b5f6..5eaf3e3e4ea 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java @@ -12,7 +12,7 @@ public class OverlayManager { public void show(ViewGroup overlaysContainer, ViewController overlay, CommandListener listener) { overlayRegistry.put(overlay.getId(), overlay); - overlay.setOnAppearedListener(() -> listener.onSuccess(overlay.getId())); + overlay.addOnAppearedListener(() -> listener.onSuccess(overlay.getId())); overlaysContainer.addView(overlay.getView()); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java b/lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java index 5710b8d96e2..081c55e3028 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java @@ -69,12 +69,27 @@ public static List merge(@Nullable Collection a, @Nullable Collection< } public static void forEach(@Nullable Collection items, Apply apply) { + if (items != null) forEach(new ArrayList(items), 0, apply); + } + + public static void forEach(@Nullable T[] items, Apply apply) { if (items == null) return; for (T item : items) { apply.on(item); } } + public static void forEach(@Nullable List items, Apply apply) { + forEach(items, 0, apply); + } + + public static void forEach(@Nullable List items, int startIndex, Apply apply) { + if (items == null) return; + for (int i = startIndex; i < items.size(); i++) { + apply.on(items.get(i)); + } + } + public static @Nullable T first(@Nullable Collection items, Filter by) { if (isNullOrEmpty(items)) return null; for (T item : items) { diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java index 6aa281c8acb..3558c9e8c30 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java @@ -25,12 +25,15 @@ import com.reactnativenavigation.views.Renderable; import com.reactnativenavigation.views.element.Element; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static com.reactnativenavigation.utils.CollectionUtils.forEach; + public abstract class ViewController implements ViewTreeObserver.OnGlobalLayoutListener, ViewGroup.OnHierarchyChangeListener { - private Runnable onAppearedListener; + private final List onAppearedListeners = new ArrayList(); private boolean appearEventPosted; private boolean isFirstLayout = true; private Bool waitForRender = new NullBool(); @@ -47,7 +50,7 @@ public interface ViewVisibilityListener { boolean onViewDisappear(View view); } - protected Options initialOptions; + public Options initialOptions; public Options options; private final Activity activity; @@ -77,8 +80,12 @@ public void setWaitForRender(Bool waitForRender) { this.waitForRender = waitForRender; } - public void setOnAppearedListener(Runnable onAppearedListener) { - this.onAppearedListener = onAppearedListener; + public void addOnAppearedListener(Runnable onAppearedListener) { + onAppearedListeners.add(onAppearedListener); + } + + public void removeOnAppearedListener(Runnable onAppearedListener) { + onAppearedListeners.remove(onAppearedListener); } protected abstract T createView(); @@ -199,11 +206,11 @@ public void onViewAppeared() { parentController.clearOptions(); if (getView() instanceof Component) parentController.applyChildOptions(options, (Component) getView()); }); - if (onAppearedListener != null && !appearEventPosted) { + if (!onAppearedListeners.isEmpty() && !appearEventPosted) { appearEventPosted = true; UiThread.post(() -> { - onAppearedListener.run(); - onAppearedListener = null; + forEach(onAppearedListeners, Runnable::run); + onAppearedListeners.clear(); }); } } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java new file mode 100644 index 00000000000..5bd5c0d6ac2 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java @@ -0,0 +1,36 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import android.view.*; + +import com.reactnativenavigation.parse.*; +import com.reactnativenavigation.presentation.*; +import com.reactnativenavigation.viewcontrollers.*; + +import java.util.*; + +import static com.reactnativenavigation.utils.CollectionUtils.filter; +import static com.reactnativenavigation.utils.CollectionUtils.forEach; + +public class AfterInitialTab extends AttachMode { + private final Runnable attachOtherTabs; + + public AfterInitialTab(ViewGroup parent, List tabs, BottomTabsPresenter presenter, Options resolved) { + super(parent, tabs, presenter, resolved); + attachOtherTabs = () -> forEach(otherTabs(), this::attach); + } + + @Override + public void attach() { + initialTab.addOnAppearedListener(attachOtherTabs); + attach(initialTab); + } + + @Override + public void destroy() { + initialTab.removeOnAppearedListener(attachOtherTabs); + } + + private List otherTabs() { + return filter(tabs, t -> t != initialTab); + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java new file mode 100644 index 00000000000..4fe3883e5ca --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java @@ -0,0 +1,62 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import android.support.annotation.*; +import android.view.*; +import android.widget.*; + +import com.reactnativenavigation.parse.*; +import com.reactnativenavigation.presentation.*; +import com.reactnativenavigation.viewcontrollers.*; + +import java.util.*; + +import static android.view.ViewGroup.LayoutParams.*; + +public abstract class AttachMode { + protected final ViewGroup parent; + protected final BottomTabsPresenter presenter; + protected final List tabs; + final ViewController initialTab; + private final Options resolved; + + + public static AttachMode get(ViewGroup parent, List tabs, BottomTabsPresenter presenter, Options resolved) { + switch (resolved.bottomTabsOptions.tabsAttachMode) { + case AFTER_INITIAL_TAB: + return new AfterInitialTab(parent, tabs, presenter, resolved); + case ON_SWITCH_TO_TAB: + return new OnSwitchToTab(parent, tabs, presenter, resolved); + case UNDEFINED: + case TOGETHER: + default: + return new Together(parent, tabs, presenter, resolved); + } + } + + AttachMode(ViewGroup parent, List tabs, BottomTabsPresenter presenter, Options resolved) { + this.parent = parent; + this.tabs = tabs; + this.presenter = presenter; + this.resolved = resolved; + initialTab = tabs.get(resolved.bottomTabsOptions.currentTabIndex.get(0)); + } + + public abstract void attach(); + + public void destroy() { + + } + + public void onTabSelected(ViewController tab) { + + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public void attach(ViewController tab) { + ViewGroup view = tab.getView(); + view.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + presenter.applyLayoutParamsOptions(resolved, tabs.indexOf(tab)); + view.setVisibility(tab == initialTab ? View.VISIBLE : View.INVISIBLE); + parent.addView(view); + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java new file mode 100644 index 00000000000..d03434d9dd9 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java @@ -0,0 +1,38 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import android.support.annotation.VisibleForTesting; +import android.view.ViewGroup; + +import com.reactnativenavigation.parse.Options; +import com.reactnativenavigation.presentation.BottomTabsPresenter; +import com.reactnativenavigation.viewcontrollers.ViewController; + +import java.util.List; + +public class BottomTabsAttacher { + private final List tabs; + private final BottomTabsPresenter presenter; + @VisibleForTesting + AttachMode attachStrategy; + + public BottomTabsAttacher(List tabs, BottomTabsPresenter presenter) { + this.tabs = tabs; + this.presenter = presenter; + } + + void init(ViewGroup parent, Options resolved) { + attachStrategy = AttachMode.get(parent, tabs, presenter, resolved); + } + + void attach() { + attachStrategy.attach(); + } + + public void destroy() { + attachStrategy.destroy(); + } + + void onTabSelected(ViewController tab) { + attachStrategy.onTabSelected(tab); + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java index 5f87be6d6ef..86cdfe459e5 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java @@ -38,14 +38,16 @@ public class BottomTabsController extends ParentController implements AHBottomNa private List tabs; private EventEmitter eventEmitter; private ImageLoader imageLoader; + private final BottomTabsAttacher tabsAttacher; private BottomTabsPresenter presenter; private BottomTabPresenter tabPresenter; - public BottomTabsController(Activity activity, List tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) { + public BottomTabsController(Activity activity, List tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsAttacher tabsAttacher, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) { super(activity, childRegistry, id, presenter, initialOptions); this.tabs = tabs; this.eventEmitter = eventEmitter; this.imageLoader = imageLoader; + this.tabsAttacher = tabsAttacher; this.presenter = bottomTabsPresenter; this.tabPresenter = bottomTabPresenter; forEach(tabs, (tab) -> tab.setParentController(this)); @@ -63,6 +65,7 @@ public void setDefaultOptions(Options defaultOptions) { protected ViewGroup createView() { RelativeLayout root = new RelativeLayout(getActivity()); bottomTabs = createBottomTabs(); + tabsAttacher.init(root, resolveCurrentOptions()); presenter.bindView(bottomTabs, this); tabPresenter.bindView(bottomTabs); bottomTabs.setOnTabSelectedListener(this); @@ -70,7 +73,7 @@ protected ViewGroup createView() { lp.addRule(ALIGN_PARENT_BOTTOM); root.addView(bottomTabs, lp); bottomTabs.addItems(createTabs()); - attachTabs(root); + tabsAttacher.attach(); return root; } @@ -155,17 +158,6 @@ private List createTabs() { }); } - private void attachTabs(RelativeLayout root) { - for (int i = 0; i < tabs.size(); i++) { - ViewGroup tab = tabs.get(i).getView(); - tab.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - Options options = resolveCurrentOptions(); - presenter.applyLayoutParamsOptions(options, i); - if (i != 0) tab.setVisibility(View.INVISIBLE); - root.addView(tab); - } - } - public int getSelectedIndex() { return bottomTabs.getCurrentItem(); } @@ -176,8 +168,15 @@ public Collection getChildControllers() { return tabs; } + @Override + public void destroy() { + tabsAttacher.destroy(); + super.destroy(); + } + @Override public void selectTab(final int newIndex) { + tabsAttacher.onTabSelected(tabs.get(newIndex)); getCurrentView().setVisibility(View.INVISIBLE); bottomTabs.setCurrentItem(newIndex, false); getCurrentView().setVisibility(View.VISIBLE); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java new file mode 100644 index 00000000000..2ba31e3225d --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java @@ -0,0 +1,34 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import android.view.*; + +import com.reactnativenavigation.parse.*; +import com.reactnativenavigation.presentation.*; +import com.reactnativenavigation.viewcontrollers.*; + +import java.util.*; + +public class OnSwitchToTab extends AttachMode { + private final ViewController initialTab; + + public OnSwitchToTab(ViewGroup parent, List tabs, BottomTabsPresenter presenter, Options resolved) { + super(parent, tabs, presenter, resolved); + this.initialTab = tabs.get(resolved.bottomTabsOptions.currentTabIndex.get(0)); + } + + @Override + public void attach() { + attach(initialTab); + } + + @Override + public void onTabSelected(ViewController tab) { + if (tab != initialTab && isNotAttached(tab)) { + attach(tab); + } + } + + private boolean isNotAttached(ViewController tab) { + return tab.getView().getParent() == null; + } +} diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java new file mode 100644 index 00000000000..d0abc604a62 --- /dev/null +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java @@ -0,0 +1,22 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import android.view.*; + +import com.reactnativenavigation.parse.*; +import com.reactnativenavigation.presentation.*; +import com.reactnativenavigation.viewcontrollers.*; + +import java.util.*; + +import static com.reactnativenavigation.utils.CollectionUtils.*; + +public class Together extends AttachMode { + public Together(ViewGroup parent, List tabs, BottomTabsPresenter presenter, Options resolved) { + super(parent, tabs, presenter, resolved); + } + + @Override + public void attach() { + forEach(tabs, this::attach); + } +} \ No newline at end of file diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java index 27d74572d2b..68fd5fa19c2 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java @@ -44,13 +44,13 @@ void showModal(ViewController toAdd, ViewController toRemove, CommandListener li modalsLayout.addView(toAdd.getView()); if (options.animations.showModal.enabled.isTrueOrUndefined()) { if (options.animations.showModal.waitForRender.isTrue()) { - toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options)); + toAdd.addOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options)); } else { animateShow(toAdd, toRemove, listener, options); } } else { if (options.animations.showModal.waitForRender.isTrue()) { - toAdd.setOnAppearedListener(() -> onShowModalEnd(toAdd, toRemove, listener)); + toAdd.addOnAppearedListener(() -> onShowModalEnd(toAdd, toRemove, listener)); } else { onShowModalEnd(toAdd, toRemove, listener); } diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java index 691398140c2..18b7a3b28f1 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java @@ -31,7 +31,7 @@ void setRoot(ViewController root, Options defaultOptions, CommandListener listen root.setWaitForRender(options.animations.setRoot.waitForRender); if (options.animations.setRoot.waitForRender.isTrue()) { root.getView().setAlpha(0); - root.setOnAppearedListener(() -> { + root.addOnAppearedListener(() -> { root.getView().setAlpha(1); animateSetRootAndReportSuccess(root, listener, options); }); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java index 2b9faa95200..740dd4576da 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java @@ -162,7 +162,7 @@ public void push(ViewController child, CommandListener listener) { if (resolvedOptions.animations.push.enabled.isTrueOrUndefined()) { if (resolvedOptions.animations.push.waitForRender.isTrue()) { child.getView().setAlpha(0); - child.setOnAppearedListener(() -> animator.push(child.getView(), resolvedOptions.animations.push, resolvedOptions.transitions, toRemove.getElements(), child.getElements(), () -> { + child.addOnAppearedListener(() -> animator.push(child.getView(), resolvedOptions.animations.push, resolvedOptions.transitions, toRemove.getElements(), child.getElements(), () -> { getView().removeView(toRemove.getView()); listener.onSuccess(child.getId()); })); diff --git a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java index b1fda5a1594..3638921ebc6 100644 --- a/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java +++ b/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java @@ -25,7 +25,9 @@ protected TopBar createTopBar(Context context, StackLayout stackLayout) { } public void clear() { - topBar.clear(); + if (topBar != null) { + topBar.clear(); + } } public TopBar getView() { diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java index 552722a622e..e3498007b9b 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java @@ -19,6 +19,9 @@ import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; +import java.util.Arrays; + +import static com.reactnativenavigation.utils.CollectionUtils.forEach; import static org.assertj.core.api.Java6Assertions.assertThat; @RunWith(RobolectricTestRunner.class) @@ -42,12 +45,20 @@ public ActivityController newActivityController return Robolectric.buildActivity(clazz); } + public void assertIsChild(ViewGroup parent, ViewController... children) { + forEach(Arrays.asList(children),c -> assertIsChild(parent, c.getView())); + } + public void assertIsChild(ViewGroup parent, View child) { assertThat(parent).isNotNull(); assertThat(child).isNotNull(); assertThat(ViewUtils.isChildOf(parent, child)).isTrue(); } + public void assertNotChildOf(ViewGroup parent, ViewController... children) { + forEach(Arrays.asList(children), c -> assertNotChildOf(parent, c.getView())); + } + public void assertNotChildOf(ViewGroup parent, View child) { assertThat(parent).isNotNull(); assertThat(child).isNotNull(); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java index 07f1cd1568d..0a7cbb78821 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java @@ -13,6 +13,7 @@ import org.junit.Test; import org.mockito.Mockito; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -47,4 +48,15 @@ protected TitleBar createTitleBar(Context context) { uut.clear(); verify(titleBar[0], times(1)).clear(); } + + @Test + public void destroy() { + uut.createView(newActivity(), mock(StackLayout.class)); + uut.clear(); + } + + @Test + public void destroy_canBeCalledBeforeViewIsCreated() { + uut.clear(); + } } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java new file mode 100644 index 00000000000..830728ed829 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java @@ -0,0 +1,45 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs; + +import com.reactnativenavigation.BaseTest; +import com.reactnativenavigation.presentation.BottomTabsPresenter; +import com.reactnativenavigation.viewcontrollers.*; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Collections; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class BottomTabsAttacherTest extends BaseTest { + + private BottomTabsAttacher uut; + private AttachMode mode; + + @Override + public void beforeEach() { + mode = Mockito.mock(AttachMode.class); + uut = new BottomTabsAttacher(Collections.EMPTY_LIST, Mockito.mock(BottomTabsPresenter.class)); + uut.attachStrategy = mode; + } + + @Test + public void attach_delegatesToStrategy() { + uut.attach(); + verify(mode).attach(); + } + + @Test + public void onTabSelected() { + ViewController tab = mock(ViewController.class); + uut.onTabSelected(tab); + verify(mode).onTabSelected(tab); + } + + @Test + public void destroy_delegatesToStrategy() { + uut.destroy(); + verify(mode).destroy(); + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java similarity index 95% rename from lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java rename to lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java index f09348259d0..71480762a75 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java @@ -1,4 +1,4 @@ -package com.reactnativenavigation.viewcontrollers; +package com.reactnativenavigation.viewcontrollers.bottomtabs; import android.app.Activity; import android.graphics.Color; @@ -24,7 +24,8 @@ import com.reactnativenavigation.utils.ImageLoader; import com.reactnativenavigation.utils.OptionHelper; import com.reactnativenavigation.utils.ViewUtils; -import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController; +import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry; +import com.reactnativenavigation.viewcontrollers.ViewController; import com.reactnativenavigation.viewcontrollers.stack.StackController; import com.reactnativenavigation.views.BottomTabs; import com.reactnativenavigation.views.ReactComponent; @@ -66,6 +67,7 @@ public class BottomTabsControllerTest extends BaseTest { private ChildControllersRegistry childRegistry; private List tabs; private BottomTabsPresenter presenter; + private BottomTabsAttacher tabsAttacher; @Override public void beforeEach() { @@ -88,6 +90,7 @@ public void superCreateItems() { when(child5.handleBack(any())).thenReturn(true); tabs = createTabs(); presenter = spy(new BottomTabsPresenter(tabs, new Options())); + tabsAttacher = spy(new BottomTabsAttacher(tabs, presenter)); uut = createBottomTabs(); activity.setContentView(uut.getView()); } @@ -241,8 +244,9 @@ public void applyChildOptions_resolvedOptionsAreUsed() { child4 = createStack(pushedScreen); tabs = new ArrayList<>(Collections.singletonList(child4)); + tabsAttacher = new BottomTabsAttacher(tabs, presenter); - initialOptions.bottomTabsOptions.currentTabIndex = new Number(3); + initialOptions.bottomTabsOptions.currentTabIndex = new Number(0); Options resolvedOptions = new Options(); uut = new BottomTabsController(activity, tabs, @@ -252,6 +256,7 @@ public void applyChildOptions_resolvedOptionsAreUsed() { "uut", initialOptions, new Presenter(activity, new Options()), + tabsAttacher, presenter, new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) { @Override @@ -340,6 +345,18 @@ public void oneTimeOptionsAreAppliedOnce() { assertThat(uut.initialOptions.bottomTabsOptions.currentTabIndex.hasValue()).isFalse(); } + @Test + public void selectTab() { + uut.selectTab(1); + verify(tabsAttacher).onTabSelected(tabs.get(1)); + } + + @Test + public void destroy() { + uut.destroy(); + verify(tabsAttacher).destroy(); + } + @NonNull private List createTabs() { return Arrays.asList(child1, child2, child3, child4, child5); @@ -372,6 +389,7 @@ private BottomTabsController createBottomTabs() { "uut", initialOptions, new Presenter(activity, new Options()), + tabsAttacher, presenter, new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) { @Override diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java new file mode 100644 index 00000000000..2f4f1dc7745 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java @@ -0,0 +1,39 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode; + + +import com.reactnativenavigation.viewcontrollers.bottomtabs.*; + +import org.junit.*; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class AfterInitialTabTest extends AttachModeTest { + + @Override + public void beforeEach() { + super.beforeEach(); + uut = new AfterInitialTab(parent, tabs, presenter, options); + } + + @Test + public void attach_initialTabIsAttached() { + uut.attach(); + assertIsChild(parent, tab2); + } + + @Test + public void attach_otherTabsAreAttachedAfterInitialTab() { + uut.attach(); + assertNotChildOf(parent, otherTabs()); + + initialTab().onViewAppeared(); + assertIsChild(parent, otherTabs()); + } + + @Test + public void destroy() { + uut.destroy(); + verify(initialTab()).removeOnAppearedListener(any()); + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java new file mode 100644 index 00000000000..a43e6e07e90 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java @@ -0,0 +1,80 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode; + +import android.app.*; +import android.view.*; +import android.widget.*; + +import com.reactnativenavigation.*; +import com.reactnativenavigation.mocks.*; +import com.reactnativenavigation.parse.*; +import com.reactnativenavigation.parse.params.Number; +import com.reactnativenavigation.presentation.*; +import com.reactnativenavigation.viewcontrollers.*; +import com.reactnativenavigation.viewcontrollers.bottomtabs.*; + +import org.junit.*; +import org.mockito.*; + +import java.util.*; + +import static com.reactnativenavigation.utils.CollectionUtils.*; +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.*; + +public abstract class AttachModeTest extends BaseTest { + private static final int INITIAL_TAB = 1; + + private Activity activity; + private ChildControllersRegistry childRegistry; + protected ViewGroup parent; + ViewController tab1; + ViewController tab2; + List tabs; + protected Options options; + protected BottomTabsPresenter presenter; + protected AttachMode uut; + + @Override + public void beforeEach() { + activity = newActivity(); + childRegistry = new ChildControllersRegistry(); + parent = new FrameLayout(activity); + tabs = createTabs(); + options = new Options(); + options.bottomTabsOptions.currentTabIndex = new Number(INITIAL_TAB); + presenter = Mockito.mock(BottomTabsPresenter.class); + } + + @Test + public void attach_layoutOptionsAreApplied() { + uut.attach(tab1); + verify(presenter).applyLayoutParamsOptions(options, tabs.indexOf(tab1)); + } + + @Test + public void attach_initialTabIsVisible() { + uut.attach(initialTab()); + assertThat(initialTab().getView().getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void attach_otherTabsAreInvisibleWhenAttached() { + forEach(otherTabs(), t -> uut.attach(t)); + forEach(otherTabs(), t -> assertThat(t.getView().getVisibility()).isEqualTo(View.INVISIBLE)); + } + + ViewController[] otherTabs() { + return filter(tabs, t -> t != initialTab()).toArray(new ViewController[0]); + } + + ViewController initialTab() { + return tabs.get(INITIAL_TAB); + } + + private List createTabs() { + tab1 = new SimpleViewController(activity, childRegistry, "child1", new Options()); + tab2 = spy(new SimpleViewController(activity, childRegistry, "child2", new Options())); + ViewController tab3 = new SimpleViewController(activity, childRegistry, "child3", new Options()); + return Arrays.asList(tab1, tab2, tab3); + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java new file mode 100644 index 00000000000..04a2f30c99b --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java @@ -0,0 +1,33 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode; + +import com.reactnativenavigation.viewcontrollers.bottomtabs.*; + +import org.junit.*; + +public class OnSwitchToTabTest extends AttachModeTest { + + @Override + public void beforeEach() { + super.beforeEach(); + uut = new OnSwitchToTab(parent, tabs, presenter, options); + } + + @Test + public void attach_onlyInitialTabIsAttached() { + uut.attach(); + assertIsChild(parent, initialTab()); + assertNotChildOf(parent, otherTabs()); + } + + @Test + public void onTabSelected_initialTabIsNotHandled() { + uut.onTabSelected(initialTab()); + assertNotChildOf(parent, initialTab()); + } + + @Test + public void onTabSelected_otherTabIsAttached() { + uut.onTabSelected(tab1); + assertIsChild(parent, tab1); + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java new file mode 100644 index 00000000000..de9c2a60f77 --- /dev/null +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java @@ -0,0 +1,21 @@ +package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode; + +import com.reactnativenavigation.viewcontrollers.*; +import com.reactnativenavigation.viewcontrollers.bottomtabs.*; + +import org.junit.*; + +public class TogetherTest extends AttachModeTest { + + @Override + public void beforeEach() { + super.beforeEach(); + uut = new Together(parent, tabs, presenter, options); + } + + @Test + public void attach_allTabsAreAttached() { + uut.attach(); + assertIsChild(parent, tabs.toArray(new ViewController[0])); + } +} diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java index 969e07deed1..0326631e14a 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java @@ -138,7 +138,7 @@ public void onSuccess(String childId) { public void showModal_waitForRender() { modal1.options.animations.showModal.waitForRender = new Bool(true); uut.showModal(modal1, root, new CommandListenerAdapter()); - verify(modal1).setOnAppearedListener(any()); + verify(modal1).addOnAppearedListener(any()); verifyZeroInteractions(animator); } diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java index 15ae2dca12d..5ebb39c063c 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java @@ -28,6 +28,7 @@ import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry; import com.reactnativenavigation.viewcontrollers.ComponentViewController; import com.reactnativenavigation.viewcontrollers.ViewController; +import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsAttacher; import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController; import com.reactnativenavigation.viewcontrollers.modal.ModalStack; import com.reactnativenavigation.viewcontrollers.stack.StackController; @@ -350,7 +351,8 @@ public void mergeOptions_AffectsOnlyComponentViewControllers() { @NonNull private BottomTabsController newTabs(List tabs) { - return new BottomTabsController(activity, tabs, childRegistry, eventEmitter, imageLoaderMock, "tabsController", new Options(), new Presenter(activity, new Options()), new BottomTabsPresenter(tabs, new Options()), new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) { + BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, new Options()); + return new BottomTabsController(activity, tabs, childRegistry, eventEmitter, imageLoaderMock, "tabsController", new Options(), new Presenter(activity, new Options()), new BottomTabsAttacher(tabs, bottomTabsPresenter), bottomTabsPresenter, new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) { @NonNull @Override protected BottomTabs createBottomTabs() { diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java index d9be0f9f738..2d6110b119f 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java @@ -106,7 +106,7 @@ public void setRoot_waitForRender() { ViewController spy = spy(root); CommandListenerAdapter listener = spy(new CommandListenerAdapter()); uut.setRoot(spy, defaultOptions, listener); - verify(spy).setOnAppearedListener(any()); + verify(spy).addOnAppearedListener(any()); assertThat(spy.getView().getAlpha()).isZero(); verifyZeroInteractions(listener); diff --git a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java index 8fa86bcf5e3..b46d28555ac 100644 --- a/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java +++ b/lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java @@ -203,7 +203,7 @@ public void push_waitForRender() { child2.options.animations.push.waitForRender = new Bool(true); uut.push(child2, new CommandListenerAdapter()); - verify(child2).setOnAppearedListener(any()); + verify(child2).addOnAppearedListener(any()); verify(animator, times(0)).push(eq(child1.getView()), eq(child1.options.animations.push), any()); } @@ -989,7 +989,6 @@ public void resolvedOptionsAreAppliedWhenStackIsAttachedToParentAndNotVisible() @Test public void destroy() { - uut.ensureViewIsCreated(); uut.destroy(); verify(topBarController, times(1)).clear(); } diff --git a/lib/src/interfaces/Options.ts b/lib/src/interfaces/Options.ts index 1e46becef14..df9dba6a27e 100644 --- a/lib/src/interfaces/Options.ts +++ b/lib/src/interfaces/Options.ts @@ -445,6 +445,11 @@ export interface OptionsBottomTabs { * Set a background color for the bottom tabs */ backgroundColor?: Color; + /** + * Set when tabs are attached to hierarchy consequently when the + * RootView's constructor is called. + */ + tabsAttachMode?: 'together' | 'afterInitialTab' | 'onSwitchToTab'; /** * Control the Bottom Tabs blur style * #### (iOS specific)