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
 
2
 
3
 import android.annotation.TargetApi;
3
 import android.annotation.TargetApi;
4
 import android.content.Intent;
4
 import android.content.Intent;
5
+import android.graphics.Color;
5
 import android.os.Build;
6
 import android.os.Build;
6
 import android.os.Bundle;
7
 import android.os.Bundle;
7
 import android.support.annotation.NonNull;
8
 import android.support.annotation.NonNull;
8
 import android.support.annotation.Nullable;
9
 import android.support.annotation.Nullable;
9
 import android.support.v7.app.AppCompatActivity;
10
 import android.support.v7.app.AppCompatActivity;
10
 import android.view.KeyEvent;
11
 import android.view.KeyEvent;
12
+import android.view.View;
11
 
13
 
12
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
14
 import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
13
 import com.facebook.react.modules.core.PermissionAwareActivity;
15
 import com.facebook.react.modules.core.PermissionAwareActivity;
29
     @Override
31
     @Override
30
     protected void onCreate(@Nullable Bundle savedInstanceState) {
32
     protected void onCreate(@Nullable Bundle savedInstanceState) {
31
         super.onCreate(savedInstanceState);
33
         super.onCreate(savedInstanceState);
34
+        addDefaultSplashLayout();
32
         navigator = new Navigator(this, new ChildControllersRegistry(), new ModalStack(this), new OverlayManager());
35
         navigator = new Navigator(this, new ChildControllersRegistry(), new ModalStack(this), new OverlayManager());
33
         getReactGateway().onActivityCreated(this);
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
     @Override
45
     @Override
37
     protected void onResume() {
46
     protected void onResume() {
38
         super.onResume();
47
         super.onResume();
112
     public void onReload() {
121
     public void onReload() {
113
         navigator.destroyViews();
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
 import android.graphics.Color;
4
 import android.graphics.Color;
5
 import android.os.Build;
5
 import android.os.Build;
6
 import android.view.View;
6
 import android.view.View;
7
-import android.view.ViewGroup;
8
 import android.view.ViewGroup.MarginLayoutParams;
7
 import android.view.ViewGroup.MarginLayoutParams;
9
 
8
 
10
 import com.reactnativenavigation.parse.Options;
9
 import com.reactnativenavigation.parse.Options;
29
         this.defaultOptions = defaultOptions;
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
         Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
36
         Options withDefaultOptions = options.copy().withDefaultOptions(defaultOptions);
38
         applyOrientation(withDefaultOptions.layout.orientation);
37
         applyOrientation(withDefaultOptions.layout.orientation);
39
         applyViewOptions(view, withDefaultOptions);
38
         applyViewOptions(view, withDefaultOptions);
112
                     0 : UiUtils.getStatusBarHeight(activity);
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
     @Override
47
     @Override
48
     public void applyOptions(Options options) {
48
     public void applyOptions(Options options) {
49
         super.applyOptions(options);
49
         super.applyOptions(options);
50
-        presenter.present(getView(), options);
50
+        presenter.applyOptions(getView(), options);
51
         if (isRoot()) {
51
         if (isRoot()) {
52
             presenter.applyRootOptions(getView(), options);
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
     @Override
62
     @Override
57
     public void destroy() {
63
     public void destroy() {
58
         if (!isDestroyed() && getView() instanceof Component) {
64
         if (!isDestroyed() && getView() instanceof Component) {

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

14
 import com.reactnativenavigation.presentation.OverlayManager;
14
 import com.reactnativenavigation.presentation.OverlayManager;
15
 import com.reactnativenavigation.react.EventEmitter;
15
 import com.reactnativenavigation.react.EventEmitter;
16
 import com.reactnativenavigation.utils.CommandListener;
16
 import com.reactnativenavigation.utils.CommandListener;
17
-import com.reactnativenavigation.utils.CommandListenerAdapter;
18
 import com.reactnativenavigation.utils.CompatUtils;
17
 import com.reactnativenavigation.utils.CompatUtils;
19
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
18
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
20
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
19
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
26
 public class Navigator extends ParentController {
25
 public class Navigator extends ParentController {
27
 
26
 
28
     private final ModalStack modalStack;
27
     private final ModalStack modalStack;
29
-    private ViewController root;
30
-    private FrameLayout rootLayout;
31
-    private FrameLayout contentLayout;
32
     private final OverlayManager overlayManager;
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
     private Options defaultOptions = new Options();
34
     private Options defaultOptions = new Options();
34
 
35
 
35
     @Override
36
     @Override
43
         return defaultOptions;
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
     public Navigator(final Activity activity, ChildControllersRegistry childRegistry, ModalStack modalStack, OverlayManager overlayManager) {
62
     public Navigator(final Activity activity, ChildControllersRegistry childRegistry, ModalStack modalStack, OverlayManager overlayManager) {
47
         super(activity, childRegistry,"navigator" + CompatUtils.generateViewId(), new OptionsPresenter(activity, new Options()), new Options());
63
         super(activity, childRegistry,"navigator" + CompatUtils.generateViewId(), new OptionsPresenter(activity, new Options()), new Options());
48
         this.modalStack = modalStack;
64
         this.modalStack = modalStack;
49
         this.overlayManager = overlayManager;
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
     @NonNull
72
     @NonNull
57
     @Override
73
     @Override
58
     protected ViewGroup createView() {
74
     protected ViewGroup createView() {
59
-        rootLayout = new FrameLayout(getActivity());
60
-        contentLayout = new FrameLayout(getActivity());
61
-        rootLayout.addView(contentLayout);
62
-        modalStack.setContentLayout(contentLayout);
63
         return rootLayout;
75
         return rootLayout;
64
     }
76
     }
65
 
77
 
88
     }
100
     }
89
 
101
 
90
     public void destroyViews() {
102
     public void destroyViews() {
91
-        modalStack.dismissAllModals(new CommandListenerAdapter(), root);
103
+        modalStack.destroy();
92
         overlayManager.destroy();
104
         overlayManager.destroy();
93
         destroyRoot();
105
         destroyRoot();
94
     }
106
     }
105
 
117
 
106
     public void setRoot(final ViewController viewController, CommandListener commandListener) {
118
     public void setRoot(final ViewController viewController, CommandListener commandListener) {
107
         destroyRoot();
119
         destroyRoot();
108
-        if (view == null) {
109
-            getActivity().setContentView(getView());
120
+        if (isRootNotCreated()) {
121
+            removePreviousContentView();
122
+            getView();
110
         }
123
         }
111
         root = viewController;
124
         root = viewController;
112
-        contentLayout.addView(viewController.getView());
125
+        rootLayout.addView(viewController.getView());
113
         if (viewController.options.animations.startApp.hasAnimation()) {
126
         if (viewController.options.animations.startApp.hasAnimation()) {
114
             new NavigationAnimator(viewController.getActivity(), new ElementTransitionManager())
127
             new NavigationAnimator(viewController.getActivity(), new ElementTransitionManager())
115
                     .animateStartApp(viewController.getView(), viewController.options.animations.startApp, new AnimatorListenerAdapter() {
128
                     .animateStartApp(viewController.getView(), viewController.options.animations.startApp, new AnimatorListenerAdapter() {
123
         }
136
         }
124
     }
137
     }
125
 
138
 
139
+    private void removePreviousContentView() {
140
+        contentLayout.removeViewAt(0);
141
+    }
142
+
126
     public void mergeOptions(final String componentId, Options options) {
143
     public void mergeOptions(final String componentId, Options options) {
127
         ViewController target = findControllerById(componentId);
144
         ViewController target = findControllerById(componentId);
128
         if (target != null) {
145
         if (target != null) {
186
     }
203
     }
187
 
204
 
188
     public void dismissModal(final String componentId, CommandListener listener) {
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
         modalStack.dismissModal(componentId, root, listener);
210
         modalStack.dismissModal(componentId, root, listener);
190
     }
211
     }
191
 
212
 
194
     }
215
     }
195
 
216
 
196
     public void showOverlay(ViewController overlay, CommandListener listener) {
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
     public void dismissOverlay(final String componentId, CommandListener listener) {
221
     public void dismissOverlay(final String componentId, CommandListener listener) {
216
                          " was not found.");
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
 import com.reactnativenavigation.anim.ModalAnimator;
9
 import com.reactnativenavigation.anim.ModalAnimator;
10
 import com.reactnativenavigation.parse.ModalPresentationStyle;
10
 import com.reactnativenavigation.parse.ModalPresentationStyle;
11
 import com.reactnativenavigation.parse.Options;
11
 import com.reactnativenavigation.parse.Options;
12
-import com.reactnativenavigation.react.EventEmitter;
13
 import com.reactnativenavigation.utils.CommandListener;
12
 import com.reactnativenavigation.utils.CommandListener;
14
 import com.reactnativenavigation.viewcontrollers.ViewController;
13
 import com.reactnativenavigation.viewcontrollers.ViewController;
15
 
14
 
16
 public class ModalPresenter {
15
 public class ModalPresenter {
17
 
16
 
18
-    @Nullable private ViewGroup content;
17
+    private ViewGroup modalsContainer;
19
     private ModalAnimator animator;
18
     private ModalAnimator animator;
20
     private Options defaultOptions = new Options();
19
     private Options defaultOptions = new Options();
21
-    private EventEmitter eventEmitter;
22
 
20
 
23
     ModalPresenter(ModalAnimator animator) {
21
     ModalPresenter(ModalAnimator animator) {
24
         this.animator = animator;
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
     public void setDefaultOptions(Options defaultOptions) {
29
     public void setDefaultOptions(Options defaultOptions) {
33
     }
31
     }
34
 
32
 
35
     public void showModal(ViewController toAdd, ViewController toRemove, CommandListener listener) {
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
             return;
36
             return;
39
         }
37
         }
40
         Options options = toAdd.resolveCurrentOptions(defaultOptions);
38
         Options options = toAdd.resolveCurrentOptions(defaultOptions);
41
         toAdd.setWaitForRender(options.animations.showModal.waitForRender);
39
         toAdd.setWaitForRender(options.animations.showModal.waitForRender);
42
-        content.addView(toAdd.getView());
40
+        modalsContainer.addView(toAdd.getView());
43
         if (options.animations.showModal.enable.isTrueOrUndefined()) {
41
         if (options.animations.showModal.enable.isTrueOrUndefined()) {
44
             if (options.animations.showModal.waitForRender.isTrue()) {
42
             if (options.animations.showModal.waitForRender.isTrue()) {
45
                 toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
43
                 toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
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
             toRemove.detachView();
67
             toRemove.detachView();
70
         }
68
         }
71
         listener.onSuccess(toAdd.getId());
69
         listener.onSuccess(toAdd.getId());
72
     }
70
     }
73
 
71
 
74
     public void dismissTopModal(ViewController toDismiss, @NonNull ViewController toAdd, CommandListener listener) {
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
             return;
75
             return;
78
         }
76
         }
79
-        toAdd.attachView(content, 0);
77
+        toAdd.attachView(modalsContainer, 0);
80
         dismissModal(toDismiss, listener);
78
         dismissModal(toDismiss, listener);
81
     }
79
     }
82
 
80
 
83
     public void dismissModal(ViewController toDismiss, CommandListener listener) {
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
             return;
84
             return;
87
         }
85
         }
88
         if (toDismiss.options.animations.dismissModal.enable.isTrueOrUndefined()) {
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
         this.presenter = presenter;
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
     public void setDefaultOptions(Options defaultOptions) {
42
     public void setDefaultOptions(Options defaultOptions) {
49
         presenter.showModal(viewController, toRemove, listener);
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
         ViewController toDismiss = findModalByComponentId(componentId);
53
         ViewController toDismiss = findModalByComponentId(componentId);
54
         if (toDismiss != null) {
54
         if (toDismiss != null) {
55
             boolean isTop = isTop(toDismiss);
55
             boolean isTop = isTop(toDismiss);
56
             modals.remove(toDismiss);
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
             CommandListenerAdapter onDismiss = new CommandListenerAdapter(listener) {
58
             CommandListenerAdapter onDismiss = new CommandListenerAdapter(listener) {
59
                 @Override
59
                 @Override
60
                 public void onSuccess(String childId) {
60
                 public void onSuccess(String childId) {
63
                 }
63
                 }
64
             };
64
             };
65
             if (isTop) {
65
             if (isTop) {
66
+                if (toAdd == null) {
67
+                    listener.onError("Could not dismiss modal");
68
+                    return false;
69
+                }
66
                 presenter.dismissTopModal(toDismiss, toAdd, onDismiss);
70
                 presenter.dismissTopModal(toDismiss, toAdd, onDismiss);
67
             } else {
71
             } else {
68
                 presenter.dismissModal(toDismiss, onDismiss);
72
                 presenter.dismissModal(toDismiss, onDismiss);
69
             }
73
             }
74
+            return true;
70
         } else {
75
         } else {
71
             listener.onError("Nothing to dismiss");
76
             listener.onError("Nothing to dismiss");
77
+            return false;
72
         }
78
         }
73
     }
79
     }
74
 
80
 
102
         if (peek().handleBack(listener)) {
108
         if (peek().handleBack(listener)) {
103
             return true;
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
     public ViewController peek() {
114
     public ViewController peek() {
148
         }
153
         }
149
         return null;
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
         }
66
         }
67
     }
67
     }
68
 
68
 
69
+    protected void disableModalAnimations(ViewController... modals) {
70
+        disableShowModalAnimation(modals);
71
+        disableDismissModalAnimation(modals);
72
+    }
73
+
69
     protected void disableShowModalAnimation(ViewController... modals) {
74
     protected void disableShowModalAnimation(ViewController... modals) {
70
         for (ViewController modal : modals) {
75
         for (ViewController modal : modals) {
71
             modal.options.animations.showModal.enable = new Bool(false);
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
 package com.reactnativenavigation.viewcontrollers;
1
 package com.reactnativenavigation.viewcontrollers;
2
 
2
 
3
+import android.os.Bundle;
3
 import android.support.annotation.NonNull;
4
 import android.support.annotation.NonNull;
4
 import android.view.View;
5
 import android.view.View;
6
+import android.widget.FrameLayout;
5
 
7
 
6
 import com.reactnativenavigation.BaseTest;
8
 import com.reactnativenavigation.BaseTest;
7
 import com.reactnativenavigation.TestActivity;
9
 import com.reactnativenavigation.TestActivity;
91
         child5 = new SimpleViewController(activity, childRegistry, "child5", tabOptions);
93
         child5 = new SimpleViewController(activity, childRegistry, "child5", tabOptions);
92
 
94
 
93
         activityController.visible();
95
         activityController.visible();
96
+        activityController.postCreate(Bundle.EMPTY);
94
     }
97
     }
95
 
98
 
96
     @Test
99
     @Test
107
     }
110
     }
108
 
111
 
109
     @Test
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
     @Test
124
     @Test
117
     public void setRoot_AddsChildControllerView() {
125
     public void setRoot_AddsChildControllerView() {
118
         uut.setRoot(child1, new CommandListenerAdapter());
126
         uut.setRoot(child1, new CommandListenerAdapter());
119
-        assertIsChild(uut.getContentLayout(), child1.getView());
127
+        assertIsChild(uut.getRootLayout(), child1.getView());
120
     }
128
     }
121
 
129
 
122
     @Test
130
     @Test
123
     public void setRoot_ReplacesExistingChildControllerViews() {
131
     public void setRoot_ReplacesExistingChildControllerViews() {
124
         uut.setRoot(child1, new CommandListenerAdapter());
132
         uut.setRoot(child1, new CommandListenerAdapter());
125
         uut.setRoot(child2, new CommandListenerAdapter());
133
         uut.setRoot(child2, new CommandListenerAdapter());
126
-        assertIsChild(uut.getContentLayout(), child2.getView());
134
+        assertIsChild(uut.getRootLayout(), child2.getView());
127
     }
135
     }
128
 
136
 
129
     @Test
137
     @Test
466
         verify(parentVisibilityListener, times(2)).onViewAppeared(parentController.getView());
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
     @Test
494
     @Test
470
     public void dismissAllModals_onViewAppearedInvokedOnRoot() {
495
     public void dismissAllModals_onViewAppearedInvokedOnRoot() {
471
         disablePushAnimation(child2);
496
         disablePushAnimation(child2);
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
     @Test
544
     @Test
510
     public void destroy_destroyedRoot() {
545
     public void destroy_destroyedRoot() {
511
         disablePushAnimation(child1);
546
         disablePushAnimation(child1);
527
 
562
 
528
     @Test
563
     @Test
529
     public void destroyViews() {
564
     public void destroyViews() {
530
-        disableShowModalAnimation(child1);
531
         uut.setRoot(parentController, new CommandListenerAdapter());
565
         uut.setRoot(parentController, new CommandListenerAdapter());
532
         uut.showModal(child1, new CommandListenerAdapter());
566
         uut.showModal(child1, new CommandListenerAdapter());
533
         uut.showOverlay(child2, new CommandListenerAdapter());
567
         uut.showOverlay(child2, new CommandListenerAdapter());

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

59
         TopTabsLayoutCreator layoutCreator = Mockito.mock(TopTabsLayoutCreator.class);
59
         TopTabsLayoutCreator layoutCreator = Mockito.mock(TopTabsLayoutCreator.class);
60
         Mockito.when(layoutCreator.create()).thenReturn(topTabsLayout);
60
         Mockito.when(layoutCreator.create()).thenReturn(topTabsLayout);
61
         OptionsPresenter presenter = new OptionsPresenter(activity, new Options());
61
         OptionsPresenter presenter = new OptionsPresenter(activity, new Options());
62
+        options.topBar.buttons.back.visible = new Bool(false);
62
         uut = spy(new TopTabsController(activity, childRegistry, "componentId", tabControllers, layoutCreator, options, presenter));
63
         uut = spy(new TopTabsController(activity, childRegistry, "componentId", tabControllers, layoutCreator, options, presenter));
63
         tabControllers.forEach(viewController -> viewController.setParentController(uut));
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
         uut.applyOptions(options);
57
         uut.applyOptions(options);
58
         verify(presenter, times(0)).applyRootOptions(uut.getView(), options);
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
 
52
 
53
         animator = spy(new ModalAnimator(activity));
53
         animator = spy(new ModalAnimator(activity));
54
         uut = new ModalPresenter(animator);
54
         uut = new ModalPresenter(animator);
55
-        uut.setContentLayout(contentLayout);
55
+        uut.setModalsContainer(contentLayout);
56
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
56
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
57
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
57
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
58
     }
58
     }
139
 
139
 
140
     @Test
140
     @Test
141
     public void showModal_rejectIfContentIsNull() {
141
     public void showModal_rejectIfContentIsNull() {
142
-        uut.setContentLayout(null);
142
+        uut.setModalsContainer(null);
143
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
143
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
144
         uut.showModal(modal1, modal2, listener);
144
         uut.showModal(modal1, modal2, listener);
145
         verify(listener).onError(any());
145
         verify(listener).onError(any());
165
     public void dismissModal_previousViewIsAddedAtIndex0() {
165
     public void dismissModal_previousViewIsAddedAtIndex0() {
166
         modal2.ensureViewIsCreated();
166
         modal2.ensureViewIsCreated();
167
         FrameLayout spy = spy(new FrameLayout(newActivity()));
167
         FrameLayout spy = spy(new FrameLayout(newActivity()));
168
-        uut.setContentLayout(spy);
168
+        uut.setModalsContainer(spy);
169
         uut.dismissTopModal(modal1, modal2, new CommandListenerAdapter());
169
         uut.dismissTopModal(modal1, modal2, new CommandListenerAdapter());
170
         verify(spy, times(1)).addView(modal2.getView(), 0);
170
         verify(spy, times(1)).addView(modal2.getView(), 0);
171
     }
171
     }
222
 
222
 
223
     @Test
223
     @Test
224
     public void dismissTopModal_rejectIfContentIsNull() {
224
     public void dismissTopModal_rejectIfContentIsNull() {
225
-        uut.setContentLayout(null);
225
+        uut.setModalsContainer(null);
226
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
226
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
227
         uut.dismissTopModal(modal1, modal2, listener);
227
         uut.dismissTopModal(modal1, modal2, listener);
228
         verify(listener).onError(any());
228
         verify(listener).onError(any());
230
 
230
 
231
     @Test
231
     @Test
232
     public void dismissModal_rejectIfContentIsNull() {
232
     public void dismissModal_rejectIfContentIsNull() {
233
-        uut.setContentLayout(null);
233
+        uut.setModalsContainer(null);
234
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
234
         CommandListenerAdapter listener = Mockito.mock(CommandListenerAdapter.class);
235
         uut.dismissModal(modal1, listener);
235
         uut.dismissModal(modal1, listener);
236
         verify(listener).onError(any());
236
         verify(listener).onError(any());

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

57
         animator = spy(new ModalAnimatorMock(activity));
57
         animator = spy(new ModalAnimatorMock(activity));
58
         presenter = spy(new ModalPresenter(animator));
58
         presenter = spy(new ModalPresenter(animator));
59
         uut = new ModalStack(presenter);
59
         uut = new ModalStack(presenter);
60
-        uut.setContentLayout(activityContentView);
60
+        uut.setModalsContainer(activityContentView);
61
         uut.setEventEmitter(Mockito.mock(EventEmitter.class));
61
         uut.setEventEmitter(Mockito.mock(EventEmitter.class));
62
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
62
         modal1 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_1, new Options()));
63
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
63
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
268
         verify(presenter).setDefaultOptions(defaultOptions);
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
     private ViewController findModal(String id) {
279
     private ViewController findModal(String id) {
272
         return uut.findControllerById(id);
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
 
18
 
19
     defaultConfig {
19
     defaultConfig {
20
         applicationId "com.reactnativenavigation.playground"
20
         applicationId "com.reactnativenavigation.playground"
21
-        minSdkVersion 19
21
+        minSdkVersion 21
22
         targetSdkVersion 25
22
         targetSdkVersion 25
23
         versionCode 1
23
         versionCode 1
24
         versionName "1.0"
24
         versionName "1.0"

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

1
 package com.reactnativenavigation.playground;
1
 package com.reactnativenavigation.playground;
2
 
2
 
3
 import android.content.Intent;
3
 import android.content.Intent;
4
+import android.os.Bundle;
5
+import android.support.annotation.Nullable;
6
+import android.widget.ImageView;
4
 
7
 
5
 import com.reactnativenavigation.NavigationActivity;
8
 import com.reactnativenavigation.NavigationActivity;
6
 
9
 
7
 public class MainActivity extends NavigationActivity {
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
     @Override
24
     @Override
9
     public void onWindowFocusChanged(boolean hasFocus) {
25
     public void onWindowFocusChanged(boolean hasFocus) {
10
         dismissSystemAlertsToPreventDetoxFromTimingOut(hasFocus);
26
         dismissSystemAlertsToPreventDetoxFromTimingOut(hasFocus);

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

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
 
22
 
23
 function start() {
23
 function start() {
24
   registerScreens();
24
   registerScreens();
25
-  Navigation.events().registerAppLaunchedListener(() => {
25
+  Navigation.events().registerAppLaunchedListener(async () => {
26
     Navigation.setDefaultOptions({
26
     Navigation.setDefaultOptions({
27
       bottomTab: {
27
       bottomTab: {
28
         iconColor: '#1B4C77',
28
         iconColor: '#1B4C77',
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
     Navigation.setRoot({
162
     Navigation.setRoot({
151
       root: {
163
       root: {
152
         stack: {
164
         stack: {