Browse Source

Wait for render android (#3517)

This commit introduces an option to control the way components are pushed into the stack.
When pushing components into a stack, they are pushed the moment they are measured.
If `waitForRender` is true, components will be pushed only after the initial render.

api:

```js
options: {
  animations: {
    push: {
      waitForRender: true
    },
    showModal: {
      waitForRender: true
    }
  }
}
```
Guy Carmeli 6 years ago
parent
commit
903601fa40
No account linked to committer's email address
30 changed files with 423 additions and 67 deletions
  1. 2
    1
      lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java
  2. 18
    15
      lib/android/app/src/main/java/com/reactnativenavigation/parse/AnimationOptions.java
  3. 5
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/NestedAnimationsOptions.java
  4. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/StackOptionsPresenter.java
  5. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/react/ReactView.java
  6. 2
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/IReactView.java
  7. 7
    6
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  8. 23
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java
  9. 28
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  10. 24
    8
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenter.java
  11. 14
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalStack.java
  12. 21
    8
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java
  13. 2
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/Component.java
  14. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/ComponentLayout.java
  15. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/ExternalComponentLayout.java
  16. 19
    1
      lib/android/app/src/main/java/com/reactnativenavigation/views/StackLayout.java
  17. 12
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/toptabs/TopTabsViewPager.java
  18. 5
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/SimpleOverlay.java
  19. 5
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/SimpleViewController.java
  20. 5
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestReactView.java
  21. 15
    4
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  22. 43
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java
  23. 26
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ViewControllerTest.java
  24. 51
    17
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalPresenterTest.java
  25. 7
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/modal/ModalStackTest.java
  26. 11
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java
  27. 5
    0
      playground/src/app.js
  28. 13
    1
      playground/src/screens/ModalScreen.js
  29. 40
    0
      playground/src/screens/PushedScreen.js
  30. 1
    0
      playground/src/testIDs.js

+ 2
- 1
lib/android/app/src/main/java/com/reactnativenavigation/NavigationActivity.java View File

@@ -19,6 +19,7 @@ import com.reactnativenavigation.react.ReactGateway;
19 19
 import com.reactnativenavigation.utils.CommandListenerAdapter;
20 20
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
21 21
 import com.reactnativenavigation.viewcontrollers.Navigator;
22
+import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
22 23
 
23 24
 public class NavigationActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity, JsDevReloadHandler.ReloadListener {
24 25
     @Nullable
@@ -29,7 +30,7 @@ public class NavigationActivity extends AppCompatActivity implements DefaultHard
29 30
     @Override
30 31
     protected void onCreate(@Nullable Bundle savedInstanceState) {
31 32
         super.onCreate(savedInstanceState);
32
-        navigator = new Navigator(this, new ChildControllersRegistry(), new OverlayManager());
33
+        navigator = new Navigator(this, new ChildControllersRegistry(), new ModalStack(this), new OverlayManager());
33 34
         getReactGateway().onActivityCreated(this);
34 35
         navigator.getView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
35 36
         setContentView(navigator.getView());

+ 18
- 15
lib/android/app/src/main/java/com/reactnativenavigation/parse/AnimationOptions.java View File

@@ -26,7 +26,6 @@ public class AnimationOptions {
26 26
         AnimationOptions options = new AnimationOptions();
27 27
         if (json == null) return options;
28 28
 
29
-        options.hasValue = true;
30 29
         for (Iterator<String> it = json.keys(); it.hasNext(); ) {
31 30
             String key = it.next();
32 31
             switch (key) {
@@ -36,6 +35,9 @@ public class AnimationOptions {
36 35
                 case "enable":
37 36
                     options.enable = BoolParser.parse(json, key);
38 37
                     break;
38
+                case "waitForRender":
39
+                    options.waitForRender = BoolParser.parse(json, key);
40
+                    break;
39 41
                 default:
40 42
                     options.valueOptions.add(ValueAnimationOptions.parse(json.optJSONObject(key), getAnimProp(key)));
41 43
             }
@@ -44,30 +46,27 @@ public class AnimationOptions {
44 46
         return options;
45 47
     }
46 48
 
47
-    private boolean hasValue = false;
48
-
49 49
     public Text id = new NullText();
50 50
     public Bool enable = new NullBool();
51
+    public Bool waitForRender = new NullBool();
51 52
     private HashSet<ValueAnimationOptions> valueOptions = new HashSet<>();
52 53
 
53 54
     void mergeWith(AnimationOptions other) {
54
-        if (other.hasValue()) {
55
-            hasValue = true;
56
-            id = other.id;
57
-            valueOptions = other.valueOptions;
58
-        }
55
+        if (other.id.hasValue()) id = other.id;
56
+        if (other.enable.hasValue()) enable = other.enable;
57
+        if (other.waitForRender.hasValue()) waitForRender = other.waitForRender;
58
+        if (!other.valueOptions.isEmpty()) valueOptions = other.valueOptions;
59 59
     }
60 60
 
61 61
     void mergeWithDefault(AnimationOptions defaultOptions) {
62
-        if (defaultOptions.hasValue()) {
63
-            hasValue = true;
64
-            id = defaultOptions.id;
65
-            valueOptions = defaultOptions.valueOptions;
66
-        }
62
+        if (!id.hasValue()) id = defaultOptions.id;
63
+        if (!enable.hasValue()) enable = defaultOptions.enable;
64
+        if (!waitForRender.hasValue()) waitForRender = defaultOptions.waitForRender;
65
+        if (valueOptions.isEmpty()) valueOptions = defaultOptions.valueOptions;
67 66
     }
68 67
 
69 68
     public boolean hasValue() {
70
-        return hasValue;
69
+        return id.hasValue() || enable.hasValue() || waitForRender.hasValue();
71 70
     }
72 71
 
73 72
     public AnimatorSet getAnimation(View view) {
@@ -75,7 +74,7 @@ public class AnimationOptions {
75 74
     }
76 75
 
77 76
     public AnimatorSet getAnimation(View view, AnimatorSet defaultAnimation) {
78
-        if (!hasValue()) return defaultAnimation;
77
+        if (!hasAnimation()) return defaultAnimation;
79 78
         AnimatorSet animationSet = new AnimatorSet();
80 79
         List<Animator> animators = new ArrayList<>();
81 80
         for (ValueAnimationOptions options : valueOptions) {
@@ -106,4 +105,8 @@ public class AnimationOptions {
106 105
         }
107 106
         throw new IllegalArgumentException("This animation is not supported: " + key);
108 107
     }
108
+
109
+    public boolean hasAnimation() {
110
+        return !valueOptions.isEmpty();
111
+    }
109 112
 }

+ 5
- 1
lib/android/app/src/main/java/com/reactnativenavigation/parse/NestedAnimationsOptions.java View File

@@ -16,11 +16,13 @@ public class NestedAnimationsOptions {
16 16
         options.bottomTabs = AnimationOptions.parse(json.optJSONObject("bottomTabs"));
17 17
         options.topBar = AnimationOptions.parse(json.optJSONObject("topBar"));
18 18
         options.enable = BoolParser.parse(json, "enable");
19
+        options.waitForRender = BoolParser.parse(json, "waitForRender");
19 20
 
20 21
         return options;
21 22
     }
22 23
 
23 24
     public Bool enable = new NullBool();
25
+    public Bool waitForRender = new NullBool();
24 26
     public AnimationOptions content = new AnimationOptions();
25 27
     public AnimationOptions bottomTabs = new AnimationOptions();
26 28
     public AnimationOptions topBar = new AnimationOptions();
@@ -30,6 +32,7 @@ public class NestedAnimationsOptions {
30 32
         content.mergeWith(other.content);
31 33
         bottomTabs.mergeWith(other.bottomTabs);
32 34
         if (other.enable.hasValue()) enable = other.enable;
35
+        if (other.waitForRender.hasValue()) waitForRender = other.waitForRender;
33 36
     }
34 37
 
35 38
     void mergeWithDefault(NestedAnimationsOptions defaultOptions) {
@@ -37,9 +40,10 @@ public class NestedAnimationsOptions {
37 40
         bottomTabs.mergeWithDefault(defaultOptions.bottomTabs);
38 41
         topBar.mergeWithDefault(defaultOptions.topBar);
39 42
         if (!enable.hasValue()) enable = defaultOptions.enable;
43
+        if (!waitForRender.hasValue()) waitForRender = defaultOptions.waitForRender;
40 44
     }
41 45
 
42 46
     public boolean hasValue() {
43
-        return topBar.hasValue() || content.hasValue() || bottomTabs.hasValue();
47
+        return topBar.hasValue() || content.hasValue() || bottomTabs.hasValue() || waitForRender.hasValue();
44 48
     }
45 49
 }

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

@@ -38,6 +38,10 @@ public class StackOptionsPresenter {
38 38
         this.defaultOptions = defaultOptions;
39 39
     }
40 40
 
41
+    public Options getDefaultOptions() {
42
+        return defaultOptions;
43
+    }
44
+
41 45
     public void bindView(TopBar topBar) {
42 46
         this.topBar = topBar;
43 47
     }

+ 5
- 0
lib/android/app/src/main/java/com/reactnativenavigation/react/ReactView.java View File

@@ -92,6 +92,11 @@ public class ReactView extends ReactRootView implements IReactView {
92 92
         jsTouchDispatcher.handleTouchEvent(event, getEventDispatcher());
93 93
     }
94 94
 
95
+    @Override
96
+    public boolean isRendered() {
97
+        return getChildCount() >= 1;
98
+    }
99
+
95 100
     public EventDispatcher getEventDispatcher() {
96 101
         ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
97 102
         return reactContext == null ? null : reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();

+ 2
- 0
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/IReactView.java View File

@@ -20,4 +20,6 @@ public interface IReactView extends Destroyable {
20 20
     ScrollEventListener getScrollEventListener();
21 21
 
22 22
     void dispatchTouchEventToJs(MotionEvent event);
23
+
24
+    boolean isRendered();
23 25
 }

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

@@ -8,7 +8,6 @@ import android.support.annotation.Nullable;
8 8
 import android.view.ViewGroup;
9 9
 import android.widget.FrameLayout;
10 10
 
11
-import com.reactnativenavigation.anim.ModalAnimator;
12 11
 import com.reactnativenavigation.anim.NavigationAnimator;
13 12
 import com.reactnativenavigation.parse.Options;
14 13
 import com.reactnativenavigation.presentation.OptionsPresenter;
@@ -16,7 +15,6 @@ import com.reactnativenavigation.presentation.OverlayManager;
16 15
 import com.reactnativenavigation.utils.CommandListener;
17 16
 import com.reactnativenavigation.utils.CommandListenerAdapter;
18 17
 import com.reactnativenavigation.utils.CompatUtils;
19
-import com.reactnativenavigation.viewcontrollers.modal.ModalPresenter;
20 18
 import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
21 19
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
22 20
 
@@ -35,16 +33,19 @@ public class Navigator extends ParentController {
35 33
     @Override
36 34
     public void setDefaultOptions(Options defaultOptions) {
37 35
         this.defaultOptions = defaultOptions;
38
-        if (root != null) root.setDefaultOptions(defaultOptions);
36
+        if (root != null) {
37
+            root.setDefaultOptions(defaultOptions);
38
+            modalStack.setDefaultOptions(defaultOptions);
39
+        }
39 40
     }
40 41
 
41 42
     public Options getDefaultOptions() {
42 43
         return defaultOptions;
43 44
     }
44 45
 
45
-    public Navigator(final Activity activity, ChildControllersRegistry childRegistry, OverlayManager overlayManager) {
46
+    public Navigator(final Activity activity, ChildControllersRegistry childRegistry, ModalStack modalStack, OverlayManager overlayManager) {
46 47
         super(activity, childRegistry,"navigator" + CompatUtils.generateViewId(), new OptionsPresenter(activity, new Options()), new Options());
47
-        modalStack = new ModalStack(new ModalPresenter(new ModalAnimator(activity)));
48
+        this.modalStack = modalStack;
48 49
         this.overlayManager = overlayManager;
49 50
     }
50 51
 
@@ -105,7 +106,7 @@ public class Navigator extends ParentController {
105 106
         destroyRoot();
106 107
         root = viewController;
107 108
         contentLayout.addView(viewController.getView());
108
-        if (viewController.options.animations.startApp.hasValue()) {
109
+        if (viewController.options.animations.startApp.hasAnimation()) {
109 110
             new NavigationAnimator(viewController.getActivity(), viewController.options.animations)
110 111
                     .animateStartApp(viewController.getView(), new AnimatorListenerAdapter() {
111 112
                         @Override

+ 23
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java View File

@@ -9,6 +9,7 @@ import android.support.v4.view.ViewPager;
9 9
 import android.view.ViewGroup;
10 10
 
11 11
 import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.parse.params.Bool;
12 13
 import com.reactnativenavigation.presentation.OptionsPresenter;
13 14
 import com.reactnativenavigation.utils.CollectionUtils;
14 15
 import com.reactnativenavigation.views.Component;
@@ -21,7 +22,13 @@ public abstract class ParentController<T extends ViewGroup> extends ChildControl
21 22
 		super(activity, childRegistry, id, presenter, initialOptions);
22 23
 	}
23 24
 
24
-	@Override
25
+    @Override
26
+    public void setWaitForRender(Bool waitForRender) {
27
+        super.setWaitForRender(waitForRender);
28
+        applyOnController(getCurrentChild(), controller -> ((ViewController) controller).setWaitForRender(waitForRender));
29
+    }
30
+
31
+    @Override
25 32
     public void setDefaultOptions(Options defaultOptions) {
26 33
         Collection<? extends ViewController> children = getChildControllers();
27 34
         if (!CollectionUtils.isNullOrEmpty(children)) {
@@ -35,8 +42,16 @@ public abstract class ParentController<T extends ViewGroup> extends ChildControl
35 42
     @CheckResult
36 43
     public Options resolveCurrentOptions() {
37 44
 	    if (CollectionUtils.isNullOrEmpty(getChildControllers())) return options;
38
-        Options o = getCurrentChild().resolveCurrentOptions();
39
-        return o.copy().mergeWith(options);
45
+        return getCurrentChild()
46
+                .resolveCurrentOptions()
47
+                .copy()
48
+                .mergeWith(options);
49
+    }
50
+
51
+    @Override
52
+    @CheckResult
53
+    public Options resolveCurrentOptions(Options defaultOptions) {
54
+        return resolveCurrentOptions().withDefaultOptions(defaultOptions);
40 55
     }
41 56
 
42 57
     protected abstract ViewController getCurrentChild();
@@ -113,4 +128,9 @@ public abstract class ParentController<T extends ViewGroup> extends ChildControl
113 128
     public void clearTopTabs() {
114 129
 
115 130
     }
131
+
132
+    @Override
133
+    public boolean isRendered() {
134
+        return getCurrentChild() != null && getCurrentChild().isRendered();
135
+    }
116 136
 }

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

@@ -12,6 +12,8 @@ import android.view.ViewManager;
12 12
 import android.view.ViewTreeObserver;
13 13
 
14 14
 import com.reactnativenavigation.parse.Options;
15
+import com.reactnativenavigation.parse.params.Bool;
16
+import com.reactnativenavigation.parse.params.NullBool;
15 17
 import com.reactnativenavigation.presentation.FabOptionsPresenter;
16 18
 import com.reactnativenavigation.utils.CommandListener;
17 19
 import com.reactnativenavigation.utils.StringUtils;
@@ -23,6 +25,7 @@ import com.reactnativenavigation.views.Component;
23 25
 public abstract class ViewController<T extends ViewGroup> implements ViewTreeObserver.OnGlobalLayoutListener {
24 26
 
25 27
     private Runnable onAppearedListener;
28
+    private Bool waitForRender = new NullBool();
26 29
 
27 30
     public interface ViewVisibilityListener {
28 31
         /**
@@ -56,6 +59,10 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
56 59
         options = initialOptions.copy();
57 60
     }
58 61
 
62
+    public void setWaitForRender(Bool waitForRender) {
63
+        this.waitForRender = waitForRender;
64
+    }
65
+
59 66
     public void setOnAppearedListener(Runnable onAppearedListener) {
60 67
         this.onAppearedListener = onAppearedListener;
61 68
     }
@@ -80,6 +87,11 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
80 87
         return options;
81 88
     }
82 89
 
90
+    @CheckResult
91
+    public Options resolveCurrentOptions(Options defaultOptions) {
92
+        return options.copy().withDefaultOptions(defaultOptions);
93
+    }
94
+
83 95
     @CallSuper
84 96
     public void mergeOptions(Options options) {
85 97
         this.options = this.options.mergeWith(options);
@@ -235,6 +247,21 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
235 247
     public abstract void sendOnNavigationButtonPressed(String buttonId);
236 248
 
237 249
     protected boolean isViewShown() {
238
-        return !isDestroyed && getView().isShown();
250
+        return !isDestroyed &&
251
+               getView().isShown() &&
252
+               view != null &&
253
+               isRendered();
254
+    }
255
+
256
+    public boolean isRendered() {
257
+        return view != null && (
258
+                waitForRender.isFalseOrUndefined() ||
259
+                !(view instanceof Component) ||
260
+                ((Component) view).isRendered()
261
+        );
262
+    }
263
+
264
+    void applyOnController(ViewController controller, Task<ViewController> task) {
265
+        if (controller != null) task.run(controller);
239 266
     }
240 267
 }

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

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
7 7
 
8 8
 import com.reactnativenavigation.anim.ModalAnimator;
9 9
 import com.reactnativenavigation.parse.ModalPresentationStyle;
10
+import com.reactnativenavigation.parse.Options;
10 11
 import com.reactnativenavigation.utils.CommandListener;
11 12
 import com.reactnativenavigation.viewcontrollers.ViewController;
12 13
 
@@ -14,8 +15,9 @@ public class ModalPresenter {
14 15
 
15 16
     private ViewGroup content;
16 17
     private ModalAnimator animator;
18
+    private Options defaultOptions = new Options();
17 19
 
18
-    public ModalPresenter(ModalAnimator animator) {
20
+    ModalPresenter(ModalAnimator animator) {
19 21
         this.animator = animator;
20 22
     }
21 23
 
@@ -23,20 +25,34 @@ public class ModalPresenter {
23 25
         this.content = contentLayout;
24 26
     }
25 27
 
28
+    public void setDefaultOptions(Options defaultOptions) {
29
+        this.defaultOptions = defaultOptions;
30
+    }
31
+
26 32
     public void showModal(ViewController toAdd, ViewController toRemove, CommandListener listener) {
33
+        Options options = toAdd.resolveCurrentOptions(defaultOptions);
34
+        toAdd.setWaitForRender(options.animations.showModal.waitForRender);
27 35
         content.addView(toAdd.getView());
28
-        if (toAdd.options.animations.showModal.enable.isTrueOrUndefined()) {
29
-            animator.show(toAdd.getView(), toAdd.options.animations.showModal, new AnimatorListenerAdapter() {
30
-                @Override
31
-                public void onAnimationEnd(Animator animation) {
32
-                    onShowModalEnd(toAdd, toRemove, listener);
33
-                }
34
-            });
36
+        if (options.animations.showModal.enable.isTrueOrUndefined()) {
37
+            if (options.animations.showModal.waitForRender.isTrue()) {
38
+                toAdd.setOnAppearedListener(() -> animateShow(toAdd, toRemove, listener, options));
39
+            } else {
40
+                animateShow(toAdd, toRemove, listener, options);
41
+            }
35 42
         } else {
36 43
             onShowModalEnd(toAdd, toRemove, listener);
37 44
         }
38 45
     }
39 46
 
47
+    private void animateShow(ViewController toAdd, ViewController toRemove, CommandListener listener, Options options) {
48
+        animator.show(toAdd.getView(), options.animations.showModal, new AnimatorListenerAdapter() {
49
+            @Override
50
+            public void onAnimationEnd(Animator animation) {
51
+                onShowModalEnd(toAdd, toRemove, listener);
52
+            }
53
+        });
54
+    }
55
+
40 56
     private void onShowModalEnd(ViewController toAdd, ViewController toRemove, CommandListener listener) {
41 57
         if (toAdd.options.modal.presentationStyle != ModalPresentationStyle.OverCurrentContext) {
42 58
             toRemove.detachView();

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

@@ -1,7 +1,11 @@
1 1
 package com.reactnativenavigation.viewcontrollers.modal;
2 2
 
3
+import android.app.Activity;
4
+import android.support.annotation.RestrictTo;
3 5
 import android.view.ViewGroup;
4 6
 
7
+import com.reactnativenavigation.anim.ModalAnimator;
8
+import com.reactnativenavigation.parse.Options;
5 9
 import com.reactnativenavigation.utils.CommandListener;
6 10
 import com.reactnativenavigation.viewcontrollers.ViewController;
7 11
 
@@ -15,7 +19,12 @@ public class ModalStack {
15 19
     private List<ViewController> modals = new ArrayList<>();
16 20
     private final ModalPresenter presenter;
17 21
 
18
-    public ModalStack(ModalPresenter presenter) {
22
+    public ModalStack(Activity activity) {
23
+        this.presenter = new ModalPresenter(new ModalAnimator(activity));
24
+    }
25
+
26
+    @RestrictTo(RestrictTo.Scope.TESTS)
27
+    ModalStack(ModalPresenter presenter) {
19 28
         this.presenter = presenter;
20 29
     }
21 30
 
@@ -23,6 +32,10 @@ public class ModalStack {
23 32
         presenter.setContentLayout(contentLayout);
24 33
     }
25 34
 
35
+    public void setDefaultOptions(Options defaultOptions) {
36
+        presenter.setDefaultOptions(defaultOptions);
37
+    }
38
+
26 39
     public void showModal(ViewController viewController, ViewController root, CommandListener listener) {
27 40
         ViewController toRemove = isEmpty() ? root : peek();
28 41
         modals.add(viewController);

+ 21
- 8
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java View File

@@ -5,6 +5,7 @@ import android.support.annotation.NonNull;
5 5
 import android.support.annotation.RestrictTo;
6 6
 import android.support.annotation.VisibleForTesting;
7 7
 import android.support.v4.view.ViewPager;
8
+import android.view.View;
8 9
 import android.view.ViewGroup;
9 10
 import android.widget.RelativeLayout;
10 11
 
@@ -130,17 +131,22 @@ public class StackController extends ParentController<StackLayout> {
130 131
         child.setParentController(this);
131 132
         stack.push(child.getId(), child);
132 133
         backButtonHelper.addToPushedChild(this, child);
133
-        ViewGroup childView = child.getView();
134
-        childView.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
135
-        presenter.applyLayoutParamsOptions(resolveCurrentOptions(), childView);
136
-        getView().addView(childView);
134
+        Options resolvedOptions = resolveCurrentOptions(presenter.getDefaultOptions());
135
+        addChildToStack(child, child.getView(), resolvedOptions);
137 136
 
138 137
         if (toRemove != null) {
139 138
             if (child.options.animations.push.enable.isTrueOrUndefined()) {
140
-                animator.push(child.getView(), () -> {
141
-                    getView().removeView(toRemove.getView());
142
-                    listener.onSuccess(child.getId());
143
-                });
139
+                if (resolvedOptions.animations.push.waitForRender.isTrue()) {
140
+                    child.setOnAppearedListener(() -> animator.push(child.getView(), () -> {
141
+                        getView().removeView(toRemove.getView());
142
+                        listener.onSuccess(child.getId());
143
+                    }));
144
+                } else {
145
+                    animator.push(child.getView(), () -> {
146
+                        getView().removeView(toRemove.getView());
147
+                        listener.onSuccess(child.getId());
148
+                    });
149
+                }
144 150
             } else {
145 151
                 getView().removeView(toRemove.getView());
146 152
                 listener.onSuccess(child.getId());
@@ -150,6 +156,13 @@ public class StackController extends ParentController<StackLayout> {
150 156
         }
151 157
     }
152 158
 
159
+    private void addChildToStack(ViewController child, View view, Options resolvedOptions) {
160
+        view.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
161
+        child.setWaitForRender(resolvedOptions.animations.push.waitForRender);
162
+        presenter.applyLayoutParamsOptions(resolvedOptions, view);
163
+        getView().addView(view);
164
+    }
165
+
153 166
     public void setRoot(ViewController child, CommandListener listener) {
154 167
         backButtonHelper.clear(child);
155 168
         push(child, new CommandListenerAdapter() {

+ 2
- 0
lib/android/app/src/main/java/com/reactnativenavigation/views/Component.java View File

@@ -6,4 +6,6 @@ public interface Component {
6 6
     void drawBehindTopBar();
7 7
 
8 8
     void drawBelowTopBar(TopBar topBar);
9
+
10
+    boolean isRendered();
9 11
 }

+ 5
- 0
lib/android/app/src/main/java/com/reactnativenavigation/views/ComponentLayout.java View File

@@ -99,6 +99,11 @@ public class ComponentLayout extends FrameLayout implements ReactComponent, TopB
99 99
         }
100 100
     }
101 101
 
102
+    @Override
103
+    public boolean isRendered() {
104
+        return reactView.isRendered();
105
+    }
106
+
102 107
     @Override
103 108
     public void onPress(String buttonId) {
104 109
         reactView.sendOnNavigationButtonPressed(buttonId);

+ 5
- 0
lib/android/app/src/main/java/com/reactnativenavigation/views/ExternalComponentLayout.java View File

@@ -33,4 +33,9 @@ public class ExternalComponentLayout extends FrameLayout implements Component {
33 33
             setLayoutParams(layoutParams);
34 34
         }
35 35
     }
36
+
37
+    @Override
38
+    public boolean isRendered() {
39
+        return getChildCount() >= 1;
40
+    }
36 41
 }

+ 19
- 1
lib/android/app/src/main/java/com/reactnativenavigation/views/StackLayout.java View File

@@ -9,12 +9,13 @@ import com.reactnativenavigation.viewcontrollers.TopBarButtonController;
9 9
 import com.reactnativenavigation.viewcontrollers.topbar.TopBarBackgroundViewController;
10 10
 import com.reactnativenavigation.viewcontrollers.topbar.TopBarController;
11 11
 import com.reactnativenavigation.views.titlebar.TitleBarReactViewCreator;
12
+import com.reactnativenavigation.views.topbar.TopBar;
12 13
 
13 14
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
14 15
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
15 16
 
16 17
 @SuppressLint("ViewConstructor")
17
-public class StackLayout extends RelativeLayout {
18
+public class StackLayout extends RelativeLayout implements Component {
18 19
     private String stackId;
19 20
 
20 21
     public StackLayout(Context context, ReactViewCreator topBarButtonCreator, TitleBarReactViewCreator titleBarReactViewCreator, TopBarBackgroundViewController topBarBackgroundViewController, TopBarController topBarController, TopBarButtonController.OnClickListener topBarButtonClickListener, String stackId) {
@@ -31,4 +32,21 @@ public class StackLayout extends RelativeLayout {
31 32
     public String getStackId() {
32 33
         return stackId;
33 34
     }
35
+
36
+    @Override
37
+    public void drawBehindTopBar() {
38
+
39
+    }
40
+
41
+    @Override
42
+    public void drawBelowTopBar(TopBar topBar) {
43
+
44
+    }
45
+
46
+    @Override
47
+    public boolean isRendered() {
48
+        return getChildCount() >= 2 &&
49
+               getChildAt(1) instanceof Component &&
50
+               ((Component) getChildAt(1)).isRendered();
51
+    }
34 52
 }

+ 12
- 0
lib/android/app/src/main/java/com/reactnativenavigation/views/toptabs/TopTabsViewPager.java View File

@@ -55,6 +55,18 @@ public class TopTabsViewPager extends ViewPager implements Component, TopBarButt
55 55
         setLayoutParams(layoutParams);
56 56
     }
57 57
 
58
+    @Override
59
+    public boolean isRendered() {
60
+        return tabs.size() != 0 && areAllTabsRendered();
61
+    }
62
+
63
+    private boolean areAllTabsRendered() {
64
+        for (ViewController tab : tabs) {
65
+            if (!tab.isRendered()) return false;
66
+        }
67
+        return true;
68
+    }
69
+
58 70
     public void switchToTab(int index) {
59 71
         setCurrentItem(index);
60 72
     }

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

@@ -56,4 +56,9 @@ public class SimpleOverlay extends RelativeLayout implements IReactView {
56 56
     public void dispatchTouchEventToJs(MotionEvent event) {
57 57
 
58 58
     }
59
+
60
+    @Override
61
+    public boolean isRendered() {
62
+        return getChildCount() >= 1;
63
+    }
59 64
 }

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

@@ -77,6 +77,11 @@ public class SimpleViewController extends ChildController<SimpleViewController.S
77 77
             }
78 78
         }
79 79
 
80
+        @Override
81
+        public boolean isRendered() {
82
+            return getChildCount() >= 1;
83
+        }
84
+
80 85
         @Override
81 86
         public boolean isReady() {
82 87
             return false;

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

@@ -54,4 +54,9 @@ public class TestReactView extends FrameLayout implements IReactView {
54 54
     public void dispatchTouchEventToJs(MotionEvent event) {
55 55
 
56 56
     }
57
+
58
+    @Override
59
+    public boolean isRendered() {
60
+        return getChildCount() >= 1;
61
+    }
57 62
 }

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

@@ -23,6 +23,7 @@ import com.reactnativenavigation.utils.CompatUtils;
23 23
 import com.reactnativenavigation.utils.ImageLoader;
24 24
 import com.reactnativenavigation.utils.OptionHelper;
25 25
 import com.reactnativenavigation.viewcontrollers.bottomtabs.BottomTabsController;
26
+import com.reactnativenavigation.viewcontrollers.modal.ModalStack;
26 27
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
27 28
 
28 29
 import org.junit.Test;
@@ -55,6 +56,7 @@ public class NavigatorTest extends BaseTest {
55 56
     private OverlayManager overlayManager;
56 57
     private EventEmitter eventEmitter;
57 58
     private ViewController.ViewVisibilityListener parentVisibilityListener;
59
+    private ModalStack modalStack;
58 60
 
59 61
     @Override
60 62
     public void beforeEach() {
@@ -64,7 +66,8 @@ public class NavigatorTest extends BaseTest {
64 66
         imageLoaderMock = ImageLoaderMock.mock();
65 67
         activityController = newActivityController(TestActivity.class);
66 68
         activity = activityController.create().get();
67
-        uut = new Navigator(activity, childRegistry, overlayManager);
69
+        modalStack = spy(new ModalStack(activity));
70
+        uut = new Navigator(activity, childRegistry, modalStack, overlayManager);
68 71
         activity.setNavigator(uut);
69 72
 
70 73
         parentController = newStack();
@@ -100,6 +103,7 @@ public class NavigatorTest extends BaseTest {
100 103
         Options defaultOptions = new Options();
101 104
         uut.setDefaultOptions(defaultOptions);
102 105
         verify(spy, times(1)).setDefaultOptions(defaultOptions);
106
+        verify(modalStack).setDefaultOptions(defaultOptions);
103 107
     }
104 108
 
105 109
     @Test
@@ -119,7 +123,7 @@ public class NavigatorTest extends BaseTest {
119 123
     @Test
120 124
     public void hasUniqueId() {
121 125
         assertThat(uut.getId()).startsWith("navigator");
122
-        assertThat(new Navigator(activity, childRegistry, overlayManager).getId()).isNotEqualTo(uut.getId());
126
+        assertThat(new Navigator(activity, childRegistry, modalStack, overlayManager).getId()).isNotEqualTo(uut.getId());
123 127
     }
124 128
 
125 129
     @Test
@@ -429,10 +433,11 @@ public class NavigatorTest extends BaseTest {
429 433
 
430 434
     @Test
431 435
     public void dismissModal_onViewAppearedInvokedOnRoot() {
432
-        disableShowModalAnimation(child1, child2);
436
+        disableShowModalAnimation(child1, child2, child3);
433 437
         disableDismissModalAnimation(child1, child2);
434 438
 
435 439
         uut.setRoot(parentController, new CommandListenerAdapter());
440
+        parentController.push(child3, new CommandListenerAdapter());
436 441
         uut.showModal(child1, new CommandListenerAdapter());
437 442
         uut.showModal(child2, new CommandListenerAdapter());
438 443
 
@@ -448,12 +453,14 @@ public class NavigatorTest extends BaseTest {
448 453
 
449 454
     @Test
450 455
     public void dismissAllModals_onViewAppearedInvokedOnRoot() {
456
+        disablePushAnimation(child2);
451 457
         disableShowModalAnimation(child1);
452 458
 
453 459
         uut.dismissAllModals(new CommandListenerAdapter());
454 460
         verify(parentVisibilityListener, times(0)).onViewAppeared(parentController.getView());
455 461
 
456 462
         uut.setRoot(parentController, new CommandListenerAdapter());
463
+        parentController.push(child2, new CommandListenerAdapter());
457 464
 
458 465
         verify(parentVisibilityListener, times(1)).onViewAppeared(parentController.getView());
459 466
         uut.showModal(child1, new CommandListenerAdapter());
@@ -464,8 +471,9 @@ public class NavigatorTest extends BaseTest {
464 471
 
465 472
     @Test
466 473
     public void handleBack_onViewAppearedInvokedOnRoot() {
467
-        disableShowModalAnimation(child1, child2);
474
+        disableShowModalAnimation(child1, child2, child3);
468 475
 
476
+        parentController.push(child3, new CommandListenerAdapter());
469 477
         StackController spy = spy(parentController);
470 478
         uut.setRoot(spy, new CommandListenerAdapter());
471 479
         uut.showModal(child1, new CommandListenerAdapter());
@@ -485,9 +493,12 @@ public class NavigatorTest extends BaseTest {
485 493
 
486 494
     @Test
487 495
     public void destroy_destroyedRoot() {
496
+        disablePushAnimation(child1);
497
+
488 498
         StackController spy = spy(parentController);
489 499
         spy.options.animations.startApp.enable = new Bool(false);
490 500
         uut.setRoot(spy, new CommandListenerAdapter());
501
+        spy.push(child1, new CommandListenerAdapter());
491 502
         activityController.destroy();
492 503
         verify(spy, times(1)).destroy();
493 504
     }

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

@@ -27,6 +27,7 @@ import static org.assertj.core.api.Java6Assertions.assertThat;
27 27
 import static org.mockito.Mockito.spy;
28 28
 import static org.mockito.Mockito.times;
29 29
 import static org.mockito.Mockito.verify;
30
+import static org.mockito.Mockito.when;
30 31
 
31 32
 public class ParentControllerTest extends BaseTest {
32 33
 
@@ -182,4 +183,46 @@ public class ParentControllerTest extends BaseTest {
182 183
         uut.applyChildOptions(options, child1.getView());
183 184
         verify(presenter, times(0)).applyRootOptions(uut.getView(), options);
184 185
     }
186
+
187
+    @Test
188
+    public void resolveCurrentOptions_returnOptionsIfNoChildren() {
189
+        assertThat(uut.getChildControllers().size()).isZero();
190
+        assertThat(uut.resolveCurrentOptions()).isEqualTo(uut.options);
191
+    }
192
+
193
+    @Test
194
+    public void resolveCurrentOptions_mergesWithCurrentChild() {
195
+        ViewController child1 = Mockito.mock(ViewController.class);
196
+        when(child1.getView()).thenReturn(new FrameLayout(activity));
197
+        Options copiedChildOptions = spy(new Options());
198
+        Options childOptions = spy(new Options() {
199
+            @Override
200
+            public Options copy() {
201
+                return copiedChildOptions;
202
+            }
203
+        });
204
+        when(child1.resolveCurrentOptions()).thenReturn(childOptions);
205
+
206
+        children.add(child1);
207
+
208
+        uut.ensureViewIsCreated();
209
+        assertThat(uut.getCurrentChild()).isEqualTo(child1);
210
+        uut.resolveCurrentOptions();
211
+        verify(child1).resolveCurrentOptions();
212
+        verify(copiedChildOptions).mergeWith(uut.options);
213
+    }
214
+
215
+    @Test
216
+    public void resolveCurrentOptions_withDefaultOptions() {
217
+        SimpleViewController child1 = new SimpleViewController(activity, childRegistry, "child1", new Options());
218
+        children.add(child1);
219
+        uut.ensureViewIsCreated();
220
+
221
+        Options defaultOptions = new Options();
222
+        Options currentOptions = spy(new Options());
223
+        ParentController spy = spy(uut);
224
+        Mockito.when(spy.resolveCurrentOptions()).thenReturn(currentOptions);
225
+        spy.resolveCurrentOptions(defaultOptions);
226
+        verify(currentOptions).withDefaultOptions(defaultOptions);
227
+    }
185 228
 }

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

@@ -2,6 +2,7 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4 4
 import android.view.View;
5
+import android.view.ViewGroup;
5 6
 import android.view.ViewParent;
6 7
 import android.widget.FrameLayout;
7 8
 import android.widget.LinearLayout;
@@ -10,8 +11,11 @@ import com.reactnativenavigation.BaseTest;
10 11
 import com.reactnativenavigation.TestUtils;
11 12
 import com.reactnativenavigation.mocks.SimpleViewController;
12 13
 import com.reactnativenavigation.parse.Options;
14
+import com.reactnativenavigation.parse.params.Bool;
15
+import com.reactnativenavigation.parse.params.NullBool;
13 16
 import com.reactnativenavigation.utils.CommandListenerAdapter;
14 17
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
18
+import com.reactnativenavigation.views.Component;
15 19
 
16 20
 import org.assertj.android.api.Assertions;
17 21
 import org.junit.Test;
@@ -25,6 +29,7 @@ import static org.mockito.Mockito.mock;
25 29
 import static org.mockito.Mockito.spy;
26 30
 import static org.mockito.Mockito.times;
27 31
 import static org.mockito.Mockito.verify;
32
+import static org.mockito.Mockito.withSettings;
28 33
 
29 34
 public class ViewControllerTest extends BaseTest {
30 35
 
@@ -222,5 +227,26 @@ public class ViewControllerTest extends BaseTest {
222 227
         spy.ensureViewIsCreated();
223 228
         verify(spy, times(1)).getView();
224 229
     }
230
+
231
+    @Test
232
+    public void isRendered_falseIfViewIsNotCreated() {
233
+        uut.setWaitForRender(new Bool(true));
234
+        assertThat(uut.isRendered()).isFalse();
235
+    }
236
+
237
+    @Test
238
+    public void isRendered_delegatesToView() {
239
+        uut.setWaitForRender(new Bool(true));
240
+        uut.view = Mockito.mock(ViewGroup.class, withSettings().extraInterfaces(Component.class));
241
+        uut.isRendered();
242
+        verify((Component) uut.view).isRendered();
243
+    }
244
+
245
+    @Test
246
+    public void isRendered_returnsTrueForEveryViewByDefault() {
247
+        uut.setWaitForRender(new NullBool());
248
+        uut.view = Mockito.mock(ViewGroup.class);
249
+        assertThat(uut.isRendered()).isTrue();
250
+    }
225 251
 }
226 252
 

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

@@ -6,14 +6,18 @@ import android.widget.FrameLayout;
6 6
 import com.reactnativenavigation.BaseTest;
7 7
 import com.reactnativenavigation.anim.ModalAnimator;
8 8
 import com.reactnativenavigation.mocks.SimpleViewController;
9
+import com.reactnativenavigation.parse.AnimationOptions;
9 10
 import com.reactnativenavigation.parse.ModalPresentationStyle;
10 11
 import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.parse.params.Bool;
11 13
 import com.reactnativenavigation.utils.CommandListener;
12 14
 import com.reactnativenavigation.utils.CommandListenerAdapter;
13 15
 import com.reactnativenavigation.viewcontrollers.ChildController;
14 16
 import com.reactnativenavigation.viewcontrollers.ChildControllersRegistry;
15 17
 import com.reactnativenavigation.viewcontrollers.ViewController;
16 18
 
19
+import org.json.JSONException;
20
+import org.json.JSONObject;
17 21
 import org.junit.Test;
18 22
 
19 23
 import static org.assertj.core.api.Java6Assertions.assertThat;
@@ -22,6 +26,7 @@ import static org.mockito.ArgumentMatchers.eq;
22 26
 import static org.mockito.Mockito.spy;
23 27
 import static org.mockito.Mockito.times;
24 28
 import static org.mockito.Mockito.verify;
29
+import static org.mockito.Mockito.verifyZeroInteractions;
25 30
 
26 31
 public class ModalPresenterTest extends BaseTest {
27 32
     private static final String MODAL_ID_1 = "modalId1";
@@ -32,16 +37,16 @@ public class ModalPresenterTest extends BaseTest {
32 37
     private ModalPresenter uut;
33 38
     private FrameLayout contentLayout;
34 39
     private ModalAnimator animator;
35
-    private ViewController rootController;
40
+    private ViewController root;
36 41
 
37 42
     @Override
38 43
     public void beforeEach() {
39 44
         Activity activity = newActivity();
40 45
         ChildControllersRegistry childRegistry = new ChildControllersRegistry();
41 46
 
42
-        rootController = spy(new SimpleViewController(activity, childRegistry, "root", new Options()));
47
+        root = spy(new SimpleViewController(activity, childRegistry, "root", new Options()));
43 48
         contentLayout = new FrameLayout(activity);
44
-        contentLayout.addView(rootController.getView());
49
+        contentLayout.addView(root.getView());
45 50
         activity.setContentView(contentLayout);
46 51
 
47 52
         animator = spy(new ModalAnimator(activity));
@@ -51,6 +56,16 @@ public class ModalPresenterTest extends BaseTest {
51 56
         modal2 = spy(new SimpleViewController(activity, childRegistry, MODAL_ID_2, new Options()));
52 57
     }
53 58
 
59
+    @Test
60
+    public void showModal() {
61
+        Options defaultOptions = new Options();
62
+        uut.setDefaultOptions(defaultOptions);
63
+        disableShowModalAnimation(modal1);
64
+        uut.showModal(modal1, root, new CommandListenerAdapter());
65
+        verify(modal1).setWaitForRender(any());
66
+        verify(modal1).resolveCurrentOptions(defaultOptions);
67
+    }
68
+
54 69
     @Test
55 70
     public void showModal_noAnimation() {
56 71
         disableShowModalAnimation(modal1);
@@ -61,7 +76,7 @@ public class ModalPresenterTest extends BaseTest {
61 76
                 verify(modal1, times(1)).onViewAppeared();
62 77
             }
63 78
         });
64
-        uut.showModal(modal1, rootController, listener);
79
+        uut.showModal(modal1, root, listener);
65 80
         verify(animator, times(0)).show(
66 81
                 eq(modal1.getView()),
67 82
                 eq(modal1.options.animations.showModal),
@@ -70,6 +85,17 @@ public class ModalPresenterTest extends BaseTest {
70 85
         verify(listener, times(1)).onSuccess(MODAL_ID_1);
71 86
     }
72 87
 
88
+    @Test
89
+    public void showModal_resolvesDefaultOptions() throws JSONException {
90
+        Options defaultOptions = new Options();
91
+        JSONObject disabledShowModalAnimation = new JSONObject().put("enable", false);
92
+        defaultOptions.animations.showModal = AnimationOptions.parse(disabledShowModalAnimation);
93
+
94
+        uut.setDefaultOptions(defaultOptions);
95
+        uut.showModal(modal1, root, new CommandListenerAdapter());
96
+        verifyZeroInteractions(animator);
97
+    }
98
+
73 99
     @Test
74 100
     public void showModal_previousModalIsRemovedFromHierarchy() {
75 101
         uut.showModal(modal1, null, new CommandListenerAdapter() {
@@ -102,12 +128,20 @@ public class ModalPresenterTest extends BaseTest {
102 128
         });
103 129
     }
104 130
 
131
+    @Test
132
+    public void showModal_waitForRender() {
133
+        modal1.options.animations.showModal.waitForRender = new Bool(true);
134
+        uut.showModal(modal1, root, new CommandListenerAdapter());
135
+        verify(modal1).setOnAppearedListener(any());
136
+        verifyZeroInteractions(animator);
137
+    }
138
+
105 139
     @Test
106 140
     public void dismissModal_animatesByDefault() {
107 141
         disableShowModalAnimation(modal1);
108 142
 
109
-        uut.showModal(modal1, rootController, new CommandListenerAdapter());
110
-        uut.dismissTopModal(modal1, rootController, new CommandListenerAdapter() {
143
+        uut.showModal(modal1, root, new CommandListenerAdapter());
144
+        uut.dismissTopModal(modal1, root, new CommandListenerAdapter() {
111 145
             @Override
112 146
             public void onSuccess(String childId) {
113 147
                 verify(modal1, times(1)).onViewDisappear();
@@ -132,8 +166,8 @@ public class ModalPresenterTest extends BaseTest {
132 166
         disableShowModalAnimation(modal1);
133 167
         disableDismissModalAnimation(modal1);
134 168
 
135
-        uut.showModal(modal1, rootController, new CommandListenerAdapter());
136
-        uut.dismissTopModal(modal1, rootController, new CommandListenerAdapter());
169
+        uut.showModal(modal1, root, new CommandListenerAdapter());
170
+        uut.dismissTopModal(modal1, root, new CommandListenerAdapter());
137 171
         verify(modal1, times(1)).onViewDisappear();
138 172
         verify(modal1, times(1)).destroy();
139 173
         verify(animator, times(0)).dismiss(any(), any());
@@ -143,7 +177,7 @@ public class ModalPresenterTest extends BaseTest {
143 177
     public void dismissModal_previousModalIsAddedBackToHierarchy() {
144 178
         disableShowModalAnimation(modal1, modal2);
145 179
 
146
-        uut.showModal(modal1, rootController, new CommandListenerAdapter());
180
+        uut.showModal(modal1, root, new CommandListenerAdapter());
147 181
         uut.showModal(modal2, modal1, new CommandListenerAdapter());
148 182
         assertThat(modal1.getView().getParent()).isNull();
149 183
         uut.dismissTopModal(modal2, modal1, new CommandListenerAdapter());
@@ -155,16 +189,16 @@ public class ModalPresenterTest extends BaseTest {
155 189
         disableShowModalAnimation(modal1, modal2);
156 190
         disableDismissModalAnimation(modal1, modal2);
157 191
 
158
-        uut.showModal(modal1, rootController, new CommandListenerAdapter());
192
+        uut.showModal(modal1, root, new CommandListenerAdapter());
159 193
         uut.showModal(modal2, modal1, new CommandListenerAdapter());
160 194
         assertThat(modal1.getView().getParent()).isNull();
161
-        assertThat(rootController.getView().getParent()).isNull();
195
+        assertThat(root.getView().getParent()).isNull();
162 196
 
163 197
         uut.dismissModal(modal1, new CommandListenerAdapter());
164
-        assertThat(rootController.getView().getParent()).isNull();
198
+        assertThat(root.getView().getParent()).isNull();
165 199
 
166
-        uut.dismissTopModal(modal2, rootController, new CommandListenerAdapter());
167
-        assertThat(rootController.getView().getParent()).isNotNull();
200
+        uut.dismissTopModal(modal2, root, new CommandListenerAdapter());
201
+        assertThat(root.getView().getParent()).isNotNull();
168 202
     }
169 203
 
170 204
     @Test
@@ -172,8 +206,8 @@ public class ModalPresenterTest extends BaseTest {
172 206
         modal1.options.modal.presentationStyle = ModalPresentationStyle.OverCurrentContext;
173 207
         disableShowModalAnimation(modal1, modal2);
174 208
 
175
-        uut.showModal(modal1, rootController, new CommandListenerAdapter());
176
-        assertThat(rootController.getView().getParent()).isNotNull();
177
-        verify(rootController, times(0)).onViewDisappear();
209
+        uut.showModal(modal1, root, new CommandListenerAdapter());
210
+        assertThat(root.getView().getParent()).isNotNull();
211
+        verify(root, times(0)).onViewDisappear();
178 212
     }
179 213
 }

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

@@ -256,6 +256,13 @@ public class ModalStackTest extends BaseTest {
256 256
         verify(backHandlingModal, times(0)).onViewDisappear();
257 257
     }
258 258
 
259
+    @Test
260
+    public void setDefaultOptions() {
261
+        Options defaultOptions = new Options();
262
+        uut.setDefaultOptions(defaultOptions);
263
+        verify(presenter).setDefaultOptions(defaultOptions);
264
+    }
265
+
259 266
     private ViewController findModal(String id) {
260 267
         return uut.findControllerById(id);
261 268
     }

+ 11
- 0
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java View File

@@ -158,6 +158,17 @@ public class StackControllerTest extends BaseTest {
158 158
         verify(child1, times(0)).mergeOptions(any());
159 159
     }
160 160
 
161
+    @Test
162
+    public void push_waitForRender() {
163
+        disablePushAnimation(child1);
164
+        uut.push(child1, new CommandListenerAdapter());
165
+
166
+        child2.options.animations.push.waitForRender = new Bool(true);
167
+        uut.push(child2, new CommandListenerAdapter());
168
+        verify(child2).setOnAppearedListener(any());
169
+        verify(animator, times(0)).push(eq(child1.getView()), any());
170
+    }
171
+
161 172
     @Test
162 173
     public void animateSetRoot() {
163 174
         disablePushAnimation(child1, child2, child3);

+ 5
- 0
playground/src/app.js View File

@@ -32,6 +32,11 @@ function start() {
32 32
         fontFamily: 'HelveticaNeue-Italic',
33 33
         fontSize: 13
34 34
       },
35
+      _animations: {
36
+        push: {
37
+          waitForRender: false,
38
+        }
39
+      },
35 40
       _animations: {
36 41
         startApp: {
37 42
           y: {

+ 13
- 1
playground/src/screens/ModalScreen.js View File

@@ -17,13 +17,20 @@ class ModalScreen extends Component {
17 17
         backgroundColor: 'transparent'
18 18
       },
19 19
       layout: {
20
-        orientation: ['portrait']
20
+        orientation: ['portrait'],
21
+        backgroundColor: '#f5fcff'
22
+      },
23
+      _animations: {
24
+        showModal: {
25
+          waitForRender: true
26
+        }
21 27
       }
22 28
     };
23 29
   }
24 30
 
25 31
   constructor(props) {
26 32
     super(props);
33
+    // this.simulateLongRunningTask();
27 34
     this.onClickShowModal = this.onClickShowModal.bind(this);
28 35
     this.onClickDismissModal = this.onClickDismissModal.bind(this);
29 36
     this.onClickDismissPreviousModal = this.onClickDismissPreviousModal.bind(this);
@@ -36,6 +43,11 @@ class ModalScreen extends Component {
36 43
     this.onClickModalLifecycle = this.onClickModalLifecycle.bind(this);
37 44
   }
38 45
 
46
+  simulateLongRunningTask = () => {
47
+    // tslint:disable-next-line
48
+    for (let i = 0; i < Math.pow(2, 25); i++);
49
+  }
50
+
39 51
   render() {
40 52
     return (
41 53
       <View style={styles.root}>

+ 40
- 0
playground/src/screens/PushedScreen.js View File

@@ -18,12 +18,18 @@ class PushedScreen extends Component {
18 18
       },
19 19
       topBar: {
20 20
         testID: testIDs.TOP_BAR_ELEMENT
21
+      },
22
+      layout: {
23
+        backgroundColor: '#f5fcff'
21 24
       }
22 25
     };
23 26
   }
24 27
 
25 28
   constructor(props) {
26 29
     super(props);
30
+    if (this.props.simulateLongRunningTask) {
31
+      this.simulateLongRunningTask();
32
+    }
27 33
     this.onClickPush = this.onClickPush.bind(this);
28 34
     this.onClickPop = this.onClickPop.bind(this);
29 35
     this.onClickPopPrevious = this.onClickPopPrevious.bind(this);
@@ -33,6 +39,11 @@ class PushedScreen extends Component {
33 39
     this.state = { disabled: false };
34 40
   }
35 41
 
42
+  simulateLongRunningTask() {
43
+    // tslint:disable-next-line
44
+    for (let i = 0; i < Math.pow(2, 25); i++);
45
+  }
46
+
36 47
   listeners = [];
37 48
 
38 49
   componentDidMount() {
@@ -76,6 +87,7 @@ class PushedScreen extends Component {
76 87
         <Button title='Pop Previous' testID={testIDs.POP_PREVIOUS_BUTTON} onPress={this.onClickPopPrevious} />
77 88
         <Button title='Pop To Root' testID={testIDs.POP_TO_ROOT} onPress={this.onClickPopToRoot} />
78 89
         <Button title='Set Stack Root' testID={testIDs.SET_STACK_ROOT_BUTTON} onPress={this.onClickSetStackRoot} />
90
+        <Button title='Push and Wait for Render' testID={testIDs.PUSH_BUTTON_WAIT_FOR_RENDER} onPress={this.onClickPushWaitForRender} />
79 91
         {stackPosition > 2 && <Button title='Pop To Stack Position 1' testID={testIDs.POP_STACK_POSITION_ONE_BUTTON} onPress={this.onClickPopToFirstPosition} />}
80 92
         <Text style={styles.footer}>{`this.props.componentId = ${this.props.componentId}`}</Text>
81 93
       </View>
@@ -174,6 +186,34 @@ class PushedScreen extends Component {
174 186
     });
175 187
   }
176 188
 
189
+  onClickPushWaitForRender = async () => {
190
+    await Navigation.push(this.props.componentId, {
191
+      component: {
192
+        name: 'navigation.playground.PushedScreen',
193
+        passProps: {
194
+          stackPosition: this.getStackPosition() + 1,
195
+          previousScreenIds: _.concat([], this.props.previousScreenIds || [], this.props.componentId),
196
+          simulateLongRunningTask: true
197
+        },
198
+        options: {
199
+          layout: {
200
+            backgroundColor: 'transparent'
201
+          },
202
+          topBar: {
203
+            title: {
204
+              text: `Pushed ${this.getStackPosition() + 1}`
205
+            }
206
+          },
207
+          animations: {
208
+            push: {
209
+              waitForRender: true
210
+            }
211
+          }
212
+        }
213
+      }
214
+    });
215
+  }
216
+
177 217
   getStackPosition() {
178 218
     return this.props.stackPosition || 1;
179 219
   }

+ 1
- 0
playground/src/testIDs.js View File

@@ -44,6 +44,7 @@ module.exports = {
44 44
   TOGGLE_TOP_BAR_HIDE_ON_SCROLL: `TOGGLE_TOP_BAR_HIDE_ON_SCROLL`,
45 45
   SET_TAB_BADGE_BUTTON: `SET_TAB_BADGE_BUTTON`,
46 46
   PUSH_TO_TEST_DID_DISAPPEAR_BUTTON: `PUSH_TO_TEST_DID_DISAPPEAR_BUTTON`,
47
+  PUSH_BUTTON_WAIT_FOR_RENDER: `PUSH_AND_WAIT_FOR_RENDER`,
47 48
   SHOW_LEFT_SIDE_MENU_BUTTON: `SHOW_LEFT_SIDE_MENU_BUTTON`,
48 49
   SHOW_RIGHT_SIDE_MENU_BUTTON: `SHOW_RIGHT_SIDE_MENU_BUTTON`,
49 50
   HIDE_LEFT_SIDE_MENU_BUTTON: `HIDE_LEFT_SIDE_MENU_BUTTON`,