Browse Source

Support showing Overlays and Modals before root is set (#3910)

Support displaying modals and overlays before root is set
Guy Carmeli 6 years ago
parent
commit
7254a66783
No account linked to committer's email address
16 changed files with 254 additions and 60 deletions
  1. 15
    0
      lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java
  2. 47
    4
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java
  3. 7
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ChildController.java
  4. 40
    19
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  5. 13
    15
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java
  6. 18
    6
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalStack.java
  7. 5
    0
      lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java
  8. 41
    7
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  9. 1
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java
  10. 7
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/child/ChildControllerTest.java
  11. 5
    5
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java
  12. 20
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java
  13. 1
    1
      playground/android/app/build.gradle
  14. 16
    0
      playground/android/app/src/main/java/com/reactnativenavigation/playground/MainActivity.java
  15. 5
    0
      playground/android/app/src/main/res/drawable/ic_android.xml
  16. 13
    1
      playground/src/app.js

+ 15
- 0
lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java View File

@@ -2,12 +2,14 @@ package com.reactnativenavigation;
2 2
 
3 3
 import android.annotation.TargetApi;
4 4
 import android.content.Intent;
5
+import android.graphics.Color;
5 6
 import android.os.Build;
6 7
 import android.os.Bundle;
7 8
 import android.support.annotation.NonNull;
8 9
 import android.support.annotation.Nullable;
9 10
 import android.support.v7.app.AppCompatActivity;
10 11
 import android.view.KeyEvent;
12
+import android.view.View;
11 13
 
12 14
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
13 15
 import com.facebook.react.modules.core.PermissionAwareActivity;
@@ -29,10 +31,17 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
29 31
     @Override
30 32
     protected void onCreate(@Nullable Bundle savedInstanceState) {
31 33
         super.onCreate(savedInstanceState);
34
+        addDefaultSplashLayout();
32 35
         navigator = new Navigator(this, new ChildControllersRegistry(), new ModalStack(this), new OverlayManager());
33 36
         getReactGateway().onActivityCreated(this);
34 37
     }
35 38
 
39
+    @Override
40
+    public void onPostCreate(@Nullable Bundle savedInstanceState) {
41
+        super.onPostCreate(savedInstanceState);
42
+        navigator.setContentLayout(findViewById(android.R.id.content));
43
+    }
44
+
36 45
     @Override
37 46
     protected void onResume() {
38 47
         super.onResume();
@@ -112,4 +121,10 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
112 121
     public void onReload() {
113 122
         navigator.destroyViews();
114 123
     }
124
+
125
+    private void addDefaultSplashLayout() {
126
+        View view = new View(this);
127
+        view.setBackgroundColor(Color.WHITE);
128
+        setContentView(view);
129
+    }
115 130
 }

+ 47
- 4
lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java View File

@@ -4,7 +4,6 @@ import android.app.Activity;
4 4
 import android.graphics.Color;
5 5
 import android.os.Build;
6 6
 import android.view.View;
7
-import android.view.ViewGroup;
8 7
 import android.view.ViewGroup.MarginLayoutParams;
9 8
 
10 9
 import com.reactnativenavigation.parse.Options;
@@ -29,11 +28,11 @@ public class OptionsPresenter {
29 28
         this.defaultOptions = defaultOptions;
30 29
     }
31 30
 
32
-    public void applyLayoutOptions(ViewGroup.LayoutParams layoutParams) {
33
-
31
+    public void mergeOptions(View view, Options options) {
32
+        mergeStatusBarOptions(view, options.statusBar);
34 33
     }
35 34
 
36
-    public void present(View view, Options options) {
35
+    public void applyOptions(View view, Options options) {
37 36
         Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
38 37
         applyOrientation(withDefaultOptions.layout.orientation);
39 38
         applyViewOptions(view, withDefaultOptions);
@@ -112,4 +111,48 @@ public class OptionsPresenter {
112 111
                     0 : UiUtils.getStatusBarHeight(activity);
113 112
         }
114 113
     }
114
+
115
+
116
+    private void mergeStatusBarOptions(View view, StatusBarOptions statusBar) {
117
+        mergeStatusBarBackgroundColor(statusBar);
118
+        mergeTextColorScheme(statusBar.textColorScheme);
119
+        mergeStatusBarVisible(view, statusBar.visible, statusBar.drawBehind);
120
+    }
121
+
122
+    private void mergeStatusBarBackgroundColor(StatusBarOptions statusBar) {
123
+        if (statusBar.backgroundColor.hasValue() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
124
+            activity.getWindow().setStatusBarColor(statusBar.backgroundColor.get(Color.BLACK));
125
+        }
126
+    }
127
+
128
+    private void mergeTextColorScheme(TextColorScheme scheme) {
129
+        if (!scheme.hasValue() || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
130
+        final View view = activity.getWindow().getDecorView();
131
+        if (scheme == TextColorScheme.Dark) {
132
+            int flags = view.getSystemUiVisibility();
133
+            flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
134
+            view.setSystemUiVisibility(flags);
135
+        } else {
136
+            clearDarkTextColorScheme(view);
137
+        }
138
+    }
139
+
140
+    private void mergeStatusBarVisible(View view, Bool visible, Bool drawBehind) {
141
+        if (visible.hasValue()) {
142
+            int flags = view.getSystemUiVisibility();
143
+            if (visible.isTrue()) {
144
+                flags &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN & ~View.SYSTEM_UI_FLAG_FULLSCREEN;
145
+            } else {
146
+                flags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN;
147
+            }
148
+            view.setSystemUiVisibility(flags);
149
+        } else if (drawBehind.hasValue()) {
150
+            if (drawBehind.isTrue()) {
151
+                view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
152
+            } else {
153
+                view.setSystemUiVisibility(~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
154
+            }
155
+        }
156
+    }
157
+
115 158
 }

+ 7
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ChildController.java View File

@@ -47,12 +47,18 @@ public abstract class ChildController<T extends ViewGroup> extends ViewControlle
47 47
     @Override
48 48
     public void applyOptions(Options options) {
49 49
         super.applyOptions(options);
50
-        presenter.present(getView(), options);
50
+        presenter.applyOptions(getView(), options);
51 51
         if (isRoot()) {
52 52
             presenter.applyRootOptions(getView(), options);
53 53
         }
54 54
     }
55 55
 
56
+    @Override
57
+    public void mergeOptions(Options options) {
58
+        presenter.mergeOptions(getView(), options);
59
+        super.mergeOptions(options);
60
+    }
61
+
56 62
     @Override
57 63
     public void destroy() {
58 64
         if (!isDestroyed() && getView() instanceof Component) {

+ 40
- 19
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java View File

@@ -14,7 +14,6 @@ import com.reactnativenavigation.presentation.OptionsPresenter;
14 14
 import com.reactnativenavigation.presentation.OverlayManager;
15 15
 import com.reactnativenavigation.react.EventEmitter;
16 16
 import com.reactnativenavigation.utils.CommandListener;
17
-import com.reactnativenavigation.utils.CommandListenerAdapter;
18 17
 import com.reactnativenavigation.utils.CompatUtils;
19 18
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
20 19
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
@@ -26,10 +25,12 @@ import java.util.Collections;
26 25
 public class Navigator extends ParentController {
27 26
 
28 27
     private final ModalStack modalStack;
29
-    private ViewController root;
30
-    private FrameLayout rootLayout;
31
-    private FrameLayout contentLayout;
32 28
     private final OverlayManager overlayManager;
29
+    private ViewController root;
30
+    private final FrameLayout rootLayout;
31
+    private final FrameLayout modalsLayout;
32
+    private final FrameLayout overlaysLayout;
33
+    private ViewGroup contentLayout;
33 34
     private Options defaultOptions = new Options();
34 35
 
35 36
     @Override
@@ -43,23 +44,34 @@ public class Navigator extends ParentController {
43 44
         return defaultOptions;
44 45
     }
45 46
 
47
+    public FrameLayout getRootLayout() {
48
+        return rootLayout;
49
+    }
50
+
51
+    public void setEventEmitter(EventEmitter eventEmitter) {
52
+        modalStack.setEventEmitter(eventEmitter);
53
+    }
54
+
55
+    public void setContentLayout(ViewGroup contentLayout) {
56
+        this.contentLayout = contentLayout;
57
+        contentLayout.addView(rootLayout);
58
+        contentLayout.addView(modalsLayout);
59
+        contentLayout.addView(overlaysLayout);
60
+    }
61
+
46 62
     public Navigator(final Activity activity, ChildControllersRegistry childRegistry, ModalStack modalStack, OverlayManager overlayManager) {
47 63
         super(activity, childRegistry,"navigator" + CompatUtils.generateViewId(), new OptionsPresenter(activity, new Options()), new Options());
48 64
         this.modalStack = modalStack;
49 65
         this.overlayManager = overlayManager;
50
-    }
51
-
52
-    public FrameLayout getContentLayout() {
53
-        return contentLayout;
66
+        rootLayout = new FrameLayout(getActivity());
67
+        modalsLayout = new FrameLayout(getActivity());
68
+        overlaysLayout = new FrameLayout(getActivity());
69
+        modalStack.setModalsContainer(modalsLayout);
54 70
     }
55 71
 
56 72
     @NonNull
57 73
     @Override
58 74
     protected ViewGroup createView() {
59
-        rootLayout = new FrameLayout(getActivity());
60
-        contentLayout = new FrameLayout(getActivity());
61
-        rootLayout.addView(contentLayout);
62
-        modalStack.setContentLayout(contentLayout);
63 75
         return rootLayout;
64 76
     }
65 77
 
@@ -88,7 +100,7 @@ public class Navigator extends ParentController {
88 100
     }
89 101
 
90 102
     public void destroyViews() {
91
-        modalStack.dismissAllModals(new CommandListenerAdapter(), root);
103
+        modalStack.destroy();
92 104
         overlayManager.destroy();
93 105
         destroyRoot();
94 106
     }
@@ -105,11 +117,12 @@ public class Navigator extends ParentController {
105 117
 
106 118
     public void setRoot(final ViewController viewController, CommandListener commandListener) {
107 119
         destroyRoot();
108
-        if (view == null) {
109
-            getActivity().setContentView(getView());
120
+        if (isRootNotCreated()) {
121
+            removePreviousContentView();
122
+            getView();
110 123
         }
111 124
         root = viewController;
112
-        contentLayout.addView(viewController.getView());
125
+        rootLayout.addView(viewController.getView());
113 126
         if (viewController.options.animations.startApp.hasAnimation()) {
114 127
             new NavigationAnimator(viewController.getActivity(), new ElementTransitionManager())
115 128
                     .animateStartApp(viewController.getView(), viewController.options.animations.startApp, new AnimatorListenerAdapter() {
@@ -123,6 +136,10 @@ public class Navigator extends ParentController {
123 136
         }
124 137
     }
125 138
 
139
+    private void removePreviousContentView() {
140
+        contentLayout.removeViewAt(0);
141
+    }
142
+
126 143
     public void mergeOptions(final String componentId, Options options) {
127 144
         ViewController target = findControllerById(componentId);
128 145
         if (target != null) {
@@ -186,6 +203,10 @@ public class Navigator extends ParentController {
186 203
     }
187 204
 
188 205
     public void dismissModal(final String componentId, CommandListener listener) {
206
+        if (isRootNotCreated() && modalStack.size() == 1) {
207
+            listener.onError("Can not dismiss modal if root is not set and only one modal is displayed.");
208
+            return;
209
+        }
189 210
         modalStack.dismissModal(componentId, root, listener);
190 211
     }
191 212
 
@@ -194,7 +215,7 @@ public class Navigator extends ParentController {
194 215
     }
195 216
 
196 217
     public void showOverlay(ViewController overlay, CommandListener listener) {
197
-        overlayManager.show(rootLayout, overlay, listener);
218
+        overlayManager.show(overlaysLayout, overlay, listener);
198 219
     }
199 220
 
200 221
     public void dismissOverlay(final String componentId, CommandListener listener) {
@@ -216,7 +237,7 @@ public class Navigator extends ParentController {
216 237
                          " was not found.");
217 238
     }
218 239
 
219
-    public void setEventEmitter(EventEmitter eventEmitter) {
220
-        modalStack.setEventEmitter(eventEmitter);
240
+    private boolean isRootNotCreated() {
241
+        return view == null;
221 242
     }
222 243
 }

+ 13
- 15
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java View File

@@ -9,23 +9,21 @@ import android.view.ViewGroup;
9 9
 import com.reactnativenavigation.anim.ModalAnimator;
10 10
 import com.reactnativenavigation.parse.ModalPresentationStyle;
11 11
 import com.reactnativenavigation.parse.Options;
12
-import com.reactnativenavigation.react.EventEmitter;
13 12
 import com.reactnativenavigation.utils.CommandListener;
14 13
 import com.reactnativenavigation.viewcontrollers.ViewController;
15 14
 
16 15
 public class ModalPresenter {
17 16
 
18
-    @Nullable private ViewGroup content;
17
+    private ViewGroup modalsContainer;
19 18
     private ModalAnimator animator;
20 19
     private Options defaultOptions = new Options();
21
-    private EventEmitter eventEmitter;
22 20
 
23 21
     ModalPresenter(ModalAnimator animator) {
24 22
         this.animator = animator;
25 23
     }
26 24
 
27
-    public void setContentLayout(ViewGroup contentLayout) {
28
-        this.content = contentLayout;
25
+    public void setModalsContainer(ViewGroup modalsLayout) {
26
+        this.modalsContainer = modalsLayout;
29 27
     }
30 28
 
31 29
     public void setDefaultOptions(Options defaultOptions) {
@@ -33,13 +31,13 @@ public class ModalPresenter {
33 31
     }
34 32
 
35 33
     public void showModal(ViewController toAdd, ViewController toRemove, CommandListener listener) {
36
-        if (content == null) {
37
-            listener.onError("Could not show modal before setRoot is called");
34
+        if (modalsContainer == null) {
35
+            listener.onError("Can not show modal before activity is created");
38 36
             return;
39 37
         }
40 38
         Options options = toAdd.resolveCurrentOptions(defaultOptions);
41 39
         toAdd.setWaitForRender(options.animations.showModal.waitForRender);
42
-        content.addView(toAdd.getView());
40
+        modalsContainer.addView(toAdd.getView());
43 41
         if (options.animations.showModal.enable.isTrueOrUndefined()) {
44 42
             if (options.animations.showModal.waitForRender.isTrue()) {
45 43
                 toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
@@ -64,25 +62,25 @@ public class ModalPresenter {
64 62
         });
65 63
     }
66 64
 
67
-    private void onShowModalEnd(ViewController toAdd, ViewController toRemove, CommandListener listener) {
68
-        if (toAdd.options.modal.presentationStyle != ModalPresentationStyle.OverCurrentContext) {
65
+    private void onShowModalEnd(ViewController toAdd, @Nullable ViewController toRemove, CommandListener listener) {
66
+        if (toRemove != null && toAdd.options.modal.presentationStyle != ModalPresentationStyle.OverCurrentContext) {
69 67
             toRemove.detachView();
70 68
         }
71 69
         listener.onSuccess(toAdd.getId());
72 70
     }
73 71
 
74 72
     public void dismissTopModal(ViewController toDismiss, @NonNull ViewController toAdd, CommandListener listener) {
75
-        if (content == null) {
76
-            listener.onError("Could not dismiss modal before setRoot is called");
73
+        if (modalsContainer == null) {
74
+            listener.onError("Can not dismiss modal before activity is created");
77 75
             return;
78 76
         }
79
-        toAdd.attachView(content, 0);
77
+        toAdd.attachView(modalsContainer, 0);
80 78
         dismissModal(toDismiss, listener);
81 79
     }
82 80
 
83 81
     public void dismissModal(ViewController toDismiss, CommandListener listener) {
84
-        if (content == null) {
85
-            listener.onError("Could not dismiss modal before setRoot is called");
82
+        if (modalsContainer == null) {
83
+            listener.onError("Can not dismiss modal before activity is created");
86 84
             return;
87 85
         }
88 86
         if (toDismiss.options.animations.dismissModal.enable.isTrueOrUndefined()) {

+ 18
- 6
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalStack.java View File

@@ -35,8 +35,8 @@ public class ModalStack {
35 35
         this.presenter = presenter;
36 36
     }
37 37
 
38
-    public void setContentLayout(ViewGroup contentLayout) {
39
-        presenter.setContentLayout(contentLayout);
38
+    public void setModalsContainer(ViewGroup modalsLayout) {
39
+        presenter.setModalsContainer(modalsLayout);
40 40
     }
41 41
 
42 42
     public void setDefaultOptions(Options defaultOptions) {
@@ -49,12 +49,12 @@ public class ModalStack {
49 49
         presenter.showModal(viewController, toRemove, listener);
50 50
     }
51 51
 
52
-    public void dismissModal(String componentId, ViewController root, CommandListener listener) {
52
+    public boolean dismissModal(String componentId, @Nullable ViewController root, CommandListener listener) {
53 53
         ViewController toDismiss = findModalByComponentId(componentId);
54 54
         if (toDismiss != null) {
55 55
             boolean isTop = isTop(toDismiss);
56 56
             modals.remove(toDismiss);
57
-            ViewController toAdd = isEmpty() ? root : isTop ? get(size() - 1) : null;
57
+            @Nullable ViewController toAdd = isEmpty() ? root : isTop ? get(size() - 1) : null;
58 58
             CommandListenerAdapter onDismiss = new CommandListenerAdapter(listener) {
59 59
                 @Override
60 60
                 public void onSuccess(String childId) {
@@ -63,12 +63,18 @@ public class ModalStack {
63 63
                 }
64 64
             };
65 65
             if (isTop) {
66
+                if (toAdd == null) {
67
+                    listener.onError("Could not dismiss modal");
68
+                    return false;
69
+                }
66 70
                 presenter.dismissTopModal(toDismiss, toAdd, onDismiss);
67 71
             } else {
68 72
                 presenter.dismissModal(toDismiss, onDismiss);
69 73
             }
74
+            return true;
70 75
         } else {
71 76
             listener.onError("Nothing to dismiss");
77
+            return false;
72 78
         }
73 79
     }
74 80
 
@@ -102,8 +108,7 @@ public class ModalStack {
102 108
         if (peek().handleBack(listener)) {
103 109
             return true;
104 110
         }
105
-        dismissModal(peek().getId(), root, listener);
106
-        return true;
111
+        return dismissModal(peek().getId(), root, listener);
107 112
     }
108 113
 
109 114
     public ViewController peek() {
@@ -148,4 +153,11 @@ public class ModalStack {
148 153
         }
149 154
         return null;
150 155
     }
156
+
157
+    public void destroy() {
158
+        for (ViewController modal : modals) {
159
+            modal.destroy();
160
+        }
161
+        modals.clear();
162
+    }
151 163
 }

+ 5
- 0
lib/android/app/src/test/java/com/reactnativenavigation/BaseTest.java View File

@@ -66,6 +66,11 @@ public abstract class BaseTest {
66 66
         }
67 67
     }
68 68
 
69
+    protected void disableModalAnimations(ViewController... modals) {
70
+        disableShowModalAnimation(modals);
71
+        disableDismissModalAnimation(modals);
72
+    }
73
+
69 74
     protected void disableShowModalAnimation(ViewController... modals) {
70 75
         for (ViewController modal : modals) {
71 76
             modal.options.animations.showModal.enable = new Bool(false);

+ 41
- 7
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java View File

@@ -1,7 +1,9 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3
+import android.os.Bundle;
3 4
 import android.support.annotation.NonNull;
4 5
 import android.view.View;
6
+import android.widget.FrameLayout;
5 7
 
6 8
 import com.reactnativenavigation.BaseTest;
7 9
 import com.reactnativenavigation.TestActivity;
@@ -91,6 +93,7 @@ public class NavigatorTest extends BaseTest {
91 93
         child5 = new SimpleViewController(activity, childRegistry, "child5", tabOptions);
92 94
 
93 95
         activityController.visible();
96
+        activityController.postCreate(Bundle.EMPTY);
94 97
     }
95 98
 
96 99
     @Test
@@ -107,23 +110,28 @@ public class NavigatorTest extends BaseTest {
107 110
     }
108 111
 
109 112
     @Test
110
-    public void setRoot_setContentViewWhenFirstRootIsSet() {
111
-        assertThat(uut.view).isNull();
112
-        uut.setRoot(child1, new CommandListenerAdapter());
113
-        assertThat(uut.view).isNotNull();
113
+    public void setRoot_clearsSplashLayout() {
114
+        disableModalAnimations(child1);
115
+
116
+        FrameLayout content = activity.findViewById(android.R.id.content);
117
+        assertThat(content.getChildCount()).isEqualTo(4); // 3 frame layouts and the default splash layout
118
+
119
+        uut.setRoot(child2, new CommandListenerAdapter());
120
+
121
+        assertThat(content.getChildCount()).isEqualTo(3);
114 122
     }
115 123
 
116 124
     @Test
117 125
     public void setRoot_AddsChildControllerView() {
118 126
         uut.setRoot(child1, new CommandListenerAdapter());
119
-        assertIsChild(uut.getContentLayout(), child1.getView());
127
+        assertIsChild(uut.getRootLayout(), child1.getView());
120 128
     }
121 129
 
122 130
     @Test
123 131
     public void setRoot_ReplacesExistingChildControllerViews() {
124 132
         uut.setRoot(child1, new CommandListenerAdapter());
125 133
         uut.setRoot(child2, new CommandListenerAdapter());
126
-        assertIsChild(uut.getContentLayout(), child2.getView());
134
+        assertIsChild(uut.getRootLayout(), child2.getView());
127 135
     }
128 136
 
129 137
     @Test
@@ -466,6 +474,23 @@ public class NavigatorTest extends BaseTest {
466 474
         verify(parentVisibilityListener, times(2)).onViewAppeared(parentController.getView());
467 475
     }
468 476
 
477
+    @Test
478
+    public void dismissModal_rejectIfRootIsNotSetAndSingleModalIsDisplayed() {
479
+        disableModalAnimations(child1, child2);
480
+        uut.showModal(child1, new CommandListenerAdapter());
481
+        uut.showModal(child2, new CommandListenerAdapter());
482
+
483
+        CommandListenerAdapter listener1 = spy(new CommandListenerAdapter());
484
+        uut.dismissModal(child2.getId(), listener1);
485
+        verify(listener1).onSuccess(any());
486
+        assertThat(child2.isDestroyed()).isTrue();
487
+
488
+        CommandListenerAdapter listener2 = spy(new CommandListenerAdapter());
489
+        uut.dismissModal(child1.getId(), listener2);
490
+        verify(listener2).onError(any());
491
+        assertThat(child1.isDestroyed()).isFalse();
492
+    }
493
+
469 494
     @Test
470 495
     public void dismissAllModals_onViewAppearedInvokedOnRoot() {
471 496
         disablePushAnimation(child2);
@@ -506,6 +531,16 @@ public class NavigatorTest extends BaseTest {
506 531
         });
507 532
     }
508 533
 
534
+    @Test
535
+    public void handleBack_falseIfRootIsNotSetAndSingleModalIsDisplayed() {
536
+        disableShowModalAnimation(child1, child2, child3);
537
+        uut.showModal(child1, new CommandListenerAdapter());
538
+        uut.showModal(child2, new CommandListenerAdapter());
539
+
540
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isTrue();
541
+        assertThat(uut.handleBack(new CommandListenerAdapter())).isFalse();
542
+    }
543
+
509 544
     @Test
510 545
     public void destroy_destroyedRoot() {
511 546
         disablePushAnimation(child1);
@@ -527,7 +562,6 @@ public class NavigatorTest extends BaseTest {
527 562
 
528 563
     @Test
529 564
     public void destroyViews() {
530
-        disableShowModalAnimation(child1);
531 565
         uut.setRoot(parentController, new CommandListenerAdapter());
532 566
         uut.showModal(child1, new CommandListenerAdapter());
533 567
         uut.showOverlay(child2, new CommandListenerAdapter());

+ 1
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java View File

@@ -59,6 +59,7 @@ public class TopTabsViewControllerTest extends BaseTest {
59 59
         TopTabsLayoutCreator layoutCreator = Mockito.mock(TopTabsLayoutCreator.class);
60 60
         Mockito.when(layoutCreator.create()).thenReturn(topTabsLayout);
61 61
         OptionsPresenter presenter = new OptionsPresenter(activity, new Options());
62
+        options.topBar.buttons.back.visible = new Bool(false);
62 63
         uut = spy(new TopTabsController(activity, childRegistry, "componentId", tabControllers, layoutCreator, options, presenter));
63 64
         tabControllers.forEach(viewController -> viewController.setParentController(uut));
64 65
 

+ 7
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/child/ChildControllerTest.java View File

@@ -57,4 +57,11 @@ public class ChildControllerTest extends BaseTest {
57 57
         uut.applyOptions(options);
58 58
         verify(presenter, times(0)).applyRootOptions(uut.getView(), options);
59 59
     }
60
+
61
+    @Test
62
+    public void mergeOptions() {
63
+        Options options = new Options();
64
+        uut.mergeOptions(options);
65
+        verify(presenter).mergeOptions(uut.getView(), options);
66
+    }
60 67
 }

+ 5
- 5
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java View File

@@ -52,7 +52,7 @@ public class ModalPresenterTest extends BaseTest {
52 52
 
53 53
         animator = spy(new ModalAnimator(activity));
54 54
         uut = new ModalPresenter(animator);
55
-        uut.setContentLayout(contentLayout);
55
+        uut.setModalsContainer(contentLayout);
56 56
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
57 57
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
58 58
     }
@@ -139,7 +139,7 @@ public class ModalPresenterTest extends BaseTest {
139 139
 
140 140
     @Test
141 141
     public void showModal_rejectIfContentIsNull() {
142
-        uut.setContentLayout(null);
142
+        uut.setModalsContainer(null);
143 143
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
144 144
         uut.showModal(modal1, modal2, listener);
145 145
         verify(listener).onError(any());
@@ -165,7 +165,7 @@ public class ModalPresenterTest extends BaseTest {
165 165
     public void dismissModal_previousViewIsAddedAtIndex0() {
166 166
         modal2.ensureViewIsCreated();
167 167
         FrameLayout spy = spy(new FrameLayout(newActivity()));
168
-        uut.setContentLayout(spy);
168
+        uut.setModalsContainer(spy);
169 169
         uut.dismissTopModal(modal1, modal2, new CommandListenerAdapter());
170 170
         verify(spy, times(1)).addView(modal2.getView(), 0);
171 171
     }
@@ -222,7 +222,7 @@ public class ModalPresenterTest extends BaseTest {
222 222
 
223 223
     @Test
224 224
     public void dismissTopModal_rejectIfContentIsNull() {
225
-        uut.setContentLayout(null);
225
+        uut.setModalsContainer(null);
226 226
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
227 227
         uut.dismissTopModal(modal1, modal2, listener);
228 228
         verify(listener).onError(any());
@@ -230,7 +230,7 @@ public class ModalPresenterTest extends BaseTest {
230 230
 
231 231
     @Test
232 232
     public void dismissModal_rejectIfContentIsNull() {
233
-        uut.setContentLayout(null);
233
+        uut.setModalsContainer(null);
234 234
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
235 235
         uut.dismissModal(modal1, listener);
236 236
         verify(listener).onError(any());

+ 20
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java View File

@@ -57,7 +57,7 @@ public class ModalStackTest extends BaseTest {
57 57
         animator = spy(new ModalAnimatorMock(activity));
58 58
         presenter = spy(new ModalPresenter(animator));
59 59
         uut = new ModalStack(presenter);
60
-        uut.setContentLayout(activityContentView);
60
+        uut.setModalsContainer(activityContentView);
61 61
         uut.setEventEmitter(Mockito.mock(EventEmitter.class));
62 62
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
63 63
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
@@ -268,7 +268,26 @@ public class ModalStackTest extends BaseTest {
268 268
         verify(presenter).setDefaultOptions(defaultOptions);
269 269
     }
270 270
 
271
+    @Test
272
+    public void destroy() {
273
+        showModalsWithoutAnimation(modal1, modal2);
274
+        uut.destroy();
275
+        verify(modal1).destroy();
276
+        verify(modal2).destroy();
277
+    }
278
+
271 279
     private ViewController findModal(String id) {
272 280
         return uut.findControllerById(id);
273 281
     }
282
+
283
+    private void showModalsWithoutAnimation(ViewController... modals) {
284
+        for (ViewController modal : modals) {
285
+            showModalWithoutAnimation(modal);
286
+        }
287
+    }
288
+
289
+    private void showModalWithoutAnimation(ViewController modal) {
290
+        disableShowModalAnimation(modal);
291
+        uut.showModal(modal, rootController, new CommandListenerAdapter());
292
+    }
274 293
 }

+ 1
- 1
playground/android/app/build.gradle View File

@@ -18,7 +18,7 @@ android {
18 18
 
19 19
     defaultConfig {
20 20
         applicationId "com.reactnativenavigation.playground"
21
-        minSdkVersion 19
21
+        minSdkVersion 21
22 22
         targetSdkVersion 25
23 23
         versionCode 1
24 24
         versionName "1.0"

+ 16
- 0
playground/android/app/src/main/java/com/reactnativenavigation/playground/MainActivity.java View File

@@ -1,10 +1,26 @@
1 1
 package com.reactnativenavigation.playground;
2 2
 
3 3
 import android.content.Intent;
4
+import android.os.Bundle;
5
+import android.support.annotation.Nullable;
6
+import android.widget.ImageView;
4 7
 
5 8
 import com.reactnativenavigation.NavigationActivity;
6 9
 
7 10
 public class MainActivity extends NavigationActivity {
11
+
12
+    @Override
13
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
14
+        super.onCreate(savedInstanceState);
15
+        setSplashLayout();
16
+    }
17
+
18
+    private void setSplashLayout() {
19
+        ImageView img = new ImageView(this);
20
+        img.setImageDrawable(getDrawable(R.drawable.ic_android));
21
+        setContentView(img);
22
+    }
23
+
8 24
     @Override
9 25
     public void onWindowFocusChanged(boolean hasFocus) {
10 26
         dismissSystemAlertsToPreventDetoxFromTimingOut(hasFocus);

+ 5
- 0
playground/android/app/src/main/res/drawable/ic_android.xml View File

@@ -0,0 +1,5 @@
1
+<vector android:height="64dp" android:tint="#A4C639"
2
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
3
+    android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
4
+    <path android:fillColor="#FF000000" android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
5
+</vector>

+ 13
- 1
playground/src/app.js View File

@@ -22,7 +22,7 @@ if (Platform.OS === 'android') {
22 22
 
23 23
 function start() {
24 24
   registerScreens();
25
-  Navigation.events().registerAppLaunchedListener(() => {
25
+  Navigation.events().registerAppLaunchedListener(async () => {
26 26
     Navigation.setDefaultOptions({
27 27
       bottomTab: {
28 28
         iconColor: '#1B4C77',
@@ -147,6 +147,18 @@ function start() {
147 147
       }
148 148
     });
149 149
 
150
+    // await Navigation.showModal({
151
+    //   stack: {
152
+    //     children: [
153
+    //       {
154
+    //         component: {
155
+    //           name: 'navigation.playground.ModalScreen'
156
+    //         }
157
+    //       }
158
+    //     ]
159
+    //   }
160
+    // });
161
+
150 162
     Navigation.setRoot({
151 163
       root: {
152 164
         stack: {