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

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

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

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

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

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

8
 import java.util.HashMap;
8
 import java.util.HashMap;
9
 
9
 
10
 public class OverlayManager {
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
     public void show(ViewGroup root, ViewController overlay) {
13
     public void show(ViewGroup root, ViewController overlay) {
14
         View view = overlay.getView();
14
         View view = overlay.getView();
15
-        overlayRegistry.put(overlay.getId(), view.getId());
15
+        overlayRegistry.put(overlay.getId(), view);
16
         root.addView(view);
16
         root.addView(view);
17
     }
17
     }
18
 
18
 
19
     public void dismiss(ViewGroup root, String componentId) {
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
 import android.support.annotation.Nullable;
3
 import android.support.annotation.Nullable;
4
 
4
 
5
 import com.facebook.react.bridge.Promise;
5
 import com.facebook.react.bridge.Promise;
6
+import com.reactnativenavigation.utils.NoOpPromise;
7
+import com.reactnativenavigation.utils.Task;
6
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
8
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
7
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
9
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
10
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
8
 
11
 
9
 import java.util.ArrayList;
12
 import java.util.ArrayList;
10
 import java.util.List;
13
 import java.util.List;
11
 
14
 
12
-public class ModalStack {
15
+class ModalStack implements ModalListener {
13
 
16
 
14
 	private List<Modal> modals = new ArrayList<>();
17
 	private List<Modal> modals = new ArrayList<>();
15
     private ModalCreator creator;
18
     private ModalCreator creator;
19
+    private ModalListener modalListener;
16
 
20
 
17
-    public ModalStack(ModalCreator creator) {
21
+    ModalStack(ModalCreator creator, ModalListener modalListener) {
18
         this.creator = creator;
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
         modals.add(modal);
28
         modals.add(modal);
24
 		modal.show();
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
 		Modal modal = findModalByComponentId(componentId);
34
 		Modal modal = findModalByComponentId(componentId);
32
 		if (modal != null) {
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
 		} else {
37
 		} else {
39
 			Navigator.rejectPromise(promise);
38
 			Navigator.rejectPromise(promise);
40
 		}
39
 		}
41
 	}
40
 	}
42
 
41
 
43
-	public void dismissAll(Promise promise) {
42
+	void dismissAll(Promise promise) {
44
 		for (Modal modal : modals) {
43
 		for (Modal modal : modals) {
45
-			modal.dismiss();
44
+			modal.dismiss(size() == 1 ? promise : new NoOpPromise());
46
 		}
45
 		}
47
 		modals.clear();
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
 	@Nullable
57
 	@Nullable
54
-	public Modal findModalByComponentId(String componentId) {
58
+    Modal findModalByComponentId(String componentId) {
55
 		for (Modal modal : modals) {
59
 		for (Modal modal : modals) {
56
 			if (modal.containsDeepComponentId(componentId)) {
60
 			if (modal.containsDeepComponentId(componentId)) {
57
 				return modal;
61
 				return modal;
65
         Modal modal = findModalByComponentId(id);
69
         Modal modal = findModalByComponentId(id);
66
         return modal != null ? modal.viewController.findControllerById(id) : null;
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
 import com.reactnativenavigation.presentation.OverlayManager;
12
 import com.reactnativenavigation.presentation.OverlayManager;
13
 import com.reactnativenavigation.utils.CompatUtils;
13
 import com.reactnativenavigation.utils.CompatUtils;
14
 import com.reactnativenavigation.utils.NoOpPromise;
14
 import com.reactnativenavigation.utils.NoOpPromise;
15
+import com.reactnativenavigation.viewcontrollers.modal.Modal;
15
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
16
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
17
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
16
 
18
 
17
 import java.util.Collection;
19
 import java.util.Collection;
18
 import java.util.Collections;
20
 import java.util.Collections;
19
 
21
 
20
-public class Navigator extends ParentController {
22
+public class Navigator extends ParentController implements ModalListener {
21
 
23
 
22
     private static final NoOpPromise NO_OP = new NoOpPromise();
24
     private static final NoOpPromise NO_OP = new NoOpPromise();
23
-    private final ModalStack modalStack = new ModalStack(new ModalCreator());
25
+    private final ModalStack modalStack;
24
     private ViewController root;
26
     private ViewController root;
25
     private OverlayManager overlayManager = new OverlayManager();
27
     private OverlayManager overlayManager = new OverlayManager();
26
     private Options defaultOptions = new Options();
28
     private Options defaultOptions = new Options();
27
 
29
 
28
     public Navigator(final Activity activity) {
30
     public Navigator(final Activity activity) {
29
         super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
31
         super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
32
+        modalStack = new ModalStack(new ModalCreator(), this);
30
     }
33
     }
31
 
34
 
32
     @NonNull
35
     @NonNull
132
         modalStack.dismissModal(componentId, promise);
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
     public void dismissAllModals(Promise promise) {
153
     public void dismissAllModals(Promise promise) {
136
         modalStack.dismissAll(promise);
154
         modalStack.dismissAll(promise);
137
     }
155
     }

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

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

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

136
         return getView().equals(component);
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
     public void onViewWillAppear() {
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
 
2
 
3
 import android.app.Dialog;
3
 import android.app.Dialog;
4
 import android.content.DialogInterface;
4
 import android.content.DialogInterface;
5
+import android.support.annotation.Nullable;
5
 import android.view.KeyEvent;
6
 import android.view.KeyEvent;
6
 import android.view.View;
7
 import android.view.View;
7
 
8
 
9
+import com.facebook.react.bridge.Promise;
8
 import com.reactnativenavigation.R;
10
 import com.reactnativenavigation.R;
9
 import com.reactnativenavigation.viewcontrollers.ViewController;
11
 import com.reactnativenavigation.viewcontrollers.ViewController;
10
 
12
 
11
 import static android.view.View.MeasureSpec.EXACTLY;
13
 import static android.view.View.MeasureSpec.EXACTLY;
12
 import static android.view.View.MeasureSpec.makeMeasureSpec;
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
     public final ViewController viewController;
17
     public final ViewController viewController;
16
     private final Dialog dialog;
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
         this.viewController = viewController;
23
         this.viewController = viewController;
20
         dialog = new Dialog(viewController.getActivity(), R.style.Modal);
24
         dialog = new Dialog(viewController.getActivity(), R.style.Modal);
25
+        this.modalListener = modalListener;
21
         dialog.setOnKeyListener(this);
26
         dialog.setOnKeyListener(this);
27
+        dialog.setOnDismissListener(this);
28
+        dialog.setOnShowListener(this);
22
     }
29
     }
23
 
30
 
24
     public void show() {
31
     public void show() {
27
         dialog.show();
34
         dialog.show();
28
     }
35
     }
29
 
36
 
30
-    public void dismiss() {
37
+    public void dismiss(Promise promise) {
38
+        dismissPromise = promise;
31
         dialog.dismiss();
39
         dialog.dismiss();
32
     }
40
     }
33
 
41
 
52
         }
60
         }
53
         return false;
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
 import com.reactnativenavigation.viewcontrollers.ViewController;
3
 import com.reactnativenavigation.viewcontrollers.ViewController;
4
 
4
 
5
 public class ModalCreator {
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

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
 
9
 
10
 import com.reactnativenavigation.interfaces.ScrollEventListener;
10
 import com.reactnativenavigation.interfaces.ScrollEventListener;
11
 import com.reactnativenavigation.parse.Options;
11
 import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.presentation.ComponentOptionsPresenter;
12
 import com.reactnativenavigation.viewcontrollers.IReactView;
13
 import com.reactnativenavigation.viewcontrollers.IReactView;
13
 import com.reactnativenavigation.views.touch.OverlayTouchDelegate;
14
 import com.reactnativenavigation.views.touch.OverlayTouchDelegate;
14
 
15
 
56
 
57
 
57
     @Override
58
     @Override
58
     public void applyOptions(Options options) {
59
     public void applyOptions(Options options) {
60
+        new ComponentOptionsPresenter(this).present(options);
59
         touchDelegate.setInterceptTouchOutside(options.overlayOptions.interceptTouchOutside.isTrue());
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
         addView(topBar, MATCH_PARENT, WRAP_CONTENT);
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
     public void applyOptions(Options options, ReactComponent component) {
36
     public void applyOptions(Options options, ReactComponent component) {
33
         new OptionsPresenter(topBar, component).applyOptions(options);
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
 
6
 
7
 
7
 
8
 public class MockPromise implements Promise {
8
 public class MockPromise implements Promise {
9
+    private boolean invoked;
9
 
10
 
10
     @Override
11
     @Override
11
     public void resolve(@Nullable Object value) {
12
     public void resolve(@Nullable Object value) {
12
-
13
+        throwIfInvoked();
14
+        invoked = true;
13
     }
15
     }
14
 
16
 
15
     @Override
17
     @Override
16
     public void reject(String code, String message) {
18
     public void reject(String code, String message) {
17
-
19
+        throwIfInvoked();
20
+        invoked = true;
18
     }
21
     }
19
 
22
 
20
     @Override
23
     @Override
21
     public void reject(String code, Throwable e) {
24
     public void reject(String code, Throwable e) {
22
-
25
+        throwIfInvoked();
26
+        invoked = true;
23
     }
27
     }
24
 
28
 
25
     @Override
29
     @Override
26
     public void reject(String code, String message, Throwable e) {
30
     public void reject(String code, String message, Throwable e) {
27
-
31
+        throwIfInvoked();
32
+        invoked = true;
28
     }
33
     }
29
 
34
 
30
     @Override
35
     @Override
31
     public void reject(String message) {
36
     public void reject(String message) {
32
-
37
+        throwIfInvoked();
38
+        invoked = true;
33
     }
39
     }
34
 
40
 
35
     @Override
41
     @Override
36
     public void reject(Throwable reason) {
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
 import com.reactnativenavigation.viewcontrollers.ViewController;
3
 import com.reactnativenavigation.viewcontrollers.ViewController;
4
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
4
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
5
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
5
 import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
6
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
6
 
7
 
7
 import static org.mockito.Mockito.spy;
8
 import static org.mockito.Mockito.spy;
8
 
9
 
9
 public class ModalCreatorMock extends ModalCreator {
10
 public class ModalCreatorMock extends ModalCreator {
10
     @Override
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

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
 import com.reactnativenavigation.mocks.SimpleViewController;
6
 import com.reactnativenavigation.mocks.SimpleViewController;
7
 import com.reactnativenavigation.parse.Options;
7
 import com.reactnativenavigation.parse.Options;
8
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
8
 import com.reactnativenavigation.viewcontrollers.modal.Modal;
9
+import com.reactnativenavigation.viewcontrollers.modal.ModalListener;
9
 
10
 
10
 import org.junit.Test;
11
 import org.junit.Test;
11
 
12
 
12
 import javax.annotation.Nullable;
13
 import javax.annotation.Nullable;
13
 
14
 
14
 import static org.assertj.core.api.Assertions.assertThat;
15
 import static org.assertj.core.api.Assertions.assertThat;
16
+import static org.mockito.ArgumentMatchers.any;
15
 import static org.mockito.Mockito.spy;
17
 import static org.mockito.Mockito.spy;
16
 import static org.mockito.Mockito.times;
18
 import static org.mockito.Mockito.times;
17
 import static org.mockito.Mockito.verify;
19
 import static org.mockito.Mockito.verify;
20
     private static final String CONTROLLER_ID = "simpleController";
22
     private static final String CONTROLLER_ID = "simpleController";
21
     private ModalStack uut;
23
     private ModalStack uut;
22
     private SimpleViewController viewController;
24
     private SimpleViewController viewController;
25
+    private ModalListener modalListener;
23
 
26
 
24
     @Override
27
     @Override
25
     public void beforeEach() {
28
     public void beforeEach() {
26
-        uut = spy(new ModalStack(new ModalCreatorMock()));
29
+        createDismissListener();
30
+        uut = spy(new ModalStack(new ModalCreatorMock(), modalListener));
27
         viewController = new SimpleViewController(newActivity(), CONTROLLER_ID, new Options());
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
     @Test
50
     @Test
31
     public void modalRefIsSaved() throws Exception {
51
     public void modalRefIsSaved() throws Exception {
32
         uut.showModal(viewController, new MockPromise());
52
         uut.showModal(viewController, new MockPromise());
33
-        assertThat(findModal()).isNotNull();
53
+        assertThat(findModal(CONTROLLER_ID)).isNotNull();
34
     }
54
     }
35
 
55
 
36
     @Test
56
     @Test
38
         uut.showModal(viewController, new MockPromise() {
58
         uut.showModal(viewController, new MockPromise() {
39
             @Override
59
             @Override
40
             public void resolve(@Nullable Object value) {
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
     @Test
67
     @Test
47
     public void modalIsDismissed() throws Exception {
68
     public void modalIsDismissed() throws Exception {
48
         uut.showModal(viewController, new MockPromise());
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
 
307
 
308
     @Test
308
     @Test
309
     public void pushIntoModal() throws Exception {
309
     public void pushIntoModal() throws Exception {
310
+        uut.setRoot(parentController, new MockPromise());
310
         StackController stackController = newStack();
311
         StackController stackController = newStack();
311
-        stackController.animatePush(child1, new MockPromise());
312
+        stackController.push(child1, new MockPromise());
312
         uut.showModal(stackController, new MockPromise());
313
         uut.showModal(stackController, new MockPromise());
313
         uut.push(stackController.getId(), child2, new MockPromise());
314
         uut.push(stackController.getId(), child2, new MockPromise());
314
         assertIsChildById(stackController.getView(), child2.getView());
315
         assertIsChildById(stackController.getView(), child2.getView());
336
         uut.popSpecific("child2", promise);
337
         uut.popSpecific("child2", promise);
337
         verify(parentController, times(1)).popSpecific(child2, promise);
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
 package com.reactnativenavigation.viewcontrollers;
1
 package com.reactnativenavigation.viewcontrollers;
2
 
2
 
3
 import android.app.Activity;
3
 import android.app.Activity;
4
+import android.support.annotation.NonNull;
4
 import android.view.View;
5
 import android.view.View;
5
 
6
 
6
 import com.reactnativenavigation.BaseTest;
7
 import com.reactnativenavigation.BaseTest;
20
 import javax.annotation.Nullable;
21
 import javax.annotation.Nullable;
21
 
22
 
22
 import static org.assertj.core.api.Java6Assertions.assertThat;
23
 import static org.assertj.core.api.Java6Assertions.assertThat;
24
+import static org.mockito.ArgumentMatchers.eq;
23
 import static org.mockito.Mockito.spy;
25
 import static org.mockito.Mockito.spy;
24
 import static org.mockito.Mockito.times;
26
 import static org.mockito.Mockito.times;
25
 import static org.mockito.Mockito.verify;
27
 import static org.mockito.Mockito.verify;
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
     @Test
94
     @Test
81
     public void pop_layoutHandlesChildWillDisappear() throws Exception {
95
     public void pop_layoutHandlesChildWillDisappear() throws Exception {
82
         final StackLayout[] stackLayout = new StackLayout[1];
96
         final StackLayout[] stackLayout = new StackLayout[1];
83
         uut = new StackController(activity, "uut", new Options()) {
97
         uut = new StackController(activity, "uut", new Options()) {
98
+            @NonNull
84
             @Override
99
             @Override
85
             protected StackLayout createView() {
100
             protected StackLayout createView() {
86
                 stackLayout[0] = spy(super.createView());
101
                 stackLayout[0] = spy(super.createView());

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

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