Browse Source

Orientation android (#2797)

* Initial orientation implementation on Android

* Disable custom animations for now

* FIx OverlayManager

Keep view references instead of View id as id's can change by react

* Work on ModalStack

* resolve dismissModal promise after dismiss ends
* Send didDisappear when Modal is displayed
* Send didAppear to root when Modal is dismissed
* Introduce new API:
  * onViewLostFocus
  * onViewRegainedFocus
* Fix MockPromise - it can be used only once
*
Guy Carmeli 6 years ago
parent
commit
08f30c57b3
No account linked to committer's email address
22 changed files with 445 additions and 46 deletions
  1. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Options.java
  2. 56
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/OrientationOptions.java
  3. 40
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Orientation.java
  4. 23
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/ComponentOptionsPresenter.java
  5. 8
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java
  6. 3
    3
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java
  7. 47
    19
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ModalStack.java
  8. 20
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  9. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java
  10. 8
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  11. 24
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/Modal.java
  12. 2
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalCreator.java
  13. 7
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalListener.java
  14. 2
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/ComponentLayout.java
  15. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/StackLayout.java
  16. 16
    5
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/MockPromise.java
  17. 3
    2
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/ModalCreatorMock.java
  18. 50
    0
      lib/android/app/src/test/java/com/reactnativenavigation/parse/OrientationOptionsTest.java
  19. 77
    8
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ModalStackTest.java
  20. 29
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  21. 15
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackControllerTest.java
  22. 1
    1
      playground/src/app.js

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

@@ -19,6 +19,7 @@ public class Options implements DEFAULT_VALUES {
19 19
         Options result = new Options();
20 20
         if (json == null) return result;
21 21
 
22
+        result.orientationOptions = OrientationOptions.parse(json.optJSONArray("orientation"));
22 23
         result.topBarOptions = TopBarOptions.parse(typefaceManager, json.optJSONObject("topBar"));
23 24
         result.topTabsOptions = TopTabsOptions.parse(json.optJSONObject("topTabs"));
24 25
         result.topTabOptions = TopTabOptions.parse(typefaceManager, json.optJSONObject("topTab"));
@@ -32,6 +33,7 @@ public class Options implements DEFAULT_VALUES {
32 33
         return result.withDefaultOptions(defaultOptions);
33 34
     }
34 35
 
36
+    @NonNull public OrientationOptions orientationOptions = new OrientationOptions();
35 37
     @NonNull public TopBarOptions topBarOptions = new TopBarOptions();
36 38
     @NonNull public TopTabsOptions topTabsOptions = new TopTabsOptions();
37 39
     @NonNull public TopTabOptions topTabOptions = new TopTabOptions();
@@ -49,6 +51,7 @@ public class Options implements DEFAULT_VALUES {
49 51
     @CheckResult
50 52
     public Options copy() {
51 53
         Options result = new Options();
54
+        result.orientationOptions.mergeWith(orientationOptions);
52 55
         result.topBarOptions.mergeWith(topBarOptions);
53 56
         result.topTabsOptions.mergeWith(topTabsOptions);
54 57
         result.topTabOptions.mergeWith(topTabOptions);
@@ -64,6 +67,7 @@ public class Options implements DEFAULT_VALUES {
64 67
     @CheckResult
65 68
 	public Options mergeWith(final Options other) {
66 69
         Options result = copy();
70
+        result.orientationOptions.mergeWith(other.orientationOptions);
67 71
         result.topBarOptions.mergeWith(other.topBarOptions);
68 72
         result.topTabsOptions.mergeWith(other.topTabsOptions);
69 73
         result.topTabOptions.mergeWith(other.topTabOptions);
@@ -76,6 +80,7 @@ public class Options implements DEFAULT_VALUES {
76 80
     }
77 81
 
78 82
     Options withDefaultOptions(final Options other) {
83
+        orientationOptions.mergeWithDefault(other.orientationOptions);
79 84
         topBarOptions.mergeWithDefault(other.topBarOptions);
80 85
         topTabOptions.mergeWithDefault(other.topTabOptions);
81 86
         topTabsOptions.mergeWithDefault(other.topTabsOptions);

+ 56
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/OrientationOptions.java View File

@@ -0,0 +1,56 @@
1
+package com.reactnativenavigation.parse;
2
+
3
+import com.reactnativenavigation.parse.params.Orientation;
4
+
5
+import org.json.JSONArray;
6
+
7
+import java.util.ArrayList;
8
+import java.util.Arrays;
9
+import java.util.List;
10
+
11
+public class OrientationOptions {
12
+    Orientation[] orientations = new Orientation[0];
13
+
14
+    public static OrientationOptions parse(JSONArray orientations) {
15
+        OrientationOptions options = new OrientationOptions();
16
+        if (orientations == null) return options;
17
+
18
+        List<Orientation> parsed = new ArrayList<>();
19
+        for (int i = 0; i < orientations.length(); i++) {
20
+            Orientation o = Orientation.fromString(orientations.optString(i, "default"));
21
+            if (o != null) {
22
+                parsed.add(o);
23
+            }
24
+        }
25
+        options.orientations = parsed.toArray(new Orientation[0]);
26
+
27
+        return options;
28
+    }
29
+
30
+    public int getValue() {
31
+        if (!hasValue()) return Orientation.Default.orientationCode;
32
+
33
+        int result = 0;
34
+        for (Orientation orientation : orientations) {
35
+            result |= orientation.orientationCode;
36
+        }
37
+        return result;
38
+    }
39
+
40
+    public void mergeWith(OrientationOptions other) {
41
+        if (other.hasValue()) orientations = other.orientations;
42
+    }
43
+
44
+    private boolean hasValue() {
45
+        return orientations.length > 0;
46
+    }
47
+
48
+    public void mergeWithDefault(OrientationOptions defaultOptions) {
49
+        if (!hasValue()) orientations = defaultOptions.orientations;
50
+    }
51
+
52
+    @Override
53
+    public String toString() {
54
+        return hasValue() ? Arrays.toString(orientations) : Orientation.Default.toString();
55
+    }
56
+}

+ 40
- 0
lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Orientation.java View File

@@ -0,0 +1,40 @@
1
+package com.reactnativenavigation.parse.params;
2
+
3
+import android.content.pm.ActivityInfo;
4
+import android.content.res.Configuration;
5
+import android.support.annotation.Nullable;
6
+
7
+public enum Orientation {
8
+    Portrait("portrait", Configuration.ORIENTATION_PORTRAIT, ActivityInfo.SCREEN_ORIENTATION_PORTRAIT),
9
+    Landscape("landscape", Configuration.ORIENTATION_LANDSCAPE, ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE),
10
+    Default("default", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
11
+
12
+    public String name;
13
+    public int configurationCode;
14
+    public int orientationCode;
15
+
16
+    Orientation(String name, int configurationCode, int orientationCode) {
17
+        this.name = name;
18
+        this.configurationCode = configurationCode;
19
+        this.orientationCode = orientationCode;
20
+    }
21
+
22
+    @Nullable
23
+    public static Orientation fromString(String name) {
24
+        for (Orientation orientation : values()) {
25
+            if (orientation.name.equals(name)) {
26
+                return orientation;
27
+            }
28
+        }
29
+        return null;
30
+    }
31
+
32
+    public static String fromConfigurationCode(int configurationCode) {
33
+        for (Orientation orientation : values()) {
34
+            if (orientation.configurationCode == configurationCode) {
35
+                return orientation.name;
36
+            }
37
+        }
38
+        throw new RuntimeException();
39
+    }
40
+}

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

@@ -0,0 +1,23 @@
1
+package com.reactnativenavigation.presentation;
2
+
3
+import android.app.Activity;
4
+import android.view.View;
5
+
6
+import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.parse.OrientationOptions;
8
+
9
+public class ComponentOptionsPresenter {
10
+    private View component;
11
+
12
+    public ComponentOptionsPresenter(View component) {
13
+        this.component = component;
14
+    }
15
+
16
+    public void present(Options options) {
17
+        applyOrientation(options.orientationOptions);
18
+    }
19
+
20
+    private void applyOrientation(OrientationOptions options) {
21
+        ((Activity) component.getContext()).setRequestedOrientation(options.getValue());
22
+    }
23
+}

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

@@ -1,5 +1,8 @@
1 1
 package com.reactnativenavigation.presentation;
2 2
 
3
+import android.app.Activity;
4
+
5
+import com.reactnativenavigation.parse.OrientationOptions;
3 6
 import com.reactnativenavigation.parse.params.Button;
4 7
 import com.reactnativenavigation.parse.Options;
5 8
 import com.reactnativenavigation.parse.TopBarOptions;
@@ -24,12 +27,17 @@ public class OptionsPresenter {
24 27
     }
25 28
 
26 29
     public void applyOptions(Options options) {
30
+        applyOrientation(options.orientationOptions);
27 31
         applyButtons(options.topBarOptions.leftButtons, options.topBarOptions.rightButtons);
28 32
         applyTopBarOptions(options.topBarOptions);
29 33
         applyTopTabsOptions(options.topTabsOptions);
30 34
         applyTopTabOptions(options.topTabOptions);
31 35
     }
32 36
 
37
+    public void applyOrientation(OrientationOptions options) {
38
+        ((Activity) topBar.getContext()).setRequestedOrientation(options.getValue());
39
+    }
40
+
33 41
     private void applyTopBarOptions(TopBarOptions options) {
34 42
         if (options.title.hasValue()) topBar.setTitle(options.title.get());
35 43
         topBar.setBackgroundColor(options.backgroundColor);

+ 3
- 3
lib/android/app/src/main/java/com/reactnativenavigation/presentation/OverlayManager.java View File

@@ -8,15 +8,15 @@ import com.reactnativenavigation.viewcontrollers.ViewController;
8 8
 import java.util.HashMap;
9 9
 
10 10
 public class OverlayManager {
11
-    private final HashMap<String, Integer> overlayRegistry = new HashMap<>();
11
+    private final HashMap<String, View> overlayRegistry = new HashMap<>();
12 12
 
13 13
     public void show(ViewGroup root, ViewController overlay) {
14 14
         View view = overlay.getView();
15
-        overlayRegistry.put(overlay.getId(), view.getId());
15
+        overlayRegistry.put(overlay.getId(), view);
16 16
         root.addView(view);
17 17
     }
18 18
 
19 19
     public void dismiss(ViewGroup root, String componentId) {
20
-        root.removeView(root.findViewById(overlayRegistry.get(componentId)));
20
+        root.removeView(overlayRegistry.get(componentId));
21 21
     }
22 22
 }

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

@@ -3,55 +3,59 @@ package com.reactnativenavigation.viewcontrollers;
3 3
 import android.support.annotation.Nullable;
4 4
 
5 5
 import com.facebook.react.bridge.Promise;
6
+import com.reactnativenavigation.utils.NoOpPromise;
7
+import com.reactnativenavigation.utils.Task;
6 8
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
7 9
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
10
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
8 11
 
9 12
 import java.util.ArrayList;
10 13
 import java.util.List;
11 14
 
12
-public class ModalStack {
15
+class ModalStack implements ModalListener {
13 16
 
14 17
 	private List<Modal> modals = new ArrayList<>();
15 18
     private ModalCreator creator;
19
+    private ModalListener modalListener;
16 20
 
17
-    public ModalStack(ModalCreator creator) {
21
+    ModalStack(ModalCreator creator, ModalListener modalListener) {
18 22
         this.creator = creator;
23
+        this.modalListener = modalListener;
19 24
     }
20 25
 
21
-    public void showModal(final ViewController viewController, Promise promise) {
22
-        Modal modal = creator.create(viewController);
26
+    void showModal(final ViewController viewController, Promise promise) {
27
+        Modal modal = creator.create(viewController, this);
23 28
         modals.add(modal);
24 29
 		modal.show();
25
-		if (promise != null) {
26
-			promise.resolve(viewController.getId());
27
-		}
30
+        promise.resolve(viewController.getId());
28 31
 	}
29 32
 
30
-	public void dismissModal(final String componentId, Promise promise) {
33
+	void dismissModal(final String componentId, Promise promise) {
31 34
 		Modal modal = findModalByComponentId(componentId);
32 35
 		if (modal != null) {
33
-			modal.dismiss();
34
-			modals.remove(modal);
35
-			if (promise != null) {
36
-				promise.resolve(componentId);
37
-			}
36
+			modal.dismiss(promise);
38 37
 		} else {
39 38
 			Navigator.rejectPromise(promise);
40 39
 		}
41 40
 	}
42 41
 
43
-	public void dismissAll(Promise promise) {
42
+	void dismissAll(Promise promise) {
44 43
 		for (Modal modal : modals) {
45
-			modal.dismiss();
44
+			modal.dismiss(size() == 1 ? promise : new NoOpPromise());
46 45
 		}
47 46
 		modals.clear();
48
-		if (promise != null) {
49
-			promise.resolve(true);
50
-		}
51 47
 	}
52 48
 
49
+    boolean isEmpty() {
50
+        return modals.isEmpty();
51
+    }
52
+
53
+    public int size() {
54
+        return modals.size();
55
+    }
56
+
53 57
 	@Nullable
54
-	public Modal findModalByComponentId(String componentId) {
58
+    Modal findModalByComponentId(String componentId) {
55 59
 		for (Modal modal : modals) {
56 60
 			if (modal.containsDeepComponentId(componentId)) {
57 61
 				return modal;
@@ -65,4 +69,28 @@ public class ModalStack {
65 69
         Modal modal = findModalByComponentId(id);
66 70
         return modal != null ? modal.viewController.findControllerById(id) : null;
67 71
     }
72
+
73
+    @Override
74
+    public void onModalDismiss(Modal modal) {
75
+        if (peek() == modal) {
76
+            modals.remove(modal);
77
+            performOnModal(peek(), peek -> peek.viewController.onViewAppeared());
78
+        } else {
79
+            modals.remove(modal);
80
+        }
81
+        modalListener.onModalDismiss(modal);
82
+    }
83
+
84
+    @Override
85
+    public void onModalDisplay(Modal modal) {
86
+        modalListener.onModalDisplay(modal);
87
+    }
88
+
89
+    private Modal peek() {
90
+        return isEmpty() ? null : modals.get(modals.size() - 1);
91
+    }
92
+
93
+    private void performOnModal(@Nullable Modal modal, Task<Modal> task) {
94
+        if (modal != null) task.run(modal);
95
+    }
68 96
 }

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

@@ -12,21 +12,24 @@ import com.reactnativenavigation.presentation.NavigationOptionsListener;
12 12
 import com.reactnativenavigation.presentation.OverlayManager;
13 13
 import com.reactnativenavigation.utils.CompatUtils;
14 14
 import com.reactnativenavigation.utils.NoOpPromise;
15
+import com.reactnativenavigation.viewcontrollers.modal.Modal;
15 16
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
17
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
16 18
 
17 19
 import java.util.Collection;
18 20
 import java.util.Collections;
19 21
 
20
-public class Navigator extends ParentController {
22
+public class Navigator extends ParentController implements ModalListener {
21 23
 
22 24
     private static final NoOpPromise NO_OP = new NoOpPromise();
23
-    private final ModalStack modalStack = new ModalStack(new ModalCreator());
25
+    private final ModalStack modalStack;
24 26
     private ViewController root;
25 27
     private OverlayManager overlayManager = new OverlayManager();
26 28
     private Options defaultOptions = new Options();
27 29
 
28 30
     public Navigator(final Activity activity) {
29 31
         super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
32
+        modalStack = new ModalStack(new ModalCreator(), this);
30 33
     }
31 34
 
32 35
     @NonNull
@@ -132,6 +135,21 @@ public class Navigator extends ParentController {
132 135
         modalStack.dismissModal(componentId, promise);
133 136
     }
134 137
 
138
+    @Override
139
+    public void onModalDisplay(Modal modal) {
140
+        if (modalStack.size() == 1) {
141
+            root.onViewLostFocus();
142
+        }
143
+    }
144
+
145
+
146
+    @Override
147
+    public void onModalDismiss(Modal modal) {
148
+        if (modalStack.isEmpty()) {
149
+            root.onViewRegainedFocus();
150
+        }
151
+    }
152
+
135 153
     public void dismissAllModals(Promise promise) {
136 154
         modalStack.dismissAll(promise);
137 155
     }

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

@@ -38,6 +38,11 @@ public class StackController extends ParentController <StackLayout> {
38 38
     @RestrictTo(RestrictTo.Scope.TESTS)
39 39
     StackLayout getStackLayout() {return getView();}
40 40
 
41
+    public void applyOptions(Options options) {
42
+        super.applyOptions(options);
43
+        getView().applyOptions(options);
44
+    }
45
+
41 46
     @Override
42 47
     public void applyOptions(Options options, ReactComponent component) {
43 48
         super.applyOptions(options, component);

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

@@ -136,6 +136,14 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
136 136
         return getView().equals(component);
137 137
     }
138 138
 
139
+    public void onViewRegainedFocus() {
140
+        applyOptions(options);
141
+    }
142
+
143
+    public void onViewLostFocus() {
144
+
145
+    }
146
+
139 147
     public void onViewWillAppear() {
140 148
 
141 149
     }

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

@@ -2,23 +2,30 @@ package com.reactnativenavigation.viewcontrollers.modal;
2 2
 
3 3
 import android.app.Dialog;
4 4
 import android.content.DialogInterface;
5
+import android.support.annotation.Nullable;
5 6
 import android.view.KeyEvent;
6 7
 import android.view.View;
7 8
 
9
+import com.facebook.react.bridge.Promise;
8 10
 import com.reactnativenavigation.R;
9 11
 import com.reactnativenavigation.viewcontrollers.ViewController;
10 12
 
11 13
 import static android.view.View.MeasureSpec.EXACTLY;
12 14
 import static android.view.View.MeasureSpec.makeMeasureSpec;
13 15
 
14
-public class Modal implements DialogInterface.OnKeyListener {
16
+public class Modal implements DialogInterface.OnKeyListener, DialogInterface.OnDismissListener, DialogInterface.OnShowListener {
15 17
     public final ViewController viewController;
16 18
     private final Dialog dialog;
19
+    private ModalListener modalListener;
20
+    @Nullable private Promise dismissPromise;
17 21
 
18
-    public Modal(final ViewController viewController) {
22
+    public Modal(final ViewController viewController, ModalListener modalListener) {
19 23
         this.viewController = viewController;
20 24
         dialog = new Dialog(viewController.getActivity(), R.style.Modal);
25
+        this.modalListener = modalListener;
21 26
         dialog.setOnKeyListener(this);
27
+        dialog.setOnDismissListener(this);
28
+        dialog.setOnShowListener(this);
22 29
     }
23 30
 
24 31
     public void show() {
@@ -27,7 +34,8 @@ public class Modal implements DialogInterface.OnKeyListener {
27 34
         dialog.show();
28 35
     }
29 36
 
30
-    public void dismiss() {
37
+    public void dismiss(Promise promise) {
38
+        dismissPromise = promise;
31 39
         dialog.dismiss();
32 40
     }
33 41
 
@@ -52,4 +60,17 @@ public class Modal implements DialogInterface.OnKeyListener {
52 60
         }
53 61
         return false;
54 62
     }
63
+
64
+    @Override
65
+    public void onDismiss(DialogInterface dialog) {
66
+        modalListener.onModalDismiss(this);
67
+        if (dismissPromise != null) {
68
+            dismissPromise.resolve(viewController.getId());
69
+        }
70
+    }
71
+
72
+    @Override
73
+    public void onShow(DialogInterface dialog) {
74
+        modalListener.onModalDisplay(this);
75
+    }
55 76
 }

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

@@ -3,7 +3,7 @@ package com.reactnativenavigation.viewcontrollers.modal;
3 3
 import com.reactnativenavigation.viewcontrollers.ViewController;
4 4
 
5 5
 public class ModalCreator {
6
-    public Modal create(ViewController viewController) {
7
-        return new Modal(viewController);
6
+    public Modal create(ViewController viewController, ModalListener dismissListener) {
7
+        return new Modal(viewController, dismissListener);
8 8
     }
9 9
 }

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

@@ -0,0 +1,7 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+public interface ModalListener {
4
+    void onModalDismiss(Modal modal);
5
+
6
+    void onModalDisplay(Modal modal);
7
+}

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

@@ -9,6 +9,7 @@ import android.widget.RelativeLayout;
9 9
 
10 10
 import com.reactnativenavigation.interfaces.ScrollEventListener;
11 11
 import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.presentation.ComponentOptionsPresenter;
12 13
 import com.reactnativenavigation.viewcontrollers.IReactView;
13 14
 import com.reactnativenavigation.views.touch.OverlayTouchDelegate;
14 15
 
@@ -56,6 +57,7 @@ public class ComponentLayout extends FrameLayout implements ReactComponent, Titl
56 57
 
57 58
     @Override
58 59
     public void applyOptions(Options options) {
60
+        new ComponentOptionsPresenter(this).present(options);
59 61
         touchDelegate.setInterceptTouchOutside(options.overlayOptions.interceptTouchOutside.isTrue());
60 62
     }
61 63
 

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

@@ -29,6 +29,10 @@ public class StackLayout extends RelativeLayout {
29 29
         addView(topBar, MATCH_PARENT, WRAP_CONTENT);
30 30
     }
31 31
 
32
+    public void applyOptions(Options options) {
33
+        new OptionsPresenter(topBar).applyOrientation(options.orientationOptions);
34
+    }
35
+
32 36
     public void applyOptions(Options options, ReactComponent component) {
33 37
         new OptionsPresenter(topBar, component).applyOptions(options);
34 38
     }

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

@@ -6,34 +6,45 @@ import javax.annotation.*;
6 6
 
7 7
 
8 8
 public class MockPromise implements Promise {
9
+    private boolean invoked;
9 10
 
10 11
     @Override
11 12
     public void resolve(@Nullable Object value) {
12
-
13
+        throwIfInvoked();
14
+        invoked = true;
13 15
     }
14 16
 
15 17
     @Override
16 18
     public void reject(String code, String message) {
17
-
19
+        throwIfInvoked();
20
+        invoked = true;
18 21
     }
19 22
 
20 23
     @Override
21 24
     public void reject(String code, Throwable e) {
22
-
25
+        throwIfInvoked();
26
+        invoked = true;
23 27
     }
24 28
 
25 29
     @Override
26 30
     public void reject(String code, String message, Throwable e) {
27
-
31
+        throwIfInvoked();
32
+        invoked = true;
28 33
     }
29 34
 
30 35
     @Override
31 36
     public void reject(String message) {
32
-
37
+        throwIfInvoked();
38
+        invoked = true;
33 39
     }
34 40
 
35 41
     @Override
36 42
     public void reject(Throwable reason) {
43
+        throwIfInvoked();
44
+        invoked = true;
45
+    }
37 46
 
47
+    private void throwIfInvoked() {
48
+        if (invoked) throw new RuntimeException("Promise can be resolved or rejected only once");
38 49
     }
39 50
 }

+ 3
- 2
lib/android/app/src/test/java/com/reactnativenavigation/mocks/ModalCreatorMock.java View File

@@ -3,12 +3,13 @@ package com.reactnativenavigation.mocks;
3 3
 import com.reactnativenavigation.viewcontrollers.ViewController;
4 4
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
5 5
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
6
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
6 7
 
7 8
 import static org.mockito.Mockito.spy;
8 9
 
9 10
 public class ModalCreatorMock extends ModalCreator {
10 11
     @Override
11
-    public Modal create(ViewController viewController) {
12
-        return spy(new Modal(viewController));
12
+    public Modal create(ViewController viewController, ModalListener onDismissListener) {
13
+        return spy(new Modal(viewController, onDismissListener));
13 14
     }
14 15
 }

+ 50
- 0
lib/android/app/src/test/java/com/reactnativenavigation/parse/OrientationOptionsTest.java View File

@@ -0,0 +1,50 @@
1
+package com.reactnativenavigation.parse;
2
+
3
+import com.reactnativenavigation.BaseTest;
4
+import com.reactnativenavigation.parse.params.Orientation;
5
+
6
+import org.json.JSONArray;
7
+import org.junit.Test;
8
+
9
+import java.util.Arrays;
10
+
11
+import static org.assertj.core.api.Java6Assertions.assertThat;
12
+
13
+public class OrientationOptionsTest extends BaseTest {
14
+
15
+    @Override
16
+    public void beforeEach() {
17
+
18
+    }
19
+
20
+    @Test
21
+    public void parse() throws Exception {
22
+        OrientationOptions options = OrientationOptions.parse(create("default"));
23
+        assertThat(options.orientations).hasSize(1);
24
+    }
25
+
26
+    @Test
27
+    public void parseOrientations() throws Exception {
28
+        OrientationOptions options = OrientationOptions.parse(create("default", "landscape", "portrait"));
29
+        assertThat(options.orientations[0]).isEqualTo(Orientation.Default);
30
+        assertThat(options.orientations[1]).isEqualTo(Orientation.Landscape);
31
+        assertThat(options.orientations[2]).isEqualTo(Orientation.Portrait);
32
+    }
33
+
34
+    @Test
35
+    public void unsupportedOrientationsAreIgnored() throws Exception {
36
+        OrientationOptions options = OrientationOptions.parse(create("default", "autoRotate"));
37
+        assertThat(options.orientations).hasSize(1);
38
+        assertThat(options.orientations[0]).isEqualTo(Orientation.Default);
39
+    }
40
+
41
+    @Test
42
+    public void getValue_returnsDefaultIfUndefined() throws Exception {
43
+        OrientationOptions options = new OrientationOptions();
44
+        assertThat(options.getValue()).isEqualTo(Orientation.Default.orientationCode);
45
+    }
46
+
47
+    private JSONArray create(String... orientations) {
48
+        return new JSONArray(Arrays.asList(orientations));
49
+    }
50
+}

+ 77
- 8
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ModalStackTest.java View File

@@ -6,12 +6,14 @@ import com.reactnativenavigation.mocks.ModalCreatorMock;
6 6
 import com.reactnativenavigation.mocks.SimpleViewController;
7 7
 import com.reactnativenavigation.parse.Options;
8 8
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
9
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
9 10
 
10 11
 import org.junit.Test;
11 12
 
12 13
 import javax.annotation.Nullable;
13 14
 
14 15
 import static org.assertj.core.api.Assertions.assertThat;
16
+import static org.mockito.ArgumentMatchers.any;
15 17
 import static org.mockito.Mockito.spy;
16 18
 import static org.mockito.Mockito.times;
17 19
 import static org.mockito.Mockito.verify;
@@ -20,17 +22,35 @@ public class ModalStackTest extends BaseTest {
20 22
     private static final String CONTROLLER_ID = "simpleController";
21 23
     private ModalStack uut;
22 24
     private SimpleViewController viewController;
25
+    private ModalListener modalListener;
23 26
 
24 27
     @Override
25 28
     public void beforeEach() {
26
-        uut = spy(new ModalStack(new ModalCreatorMock()));
29
+        createDismissListener();
30
+        uut = spy(new ModalStack(new ModalCreatorMock(), modalListener));
27 31
         viewController = new SimpleViewController(newActivity(), CONTROLLER_ID, new Options());
28 32
     }
29 33
 
34
+    @SuppressWarnings("Convert2Lambda")
35
+    private void createDismissListener() {
36
+        ModalListener dismissListener = new ModalListener() {
37
+            @Override
38
+            public void onModalDismiss(Modal modal) {
39
+
40
+            }
41
+
42
+            @Override
43
+            public void onModalDisplay(Modal modal) {
44
+
45
+            }
46
+        };
47
+        this.modalListener = spy(dismissListener);
48
+    }
49
+
30 50
     @Test
31 51
     public void modalRefIsSaved() throws Exception {
32 52
         uut.showModal(viewController, new MockPromise());
33
-        assertThat(findModal()).isNotNull();
53
+        assertThat(findModal(CONTROLLER_ID)).isNotNull();
34 54
     }
35 55
 
36 56
     @Test
@@ -38,7 +58,8 @@ public class ModalStackTest extends BaseTest {
38 58
         uut.showModal(viewController, new MockPromise() {
39 59
             @Override
40 60
             public void resolve(@Nullable Object value) {
41
-                verify(findModal(), times(1)).show();
61
+                verify(findModal(CONTROLLER_ID), times(1)).show();
62
+                verify(modalListener, times(1)).onModalDisplay(any());
42 63
             }
43 64
         });
44 65
     }
@@ -46,12 +67,60 @@ public class ModalStackTest extends BaseTest {
46 67
     @Test
47 68
     public void modalIsDismissed() throws Exception {
48 69
         uut.showModal(viewController, new MockPromise());
49
-        assertThat(findModal()).isNotNull();
50
-        uut.dismissModal(CONTROLLER_ID, new MockPromise());
51
-        assertThat(findModal()).isNull();
70
+        final Modal modal = findModal(CONTROLLER_ID);
71
+        assertThat(modal).isNotNull();
72
+        uut.dismissModal(CONTROLLER_ID, new MockPromise() {
73
+            @Override
74
+            public void resolve(@Nullable Object value) {
75
+                assertThat(findModal(CONTROLLER_ID)).isNull();
76
+                verify(uut, times(1)).onModalDismiss(modal);
77
+            }
78
+        });
79
+    }
80
+
81
+    @Test
82
+    public void dismissAllModals() throws Exception {
83
+        uut.showModal(new SimpleViewController(newActivity(), "1", new Options()), new MockPromise());
84
+        uut.showModal(new SimpleViewController(newActivity(), "2", new Options()), new MockPromise());
85
+        uut.dismissAll(new MockPromise() {
86
+            @Override
87
+            public void resolve(@Nullable Object value) {
88
+                assertThat(uut.isEmpty()).isTrue();
89
+            }
90
+        });
91
+    }
92
+
93
+    @Test
94
+    public void isEmpty() throws Exception {
95
+        assertThat(uut.isEmpty()).isTrue();
96
+        uut.showModal(viewController, new MockPromise());
97
+        assertThat(uut.isEmpty()).isFalse();
98
+        uut.dismissAll(new MockPromise());
99
+        assertThat(uut.isEmpty()).isTrue();
100
+    }
101
+
102
+    @Test
103
+    public void onDismiss() throws Exception {
104
+        uut.showModal(viewController, new MockPromise());
105
+        uut.showModal(new SimpleViewController(newActivity(), "otherComponent", new Options()), new MockPromise());
106
+        uut.dismissAll(new MockPromise());
107
+        verify(uut, times(2)).onModalDismiss(any());
108
+    }
109
+
110
+    @Test
111
+    public void onDismiss_onViewAppearedInvokedOnPreviousModal() throws Exception {
112
+        SimpleViewController viewController = spy(new SimpleViewController(newActivity(), "otherComponent", new Options()));
113
+        uut.showModal(viewController, new MockPromise());
114
+        uut.showModal(this.viewController, new MockPromise());
115
+        uut.dismissModal(CONTROLLER_ID, new MockPromise() {
116
+            @Override
117
+            public void resolve(@Nullable Object value) {
118
+                verify(viewController, times(1)).onViewAppeared();
119
+            }
120
+        });
52 121
     }
53 122
 
54
-    private Modal findModal() {
55
-        return uut.findModalByComponentId("simpleController");
123
+    private Modal findModal(String id) {
124
+        return uut.findModalByComponentId(id);
56 125
     }
57 126
 }

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

@@ -307,8 +307,9 @@ public class NavigatorTest extends BaseTest {
307 307
 
308 308
     @Test
309 309
     public void pushIntoModal() throws Exception {
310
+        uut.setRoot(parentController, new MockPromise());
310 311
         StackController stackController = newStack();
311
-        stackController.animatePush(child1, new MockPromise());
312
+        stackController.push(child1, new MockPromise());
312 313
         uut.showModal(stackController, new MockPromise());
313 314
         uut.push(stackController.getId(), child2, new MockPromise());
314 315
         assertIsChildById(stackController.getView(), child2.getView());
@@ -336,4 +337,31 @@ public class NavigatorTest extends BaseTest {
336 337
         uut.popSpecific("child2", promise);
337 338
         verify(parentController, times(1)).popSpecific(child2, promise);
338 339
     }
340
+
341
+    @Test
342
+    public void showModal_onViewDisappearIsInvokedOnRoot() throws Exception {
343
+        uut.setRoot(parentController, new MockPromise());
344
+        uut.showModal(child1, new MockPromise() {
345
+            @Override
346
+            public void resolve(@Nullable Object value) {
347
+                verify(parentController, times(1)).onViewLostFocus();
348
+            }
349
+        });
350
+    }
351
+
352
+    @Test
353
+    public void dismissModal_onViewAppearedInvokedOnRoot() throws Exception {
354
+        uut.setRoot(parentController, new MockPromise());
355
+        uut.showModal(child1, new MockPromise() {
356
+            @Override
357
+            public void resolve(@Nullable Object value) {
358
+                uut.dismissModal("child1", new MockPromise() {
359
+                    @Override
360
+                    public void resolve(@Nullable Object value) {
361
+                        verify(parentController, times(1)).onViewRegainedFocus();
362
+                    }
363
+                });
364
+            }
365
+        });
366
+    }
339 367
 }

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

@@ -1,6 +1,7 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4
+import android.support.annotation.NonNull;
4 5
 import android.view.View;
5 6
 
6 7
 import com.reactnativenavigation.BaseTest;
@@ -20,6 +21,7 @@ import org.mockito.ArgumentCaptor;
20 21
 import javax.annotation.Nullable;
21 22
 
22 23
 import static org.assertj.core.api.Java6Assertions.assertThat;
24
+import static org.mockito.ArgumentMatchers.eq;
23 25
 import static org.mockito.Mockito.spy;
24 26
 import static org.mockito.Mockito.times;
25 27
 import static org.mockito.Mockito.verify;
@@ -77,10 +79,23 @@ public class StackControllerTest extends BaseTest {
77 79
         });
78 80
     }
79 81
 
82
+    @Test
83
+    public void pop_appliesOptionsAfterPop() throws Exception {
84
+        uut.animatePush(child1, new MockPromise());
85
+        uut.animatePush(child2, new MockPromise() {
86
+            @Override
87
+            public void resolve(@Nullable Object value) {
88
+                uut.pop(new MockPromise());
89
+                verify(uut, times(1)).applyOptions(uut.options, eq((ReactComponent) child1.getView()));
90
+            }
91
+        });
92
+    }
93
+
80 94
     @Test
81 95
     public void pop_layoutHandlesChildWillDisappear() throws Exception {
82 96
         final StackLayout[] stackLayout = new StackLayout[1];
83 97
         uut = new StackController(activity, "uut", new Options()) {
98
+            @NonNull
84 99
             @Override
85 100
             protected StackLayout createView() {
86 101
                 stackLayout[0] = spy(super.createView());

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

@@ -24,7 +24,7 @@ function start() {
24 24
   registerScreens();
25 25
   Navigation.events().onAppLaunched(() => {
26 26
     Navigation.setDefaultOptions({
27
-      animations: {
27
+      _animations: {
28 28
         push: {
29 29
           y: {
30 30
             from: 1000,