瀏覽代碼

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.
Guy Carmeli 6 年之前
父節點
當前提交
740ad3c326
No account linked to committer's email address
共有 29 個檔案被更改,包括 547 行新增35 行删除
  1. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java
  2. 4
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java
  3. 25
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java
  4. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java
  5. 15
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java
  6. 14
    7
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  7. 36
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java
  8. 62
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java
  9. 38
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java
  10. 12
    13
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java
  11. 34
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java
  12. 22
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java
  13. 2
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java
  14. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java
  15. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java
  16. 3
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java
  17. 11
    0
      lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java
  18. 12
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java
  19. 45
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java
  20. 21
    3
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java
  21. 39
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java
  22. 80
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java
  23. 33
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java
  24. 21
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java
  25. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java
  26. 3
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java
  27. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java
  28. 1
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java
  29. 5
    0
      lib/src/interfaces/Options.ts

+ 4
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java 查看文件

34
         options.elevation = FractionParser.parse(json, "elevation");
34
         options.elevation = FractionParser.parse(json, "elevation");
35
         options.testId = TextParser.parse(json, "testID");
35
         options.testId = TextParser.parse(json, "testID");
36
         options.titleDisplayMode = TitleDisplayMode.fromString(json.optString("titleDisplayMode"));
36
         options.titleDisplayMode = TitleDisplayMode.fromString(json.optString("titleDisplayMode"));
37
+        options.tabsAttachMode = TabsAttachMode.fromString(json.optString("tabsAttachMode"));
37
 
38
 
38
 		return options;
39
 		return options;
39
 	}
40
 	}
47
 	public Text currentTabId = new NullText();
48
 	public Text currentTabId = new NullText();
48
     public Text testId = new NullText();
49
     public Text testId = new NullText();
49
     public TitleDisplayMode titleDisplayMode = TitleDisplayMode.UNDEFINED;
50
     public TitleDisplayMode titleDisplayMode = TitleDisplayMode.UNDEFINED;
51
+    public TabsAttachMode tabsAttachMode = TabsAttachMode.UNDEFINED;
50
 
52
 
51
 	void mergeWith(final BottomTabsOptions other) {
53
 	void mergeWith(final BottomTabsOptions other) {
52
 		if (other.currentTabId.hasValue()) currentTabId = other.currentTabId;
54
 		if (other.currentTabId.hasValue()) currentTabId = other.currentTabId;
58
         if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
60
         if (other.backgroundColor.hasValue()) backgroundColor = other.backgroundColor;
59
         if (other.testId.hasValue()) testId = other.testId;
61
         if (other.testId.hasValue()) testId = other.testId;
60
         if (other.titleDisplayMode.hasValue()) titleDisplayMode = other.titleDisplayMode;
62
         if (other.titleDisplayMode.hasValue()) titleDisplayMode = other.titleDisplayMode;
63
+        if (other.tabsAttachMode.hasValue()) tabsAttachMode = other.tabsAttachMode;
61
     }
64
     }
62
 
65
 
63
     void mergeWithDefault(final BottomTabsOptions defaultOptions) {
66
     void mergeWithDefault(final BottomTabsOptions defaultOptions) {
69
         if (!elevation.hasValue()) elevation = defaultOptions.elevation;
72
         if (!elevation.hasValue()) elevation = defaultOptions.elevation;
70
         if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
73
         if (!backgroundColor.hasValue()) backgroundColor = defaultOptions.backgroundColor;
71
         if (!titleDisplayMode.hasValue()) titleDisplayMode = defaultOptions.titleDisplayMode;
74
         if (!titleDisplayMode.hasValue()) titleDisplayMode = defaultOptions.titleDisplayMode;
75
+        if (!tabsAttachMode.hasValue()) tabsAttachMode = defaultOptions.tabsAttachMode;
72
     }
76
     }
73
 
77
 
74
     public void clearOneTimeOptions() {
78
     public void clearOneTimeOptions() {

+ 4
- 1
lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java 查看文件

16
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
16
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
17
 import com.reactnativenavigation.viewcontrollers.ComponentViewController;
17
 import com.reactnativenavigation.viewcontrollers.ComponentViewController;
18
 import com.reactnativenavigation.viewcontrollers.ViewController;
18
 import com.reactnativenavigation.viewcontrollers.ViewController;
19
+import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsAttacher;
19
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
20
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
20
 import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator;
21
 import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentCreator;
21
 import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentViewController;
22
 import com.reactnativenavigation.viewcontrollers.externalcomponent.ExternalComponentViewController;
193
         for (int i = 0; i < node.children.size(); i++) {
194
         for (int i = 0; i < node.children.size(); i++) {
194
             tabs.add(create(node.children.get(i)));
195
             tabs.add(create(node.children.get(i)));
195
         }
196
         }
197
+        BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, defaultOptions);
196
         return new BottomTabsController(activity,
198
         return new BottomTabsController(activity,
197
                 tabs,
199
                 tabs,
198
                 childRegistry,
200
                 childRegistry,
201
                 node.id,
203
                 node.id,
202
                 parse(typefaceManager, node.getOptions()),
204
                 parse(typefaceManager, node.getOptions()),
203
                 new Presenter(activity, defaultOptions),
205
                 new Presenter(activity, defaultOptions),
204
-                new BottomTabsPresenter(tabs, defaultOptions),
206
+                new BottomTabsAttacher(tabs, bottomTabsPresenter),
207
+                bottomTabsPresenter,
205
                 new BottomTabPresenter(activity, tabs, new ImageLoader(), defaultOptions));
208
                 new BottomTabPresenter(activity, tabs, new ImageLoader(), defaultOptions));
206
 	}
209
 	}
207
 
210
 

+ 25
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/TabsAttachMode.java 查看文件

1
+package com.reactnativenavigation.parse;
2
+
3
+public enum  TabsAttachMode {
4
+    TOGETHER,
5
+    AFTER_INITIAL_TAB,
6
+    ON_SWITCH_TO_TAB,
7
+    UNDEFINED;
8
+
9
+    public static TabsAttachMode fromString(String mode) {
10
+        switch (mode) {
11
+            case "together":
12
+                return TOGETHER;
13
+            case "afterInitialTab":
14
+                return AFTER_INITIAL_TAB;
15
+            case "onSwitchToTab":
16
+                return ON_SWITCH_TO_TAB;
17
+            default:
18
+                return UNDEFINED;
19
+        }
20
+    }
21
+
22
+    public boolean hasValue() {
23
+        return this != UNDEFINED;
24
+    }
25
+}

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java 查看文件

12
 
12
 
13
     public void show(ViewGroup overlaysContainer, ViewController overlay, CommandListener listener) {
13
     public void show(ViewGroup overlaysContainer, ViewController overlay, CommandListener listener) {
14
         overlayRegistry.put(overlay.getId(), overlay);
14
         overlayRegistry.put(overlay.getId(), overlay);
15
-        overlay.setOnAppearedListener(() -> listener.onSuccess(overlay.getId()));
15
+        overlay.addOnAppearedListener(() -> listener.onSuccess(overlay.getId()));
16
         overlaysContainer.addView(overlay.getView());
16
         overlaysContainer.addView(overlay.getView());
17
     }
17
     }
18
 
18
 

+ 15
- 0
lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java 查看文件

69
     }
69
     }
70
 
70
 
71
     public static <T> void forEach(@Nullable Collection<T> items, Apply<T> apply) {
71
     public static <T> void forEach(@Nullable Collection<T> items, Apply<T> apply) {
72
+        if (items != null) forEach(new ArrayList(items), 0, apply);
73
+    }
74
+
75
+    public static <T> void forEach(@Nullable T[] items, Apply<T> apply) {
72
         if (items == null) return;
76
         if (items == null) return;
73
         for (T item : items) {
77
         for (T item : items) {
74
             apply.on(item);
78
             apply.on(item);
75
         }
79
         }
76
     }
80
     }
77
 
81
 
82
+    public static <T> void forEach(@Nullable List<T> items, Apply<T> apply) {
83
+        forEach(items, 0, apply);
84
+    }
85
+
86
+    public static <T> void forEach(@Nullable List<T> items, int startIndex, Apply<T> apply) {
87
+        if (items == null) return;
88
+        for (int i = startIndex; i < items.size(); i++) {
89
+            apply.on(items.get(i));
90
+        }
91
+    }
92
+
78
     public static @Nullable <T> T first(@Nullable Collection<T> items, Filter<T> by) {
93
     public static @Nullable <T> T first(@Nullable Collection<T> items, Filter<T> by) {
79
         if (isNullOrEmpty(items)) return null;
94
         if (isNullOrEmpty(items)) return null;
80
         for (T item : items) {
95
         for (T item : items) {

+ 14
- 7
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java 查看文件

25
 import com.reactnativenavigation.views.Renderable;
25
 import com.reactnativenavigation.views.Renderable;
26
 import com.reactnativenavigation.views.element.Element;
26
 import com.reactnativenavigation.views.element.Element;
27
 
27
 
28
+import java.util.ArrayList;
28
 import java.util.Collections;
29
 import java.util.Collections;
29
 import java.util.List;
30
 import java.util.List;
30
 
31
 
32
+import static com.reactnativenavigation.utils.CollectionUtils.forEach;
33
+
31
 public abstract class ViewController<T extends ViewGroup> implements ViewTreeObserver.OnGlobalLayoutListener, ViewGroup.OnHierarchyChangeListener {
34
 public abstract class ViewController<T extends ViewGroup> implements ViewTreeObserver.OnGlobalLayoutListener, ViewGroup.OnHierarchyChangeListener {
32
 
35
 
33
-    private Runnable onAppearedListener;
36
+    private final List<Runnable> onAppearedListeners = new ArrayList();
34
     private boolean appearEventPosted;
37
     private boolean appearEventPosted;
35
     private boolean isFirstLayout = true;
38
     private boolean isFirstLayout = true;
36
     private Bool waitForRender = new NullBool();
39
     private Bool waitForRender = new NullBool();
47
         boolean onViewDisappear(View view);
50
         boolean onViewDisappear(View view);
48
     }
51
     }
49
 
52
 
50
-    protected Options initialOptions;
53
+    public Options initialOptions;
51
     public Options options;
54
     public Options options;
52
 
55
 
53
     private final Activity activity;
56
     private final Activity activity;
77
         this.waitForRender = waitForRender;
80
         this.waitForRender = waitForRender;
78
     }
81
     }
79
 
82
 
80
-    public void setOnAppearedListener(Runnable onAppearedListener) {
81
-        this.onAppearedListener = onAppearedListener;
83
+    public void addOnAppearedListener(Runnable onAppearedListener) {
84
+        onAppearedListeners.add(onAppearedListener);
85
+    }
86
+
87
+    public void removeOnAppearedListener(Runnable onAppearedListener) {
88
+        onAppearedListeners.remove(onAppearedListener);
82
     }
89
     }
83
 
90
 
84
     protected abstract T createView();
91
     protected abstract T createView();
199
             parentController.clearOptions();
206
             parentController.clearOptions();
200
             if (getView() instanceof Component) parentController.applyChildOptions(options, (Component) getView());
207
             if (getView() instanceof Component) parentController.applyChildOptions(options, (Component) getView());
201
         });
208
         });
202
-        if (onAppearedListener != null && !appearEventPosted) {
209
+        if (!onAppearedListeners.isEmpty() && !appearEventPosted) {
203
             appearEventPosted = true;
210
             appearEventPosted = true;
204
             UiThread.post(() -> {
211
             UiThread.post(() -> {
205
-                onAppearedListener.run();
206
-                onAppearedListener = null;
212
+                forEach(onAppearedListeners, Runnable::run);
213
+                onAppearedListeners.clear();
207
             });
214
             });
208
         }
215
         }
209
     }
216
     }

+ 36
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AfterInitialTab.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import android.view.*;
4
+
5
+import com.reactnativenavigation.parse.*;
6
+import com.reactnativenavigation.presentation.*;
7
+import com.reactnativenavigation.viewcontrollers.*;
8
+
9
+import java.util.*;
10
+
11
+import static com.reactnativenavigation.utils.CollectionUtils.filter;
12
+import static com.reactnativenavigation.utils.CollectionUtils.forEach;
13
+
14
+public class AfterInitialTab extends AttachMode {
15
+    private final Runnable attachOtherTabs;
16
+
17
+    public AfterInitialTab(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
18
+        super(parent, tabs, presenter, resolved);
19
+        attachOtherTabs = () -> forEach(otherTabs(), this::attach);
20
+    }
21
+
22
+    @Override
23
+    public void attach() {
24
+        initialTab.addOnAppearedListener(attachOtherTabs);
25
+        attach(initialTab);
26
+    }
27
+
28
+    @Override
29
+    public void destroy() {
30
+        initialTab.removeOnAppearedListener(attachOtherTabs);
31
+    }
32
+
33
+    private List<ViewController> otherTabs() {
34
+        return filter(tabs, t -> t != initialTab);
35
+    }
36
+}

+ 62
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/AttachMode.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import android.support.annotation.*;
4
+import android.view.*;
5
+import android.widget.*;
6
+
7
+import com.reactnativenavigation.parse.*;
8
+import com.reactnativenavigation.presentation.*;
9
+import com.reactnativenavigation.viewcontrollers.*;
10
+
11
+import java.util.*;
12
+
13
+import static android.view.ViewGroup.LayoutParams.*;
14
+
15
+public abstract class AttachMode {
16
+    protected final ViewGroup parent;
17
+    protected final BottomTabsPresenter presenter;
18
+    protected final List<ViewController> tabs;
19
+    final ViewController initialTab;
20
+    private final Options resolved;
21
+
22
+
23
+    public static AttachMode get(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
24
+        switch (resolved.bottomTabsOptions.tabsAttachMode) {
25
+            case AFTER_INITIAL_TAB:
26
+                return new AfterInitialTab(parent, tabs, presenter, resolved);
27
+            case ON_SWITCH_TO_TAB:
28
+                return new OnSwitchToTab(parent, tabs, presenter, resolved);
29
+            case UNDEFINED:
30
+            case TOGETHER:
31
+            default:
32
+                return new Together(parent, tabs, presenter, resolved);
33
+        }
34
+    }
35
+
36
+    AttachMode(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
37
+        this.parent = parent;
38
+        this.tabs = tabs;
39
+        this.presenter = presenter;
40
+        this.resolved = resolved;
41
+        initialTab = tabs.get(resolved.bottomTabsOptions.currentTabIndex.get(0));
42
+    }
43
+
44
+    public abstract void attach();
45
+
46
+    public void destroy() {
47
+
48
+    }
49
+
50
+    public void onTabSelected(ViewController tab) {
51
+
52
+    }
53
+
54
+    @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
55
+    public void attach(ViewController tab) {
56
+        ViewGroup view = tab.getView();
57
+        view.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
58
+        presenter.applyLayoutParamsOptions(resolved, tabs.indexOf(tab));
59
+        view.setVisibility(tab == initialTab ? View.VISIBLE : View.INVISIBLE);
60
+        parent.addView(view);
61
+    }
62
+}

+ 38
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacher.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import android.support.annotation.VisibleForTesting;
4
+import android.view.ViewGroup;
5
+
6
+import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.presentation.BottomTabsPresenter;
8
+import com.reactnativenavigation.viewcontrollers.ViewController;
9
+
10
+import java.util.List;
11
+
12
+public class BottomTabsAttacher {
13
+    private final List<ViewController> tabs;
14
+    private final BottomTabsPresenter presenter;
15
+    @VisibleForTesting
16
+    AttachMode attachStrategy;
17
+
18
+    public BottomTabsAttacher(List<ViewController> tabs, BottomTabsPresenter presenter) {
19
+        this.tabs = tabs;
20
+        this.presenter = presenter;
21
+    }
22
+
23
+    void init(ViewGroup parent, Options resolved) {
24
+        attachStrategy = AttachMode.get(parent, tabs, presenter, resolved);
25
+    }
26
+
27
+    void attach() {
28
+        attachStrategy.attach();
29
+    }
30
+
31
+    public void destroy() {
32
+        attachStrategy.destroy();
33
+    }
34
+
35
+    void onTabSelected(ViewController tab) {
36
+        attachStrategy.onTabSelected(tab);
37
+    }
38
+}

+ 12
- 13
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java 查看文件

38
 	private List<ViewController> tabs;
38
 	private List<ViewController> tabs;
39
     private EventEmitter eventEmitter;
39
     private EventEmitter eventEmitter;
40
     private ImageLoader imageLoader;
40
     private ImageLoader imageLoader;
41
+    private final BottomTabsAttacher tabsAttacher;
41
     private BottomTabsPresenter presenter;
42
     private BottomTabsPresenter presenter;
42
     private BottomTabPresenter tabPresenter;
43
     private BottomTabPresenter tabPresenter;
43
 
44
 
44
-    public BottomTabsController(Activity activity, List<ViewController> tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
45
+    public BottomTabsController(Activity activity, List<ViewController> tabs, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, ImageLoader imageLoader, String id, Options initialOptions, Presenter presenter, BottomTabsAttacher tabsAttacher, BottomTabsPresenter bottomTabsPresenter, BottomTabPresenter bottomTabPresenter) {
45
 		super(activity, childRegistry, id, presenter, initialOptions);
46
 		super(activity, childRegistry, id, presenter, initialOptions);
46
         this.tabs = tabs;
47
         this.tabs = tabs;
47
         this.eventEmitter = eventEmitter;
48
         this.eventEmitter = eventEmitter;
48
         this.imageLoader = imageLoader;
49
         this.imageLoader = imageLoader;
50
+        this.tabsAttacher = tabsAttacher;
49
         this.presenter = bottomTabsPresenter;
51
         this.presenter = bottomTabsPresenter;
50
         this.tabPresenter = bottomTabPresenter;
52
         this.tabPresenter = bottomTabPresenter;
51
         forEach(tabs, (tab) -> tab.setParentController(this));
53
         forEach(tabs, (tab) -> tab.setParentController(this));
63
 	protected ViewGroup createView() {
65
 	protected ViewGroup createView() {
64
 		RelativeLayout root = new RelativeLayout(getActivity());
66
 		RelativeLayout root = new RelativeLayout(getActivity());
65
 		bottomTabs = createBottomTabs();
67
 		bottomTabs = createBottomTabs();
68
+        tabsAttacher.init(root, resolveCurrentOptions());
66
         presenter.bindView(bottomTabs, this);
69
         presenter.bindView(bottomTabs, this);
67
         tabPresenter.bindView(bottomTabs);
70
         tabPresenter.bindView(bottomTabs);
68
         bottomTabs.setOnTabSelectedListener(this);
71
         bottomTabs.setOnTabSelectedListener(this);
70
 		lp.addRule(ALIGN_PARENT_BOTTOM);
73
 		lp.addRule(ALIGN_PARENT_BOTTOM);
71
 		root.addView(bottomTabs, lp);
74
 		root.addView(bottomTabs, lp);
72
 		bottomTabs.addItems(createTabs());
75
 		bottomTabs.addItems(createTabs());
73
-        attachTabs(root);
76
+        tabsAttacher.attach();
74
         return root;
77
         return root;
75
 	}
78
 	}
76
 
79
 
155
         });
158
         });
156
 	}
159
 	}
157
 
160
 
158
-    private void attachTabs(RelativeLayout root) {
159
-        for (int i = 0; i < tabs.size(); i++) {
160
-            ViewGroup tab = tabs.get(i).getView();
161
-            tab.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
162
-            Options options = resolveCurrentOptions();
163
-            presenter.applyLayoutParamsOptions(options, i);
164
-            if (i != 0) tab.setVisibility(View.INVISIBLE);
165
-            root.addView(tab);
166
-        }
167
-    }
168
-
169
     public int getSelectedIndex() {
161
     public int getSelectedIndex() {
170
 		return bottomTabs.getCurrentItem();
162
 		return bottomTabs.getCurrentItem();
171
 	}
163
 	}
176
 		return tabs;
168
 		return tabs;
177
 	}
169
 	}
178
 
170
 
171
+    @Override
172
+    public void destroy() {
173
+        tabsAttacher.destroy();
174
+        super.destroy();
175
+    }
176
+
179
     @Override
177
     @Override
180
     public void selectTab(final int newIndex) {
178
     public void selectTab(final int newIndex) {
179
+        tabsAttacher.onTabSelected(tabs.get(newIndex));
181
         getCurrentView().setVisibility(View.INVISIBLE);
180
         getCurrentView().setVisibility(View.INVISIBLE);
182
         bottomTabs.setCurrentItem(newIndex, false);
181
         bottomTabs.setCurrentItem(newIndex, false);
183
         getCurrentView().setVisibility(View.VISIBLE);
182
         getCurrentView().setVisibility(View.VISIBLE);

+ 34
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/OnSwitchToTab.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import android.view.*;
4
+
5
+import com.reactnativenavigation.parse.*;
6
+import com.reactnativenavigation.presentation.*;
7
+import com.reactnativenavigation.viewcontrollers.*;
8
+
9
+import java.util.*;
10
+
11
+public class OnSwitchToTab extends AttachMode {
12
+    private final ViewController initialTab;
13
+
14
+    public OnSwitchToTab(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
15
+        super(parent, tabs, presenter, resolved);
16
+        this.initialTab = tabs.get(resolved.bottomTabsOptions.currentTabIndex.get(0));
17
+    }
18
+
19
+    @Override
20
+    public void attach() {
21
+        attach(initialTab);
22
+    }
23
+
24
+    @Override
25
+    public void onTabSelected(ViewController tab) {
26
+        if (tab != initialTab && isNotAttached(tab)) {
27
+            attach(tab);
28
+        }
29
+    }
30
+
31
+    private boolean isNotAttached(ViewController tab) {
32
+        return tab.getView().getParent() == null;
33
+    }
34
+}

+ 22
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/Together.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import android.view.*;
4
+
5
+import com.reactnativenavigation.parse.*;
6
+import com.reactnativenavigation.presentation.*;
7
+import com.reactnativenavigation.viewcontrollers.*;
8
+
9
+import java.util.*;
10
+
11
+import static com.reactnativenavigation.utils.CollectionUtils.*;
12
+
13
+public class Together extends AttachMode {
14
+    public Together(ViewGroup parent, List<ViewController> tabs, BottomTabsPresenter presenter, Options resolved) {
15
+        super(parent, tabs, presenter, resolved);
16
+    }
17
+
18
+    @Override
19
+    public void attach() {
20
+        forEach(tabs, this::attach);
21
+    }
22
+}

+ 2
- 2
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java 查看文件

44
         modalsLayout.addView(toAdd.getView());
44
         modalsLayout.addView(toAdd.getView());
45
         if (options.animations.showModal.enabled.isTrueOrUndefined()) {
45
         if (options.animations.showModal.enabled.isTrueOrUndefined()) {
46
             if (options.animations.showModal.waitForRender.isTrue()) {
46
             if (options.animations.showModal.waitForRender.isTrue()) {
47
-                toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
47
+                toAdd.addOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
48
             } else {
48
             } else {
49
                 animateShow(toAdd, toRemove, listener, options);
49
                 animateShow(toAdd, toRemove, listener, options);
50
             }
50
             }
51
         } else {
51
         } else {
52
             if (options.animations.showModal.waitForRender.isTrue()) {
52
             if (options.animations.showModal.waitForRender.isTrue()) {
53
-                toAdd.setOnAppearedListener(() -> onShowModalEnd(toAdd, toRemove, listener));
53
+                toAdd.addOnAppearedListener(() -> onShowModalEnd(toAdd, toRemove, listener));
54
             } else {
54
             } else {
55
                 onShowModalEnd(toAdd, toRemove, listener);
55
                 onShowModalEnd(toAdd, toRemove, listener);
56
             }
56
             }

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenter.java 查看文件

31
         root.setWaitForRender(options.animations.setRoot.waitForRender);
31
         root.setWaitForRender(options.animations.setRoot.waitForRender);
32
         if (options.animations.setRoot.waitForRender.isTrue()) {
32
         if (options.animations.setRoot.waitForRender.isTrue()) {
33
             root.getView().setAlpha(0);
33
             root.getView().setAlpha(0);
34
-            root.setOnAppearedListener(() -> {
34
+            root.addOnAppearedListener(() -> {
35
                 root.getView().setAlpha(1);
35
                 root.getView().setAlpha(1);
36
                 animateSetRootAndReportSuccess(root, listener, options);
36
                 animateSetRootAndReportSuccess(root, listener, options);
37
             });
37
             });

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java 查看文件

162
             if (resolvedOptions.animations.push.enabled.isTrueOrUndefined()) {
162
             if (resolvedOptions.animations.push.enabled.isTrueOrUndefined()) {
163
                 if (resolvedOptions.animations.push.waitForRender.isTrue()) {
163
                 if (resolvedOptions.animations.push.waitForRender.isTrue()) {
164
                     child.getView().setAlpha(0);
164
                     child.getView().setAlpha(0);
165
-                    child.setOnAppearedListener(() -> animator.push(child.getView(), resolvedOptions.animations.push, resolvedOptions.transitions, toRemove.getElements(), child.getElements(), () -> {
165
+                    child.addOnAppearedListener(() -> animator.push(child.getView(), resolvedOptions.animations.push, resolvedOptions.transitions, toRemove.getElements(), child.getElements(), () -> {
166
                         getView().removeView(toRemove.getView());
166
                         getView().removeView(toRemove.getView());
167
                         listener.onSuccess(child.getId());
167
                         listener.onSuccess(child.getId());
168
                     }));
168
                     }));

+ 3
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java 查看文件

25
     }
25
     }
26
 
26
 
27
     public void clear() {
27
     public void clear() {
28
-        topBar.clear();
28
+        if (topBar != null) {
29
+            topBar.clear();
30
+        }
29
     }
31
     }
30
 
32
 
31
     public TopBar getView() {
33
     public TopBar getView() {

+ 11
- 0
lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java 查看文件

19
 import org.robolectric.android.controller.ActivityController;
19
 import org.robolectric.android.controller.ActivityController;
20
 import org.robolectric.annotation.Config;
20
 import org.robolectric.annotation.Config;
21
 
21
 
22
+import java.util.Arrays;
23
+
24
+import static com.reactnativenavigation.utils.CollectionUtils.forEach;
22
 import static org.assertj.core.api.Java6Assertions.assertThat;
25
 import static org.assertj.core.api.Java6Assertions.assertThat;
23
 
26
 
24
 @RunWith(RobolectricTestRunner.class)
27
 @RunWith(RobolectricTestRunner.class)
42
         return Robolectric.buildActivity(clazz);
45
         return Robolectric.buildActivity(clazz);
43
     }
46
     }
44
 
47
 
48
+    public void assertIsChild(ViewGroup parent, ViewController... children) {
49
+        forEach(Arrays.asList(children),c -> assertIsChild(parent, c.getView()));
50
+    }
51
+
45
     public void assertIsChild(ViewGroup parent, View child) {
52
     public void assertIsChild(ViewGroup parent, View child) {
46
         assertThat(parent).isNotNull();
53
         assertThat(parent).isNotNull();
47
         assertThat(child).isNotNull();
54
         assertThat(child).isNotNull();
48
         assertThat(ViewUtils.isChildOf(parent, child)).isTrue();
55
         assertThat(ViewUtils.isChildOf(parent, child)).isTrue();
49
     }
56
     }
50
 
57
 
58
+    public void assertNotChildOf(ViewGroup parent, ViewController... children) {
59
+        forEach(Arrays.asList(children), c -> assertNotChildOf(parent, c.getView()));
60
+    }
61
+
51
     public void assertNotChildOf(ViewGroup parent, View child) {
62
     public void assertNotChildOf(ViewGroup parent, View child) {
52
         assertThat(parent).isNotNull();
63
         assertThat(parent).isNotNull();
53
         assertThat(child).isNotNull();
64
         assertThat(child).isNotNull();

+ 12
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java 查看文件

13
 import org.junit.Test;
13
 import org.junit.Test;
14
 import org.mockito.Mockito;
14
 import org.mockito.Mockito;
15
 
15
 
16
+import static org.mockito.Mockito.mock;
16
 import static org.mockito.Mockito.spy;
17
 import static org.mockito.Mockito.spy;
17
 import static org.mockito.Mockito.times;
18
 import static org.mockito.Mockito.times;
18
 import static org.mockito.Mockito.verify;
19
 import static org.mockito.Mockito.verify;
47
         uut.clear();
48
         uut.clear();
48
         verify(titleBar[0], times(1)).clear();
49
         verify(titleBar[0], times(1)).clear();
49
     }
50
     }
51
+
52
+    @Test
53
+    public void destroy() {
54
+        uut.createView(newActivity(), mock(StackLayout.class));
55
+        uut.clear();
56
+    }
57
+
58
+    @Test
59
+    public void destroy_canBeCalledBeforeViewIsCreated() {
60
+        uut.clear();
61
+    }
50
 }
62
 }

+ 45
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsAttacherTest.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
+
3
+import com.reactnativenavigation.BaseTest;
4
+import com.reactnativenavigation.presentation.BottomTabsPresenter;
5
+import com.reactnativenavigation.viewcontrollers.*;
6
+
7
+import org.junit.Test;
8
+import org.mockito.Mockito;
9
+
10
+import java.util.Collections;
11
+
12
+import static org.mockito.Mockito.mock;
13
+import static org.mockito.Mockito.verify;
14
+
15
+public class BottomTabsAttacherTest extends BaseTest {
16
+
17
+    private BottomTabsAttacher uut;
18
+    private AttachMode mode;
19
+
20
+    @Override
21
+    public void beforeEach() {
22
+        mode = Mockito.mock(AttachMode.class);
23
+        uut = new BottomTabsAttacher(Collections.EMPTY_LIST, Mockito.mock(BottomTabsPresenter.class));
24
+        uut.attachStrategy = mode;
25
+    }
26
+
27
+    @Test
28
+    public void attach_delegatesToStrategy() {
29
+        uut.attach();
30
+        verify(mode).attach();
31
+    }
32
+
33
+    @Test
34
+    public void onTabSelected() {
35
+        ViewController tab = mock(ViewController.class);
36
+        uut.onTabSelected(tab);
37
+        verify(mode).onTabSelected(tab);
38
+    }
39
+
40
+    @Test
41
+    public void destroy_delegatesToStrategy() {
42
+        uut.destroy();
43
+        verify(mode).destroy();
44
+    }
45
+}

lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java → lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsControllerTest.java 查看文件

1
-package com.reactnativenavigation.viewcontrollers;
1
+package com.reactnativenavigation.viewcontrollers.bottomtabs;
2
 
2
 
3
 import android.app.Activity;
3
 import android.app.Activity;
4
 import android.graphics.Color;
4
 import android.graphics.Color;
24
 import com.reactnativenavigation.utils.ImageLoader;
24
 import com.reactnativenavigation.utils.ImageLoader;
25
 import com.reactnativenavigation.utils.OptionHelper;
25
 import com.reactnativenavigation.utils.OptionHelper;
26
 import com.reactnativenavigation.utils.ViewUtils;
26
 import com.reactnativenavigation.utils.ViewUtils;
27
-import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
27
+import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
28
+import com.reactnativenavigation.viewcontrollers.ViewController;
28
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
29
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
29
 import com.reactnativenavigation.views.BottomTabs;
30
 import com.reactnativenavigation.views.BottomTabs;
30
 import com.reactnativenavigation.views.ReactComponent;
31
 import com.reactnativenavigation.views.ReactComponent;
66
     private ChildControllersRegistry childRegistry;
67
     private ChildControllersRegistry childRegistry;
67
     private List<ViewController> tabs;
68
     private List<ViewController> tabs;
68
     private BottomTabsPresenter presenter;
69
     private BottomTabsPresenter presenter;
70
+    private BottomTabsAttacher tabsAttacher;
69
 
71
 
70
     @Override
72
     @Override
71
     public void beforeEach() {
73
     public void beforeEach() {
88
         when(child5.handleBack(any())).thenReturn(true);
90
         when(child5.handleBack(any())).thenReturn(true);
89
         tabs = createTabs();
91
         tabs = createTabs();
90
         presenter = spy(new BottomTabsPresenter(tabs, new Options()));
92
         presenter = spy(new BottomTabsPresenter(tabs, new Options()));
93
+        tabsAttacher = spy(new BottomTabsAttacher(tabs, presenter));
91
         uut = createBottomTabs();
94
         uut = createBottomTabs();
92
         activity.setContentView(uut.getView());
95
         activity.setContentView(uut.getView());
93
     }
96
     }
241
         child4 = createStack(pushedScreen);
244
         child4 = createStack(pushedScreen);
242
 
245
 
243
         tabs = new ArrayList<>(Collections.singletonList(child4));
246
         tabs = new ArrayList<>(Collections.singletonList(child4));
247
+        tabsAttacher = new BottomTabsAttacher(tabs, presenter);
244
 
248
 
245
-        initialOptions.bottomTabsOptions.currentTabIndex = new Number(3);
249
+        initialOptions.bottomTabsOptions.currentTabIndex = new Number(0);
246
         Options resolvedOptions = new Options();
250
         Options resolvedOptions = new Options();
247
         uut = new BottomTabsController(activity,
251
         uut = new BottomTabsController(activity,
248
                 tabs,
252
                 tabs,
252
                 "uut",
256
                 "uut",
253
                 initialOptions,
257
                 initialOptions,
254
                 new Presenter(activity, new Options()),
258
                 new Presenter(activity, new Options()),
259
+                tabsAttacher,
255
                 presenter,
260
                 presenter,
256
                 new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) {
261
                 new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) {
257
             @Override
262
             @Override
340
         assertThat(uut.initialOptions.bottomTabsOptions.currentTabIndex.hasValue()).isFalse();
345
         assertThat(uut.initialOptions.bottomTabsOptions.currentTabIndex.hasValue()).isFalse();
341
     }
346
     }
342
 
347
 
348
+    @Test
349
+    public void selectTab() {
350
+        uut.selectTab(1);
351
+        verify(tabsAttacher).onTabSelected(tabs.get(1));
352
+    }
353
+
354
+    @Test
355
+    public void destroy() {
356
+        uut.destroy();
357
+        verify(tabsAttacher).destroy();
358
+    }
359
+
343
     @NonNull
360
     @NonNull
344
     private List<ViewController> createTabs() {
361
     private List<ViewController> createTabs() {
345
         return Arrays.asList(child1, child2, child3, child4, child5);
362
         return Arrays.asList(child1, child2, child3, child4, child5);
372
                 "uut",
389
                 "uut",
373
                 initialOptions,
390
                 initialOptions,
374
                 new Presenter(activity, new Options()),
391
                 new Presenter(activity, new Options()),
392
+                tabsAttacher,
375
                 presenter,
393
                 presenter,
376
                 new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) {
394
                 new BottomTabPresenter(activity, tabs, ImageLoaderMock.mock(), new Options())) {
377
             @Override
395
             @Override

+ 39
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AfterInitialTabTest.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode;
2
+
3
+
4
+import com.reactnativenavigation.viewcontrollers.bottomtabs.*;
5
+
6
+import org.junit.*;
7
+
8
+import static org.mockito.ArgumentMatchers.any;
9
+import static org.mockito.Mockito.*;
10
+
11
+public class AfterInitialTabTest extends AttachModeTest {
12
+
13
+    @Override
14
+    public void beforeEach() {
15
+        super.beforeEach();
16
+        uut = new AfterInitialTab(parent, tabs, presenter, options);
17
+    }
18
+
19
+    @Test
20
+    public void attach_initialTabIsAttached() {
21
+        uut.attach();
22
+        assertIsChild(parent, tab2);
23
+    }
24
+
25
+    @Test
26
+    public void attach_otherTabsAreAttachedAfterInitialTab() {
27
+        uut.attach();
28
+        assertNotChildOf(parent, otherTabs());
29
+
30
+        initialTab().onViewAppeared();
31
+        assertIsChild(parent, otherTabs());
32
+    }
33
+
34
+    @Test
35
+    public void destroy() {
36
+        uut.destroy();
37
+        verify(initialTab()).removeOnAppearedListener(any());
38
+    }
39
+}

+ 80
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/AttachModeTest.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode;
2
+
3
+import android.app.*;
4
+import android.view.*;
5
+import android.widget.*;
6
+
7
+import com.reactnativenavigation.*;
8
+import com.reactnativenavigation.mocks.*;
9
+import com.reactnativenavigation.parse.*;
10
+import com.reactnativenavigation.parse.params.Number;
11
+import com.reactnativenavigation.presentation.*;
12
+import com.reactnativenavigation.viewcontrollers.*;
13
+import com.reactnativenavigation.viewcontrollers.bottomtabs.*;
14
+
15
+import org.junit.*;
16
+import org.mockito.*;
17
+
18
+import java.util.*;
19
+
20
+import static com.reactnativenavigation.utils.CollectionUtils.*;
21
+import static org.assertj.core.api.Java6Assertions.assertThat;
22
+import static org.mockito.Mockito.*;
23
+
24
+public abstract class AttachModeTest extends BaseTest {
25
+    private static final int INITIAL_TAB = 1;
26
+
27
+    private Activity activity;
28
+    private ChildControllersRegistry childRegistry;
29
+    protected ViewGroup parent;
30
+    ViewController tab1;
31
+    ViewController tab2;
32
+    List<ViewController> tabs;
33
+    protected Options options;
34
+    protected BottomTabsPresenter presenter;
35
+    protected AttachMode uut;
36
+
37
+    @Override
38
+    public void beforeEach() {
39
+        activity = newActivity();
40
+        childRegistry = new ChildControllersRegistry();
41
+        parent = new FrameLayout(activity);
42
+        tabs = createTabs();
43
+        options = new Options();
44
+        options.bottomTabsOptions.currentTabIndex = new Number(INITIAL_TAB);
45
+        presenter = Mockito.mock(BottomTabsPresenter.class);
46
+    }
47
+
48
+    @Test
49
+    public void attach_layoutOptionsAreApplied() {
50
+        uut.attach(tab1);
51
+        verify(presenter).applyLayoutParamsOptions(options, tabs.indexOf(tab1));
52
+    }
53
+
54
+    @Test
55
+    public void attach_initialTabIsVisible() {
56
+        uut.attach(initialTab());
57
+        assertThat(initialTab().getView().getVisibility()).isEqualTo(View.VISIBLE);
58
+    }
59
+
60
+    @Test
61
+    public void attach_otherTabsAreInvisibleWhenAttached() {
62
+        forEach(otherTabs(), t -> uut.attach(t));
63
+        forEach(otherTabs(), t -> assertThat(t.getView().getVisibility()).isEqualTo(View.INVISIBLE));
64
+    }
65
+
66
+    ViewController[] otherTabs() {
67
+        return filter(tabs, t -> t != initialTab()).toArray(new ViewController[0]);
68
+    }
69
+
70
+    ViewController initialTab() {
71
+        return tabs.get(INITIAL_TAB);
72
+    }
73
+
74
+    private List<ViewController> createTabs() {
75
+        tab1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
76
+        tab2 = spy(new SimpleViewController(activity, childRegistry, "child2", new Options()));
77
+        ViewController tab3 = new SimpleViewController(activity, childRegistry, "child3", new Options());
78
+        return Arrays.asList(tab1, tab2, tab3);
79
+    }
80
+}

+ 33
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/OnSwitchToTabTest.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode;
2
+
3
+import com.reactnativenavigation.viewcontrollers.bottomtabs.*;
4
+
5
+import org.junit.*;
6
+
7
+public class OnSwitchToTabTest extends AttachModeTest {
8
+
9
+    @Override
10
+    public void beforeEach() {
11
+        super.beforeEach();
12
+        uut = new OnSwitchToTab(parent, tabs, presenter, options);
13
+    }
14
+
15
+    @Test
16
+    public void attach_onlyInitialTabIsAttached() {
17
+        uut.attach();
18
+        assertIsChild(parent, initialTab());
19
+        assertNotChildOf(parent, otherTabs());
20
+    }
21
+
22
+    @Test
23
+    public void onTabSelected_initialTabIsNotHandled() {
24
+        uut.onTabSelected(initialTab());
25
+        assertNotChildOf(parent, initialTab());
26
+    }
27
+
28
+    @Test
29
+    public void onTabSelected_otherTabIsAttached() {
30
+        uut.onTabSelected(tab1);
31
+        assertIsChild(parent, tab1);
32
+    }
33
+}

+ 21
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/bottomtabs/attachmode/TogetherTest.java 查看文件

1
+package com.reactnativenavigation.viewcontrollers.bottomtabs.attachmode;
2
+
3
+import com.reactnativenavigation.viewcontrollers.*;
4
+import com.reactnativenavigation.viewcontrollers.bottomtabs.*;
5
+
6
+import org.junit.*;
7
+
8
+public class TogetherTest extends AttachModeTest {
9
+
10
+    @Override
11
+    public void beforeEach() {
12
+        super.beforeEach();
13
+        uut = new Together(parent, tabs, presenter, options);
14
+    }
15
+
16
+    @Test
17
+    public void attach_allTabsAreAttached() {
18
+        uut.attach();
19
+        assertIsChild(parent, tabs.toArray(new ViewController[0]));
20
+    }
21
+}

+ 1
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java 查看文件

138
     public void showModal_waitForRender() {
138
     public void showModal_waitForRender() {
139
         modal1.options.animations.showModal.waitForRender = new Bool(true);
139
         modal1.options.animations.showModal.waitForRender = new Bool(true);
140
         uut.showModal(modal1, root, new CommandListenerAdapter());
140
         uut.showModal(modal1, root, new CommandListenerAdapter());
141
-        verify(modal1).setOnAppearedListener(any());
141
+        verify(modal1).addOnAppearedListener(any());
142
         verifyZeroInteractions(animator);
142
         verifyZeroInteractions(animator);
143
     }
143
     }
144
 
144
 

+ 3
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/NavigatorTest.java 查看文件

28
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
28
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
29
 import com.reactnativenavigation.viewcontrollers.ComponentViewController;
29
 import com.reactnativenavigation.viewcontrollers.ComponentViewController;
30
 import com.reactnativenavigation.viewcontrollers.ViewController;
30
 import com.reactnativenavigation.viewcontrollers.ViewController;
31
+import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsAttacher;
31
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
32
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
32
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
33
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
33
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
34
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
350
 
351
 
351
     @NonNull
352
     @NonNull
352
     private BottomTabsController newTabs(List<ViewController> tabs) {
353
     private BottomTabsController newTabs(List<ViewController> tabs) {
353
-        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())) {
354
+        BottomTabsPresenter bottomTabsPresenter = new BottomTabsPresenter(tabs, new Options());
355
+        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())) {
354
             @NonNull
356
             @NonNull
355
             @Override
357
             @Override
356
             protected BottomTabs createBottomTabs() {
358
             protected BottomTabs createBottomTabs() {

+ 1
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/navigator/RootPresenterTest.java 查看文件

106
         ViewController spy = spy(root);
106
         ViewController spy = spy(root);
107
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
107
         CommandListenerAdapter listener = spy(new CommandListenerAdapter());
108
         uut.setRoot(spy, defaultOptions, listener);
108
         uut.setRoot(spy, defaultOptions, listener);
109
-        verify(spy).setOnAppearedListener(any());
109
+        verify(spy).addOnAppearedListener(any());
110
         assertThat(spy.getView().getAlpha()).isZero();
110
         assertThat(spy.getView().getAlpha()).isZero();
111
         verifyZeroInteractions(listener);
111
         verifyZeroInteractions(listener);
112
 
112
 

+ 1
- 2
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java 查看文件

203
 
203
 
204
         child2.options.animations.push.waitForRender = new Bool(true);
204
         child2.options.animations.push.waitForRender = new Bool(true);
205
         uut.push(child2, new CommandListenerAdapter());
205
         uut.push(child2, new CommandListenerAdapter());
206
-        verify(child2).setOnAppearedListener(any());
206
+        verify(child2).addOnAppearedListener(any());
207
         verify(animator, times(0)).push(eq(child1.getView()), eq(child1.options.animations.push), any());
207
         verify(animator, times(0)).push(eq(child1.getView()), eq(child1.options.animations.push), any());
208
     }
208
     }
209
 
209
 
989
 
989
 
990
     @Test
990
     @Test
991
     public void destroy() {
991
     public void destroy() {
992
-        uut.ensureViewIsCreated();
993
         uut.destroy();
992
         uut.destroy();
994
         verify(topBarController, times(1)).clear();
993
         verify(topBarController, times(1)).clear();
995
     }
994
     }

+ 5
- 0
lib/src/interfaces/Options.ts 查看文件

445
    * Set a background color for the bottom tabs
445
    * Set a background color for the bottom tabs
446
    */
446
    */
447
   backgroundColor?: Color;
447
   backgroundColor?: Color;
448
+  /**
449
+   * Set when tabs are attached to hierarchy consequently when the
450
+   * RootView's constructor is called.
451
+   */
452
+  tabsAttachMode?: 'together' | 'afterInitialTab' | 'onSwitchToTab';
448
   /**
453
   /**
449
    * Control the Bottom Tabs blur style
454
    * Control the Bottom Tabs blur style
450
    * #### (iOS specific)
455
    * #### (iOS specific)