Daniel Zlotin 6 years ago
parent
commit
c7d7f89525
93 changed files with 1287 additions and 522 deletions
  1. 2
    1
      Jenkinsfile
  2. 1
    1
      docs/_sidebar.md
  3. 3
    3
      docs/docs/Component.md
  4. 32
    33
      docs/docs/Navigation.md
  5. 2
    2
      docs/docs/Root.md
  6. 2
    2
      docs/docs/SideMenu.md
  7. 42
    9
      docs/docs/usage.md
  8. 18
    18
      docs/docs/v1tov2diff.md
  9. 4
    3
      e2e/CustomTransition.js
  10. 11
    16
      e2e/Orientations.test.js
  11. 1
    1
      e2e/ScreenStack.test.js
  12. 3
    11
      e2e/ScreenStyle.test.js
  13. 2
    2
      e2e/TopLevelApi.test.js
  14. 2
    1
      lib/android/app/build.gradle
  15. 48
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabOptions.java
  16. 26
    16
      lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java
  17. 3
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Button.java
  18. 8
    4
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java
  19. 5
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Options.java
  20. 2
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/TopBarOptions.java
  21. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/TopTabsOptions.java
  22. 25
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/BottomTabOptionsPresenter.java
  23. 1
    0
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java
  24. 15
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ArrayUtils.java
  25. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/utils/CompatUtils.java
  26. 4
    4
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageLoader.java
  27. 14
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ViewUtils.java
  28. 87
    41
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/BottomTabsController.java
  29. 2
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ComponentViewController.java
  30. 12
    56
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ModalStack.java
  31. 103
    102
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  32. 13
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java
  33. 4
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/SideMenuController.java
  34. 3
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java
  35. 7
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  36. 55
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/Modal.java
  37. 9
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/modal/ModalCreator.java
  38. 1
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java
  39. 32
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/BottomTabs.java
  40. 50
    16
      lib/android/app/src/main/java/com/reactnativenavigation/views/TitleBarButton.java
  41. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/TopBar.java
  42. 49
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/ImageLoaderMock.java
  43. 14
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/ModalCreatorMock.java
  44. 2
    3
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/SimpleViewController.java
  45. 10
    12
      lib/android/app/src/test/java/com/reactnativenavigation/parse/NavigationOptionsTest.java
  46. 13
    0
      lib/android/app/src/test/java/com/reactnativenavigation/utils/OptionHelper.java
  47. 48
    29
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java
  48. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ComponentViewControllerTest.java
  49. 57
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ModalStackTest.java
  50. 55
    21
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  51. 2
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java
  52. 10
    9
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java
  53. 26
    13
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackControllerTest.java
  54. 0
    25
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsControllerMock.java
  55. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java
  56. 5
    4
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ViewControllerTest.java
  57. 2
    0
      lib/ios/RNNBottomTabsOptions.h
  58. 4
    1
      lib/ios/RNNControllerFactory.m
  59. 7
    0
      lib/ios/RNNCustomTitleView.h
  60. 47
    0
      lib/ios/RNNCustomTitleView.m
  61. 5
    1
      lib/ios/RNNNavigationButtons.m
  62. 4
    0
      lib/ios/RNNNavigationController.m
  63. 1
    0
      lib/ios/RNNRootViewController.h
  64. 22
    1
      lib/ios/RNNRootViewController.m
  65. 3
    0
      lib/ios/RNNRootViewProtocol.h
  66. 4
    0
      lib/ios/RNNTabBarController.m
  67. 3
    0
      lib/ios/RNNTopBarOptions.h
  68. 1
    0
      lib/ios/RNNTopBarOptions.m
  69. 4
    1
      lib/ios/RNNUIBarButtonItem.h
  70. 16
    0
      lib/ios/RNNUIBarButtonItem.m
  71. 8
    0
      lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj
  72. 7
    2
      lib/src/commands/LayoutTreeCrawler.ts
  73. 13
    0
      package.json
  74. 9
    0
      playground/android/app/build.gradle
  75. 24
    0
      playground/android/app/src/androidTest/java/com/reactnativenavigation/playground/DetoxTest.java
  76. 3
    0
      playground/android/settings.gradle
  77. BIN
      playground/src/images/one@2x.png
  78. BIN
      playground/src/images/one_selected@2x.png
  79. BIN
      playground/src/images/three@2x.png
  80. BIN
      playground/src/images/three_selected@2x.png
  81. BIN
      playground/src/images/two@2x.png
  82. BIN
      playground/src/images/two_selected@2x.png
  83. 47
    0
      playground/src/screens/CustomTopBar.js
  84. 5
    0
      playground/src/screens/LifecycleScreen.js
  85. 1
    1
      playground/src/screens/OptionsScreen.js
  86. 3
    1
      playground/src/screens/OrientationDetectScreen.js
  87. 4
    8
      playground/src/screens/OrientationSelectScreen.js
  88. 1
    1
      playground/src/screens/StaticLifecycleOverlay.js
  89. 10
    0
      playground/src/screens/TextScreen.js
  90. 59
    25
      playground/src/screens/WelcomeScreen.js
  91. 2
    0
      playground/src/screens/index.js
  92. 3
    0
      playground/src/testIDs.js
  93. 12
    0
      scripts/test.e2e.android.detox.js

+ 2
- 1
Jenkinsfile View File

@@ -36,4 +36,5 @@ npm run test-e2e-android -- --release'''
36 36
       }
37 37
     }
38 38
   }
39
-}
39
+}
40
+

+ 1
- 1
docs/_sidebar.md View File

@@ -6,7 +6,7 @@
6 6
  - [Top Level](/docs/Navigation)
7 7
 - Params
8 8
  - [Root](/docs/Root)
9
- - [Container](/docs/Container)
9
+ - [Component](/docs/Component)
10 10
  - [SideMenu](/docs/SideMenu)
11 11
 - Options
12 12
  - [NavigationOptions](/docs/options/NavigationOptions)

docs/docs/Container.md → docs/docs/Component.md View File

@@ -1,11 +1,11 @@
1
-<h1>Container</h1>
1
+<h1>Component</h1>
2 2
 
3 3
 **Properties**
4 4
 
5 5
 | Name | Type | Description |
6 6
 | --- | --- | --- |
7
-| name | <code>string</code> | The container's registered name |
8
-| topTabs | [<code>Array.&lt;Container&gt;</code>](#Container) |  |
7
+| name | <code>string</code> | The components registered name |
8
+| topTabs | [<code>Array.&lt;Component&gt;</code>](#Component) |  |
9 9
 | passProps | <code>object</code> | props |
10 10
 | options | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Options">Options</a> |  |
11 11
 

+ 32
- 33
docs/docs/Navigation.md View File

@@ -3,32 +3,32 @@
3 3
 # Navigation
4 4
 
5 5
 * [Navigation](#Navigation)
6
-    * [.registerContainer(containerName, getContainerFunc)](#Navigation+registerContainer)
6
+    * [.registerComponent(componentName, getComponentFunc)](#Navigation+registerComponent)
7 7
     * [.setRoot(root)](#Navigation+setRoot)
8 8
     * [.setDefaultOptions(options)](#Navigation+setDefaultOptions)
9
-    * [.setOptions(containerId, options)](#Navigation+setOptions)
9
+    * [.setOptions(componentId, options)](#Navigation+setOptions)
10 10
     * [.showModal(params)](#Navigation+showModal)
11
-    * [.dismissModal(containerId)](#Navigation+dismissModal)
11
+    * [.dismissModal(componentId)](#Navigation+dismissModal)
12 12
     * [.dismissAllModals()](#Navigation+dismissAllModals)
13
-    * [.push(containerId, container)](#Navigation+push)
14
-    * [.pop(containerId, params)](#Navigation+pop)
15
-    * [.popTo(containerId)](#Navigation+popTo)
16
-    * [.popToRoot(containerId)](#Navigation+popToRoot)
13
+    * [.push(componentId, component)](#Navigation+push)
14
+    * [.pop(componentId, params)](#Navigation+pop)
15
+    * [.popTo(componentId)](#Navigation+popTo)
16
+    * [.popToRoot(componentId)](#Navigation+popToRoot)
17 17
     * [.events()](#Navigation+events)
18 18
 
19 19
 
20 20
 * * *
21 21
 
22
-<a name="Navigation+registerContainer"></a>
22
+<a name="Navigation+registerComponent"></a>
23 23
 
24
-## navigation.registerContainer(containerName, getContainerFunc)
24
+## navigation.registerComponent(componentName, getComponentFunc)
25 25
 Every screen component in your app must be registered with a unique name. The component itself is a traditional React component extending React.Component.
26 26
 
27 27
 
28 28
 | Param | Type | Description |
29 29
 | --- | --- | --- |
30
-| containerName | <code>string</code> | Unique container name |
31
-| getContainerFunc | <code>function</code> | generator function, typically `() => require('./myContainer')` |
30
+| componentName | <code>string</code> | Unique component name |
31
+| getComponentFunc | <code>function</code> | generator function, typically `() => require('./myComponent')` |
32 32
 
33 33
 
34 34
 * * *
@@ -41,7 +41,7 @@ Reset the navigation stack to a new screen (the stack root is changed).
41 41
 
42 42
 | Param | Type |
43 43
 | --- | --- |
44
-| root | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Root">Root</a> | 
44
+| root | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Root">Root</a> |
45 45
 
46 46
 
47 47
 * * *
@@ -54,20 +54,20 @@ Set default options to all screens. Useful for declaring a consistent style acro
54 54
 
55 55
 | Param | Type |
56 56
 | --- | --- |
57
-| options | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/options/NavigationOptions">NavigationOptions</a> | 
57
+| options | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/options/NavigationOptions">NavigationOptions</a> |
58 58
 
59 59
 
60 60
 * * *
61 61
 
62 62
 <a name="Navigation+setOptions"></a>
63 63
 
64
-## navigation.setOptions(containerId, options)
65
-Change a containers navigation options
64
+## navigation.setOptions(componentId, options)
65
+Change a components navigation options
66 66
 
67 67
 
68 68
 | Param | Type | Description |
69 69
 | --- | --- | --- |
70
-| containerId | <code>string</code> | The container's id. |
70
+| componentId | <code>string</code> | The component's id. |
71 71
 | options | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/options/NavigationOptions">NavigationOptions</a> |  |
72 72
 
73 73
 
@@ -81,20 +81,20 @@ Show a screen as a modal.
81 81
 
82 82
 | Param | Type |
83 83
 | --- | --- |
84
-| params | <code>object</code> | 
84
+| params | <code>object</code> |
85 85
 
86 86
 
87 87
 * * *
88 88
 
89 89
 <a name="Navigation+dismissModal"></a>
90 90
 
91
-## navigation.dismissModal(containerId)
92
-Dismiss a modal by containerId. The dismissed modal can be anywhere in the stack.
91
+## navigation.dismissModal(componentId)
92
+Dismiss a modal by componentId. The dismissed modal can be anywhere in the stack.
93 93
 
94 94
 
95 95
 | Param | Type | Description |
96 96
 | --- | --- | --- |
97
-| containerId | <code>string</code> | The container's id. |
97
+| componentId | <code>string</code> | The component's id. |
98 98
 
99 99
 
100 100
 * * *
@@ -109,27 +109,27 @@ Dismiss all Modals
109 109
 
110 110
 <a name="Navigation+push"></a>
111 111
 
112
-## navigation.push(containerId, container)
112
+## navigation.push(componentId, component)
113 113
 Push a new screen into this screen's navigation stack.
114 114
 
115 115
 
116 116
 | Param | Type | Description |
117 117
 | --- | --- | --- |
118
-| containerId | <code>string</code> | The container's id. |
119
-| container | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Container">Container</a> |  |
118
+| componentId | <code>string</code> | The component's id. |
119
+| component | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Component">Component</a> |  |
120 120
 
121 121
 
122 122
 * * *
123 123
 
124 124
 <a name="Navigation+pop"></a>
125 125
 
126
-## navigation.pop(containerId, params)
127
-Pop a container from the stack, regardless of it's position.
126
+## navigation.pop(componentId, params)
127
+Pop a component from the stack, regardless of it's position.
128 128
 
129 129
 
130 130
 | Param | Type | Description |
131 131
 | --- | --- | --- |
132
-| containerId | <code>string</code> | The container's id. |
132
+| componentId | <code>string</code> | The component's id. |
133 133
 | params | <code>*</code> |  |
134 134
 
135 135
 
@@ -137,26 +137,26 @@ Pop a container from the stack, regardless of it's position.
137 137
 
138 138
 <a name="Navigation+popTo"></a>
139 139
 
140
-## navigation.popTo(containerId)
141
-Pop the stack to a given container
140
+## navigation.popTo(componentId)
141
+Pop the stack to a given component
142 142
 
143 143
 
144 144
 | Param | Type | Description |
145 145
 | --- | --- | --- |
146
-| containerId | <code>string</code> | The container's id. |
146
+| componentId | <code>string</code> | The component's id. |
147 147
 
148 148
 
149 149
 * * *
150 150
 
151 151
 <a name="Navigation+popToRoot"></a>
152 152
 
153
-## navigation.popToRoot(containerId)
154
-Pop the container's stack to root.
153
+## navigation.popToRoot(componentId)
154
+Pop the component's stack to root.
155 155
 
156 156
 
157 157
 | Param | Type |
158 158
 | --- | --- |
159
-| containerId | <code>*</code> | 
159
+| componentId | <code>*</code> |
160 160
 
161 161
 
162 162
 * * *
@@ -165,4 +165,3 @@ Pop the container's stack to root.
165 165
 
166 166
 ## navigation.events()
167 167
 Obtain the events registery instance
168
-

+ 2
- 2
docs/docs/Root.md View File

@@ -4,7 +4,7 @@
4 4
 
5 5
 | Name | Type |
6 6
 | --- | --- |
7
-| container | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Container">Container</a> | 
7
+| component | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Component">Component</a> | 
8 8
 | sideMenu | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/SideMenu">SideMenu</a> | 
9
-| bottomTabs | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Container">Container[]</a> | 
9
+| bottomTabs | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Component">Component[]</a> | 
10 10
 

+ 2
- 2
docs/docs/SideMenu.md View File

@@ -4,6 +4,6 @@
4 4
 
5 5
 | Name | Type |
6 6
 | --- | --- |
7
-| left | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Container">Container</a> | 
8
-| right | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Container">Container</a> | 
7
+| left | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Component">Component</a> | 
8
+| right | <a href="https://wix.github.io/react-native-navigation/v2/#/docs/Component">Component</a> | 
9 9
 

+ 42
- 9
docs/docs/usage.md View File

@@ -4,7 +4,7 @@
4 4
 
5 5
 ### Navigation
6 6
 ```js
7
-import Navigation from 'react-native-navigation';
7
+import { Navigation } from 'react-native-navigation';
8 8
 ```
9 9
 ### Events - On App Launched
10 10
 How to initiate your app.
@@ -83,24 +83,57 @@ Navigation.setRoot({
83 83
   },
84 84
 });
85 85
 ```
86
+
87
+Start a stack based app (with options):
88
+
89
+```js
90
+Navigation.setRoot({
91
+  stack: {
92
+    options: {
93
+      topBar: {
94
+        hidden: true,
95
+      },
96
+    },
97
+    children: [
98
+      {
99
+        component: {
100
+          name: 'navigation.playground.TextScreen',
101
+          passProps: {
102
+            text: 'This is tab 1',
103
+            myFunction: () => 'Hello from a function!',
104
+          },
105
+        },
106
+      },
107
+      {
108
+        component: {
109
+          name: 'navigation.playground.TextScreen',
110
+          passProps: {
111
+            text: 'This is tab 2',
112
+          },
113
+        },
114
+      },
115
+    ],
116
+  },
117
+});
118
+```
86 119
 ## Screen API
87 120
 
88 121
 ### push(params)
89 122
 Push a new screen into this screen's navigation stack.
90 123
 
91 124
 ```js
92
-Navigation.push(this.props.containerId, {
125
+Navigation.push(this.props.componentId, {
93 126
   name: 'navigation.playground.PushedScreen',
94 127
   passProps: {}
95 128
 });
96 129
 ```
97
-### pop(containerId)
130
+### pop(componentId)
98 131
 Pop the top screen from this screen's navigation stack.
99 132
 
100 133
 ```js
101
-Navigation.pop(this.props.containerId);
134
+Navigation.pop(this.props.componentId);
102 135
 ```
103
-### popTo(containerId)
136
+### popTo(componentId)
104 137
 ```js
105 138
 Navigation.popTo(previousScreenId);
106 139
 ```
@@ -108,14 +141,14 @@ Navigation.popTo(previousScreenId);
108 141
 Pop all the screens until the root from this screen's navigation stack
109 142
 
110 143
 ```js
111
-Navigation.popToRoot(this.props.containerId);
144
+Navigation.popToRoot(this.props.componentId);
112 145
 ```
113 146
 ### showModal(params = {})
114 147
 Show a screen as a modal.
115 148
 
116 149
 ```js
117 150
 Navigation.showModal({
118
-  container: {
151
+  component: {
119 152
     name: 'navigation.playground.ModalScreen',
120 153
     passProps: {
121 154
         key: 'value'
@@ -123,11 +156,11 @@ Navigation.showModal({
123 156
   }
124 157
 });
125 158
 ```
126
-### dismissModal(containerId)
159
+### dismissModal(componentId)
127 160
 Dismiss modal.
128 161
 
129 162
 ```js
130
-Navigation.dismissModal(this.props.containerId);
163
+Navigation.dismissModal(this.props.componentId);
131 164
 ```
132 165
 ### dismissAllModals()
133 166
 Dismiss all the current modals at the same time.

+ 18
- 18
docs/docs/v1tov2diff.md View File

@@ -17,9 +17,9 @@ These issues originate from the same problem: you cannot specify on which screen
17 17
 There are ways to solve some of these problems in v1 but they are not straightforward. We want to change that.
18 18
 
19 19
 #### New API
20
-To solve this problem in v2, every screen receives as a prop it’s containerId. Whenever you want to perform an action from that screen you need to pass the containerId to the function:
20
+To solve this problem in v2, every screen receives as a prop it’s componentId. Whenever you want to perform an action from that screen you need to pass the componentId to the function:
21 21
 ```js
22
-Navigator.pop(this.props.containerId)
22
+Navigator.pop(this.props.componentId)
23 23
 ```
24 24
 ### Built for Contributors
25 25
 Currently, it requires a lot of work to accept pull requests. We need to manually make sure that everything works before we approve them because v1 is not thoroughly tested. <br>
@@ -179,18 +179,18 @@ How to initiate your app.
179 179
 ```js
180 180
 Navigation.events().onAppLaunched(() => {
181 181
     Navigation.setRoot({
182
-      container: {
182
+      component: {
183 183
         name: 'navigation.playground.WelcomeScreen'
184 184
       }
185 185
     });
186 186
   });
187 187
 ```
188 188
 
189
-#### registerContainer(screenID, generator)
189
+#### registerComponent(screenID, generator)
190 190
 Every screen component in your app must be registered with a unique name. The component itself is a traditional React component extending React.Component.
191 191
 
192 192
 ```js
193
-Navigation.registerContainer(`navigation.playground.WelcomeScreen`, () => WelcomeScreen);
193
+Navigation.registerComponent(`navigation.playground.WelcomeScreen`, () => WelcomeScreen);
194 194
 ```
195 195
 
196 196
 #### setRoot({params})
@@ -198,12 +198,12 @@ Start a Single page app with two side menus:
198 198
 
199 199
 ```js
200 200
 Navigation.setRoot({
201
-      container: {
201
+      component: {
202 202
         name: 'navigation.playground.WelcomeScreen'
203 203
       },
204 204
       sideMenu: {
205 205
         left: {
206
-          container: {
206
+          component: {
207 207
             name: 'navigation.playground.TextScreen',
208 208
             passProps: {
209 209
               text: 'This is a left side menu screen'
@@ -211,7 +211,7 @@ Navigation.setRoot({
211 211
           }
212 212
         },
213 213
         right: {
214
-          container: {
214
+          component: {
215 215
             name: 'navigation.playground.TextScreen',
216 216
             passProps: {
217 217
               text: 'This is a right side menu screen'
@@ -227,7 +227,7 @@ Start a tab based app:
227 227
 Navigation.setRoot({
228 228
       tabs: [
229 229
         {
230
-          container: {
230
+          component: {
231 231
             name: 'navigation.playground.TextScreen',
232 232
             passProps: {
233 233
               text: 'This is tab 1',
@@ -236,7 +236,7 @@ Navigation.setRoot({
236 236
           }
237 237
         },
238 238
         {
239
-          container: {
239
+          component: {
240 240
             name: 'navigation.playground.TextScreen',
241 241
             passProps: {
242 242
               text: 'This is tab 2'
@@ -252,34 +252,34 @@ Navigation.setRoot({
252 252
 Push a new screen into this screen's navigation stack.
253 253
 
254 254
 ```js
255
-Navigation.push(this.props.containerId, {
255
+Navigation.push(this.props.componentId, {
256 256
       name: 'navigation.playground.PushedScreen',
257 257
       passProps: {}
258 258
     });
259 259
 ```
260
-#### pop(containerId)
260
+#### pop(componentId)
261 261
 Pop the top screen from this screen's navigation stack.
262 262
 
263 263
 ```js
264
-Navigation.pop(this.props.containerId);
264
+Navigation.pop(this.props.componentId);
265 265
 ```
266 266
 #### popTo(params)
267 267
 
268 268
 ```js
269
-Navigation.popTo(this.props.containerId, this.props.previousScreenIds[0]);
269
+Navigation.popTo(this.props.componentId, this.props.previousScreenIds[0]);
270 270
 ```
271 271
 #### popToRoot()
272 272
 Pop all the screens until the root from this screen's navigation stack
273 273
 
274 274
 ```js
275
-Navigation.popToRoot(this.props.containerId);
275
+Navigation.popToRoot(this.props.componentId);
276 276
 ```
277 277
 #### showModal(params = {})
278 278
 Show a screen as a modal.
279 279
 
280 280
 ```js
281 281
 Navigation.showModal({
282
-      container: {
282
+      component: {
283 283
         name: 'navigation.playground.ModalScreen',
284 284
         passProps: {
285 285
             key: 'value'
@@ -287,11 +287,11 @@ Navigation.showModal({
287 287
       }
288 288
     });
289 289
 ```
290
-#### dismissModal(containerId)
290
+#### dismissModal(componentId)
291 291
 Dismiss modal.
292 292
 
293 293
 ```js
294
-Navigation.dismissModal(this.props.containerId);
294
+Navigation.dismissModal(this.props.componentId);
295 295
 ```
296 296
 #### dismissAllModals()
297 297
 Dismiss all the current modals at the same time.

+ 4
- 3
e2e/CustomTransition.js View File

@@ -1,7 +1,8 @@
1 1
 
2 2
 const Utils = require('./Utils');
3
+const testIDs = require('../playground/src/testIDs');
3 4
 
4
-const elementByLabel = Utils.elementByLabel;
5
+const elementById = Utils.elementById;
5 6
 
6 7
 describe('custom transition', () => {
7 8
   beforeEach(async () => {
@@ -9,8 +10,8 @@ describe('custom transition', () => {
9 10
   });
10 11
 
11 12
   it('sanity', async () => {
12
-    await elementByLabel('Push Options Screen').tap();
13
-    await elementByLabel('Custom Transition').tap();
13
+    await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
14
+    await elementById(testIDs.CUSTOM_TRANSITION_BUTTON).tap();
14 15
     await expect(element(by.id('shared_image1'))).toExist();
15 16
     await element(by.id('shared_image1')).tap();
16 17
     await expect(element(by.id('shared_image2'))).toExist();

+ 11
- 16
e2e/Orientations.test.js View File

@@ -9,41 +9,36 @@ describe('orientation', () => {
9 9
     await device.relaunchApp();
10 10
   });
11 11
 
12
-  afterEach(async () => {
13
-    await device.setOrientation('landscape');
14
-    await device.setOrientation('portrait');
15
-  });
16
-
17 12
   it('default allows all', async () => {
18 13
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
19 14
     await elementById(testIDs.DEFAULT_ORIENTATION_BUTTON).tap();
20
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
15
+    await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
21 16
     await device.setOrientation('landscape');
22
-    await expect(element(by.id('currentOrientation'))).toHaveText('Landscape');
17
+    await expect(elementById(testIDs.LANDSCAPE_ELEMENT)).toBeVisible();
23 18
     await device.setOrientation('portrait');
24
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
19
+    await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
25 20
     await elementById(testIDs.DISMISS_BUTTON).tap();
26 21
   });
27 22
 
28 23
   it('landscape and portrait array', async () => {
29 24
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
30 25
     await elementById(testIDs.LANDSCAPE_PORTRAIT_ORIENTATION_BUTTON).tap();
31
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
26
+    await expect(element(by.id(testIDs.PORTRAIT_ELEMENT))).toBeVisible();
32 27
     await device.setOrientation('landscape');
33
-    await expect(element(by.id('currentOrientation'))).toHaveText('Landscape');
28
+    await expect(element(by.id(testIDs.LANDSCAPE_ELEMENT))).toBeVisible();
34 29
     await device.setOrientation('portrait');
35
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
30
+    await expect(element(by.id(testIDs.PORTRAIT_ELEMENT))).toBeVisible();
36 31
     await elementById(testIDs.DISMISS_BUTTON).tap();
37 32
   });
38 33
 
39 34
   it('portrait only', async () => {
40 35
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
41 36
     await elementById(testIDs.PORTRAIT_ORIENTATION_BUTTON).tap();
42
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
37
+    await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
43 38
     await device.setOrientation('landscape');
44
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
39
+    await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
45 40
     await device.setOrientation('portrait');
46
-    await expect(element(by.id('currentOrientation'))).toHaveText('Portrait');
41
+    await expect(elementById(testIDs.PORTRAIT_ELEMENT)).toBeVisible();
47 42
     await elementById(testIDs.DISMISS_BUTTON).tap();
48 43
   });
49 44
 
@@ -51,9 +46,9 @@ describe('orientation', () => {
51 46
     await elementById(testIDs.ORIENTATION_BUTTON).tap();
52 47
     await elementById(testIDs.LANDSCAPE_ORIENTATION_BUTTON).tap();
53 48
     await device.setOrientation('landscape');
54
-    await expect(element(by.id('currentOrientation'))).toHaveText('Landscape');
49
+    await expect(element(by.id(testIDs.LANDSCAPE_ELEMENT))).toBeVisible();
55 50
     await device.setOrientation('portrait');
56
-    await expect(element(by.id('currentOrientation'))).toHaveText('Landscape');
51
+    await expect(element(by.id(testIDs.LANDSCAPE_ELEMENT))).toBeVisible();
57 52
     await elementById(testIDs.DISMISS_BUTTON).tap();
58 53
   });
59 54
 });

+ 1
- 1
e2e/ScreenStack.test.js View File

@@ -90,7 +90,7 @@ describe('screen stack', () => {
90 90
     await elementById(testIDs.SHOW_MODAL_BUTTON).tap();
91 91
     await elementById(testIDs.MODAL_WITH_STACK_BUTTON).tap();
92 92
     await expect(elementByLabel('Screen 2')).toBeVisible();
93
-    await Utils.tapBackIos();
93
+    await elementById(testIDs.POP_BUTTON).tap();
94 94
     await expect(elementByLabel('Screen 1')).toBeVisible();
95 95
   });
96 96
 });

+ 3
- 11
e2e/ScreenStyle.test.js View File

@@ -10,14 +10,14 @@ describe('screen style', () => {
10 10
 
11 11
   it('declare a options on component component', async () => {
12 12
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
13
-    await expect(element(by.label('Static Title'))).toBeVisible();
13
+    await expect(elementByLabel('Static Title')).toBeVisible();
14 14
   });
15 15
 
16 16
   it('change title on component component', async () => {
17 17
     await elementById(testIDs.PUSH_OPTIONS_BUTTON).tap();
18
-    await expect(element(by.label('Static Title'))).toBeVisible();
18
+    await expect(elementByLabel('Static Title')).toBeVisible();
19 19
     await elementById(testIDs.DYNAMIC_OPTIONS_BUTTON).tap();
20
-    await expect(element(by.label('Dynamic Title'))).toBeVisible();
20
+    await expect(elementByLabel('Dynamic Title')).toBeVisible();
21 21
   });
22 22
 
23 23
   it('set dynamic options with valid options will do something and not crash', async () => {
@@ -46,14 +46,6 @@ describe('screen style', () => {
46 46
     await expect(elementById(testIDs.TOP_BAR_ELEMENT)).toBeVisible();
47 47
   });
48 48
 
49
-  it('makes topBar transparent and opaque', async () => {
50
-    await elementByLabel('Push Options Screen').tap();
51
-    await elementByLabel('Top Bar Transparent').tap();
52
-    await expect(element(by.type('_UIVisualEffectBackdropView'))).toBeNotVisible();
53
-    await elementByLabel('Top Bar Opaque').tap();
54
-    await expect(element(by.type('_UIVisualEffectBackdropView')).atIndex(1)).toBeVisible();
55
-  });
56
-
57 49
   it('set Tab Bar badge on a current Tab', async () => {
58 50
     await elementById(testIDs.TAB_BASED_APP_BUTTON).tap();
59 51
     await elementById(testIDs.SET_TAB_BADGE_BUTTON).tap();

+ 2
- 2
e2e/TopLevelApi.test.js View File

@@ -35,9 +35,9 @@ describe('top level api', () => {
35 35
   it('unmount is called on pop', async () => {
36 36
     await elementById(testIDs.PUSH_LIFECYCLE_BUTTON).tap();
37 37
     await expect(elementByLabel('didAppear')).toBeVisible();
38
-    await Utils.tapBackIos();
38
+    await elementById(testIDs.POP_BUTTON).tap();
39 39
     await expect(elementByLabel('componentWillUnmount')).toBeVisible();
40
-    await element(by.traits(['button']).and(by.label('OK'))).atIndex(0).tap();
40
+    await elementByLabel('OK').atIndex(0).tap();
41 41
     await expect(elementByLabel('didDisappear')).toBeVisible();
42 42
   });
43 43
 });

+ 2
- 1
lib/android/app/build.gradle View File

@@ -59,7 +59,8 @@ dependencies {
59 59
     implementation fileTree(include: ['*.jar'], dir: 'libs')
60 60
     implementation 'com.android.support:design:25.4.0'
61 61
     implementation 'com.android.support:appcompat-v7:25.4.0'
62
-    implementation "com.android.support:support-v4:25.4.0"
62
+    implementation 'com.android.support:support-v4:25.4.0'
63
+    implementation 'com.aurelhubert:ahbottomnavigation:2.1.0'
63 64
 
64 65
     // node_modules
65 66
     //noinspection GradleDynamicVersion

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

@@ -0,0 +1,48 @@
1
+package com.reactnativenavigation.parse;
2
+
3
+import org.json.JSONObject;
4
+
5
+public class BottomTabOptions implements DEFAULT_VALUES {
6
+
7
+    public static BottomTabOptions parse(JSONObject json) {
8
+        BottomTabOptions options = new BottomTabOptions();
9
+        if (json == null) return options;
10
+
11
+        options.title = TextParser.parse(json, "title");
12
+        if (json.has("icon")) {
13
+            options.icon = TextParser.parse(json.optJSONObject("icon"), "uri");
14
+        }
15
+        options.badge = TextParser.parse(json, "badge");
16
+        options.testId = TextParser.parse(json, "testID");
17
+        return options;
18
+    }
19
+
20
+    public Text title = new NullText();
21
+    public Text icon = new NullText();
22
+    public Text testId = new NullText();
23
+    public Text badge = new NullText();
24
+
25
+    void mergeWith(final BottomTabOptions other) {
26
+        if (other.title.hasValue()) {
27
+            title = other.title;
28
+        }
29
+        if (other.icon.hasValue()) {
30
+            icon = other.icon;
31
+        }
32
+        if (other.badge.hasValue()) {
33
+            badge = other.badge;
34
+        }
35
+    }
36
+
37
+    void mergeWithDefault(final BottomTabOptions defaultOptions) {
38
+        if (!title.hasValue()) {
39
+            title = defaultOptions.title;
40
+        }
41
+        if (!icon.hasValue()) {
42
+            icon = defaultOptions.icon;
43
+        }
44
+        if (!badge.hasValue()) {
45
+            badge = defaultOptions.badge;
46
+        }
47
+    }
48
+}

+ 26
- 16
lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java View File

@@ -11,20 +11,24 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
11 11
 		BottomTabsOptions options = new BottomTabsOptions();
12 12
 		if (json == null) return options;
13 13
 
14
-		options.currentTabId = TextParser.parse(json, "currentTabId");
14
+        options.color = ColorParser.parse(json, "tabColor");
15
+        options.selectedColor = ColorParser.parse(json, "selectedTabColor");
16
+        options.currentTabId = TextParser.parse(json, "currentTabId");
15 17
 		options.currentTabIndex = json.optInt("currentTabIndex", NO_INT_VALUE);
16
-		options.tabBadge = json.optInt("tabBadge", NO_INT_VALUE);
17
-		options.hidden = BooleanOptions.parse(json.optString("hidden"));
18
+		options.visible = BooleanOptions.parse(json.optString("visible"));
18 19
 		options.animateHide = BooleanOptions.parse(json.optString("animateHide"));
20
+        options.testId = TextParser.parse(json, "testID");
19 21
 
20 22
 		return options;
21 23
 	}
22 24
 
23
-	int tabBadge = NO_INT_VALUE;
24
-	BooleanOptions hidden = BooleanOptions.False;
25
+    public Color color = new NullColor();
26
+    public Color selectedColor = new NullColor();
27
+	BooleanOptions visible = BooleanOptions.False;
25 28
 	BooleanOptions animateHide = BooleanOptions.False;
26 29
 	public int currentTabIndex = NO_INT_VALUE;
27 30
 	public Text currentTabId = new NullText();
31
+    public Text testId = new NullText();
28 32
 
29 33
 	void mergeWith(final BottomTabsOptions other) {
30 34
 		if (other.currentTabId.hasValue()) {
@@ -33,16 +37,19 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
33 37
 		if (NO_INT_VALUE != other.currentTabIndex) {
34 38
             currentTabIndex = other.currentTabIndex;
35 39
 		}
36
-		if (NO_INT_VALUE != other.tabBadge) {
37
-			tabBadge = other.tabBadge;
38
-		}
39
-		if (other.hidden != BooleanOptions.NoValue) {
40
-			hidden = other.hidden;
40
+		if (other.visible != BooleanOptions.NoValue) {
41
+			visible = other.visible;
41 42
 		}
42 43
 		if (other.animateHide != BooleanOptions.NoValue) {
43 44
 			animateHide = other.animateHide;
44 45
 		}
45
-	}
46
+        if (other.color.hasValue()) {
47
+            color = other.color;
48
+        }
49
+        if (other.selectedColor.hasValue()) {
50
+            selectedColor = other.selectedColor;
51
+        }
52
+    }
46 53
 
47 54
     void mergeWithDefault(final BottomTabsOptions defaultOptions) {
48 55
         if (!currentTabId.hasValue()) {
@@ -51,14 +58,17 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
51 58
         if (NO_INT_VALUE == currentTabIndex) {
52 59
             currentTabIndex = defaultOptions.currentTabIndex;
53 60
         }
54
-        if (NO_INT_VALUE == tabBadge) {
55
-            tabBadge = defaultOptions.tabBadge;
56
-        }
57
-        if (hidden == BooleanOptions.NoValue) {
58
-            hidden = defaultOptions.hidden;
61
+        if (visible == BooleanOptions.NoValue) {
62
+            visible = defaultOptions.visible;
59 63
         }
60 64
         if (animateHide == BooleanOptions.NoValue) {
61 65
             animateHide = defaultOptions.animateHide;
62 66
         }
67
+        if (!color.hasValue()) {
68
+            color = defaultOptions.color;
69
+        }
70
+        if (!selectedColor.hasValue()) {
71
+            selectedColor = defaultOptions.selectedColor;
72
+        }
63 73
     }
64 74
 }

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

@@ -19,7 +19,8 @@ public class Button {
19 19
 	@ColorInt public int buttonColor;
20 20
 	public int buttonFontSize;
21 21
 	public Text buttonFontWeight;
22
-	public Text icon;
22
+	public Text icon = new NullText();
23
+	public Text testId;
23 24
 
24 25
 	private static Button parseJson(JSONObject json)  {
25 26
 		Button button = new Button();
@@ -31,6 +32,7 @@ public class Button {
31 32
 		button.buttonColor = json.optInt("buttonColor", NO_INT_VALUE);
32 33
 		button.buttonFontSize = json.optInt("buttonFontSize", NO_INT_VALUE);
33 34
 		button.buttonFontWeight = TextParser.parse(json, "buttonFontWeight");
35
+        button.testId = TextParser.parse(json, "testID");
34 36
 
35 37
 		if (json.has("icon")) {
36 38
 			button.icon = TextParser.parse(json.optJSONObject("icon"), "uri");

+ 8
- 4
lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java View File

@@ -3,6 +3,7 @@ package com.reactnativenavigation.parse;
3 3
 import android.app.Activity;
4 4
 
5 5
 import com.facebook.react.ReactInstanceManager;
6
+import com.reactnativenavigation.utils.ImageLoader;
6 7
 import com.reactnativenavigation.utils.NoOpPromise;
7 8
 import com.reactnativenavigation.utils.TypefaceLoader;
8 9
 import com.reactnativenavigation.viewcontrollers.BottomTabsController;
@@ -55,7 +56,8 @@ public class LayoutFactory {
55 56
 	}
56 57
 
57 58
     private ViewController createSideMenuRoot(LayoutNode node) {
58
-		SideMenuController sideMenuLayout = new SideMenuController(activity, node.id);
59
+        final Options options = Options.parse(typefaceManager, node.getNavigationOptions(), defaultOptions);
60
+		SideMenuController sideMenuLayout = new SideMenuController(activity, node.id, options);
59 61
 		for (LayoutNode child : node.children) {
60 62
 			ViewController childLayout = create(child);
61 63
 			switch (child.type) {
@@ -100,7 +102,8 @@ public class LayoutFactory {
100 102
 	}
101 103
 
102 104
 	private ViewController createStack(LayoutNode node) {
103
-		StackController stackController = new StackController(activity, node.id);
105
+        final Options options = Options.parse(typefaceManager, node.getNavigationOptions(), defaultOptions);
106
+		StackController stackController = new StackController(activity, node.id, options);
104 107
         for (int i = 0; i < node.children.size(); i++) {
105 108
             if (i < node.children.size() - 1) {
106 109
                 stackController.push(create(node.children.get(i)), new NoOpPromise());
@@ -112,10 +115,11 @@ public class LayoutFactory {
112 115
 	}
113 116
 
114 117
 	private ViewController createBottomTabs(LayoutNode node) {
115
-		final BottomTabsController tabsComponent = new BottomTabsController(activity, node.id);
118
+        final Options options = Options.parse(typefaceManager, node.getNavigationOptions(), defaultOptions);
119
+		final BottomTabsController tabsComponent = new BottomTabsController(activity, new ImageLoader(), node.id, options);
116 120
 		List<ViewController> tabs = new ArrayList<>();
117 121
 		for (int i = 0; i < node.children.size(); i++) {
118
-			tabs.add(create(node.children.get(i)));
122
+            tabs.add(create(node.children.get(i)));
119 123
 		}
120 124
 		tabsComponent.setTabs(tabs);
121 125
 		return tabsComponent;

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

@@ -35,7 +35,8 @@ public class Options implements DEFAULT_VALUES {
35 35
 		result.topBarOptions = TopBarOptions.parse(typefaceManager, json.optJSONObject("topBar"));
36 36
 		result.topTabsOptions = TopTabsOptions.parse(json.optJSONObject("topTabs"));
37 37
         result.topTabOptions = TopTabOptions.parse(typefaceManager, json.optJSONObject("topTab"));
38
-		result.bottomTabsOptions = BottomTabsOptions.parse(json.optJSONObject("bottomTabs"));
38
+        result.bottomTabOptions = BottomTabOptions.parse(json.optJSONObject("bottomTab"));
39
+        result.bottomTabsOptions = BottomTabsOptions.parse(json.optJSONObject("bottomTabs"));
39 40
         result.overlayOptions = OverlayOptions.parse(json.optJSONObject("overlay"));
40 41
 
41 42
 		return result.withDefaultOptions(defaultOptions);
@@ -44,6 +45,7 @@ public class Options implements DEFAULT_VALUES {
44 45
     @NonNull public TopBarOptions topBarOptions = new TopBarOptions();
45 46
     @NonNull public TopTabsOptions topTabsOptions = new TopTabsOptions();
46 47
     @NonNull public TopTabOptions topTabOptions = new TopTabOptions();
48
+    @NonNull public BottomTabOptions bottomTabOptions = new BottomTabOptions();
47 49
     @NonNull public BottomTabsOptions bottomTabsOptions = new BottomTabsOptions();
48 50
     @NonNull public OverlayOptions overlayOptions = new OverlayOptions();
49 51
 
@@ -54,12 +56,14 @@ public class Options implements DEFAULT_VALUES {
54 56
 	public void mergeWith(final Options other) {
55 57
         topBarOptions.mergeWith(other.topBarOptions);
56 58
         topTabsOptions.mergeWith(other.topTabsOptions);
59
+        bottomTabOptions.mergeWith(other.bottomTabOptions);
57 60
         bottomTabsOptions.mergeWith(other.bottomTabsOptions);
58 61
     }
59 62
 
60 63
     Options withDefaultOptions(final Options other) {
61 64
         topBarOptions.mergeWithDefault(other.topBarOptions);
62 65
         topTabsOptions.mergeWithDefault(other.topTabsOptions);
66
+        bottomTabOptions.mergeWithDefault(other.bottomTabOptions);
63 67
         bottomTabsOptions.mergeWithDefault(other.bottomTabsOptions);
64 68
         return this;
65 69
     }

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

@@ -27,11 +27,13 @@ public class TopBarOptions implements DEFAULT_VALUES {
27 27
         options.drawBehind = Options.BooleanOptions.parse(json.optString("drawBehind"));
28 28
         options.rightButtons = Button.parseJsonArray(json.optJSONArray("rightButtons"));
29 29
         options.leftButtons = Button.parseJsonArray(json.optJSONArray("leftButtons"));
30
+        options.testId = TextParser.parse(json, "testID");
30 31
 
31 32
         return options;
32 33
     }
33 34
 
34 35
     public Text title = new NullText();
36
+    public Text testId = new NullText();
35 37
     public Color backgroundColor = new NullColor();
36 38
     public Color textColor = new NullColor();
37 39
     public Fraction textFontSize = new NullFraction();

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

@@ -14,7 +14,7 @@ public class TopTabsOptions implements DEFAULT_VALUES {
14 14
     public static TopTabsOptions parse(@Nullable JSONObject json) {
15 15
         TopTabsOptions result = new TopTabsOptions();
16 16
         if (json == null) return result;
17
-        result.selectedTabColor = ColorParser.parse(json, "selectedTabColor");
17
+        result.selectedTabColor = ColorParser.parse(json, "selectedColor");
18 18
         result.unselectedTabColor = ColorParser.parse(json, "unselectedTabColor");
19 19
         result.fontSize = NumberParser.parse(json, "fontSize");
20 20
         return result;

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

@@ -0,0 +1,25 @@
1
+package com.reactnativenavigation.presentation;
2
+
3
+import android.support.annotation.IntRange;
4
+
5
+import com.reactnativenavigation.parse.BottomTabOptions;
6
+import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.views.BottomTabs;
8
+
9
+public class BottomTabOptionsPresenter {
10
+    private BottomTabs bottomTabs;
11
+
12
+    public BottomTabOptionsPresenter(BottomTabs bottomTabs) {
13
+        this.bottomTabs = bottomTabs;
14
+    }
15
+
16
+    public void present(Options options, @IntRange(from = 0) int bottomTabIndex) {
17
+        applyBottomTabOptions(options.bottomTabOptions, bottomTabIndex);
18
+    }
19
+
20
+    private void applyBottomTabOptions(BottomTabOptions options, int bottomTabIndex) {
21
+        if (options.badge.hasValue()) {
22
+            bottomTabs.setBadge(bottomTabIndex, options.badge);
23
+        }
24
+    }
25
+}

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

@@ -34,6 +34,7 @@ public class OptionsPresenter {
34 34
         topBar.setBackgroundColor(options.backgroundColor);
35 35
         topBar.setTitleTextColor(options.textColor);
36 36
         topBar.setTitleFontSize(options.textFontSize);
37
+        if (options.testId.hasValue()) topBar.setTestId(options.testId.get());
37 38
 
38 39
         topBar.setTitleTypeface(options.textFontFamily);
39 40
         if (options.hidden == True) {

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

@@ -0,0 +1,15 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+public class ArrayUtils {
4
+    public static boolean contains(Object[] array, Object item) {
5
+        if (isNullOrEmpty(array)) return false;
6
+        for (Object o : array) {
7
+            if (o == item) return true;
8
+        }
9
+        return false;
10
+    }
11
+
12
+    private static boolean isNullOrEmpty(Object[] array) {
13
+        return array == null || array.length == 0;
14
+    }
15
+}

+ 1
- 1
lib/android/app/src/main/java/com/reactnativenavigation/utils/CompatUtils.java View File

@@ -9,7 +9,7 @@ public class CompatUtils {
9 9
 	private static final AtomicInteger viewId = new AtomicInteger(1);
10 10
 
11 11
 	public static int generateViewId() {
12
-		if (Build.VERSION.SDK_INT >= 17) {
12
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
13 13
 			return View.generateViewId();
14 14
 		} else {
15 15
 			while (true) {

lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageUtils.java → lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageLoader.java View File

@@ -17,7 +17,7 @@ import java.io.IOException;
17 17
 import java.io.InputStream;
18 18
 import java.net.URL;
19 19
 
20
-public class ImageUtils {
20
+public class ImageLoader {
21 21
 
22 22
 	public interface ImageLoadingListener {
23 23
 		void onComplete(@NonNull Drawable drawable);
@@ -25,7 +25,7 @@ public class ImageUtils {
25 25
 		void onError(Throwable error);
26 26
 	}
27 27
 
28
-	public static void loadIcon(final Context context, final String uri, final ImageLoadingListener listener) {
28
+	public void loadIcon(final Context context, final String uri, final ImageLoadingListener listener) {
29 29
         try {
30 30
             StrictMode.ThreadPolicy threadPolicy = adjustThreadPolicyDebug();
31 31
             
@@ -40,7 +40,7 @@ public class ImageUtils {
40 40
         }
41 41
     }
42 42
 
43
-    private static StrictMode.ThreadPolicy adjustThreadPolicyDebug() {
43
+    private StrictMode.ThreadPolicy adjustThreadPolicyDebug() {
44 44
         StrictMode.ThreadPolicy threadPolicy = null;
45 45
         if (NavigationApplication.instance.isDebug()) {
46 46
             threadPolicy = StrictMode.getThreadPolicy();
@@ -49,7 +49,7 @@ public class ImageUtils {
49 49
         return threadPolicy;
50 50
     }
51 51
 
52
-    private static void restoreThreadPolicyDebug(@Nullable StrictMode.ThreadPolicy threadPolicy) {
52
+    private void restoreThreadPolicyDebug(@Nullable StrictMode.ThreadPolicy threadPolicy) {
53 53
         if (NavigationApplication.instance.isDebug() && threadPolicy != null) {
54 54
             StrictMode.setThreadPolicy(threadPolicy);
55 55
         }

+ 14
- 0
lib/android/app/src/main/java/com/reactnativenavigation/utils/ViewUtils.java View File

@@ -4,6 +4,9 @@ import android.support.annotation.Nullable;
4 4
 import android.view.View;
5 5
 import android.view.ViewGroup;
6 6
 
7
+import java.util.ArrayList;
8
+import java.util.List;
9
+
7 10
 public class ViewUtils {
8 11
     @Nullable
9 12
     public static <T> T findChildByClass(ViewGroup root, Class clazz) {
@@ -22,4 +25,15 @@ public class ViewUtils {
22 25
         }
23 26
         return null;
24 27
     }
28
+
29
+    public static <T> List<T> findChildrenByClass(ViewGroup root, Class clazz) {
30
+        List<T> ret = new ArrayList<>();
31
+        for (int i = 0; i < root.getChildCount(); i++) {
32
+            View view = root.getChildAt(i);
33
+            if (clazz.isAssignableFrom(view.getClass())) {
34
+                ret.add((T) view);
35
+            }
36
+        }
37
+        return ret;
38
+    }
25 39
 }

+ 87
- 41
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/BottomTabsController.java View File

@@ -2,18 +2,24 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4 4
 import android.graphics.Color;
5
+import android.graphics.drawable.Drawable;
6
+import android.support.annotation.IntRange;
5 7
 import android.support.annotation.NonNull;
6
-import android.support.design.widget.BottomNavigationView;
7
-import android.view.Menu;
8
-import android.view.MenuItem;
9
-import android.view.View;
10 8
 import android.view.ViewGroup;
11 9
 import android.widget.RelativeLayout;
12 10
 
11
+import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
12
+import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;
13
+import com.reactnativenavigation.parse.BottomTabOptions;
14
+import com.reactnativenavigation.parse.BottomTabsOptions;
13 15
 import com.reactnativenavigation.parse.Options;
14 16
 import com.reactnativenavigation.parse.Text;
17
+import com.reactnativenavigation.presentation.BottomTabOptionsPresenter;
15 18
 import com.reactnativenavigation.presentation.NavigationOptionsListener;
16
-import com.reactnativenavigation.utils.CompatUtils;
19
+import com.reactnativenavigation.utils.ImageLoader;
20
+import com.reactnativenavigation.utils.UiUtils;
21
+import com.reactnativenavigation.views.BottomTabs;
22
+import com.reactnativenavigation.views.ReactComponent;
17 23
 
18 24
 import java.util.ArrayList;
19 25
 import java.util.Collection;
@@ -25,47 +31,46 @@ import static android.widget.RelativeLayout.ABOVE;
25 31
 import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
26 32
 import static com.reactnativenavigation.parse.DEFAULT_VALUES.NO_INT_VALUE;
27 33
 
28
-public class BottomTabsController extends ParentController
29
-		implements BottomNavigationView.OnNavigationItemSelectedListener, NavigationOptionsListener {
30
-	private BottomNavigationView bottomNavigationView;
34
+public class BottomTabsController extends ParentController implements AHBottomNavigation.OnTabSelectedListener, NavigationOptionsListener {
35
+	private BottomTabs bottomTabs;
31 36
 	private List<ViewController> tabs = new ArrayList<>();
32
-	private int selectedIndex = 0;
37
+    private ImageLoader imageLoader;
33 38
 
34
-	public BottomTabsController(final Activity activity, final String id) {
35
-		super(activity, id);
36
-	}
39
+    public BottomTabsController(final Activity activity, ImageLoader imageLoader, final String id, Options initialOptions) {
40
+		super(activity, id, initialOptions);
41
+        this.imageLoader = imageLoader;
42
+    }
37 43
 
38 44
 	@NonNull
39 45
 	@Override
40 46
 	protected ViewGroup createView() {
41 47
 		RelativeLayout root = new RelativeLayout(getActivity());
42
-		bottomNavigationView = new BottomNavigationView(getActivity());
43
-		bottomNavigationView.setId(CompatUtils.generateViewId());
44
-		bottomNavigationView.setBackgroundColor(Color.DKGRAY);
45
-		bottomNavigationView.setOnNavigationItemSelectedListener(this);
48
+		bottomTabs = new BottomTabs(getActivity(), options.bottomTabsOptions);
49
+        bottomTabs.setOnTabSelectedListener(this);
46 50
 		RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
47 51
 		lp.addRule(ALIGN_PARENT_BOTTOM);
48
-		root.addView(bottomNavigationView, lp);
52
+		root.addView(bottomTabs, lp);
49 53
 		return root;
50 54
 	}
51 55
 
52
-	@Override
53
-	public boolean handleBack() {
54
-		return !tabs.isEmpty() && tabs.get(selectedIndex).handleBack();
55
-	}
56
+    @Override
57
+    public void applyOptions(Options options, ReactComponent childComponent) {
58
+        int tabIndex = findTabContainingComponent(childComponent);
59
+        if (tabIndex >= 0) new BottomTabOptionsPresenter(bottomTabs).present(options, tabIndex);
60
+    }
56 61
 
57
-	@Override
58
-	public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
59
-		selectTabAtIndex(item.getItemId());
60
-		return true;
62
+    @Override
63
+	public boolean handleBack() {
64
+		return !tabs.isEmpty() && tabs.get(bottomTabs.getCurrentItem()).handleBack();
61 65
 	}
62 66
 
63
-	void selectTabAtIndex(final int newIndex) {
64
-		tabs.get(selectedIndex).getView().setVisibility(View.GONE);
65
-		selectedIndex = newIndex;
66
-		tabs.get(selectedIndex).getView().setVisibility(View.VISIBLE);
67
+    @Override
68
+    public boolean onTabSelected(int index, boolean wasSelected) {
69
+        if (wasSelected) return false;
70
+        selectTabAtIndex(index);
71
+        return true;
67 72
 	}
68
-
73
+	
69 74
 	public void setTabs(final List<ViewController> tabs) {
70 75
 		if (tabs.size() > 5) {
71 76
 			throw new RuntimeException("Too many tabs!");
@@ -73,22 +78,41 @@ public class BottomTabsController extends ParentController
73 78
 		this.tabs = tabs;
74 79
 		getView();
75 80
 		for (int i = 0; i < tabs.size(); i++) {
76
-			String title = String.valueOf(i);
77
-			createTab(tabs.get(i), i, title);
81
+		    tabs.get(i).setParentController(this);
82
+			createTab(i, tabs.get(i).options.bottomTabOptions, tabs.get(i).options.bottomTabsOptions);
78 83
 		}
79 84
 		selectTabAtIndex(0);
80 85
 	}
81 86
 
82
-	private void createTab(ViewController tab, final int index, final String title) {
83
-		bottomNavigationView.getMenu().add(0, index, Menu.NONE, title);
84
-		RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
85
-		params.addRule(ABOVE, bottomNavigationView.getId());
86
-		tab.getView().setVisibility(View.GONE);
87
-		getView().addView(tab.getView(), params);
87
+	private void createTab(int index, final BottomTabOptions tabOptions, final BottomTabsOptions bottomTabsOptions) {
88
+	    if (!tabOptions.icon.hasValue()) {
89
+            throw new RuntimeException("BottomTab must have an icon");
90
+        }
91
+        imageLoader.loadIcon(getActivity(), tabOptions.icon.get(), new ImageLoader.ImageLoadingListener() {
92
+            @Override
93
+            public void onComplete(@NonNull Drawable drawable) {
94
+                setIconColor(drawable, bottomTabsOptions);
95
+                AHBottomNavigationItem item = new AHBottomNavigationItem(tabOptions.title.get(""), drawable);
96
+                bottomTabs.addItem(item);
97
+                bottomTabs.post(() -> bottomTabs.setTabTag(index, tabOptions.testId));
98
+            }
99
+
100
+            @Override
101
+            public void onError(Throwable error) {
102
+                error.printStackTrace();
103
+            }
104
+        });
105
+
106
+        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
107
+        params.addRule(ABOVE, bottomTabs.getId());
88 108
 	}
89 109
 
90
-	int getSelectedIndex() {
91
-		return selectedIndex;
110
+    private void setIconColor(Drawable drawable, BottomTabsOptions options) {
111
+        UiUtils.tintDrawable(drawable, Color.RED);
112
+    }
113
+
114
+    int getSelectedIndex() {
115
+		return bottomTabs.getCurrentItem();
92 116
 	}
93 117
 
94 118
 	@NonNull
@@ -99,6 +123,7 @@ public class BottomTabsController extends ParentController
99 123
 
100 124
 	@Override
101 125
 	public void mergeOptions(Options options) {
126
+        this.options.mergeWith(options);
102 127
         if (options.bottomTabsOptions.currentTabIndex != NO_INT_VALUE) {
103 128
             selectTabAtIndex(options.bottomTabsOptions.currentTabIndex);
104 129
         }
@@ -117,7 +142,18 @@ public class BottomTabsController extends ParentController
117 142
         }
118 143
     }
119 144
 
120
-	private boolean hasControlWithId(StackController controller, String id) {
145
+    void selectTabAtIndex(final int newIndex) {
146
+        getView().removeView(getCurrentView());
147
+        bottomTabs.setCurrentItem(newIndex, false);
148
+        getView().addView(getCurrentView());
149
+    }
150
+
151
+    @NonNull
152
+    private ViewGroup getCurrentView() {
153
+        return tabs.get(bottomTabs.getCurrentItem()).getView();
154
+    }
155
+
156
+    private boolean hasControlWithId(StackController controller, String id) {
121 157
 		for (ViewController child : controller.getChildControllers()) {
122 158
 			if (id.equals(child.getId())) {
123 159
 				return true;
@@ -128,4 +164,14 @@ public class BottomTabsController extends ParentController
128 164
 		}
129 165
 		return false;
130 166
 	}
167
+
168
+	@IntRange(from = -1)
169
+    private int findTabContainingComponent(ReactComponent component) {
170
+        for (int i = 0; i < tabs.size(); i++) {
171
+            if (tabs.get(i).containsComponent(component)) {
172
+                return i;
173
+            }
174
+        }
175
+        return -1;
176
+    }
131 177
 }

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

@@ -18,11 +18,10 @@ public class ComponentViewController extends ViewController<ComponentLayout> imp
18 18
                                    final String id,
19 19
                                    final String componentName,
20 20
                                    final ReactViewCreator viewCreator,
21
-                                   final Options initialNavigationOptions) {
22
-        super(activity, id);
21
+                                   final Options initialOptions) {
22
+        super(activity, id, initialOptions);
23 23
         this.componentName = componentName;
24 24
         this.viewCreator = viewCreator;
25
-        options = initialNavigationOptions;
26 25
     }
27 26
 
28 27
     @Override

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

@@ -1,27 +1,26 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3
-import android.app.Dialog;
4
-import android.content.DialogInterface;
5 3
 import android.support.annotation.Nullable;
6
-import android.view.KeyEvent;
7
-import android.view.View;
8 4
 
9 5
 import com.facebook.react.bridge.Promise;
10
-import com.reactnativenavigation.R;
6
+import com.reactnativenavigation.viewcontrollers.modal.Modal;
7
+import com.reactnativenavigation.viewcontrollers.modal.ModalCreator;
11 8
 
12 9
 import java.util.ArrayList;
13 10
 import java.util.List;
14 11
 
15
-import static android.view.View.MeasureSpec.EXACTLY;
16
-import static android.view.View.MeasureSpec.makeMeasureSpec;
17
-
18 12
 public class ModalStack {
19 13
 
20 14
 	private List<Modal> modals = new ArrayList<>();
15
+    private ModalCreator creator;
16
+
17
+    public ModalStack(ModalCreator creator) {
18
+        this.creator = creator;
19
+    }
21 20
 
22
-	public void showModal(final ViewController viewController, Promise promise) {
23
-		Modal modal = new Modal(viewController);
24
-		modals.add(modal);
21
+    public void showModal(final ViewController viewController, Promise promise) {
22
+        Modal modal = creator.create(viewController);
23
+        modals.add(modal);
25 24
 		modal.show();
26 25
 		if (promise != null) {
27 26
 			promise.resolve(viewController.getId());
@@ -52,7 +51,7 @@ public class ModalStack {
52 51
 	}
53 52
 
54 53
 	@Nullable
55
-	private Modal findModalByComponentId(String componentId) {
54
+	public Modal findModalByComponentId(String componentId) {
56 55
 		for (Modal modal : modals) {
57 56
 			if (modal.containsDeepComponentId(componentId)) {
58 57
 				return modal;
@@ -64,49 +63,6 @@ public class ModalStack {
64 63
 	@Nullable
65 64
     ViewController findControllerById(String id) {
66 65
         Modal modal = findModalByComponentId(id);
67
-        return modal != null ? modal.viewController : null;
68
-    }
69
-
70
-    private static class Modal implements DialogInterface.OnKeyListener {
71
-		public final ViewController viewController;
72
-		private final Dialog dialog;
73
-
74
-		Modal(final ViewController viewController) {
75
-			this.viewController = viewController;
76
-			dialog = new Dialog(viewController.getActivity(), R.style.Modal);
77
-			dialog.setOnKeyListener(this);
78
-		}
79
-
80
-		void show() {
81
-			preMeasureView();
82
-			dialog.setContentView(viewController.getView());
83
-			dialog.show();
84
-		}
85
-
86
-		void dismiss() {
87
-			dialog.dismiss();
88
-		}
89
-
90
-		boolean containsDeepComponentId(String componentId) {
91
-			return viewController.findControllerById(componentId) != null;
92
-		}
93
-
94
-		private void preMeasureView() {
95
-			View decorView = viewController.getActivity().getWindow().getDecorView();
96
-			viewController.getView().measure(makeMeasureSpec(decorView.getMeasuredWidth(), EXACTLY), makeMeasureSpec(decorView.getMeasuredHeight(), EXACTLY));
97
-		}
98
-
99
-        @Override
100
-        public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
101
-            if (keyCode == KeyEvent.KEYCODE_BACK) {
102
-                if (event.getAction() == KeyEvent.ACTION_UP) {
103
-                    if (viewController.handleBack()) {
104
-                        return true;
105
-                    }
106
-                    dialog.dismiss();
107
-                }
108
-            }
109
-            return false;
110
-        }
66
+        return modal != null ? modal.viewController.findControllerById(id) : null;
111 67
     }
112 68
 }

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

@@ -12,6 +12,7 @@ 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.ModalCreator;
15 16
 
16 17
 import java.util.Collection;
17 18
 import java.util.Collections;
@@ -19,47 +20,47 @@ import java.util.Collections;
19 20
 public class Navigator extends ParentController {
20 21
 
21 22
     private static final NoOpPromise NO_OP = new NoOpPromise();
22
-    private final ModalStack modalStack = new ModalStack();
23
-	private ViewController root;
23
+    private final ModalStack modalStack = new ModalStack(new ModalCreator());
24
+    private ViewController root;
24 25
     private OverlayManager overlayManager = new OverlayManager();
25 26
     private Options defaultOptions = new Options();
26 27
 
27 28
     public Navigator(final Activity activity) {
28
-		super(activity, "navigator" + CompatUtils.generateViewId());
29
-	}
29
+        super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
30
+    }
31
+
32
+    @NonNull
33
+    @Override
34
+    protected ViewGroup createView() {
35
+        return new FrameLayout(getActivity());
36
+    }
30 37
 
31 38
     @NonNull
32
-	@Override
33
-	protected ViewGroup createView() {
34
-		return new FrameLayout(getActivity());
35
-	}
36
-
37
-	@NonNull
38
-	@Override
39
-	public Collection<ViewController> getChildControllers() {
40
-		return root == null ? Collections.emptyList() : Collections.singletonList(root);
41
-	}
42
-
43
-	@Override
44
-	public boolean handleBack() {
45
-		return root != null && root.handleBack();
46
-	}
47
-
48
-	@Override
49
-	public void destroy() {
50
-		modalStack.dismissAll(NO_OP);
51
-		super.destroy();
52
-	}
53
-
54
-	public void setRoot(final ViewController viewController, Promise promise) {
55
-		if (root != null) {
56
-			root.destroy();
57
-		}
58
-
59
-		root = viewController;
60
-		getView().addView(viewController.getView());
39
+    @Override
40
+    public Collection<ViewController> getChildControllers() {
41
+        return root == null ? Collections.emptyList() : Collections.singletonList(root);
42
+    }
43
+
44
+    @Override
45
+    public boolean handleBack() {
46
+        return root != null && root.handleBack();
47
+    }
48
+
49
+    @Override
50
+    public void destroy() {
51
+        modalStack.dismissAll(NO_OP);
52
+        super.destroy();
53
+    }
54
+
55
+    public void setRoot(final ViewController viewController, Promise promise) {
56
+        if (root != null) {
57
+            root.destroy();
58
+        }
59
+
60
+        root = viewController;
61
+        getView().addView(viewController.getView());
61 62
         promise.resolve(viewController.getId());
62
-	}
63
+    }
63 64
 
64 65
     public void setDefaultOptions(Options defaultOptions) {
65 66
         this.defaultOptions = defaultOptions;
@@ -69,78 +70,78 @@ public class Navigator extends ParentController {
69 70
         return defaultOptions;
70 71
     }
71 72
 
72
-	public void setOptions(final String componentId, Options options) {
73
-		ViewController target = findControllerById(componentId);
74
-		if (target instanceof NavigationOptionsListener) {
75
-			((NavigationOptionsListener) target).mergeOptions(options);
76
-		}
77
-		if (root instanceof NavigationOptionsListener) {
78
-			((NavigationOptionsListener) root).mergeOptions(options);
79
-		}
80
-	}
81
-
82
-	public void push(final String fromId, final ViewController viewController, Promise promise) {
83
-		ViewController from = findControllerById(fromId);
84
-		if (from != null) {
85
-		    from.performOnParentStack(stack -> ((StackController) stack).animatePush(viewController, promise));
86
-		}
87
-	}
88
-
89
-	void pop(final String fromId, Promise promise) {
90
-		ViewController from = findControllerById(fromId);
91
-		if (from != null) {
92
-		    from.performOnParentStack(stack -> ((StackController) stack).pop(promise));
93
-		}
94
-	}
95
-
96
-	public void popSpecific(final String id, Promise promise) {
97
-		ViewController from = findControllerById(id);
98
-		if (from != null) {
99
-		    from.performOnParentStack(stack -> ((StackController) stack).popSpecific(from, promise), () -> rejectPromise(promise));
100
-		} else {
101
-			rejectPromise(promise);
102
-		}
103
-	}
104
-
105
-	public void popToRoot(final String id, Promise promise) {
106
-		ViewController from = findControllerById(id);
107
-		if (from != null) {
108
-		    from.performOnParentStack(stack -> ((StackController) stack).popToRoot(promise));
109
-		}
110
-	}
111
-
112
-	public void popTo(final String componentId, Promise promise) {
113
-		ViewController target = findControllerById(componentId);
114
-		if (target != null) {
115
-		    target.performOnParentStack(stack -> ((StackController) stack).popTo(target, promise), () -> rejectPromise(promise));
116
-		} else {
117
-			rejectPromise(promise);
118
-		}
119
-	}
120
-
121
-	public void showModal(final ViewController viewController, Promise promise) {
122
-		modalStack.showModal(viewController, promise);
123
-	}
124
-
125
-	public void dismissModal(final String componentId, Promise promise) {
126
-		modalStack.dismissModal(componentId, promise);
127
-	}
128
-
129
-	public void dismissAllModals(Promise promise) {
130
-		modalStack.dismissAll(promise);
131
-	}
132
-
133
-	public void showOverlay(ViewController overlay) {
73
+    public void setOptions(final String componentId, Options options) {
74
+        ViewController target = findControllerById(componentId);
75
+        if (target instanceof NavigationOptionsListener) {
76
+            ((NavigationOptionsListener) target).mergeOptions(options);
77
+        }
78
+        if (root instanceof NavigationOptionsListener) {
79
+            ((NavigationOptionsListener) root).mergeOptions(options);
80
+        }
81
+    }
82
+
83
+    public void push(final String fromId, final ViewController viewController, Promise promise) {
84
+        ViewController from = findControllerById(fromId);
85
+        if (from != null) {
86
+            from.performOnParentStack(stack -> ((StackController) stack).animatePush(viewController, promise));
87
+        }
88
+    }
89
+
90
+    void pop(final String fromId, Promise promise) {
91
+        ViewController from = findControllerById(fromId);
92
+        if (from != null) {
93
+            from.performOnParentStack(stack -> ((StackController) stack).pop(promise));
94
+        }
95
+    }
96
+
97
+    public void popSpecific(final String id, Promise promise) {
98
+        ViewController from = findControllerById(id);
99
+        if (from != null) {
100
+            from.performOnParentStack(stack -> ((StackController) stack).popSpecific(from, promise), () -> rejectPromise(promise));
101
+        } else {
102
+            rejectPromise(promise);
103
+        }
104
+    }
105
+
106
+    public void popToRoot(final String id, Promise promise) {
107
+        ViewController from = findControllerById(id);
108
+        if (from != null) {
109
+            from.performOnParentStack(stack -> ((StackController) stack).popToRoot(promise));
110
+        }
111
+    }
112
+
113
+    public void popTo(final String componentId, Promise promise) {
114
+        ViewController target = findControllerById(componentId);
115
+        if (target != null) {
116
+            target.performOnParentStack(stack -> ((StackController) stack).popTo(target, promise), () -> rejectPromise(promise));
117
+        } else {
118
+            rejectPromise(promise);
119
+        }
120
+    }
121
+
122
+    public void showModal(final ViewController viewController, Promise promise) {
123
+        modalStack.showModal(viewController, promise);
124
+    }
125
+
126
+    public void dismissModal(final String componentId, Promise promise) {
127
+        modalStack.dismissModal(componentId, promise);
128
+    }
129
+
130
+    public void dismissAllModals(Promise promise) {
131
+        modalStack.dismissAll(promise);
132
+    }
133
+
134
+    public void showOverlay(ViewController overlay) {
134 135
         overlayManager.show(getView(), overlay);
135
-	}
136
+    }
136 137
 
137
-	public void dismissOverlay(final String componentId) {
138
-		overlayManager.dismiss(getView(), componentId);
139
-	}
138
+    public void dismissOverlay(final String componentId) {
139
+        overlayManager.dismiss(getView(), componentId);
140
+    }
140 141
 
141
-	static void rejectPromise(Promise promise) {
142
+    static void rejectPromise(Promise promise) {
142 143
         promise.reject(new Throwable("Nothing to pop"));
143
-	}
144
+    }
144 145
 
145 146
     @Nullable
146 147
     @Override

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

@@ -13,8 +13,8 @@ import java.util.Collection;
13 13
 
14 14
 public abstract class ParentController<T extends ViewGroup> extends ViewController {
15 15
 
16
-	public ParentController(final Activity activity, final String id) {
17
-		super(activity, id);
16
+	public ParentController(final Activity activity, final String id, Options initialOptions) {
17
+		super(activity, id, initialOptions);
18 18
 	}
19 19
 
20 20
 	@NonNull
@@ -44,6 +44,17 @@ public abstract class ParentController<T extends ViewGroup> extends ViewControll
44 44
 		return null;
45 45
 	}
46 46
 
47
+	@Override
48
+    public boolean containsComponent(ReactComponent component) {
49
+        if (super.containsComponent(component)) {
50
+            return true;
51
+        }
52
+        for (ViewController child : getChildControllers()) {
53
+            if (child.containsComponent(component)) return true;
54
+        }
55
+        return false;
56
+    }
57
+
47 58
     public void applyOptions(Options options, ReactComponent childComponent) {
48 59
 
49 60
     }

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

@@ -7,6 +7,8 @@ import android.view.Gravity;
7 7
 import android.view.View;
8 8
 import android.view.ViewGroup;
9 9
 
10
+import com.reactnativenavigation.parse.Options;
11
+
10 12
 import java.util.ArrayList;
11 13
 import java.util.Collection;
12 14
 
@@ -19,8 +21,8 @@ public class SideMenuController extends ParentController {
19 21
 	private ViewController leftController;
20 22
 	private ViewController rightController;
21 23
 
22
-	public SideMenuController(final Activity activity, final String id) {
23
-		super(activity, id);
24
+	public SideMenuController(final Activity activity, final String id, Options initialOptions) {
25
+		super(activity, id, initialOptions);
24 26
 	}
25 27
 
26 28
 	@NonNull

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

@@ -24,8 +24,8 @@ public class StackController extends ParentController <StackLayout> {
24 24
     private final NavigationAnimator animator;
25 25
     private StackLayout stackLayout;
26 26
 
27
-    public StackController(final Activity activity, String id) {
28
-		super(activity, id);
27
+    public StackController(final Activity activity, String id, Options initialOptions) {
28
+		super(activity, id, initialOptions);
29 29
         animator = new NavigationAnimator(activity);
30 30
     }
31 31
 
@@ -37,6 +37,7 @@ public class StackController extends ParentController <StackLayout> {
37 37
     @Override
38 38
     public void applyOptions(Options options, ReactComponent component) {
39 39
         stackLayout.applyOptions(options, component);
40
+        applyOnParentController(parentController -> ((ParentController) parentController).applyOptions(options, component));
40 41
     }
41 42
 
42 43
     @Override

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

@@ -39,9 +39,10 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
39 39
     private boolean isDestroyed;
40 40
     private ViewVisibilityListener viewVisibilityListener = new ViewVisibilityListenerAdapter();
41 41
 
42
-    public ViewController(Activity activity, String id) {
42
+    public ViewController(Activity activity, String id, Options initialOptions) {
43 43
         this.activity = activity;
44 44
         this.id = id;
45
+        options = initialOptions;
45 46
     }
46 47
 
47 48
     protected abstract T createView();
@@ -125,11 +126,15 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
125 126
         return isSameId(id) ? this : null;
126 127
     }
127 128
 
129
+    public boolean containsComponent(ReactComponent component) {
130
+        return getView().equals(component);
131
+    }
132
+
128 133
     public void onViewAppeared() {
129 134
         isShown = true;
130 135
         applyOnParentController(parentController -> {
131 136
             parentController.clearOptions();
132
-            parentController.applyOptions(options, (ReactComponent) getView());
137
+            if (getView() instanceof ReactComponent) parentController.applyOptions(options, (ReactComponent) getView());
133 138
         });
134 139
     }
135 140
 

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

@@ -0,0 +1,55 @@
1
+package com.reactnativenavigation.viewcontrollers.modal;
2
+
3
+import android.app.Dialog;
4
+import android.content.DialogInterface;
5
+import android.view.KeyEvent;
6
+import android.view.View;
7
+
8
+import com.reactnativenavigation.R;
9
+import com.reactnativenavigation.viewcontrollers.ViewController;
10
+
11
+import static android.view.View.MeasureSpec.EXACTLY;
12
+import static android.view.View.MeasureSpec.makeMeasureSpec;
13
+
14
+public class Modal implements DialogInterface.OnKeyListener {
15
+    public final ViewController viewController;
16
+    private final Dialog dialog;
17
+
18
+    public Modal(final ViewController viewController) {
19
+        this.viewController = viewController;
20
+        dialog = new Dialog(viewController.getActivity(), R.style.Modal);
21
+        dialog.setOnKeyListener(this);
22
+    }
23
+
24
+    public void show() {
25
+        preMeasureView();
26
+        dialog.setContentView(viewController.getView());
27
+        dialog.show();
28
+    }
29
+
30
+    public void dismiss() {
31
+        dialog.dismiss();
32
+    }
33
+
34
+    public boolean containsDeepComponentId(String componentId) {
35
+        return viewController.findControllerById(componentId) != null;
36
+    }
37
+
38
+    private void preMeasureView() {
39
+        View decorView = viewController.getActivity().getWindow().getDecorView();
40
+        viewController.getView().measure(makeMeasureSpec(decorView.getMeasuredWidth(), EXACTLY), makeMeasureSpec(decorView.getMeasuredHeight(), EXACTLY));
41
+    }
42
+
43
+    @Override
44
+    public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
45
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
46
+            if (event.getAction() == KeyEvent.ACTION_UP) {
47
+                if (viewController.handleBack()) {
48
+                    return true;
49
+                }
50
+                dialog.dismiss();
51
+            }
52
+        }
53
+        return false;
54
+    }
55
+}

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

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

+ 1
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java View File

@@ -21,12 +21,10 @@ public class TopTabsController extends ParentController<TopTabsViewPager> implem
21 21
 
22 22
     private List<ViewController> tabs;
23 23
     private TopTabsLayoutCreator viewCreator;
24
-    private Options options;
25 24
 
26 25
     public TopTabsController(Activity activity, String id, List<ViewController> tabs, TopTabsLayoutCreator viewCreator, Options options) {
27
-        super(activity, id);
26
+        super(activity, id, options);
28 27
         this.viewCreator = viewCreator;
29
-        this.options = options;
30 28
         this.tabs = tabs;
31 29
         for (ViewController tab : tabs) {
32 30
             tab.setParentController(this);

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

@@ -0,0 +1,32 @@
1
+package com.reactnativenavigation.views;
2
+
3
+import android.annotation.SuppressLint;
4
+import android.content.Context;
5
+
6
+import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
7
+import com.reactnativenavigation.parse.BottomTabsOptions;
8
+import com.reactnativenavigation.parse.Text;
9
+import com.reactnativenavigation.utils.CompatUtils;
10
+
11
+@SuppressLint("ViewConstructor")
12
+public class BottomTabs extends AHBottomNavigation {
13
+    public BottomTabs(Context context, BottomTabsOptions bottomTabsOptions) {
14
+        super(context);
15
+        setId(CompatUtils.generateViewId());
16
+        setTestId(bottomTabsOptions.testId);
17
+    }
18
+
19
+    private void setTestId(Text testId) {
20
+        if (testId.hasValue()) setTag(testId.get());
21
+    }
22
+
23
+    public void setTabTag(int index, Text testId) {
24
+        if (!testId.hasValue()) return;
25
+        if (testId.hasValue()) getViewAtPosition(index).setTag(testId.get());
26
+        if (testId.hasValue()) getViewAtPosition(index).setContentDescription(testId.get());
27
+    }
28
+
29
+    public void setBadge(int bottomTabIndex, Text badge) {
30
+        setNotification(badge.get(), bottomTabIndex);
31
+    }
32
+}

+ 50
- 16
lib/android/app/src/main/java/com/reactnativenavigation/views/TitleBarButton.java View File

@@ -4,6 +4,7 @@ import android.content.Context;
4 4
 import android.graphics.Color;
5 5
 import android.graphics.drawable.Drawable;
6 6
 import android.support.annotation.NonNull;
7
+import android.support.v7.widget.ActionMenuView;
7 8
 import android.support.v7.widget.Toolbar;
8 9
 import android.text.Spannable;
9 10
 import android.text.SpannableString;
@@ -12,14 +13,19 @@ import android.util.Log;
12 13
 import android.view.Menu;
13 14
 import android.view.MenuItem;
14 15
 import android.view.View;
16
+import android.widget.ImageButton;
15 17
 import android.widget.TextView;
16 18
 
17 19
 import com.reactnativenavigation.parse.Button;
18 20
 import com.reactnativenavigation.parse.Options;
19
-import com.reactnativenavigation.utils.ImageUtils;
21
+import com.reactnativenavigation.parse.Text;
22
+import com.reactnativenavigation.utils.ArrayUtils;
23
+import com.reactnativenavigation.utils.ImageLoader;
20 24
 import com.reactnativenavigation.utils.UiUtils;
25
+import com.reactnativenavigation.utils.ViewUtils;
21 26
 
22 27
 import java.util.ArrayList;
28
+import java.util.List;
23 29
 
24 30
 public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
25 31
     public interface OnClickListener {
@@ -49,22 +55,25 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
49 55
 			setTextColor();
50 56
 			setFontSize(menuItem);
51 57
 		}
52
-	}
53 58
 
54
-	void applyNavigationIcon(Context context) {
59
+        setTestId(button.testId);
60
+    }
61
+
62
+    void applyNavigationIcon(Context context) {
55 63
 		if (!hasIcon()) {
56 64
 			Log.w("RNN", "Left button needs to have an icon");
57 65
 			return;
58 66
 		}
59 67
 
60
-		ImageUtils.loadIcon(context, button.icon.get(), new ImageUtils.ImageLoadingListener() {
68
+		new ImageLoader().loadIcon(context, button.icon.get(), new ImageLoader.ImageLoadingListener() {
61 69
 			@Override
62 70
 			public void onComplete(@NonNull Drawable drawable) {
63 71
 				icon = drawable;
64 72
                 setIconColor();
65 73
                 setNavigationClickListener();
66 74
                 toolbar.setNavigationIcon(icon);
67
-			}
75
+                setLeftButtonTestId();
76
+            }
68 77
 
69 78
 			@Override
70 79
 			public void onError(Throwable error) {
@@ -73,8 +82,18 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
73 82
 		});
74 83
 	}
75 84
 
76
-	private void applyIcon(Context context, final MenuItem menuItem) {
77
-		ImageUtils.loadIcon(context, button.icon.get(), new ImageUtils.ImageLoadingListener() {
85
+    private void setLeftButtonTestId() {
86
+        if (!button.testId.hasValue()) return;
87
+        toolbar.post(() -> {
88
+            ImageButton leftButton = ViewUtils.findChildByClass(toolbar, ImageButton.class);
89
+            if (leftButton != null) {
90
+                leftButton.setTag(button.testId.get());
91
+            }
92
+        });
93
+    }
94
+
95
+    private void applyIcon(Context context, final MenuItem menuItem) {
96
+        new ImageLoader().loadIcon(context, button.icon.get(), new ImageLoader.ImageLoadingListener() {
78 97
 			@Override
79 98
 			public void onComplete(@NonNull Drawable drawable) {
80 99
 				icon = drawable;
@@ -104,7 +123,7 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
104 123
 
105 124
 	private void setTextColor() {
106 125
 		UiUtils.runOnPreDrawOnce(this.toolbar, () -> {
107
-            ArrayList<View> outViews = findActualTextViewInMenuByLabel();
126
+            ArrayList<View> outViews = findActualTextViewInMenuByText();
108 127
             setTextColorForFoundButtonViews(outViews);
109 128
         });
110 129
 	}
@@ -125,13 +144,6 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
125 144
 		return true;
126 145
 	}
127 146
 
128
-	@NonNull
129
-	private ArrayList<View> findActualTextViewInMenuByLabel() {
130
-		ArrayList<View> outViews = new ArrayList<>();
131
-		this.toolbar.findViewsWithText(outViews, button.title.get(), View.FIND_VIEWS_WITH_TEXT);
132
-		return outViews;
133
-	}
134
-
135 147
 	private void setTextColorForFoundButtonViews(ArrayList<View> buttons) {
136 148
 		for (View button : buttons) {
137 149
 			((TextView) button).setTextColor(this.button.buttonColor);
@@ -139,6 +151,28 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
139 151
 	}
140 152
 
141 153
 	private boolean hasIcon() {
142
-		return button.icon != null;
154
+		return button.icon.hasValue();
143 155
 	}
156
+
157
+    private void setTestId(Text testId) {
158
+        if (!testId.hasValue()) return;
159
+        UiUtils.runOnPreDrawOnce(this.toolbar, () -> {
160
+            ActionMenuView buttonsLayout = ViewUtils.findChildByClass(toolbar, ActionMenuView.class);
161
+            List<TextView> buttons = ViewUtils.findChildrenByClass(buttonsLayout, TextView.class);
162
+            for (TextView view : buttons) {
163
+                if (button.title.hasValue() && button.title.get().equals(view.getText())) {
164
+                    view.setTag(testId.get());
165
+                } else if (button.icon.hasValue() && ArrayUtils.contains(view.getCompoundDrawables(), icon)) {
166
+                    view.setTag(testId.get());
167
+                }
168
+            }
169
+        });
170
+    }
171
+
172
+    @NonNull
173
+    private ArrayList<View> findActualTextViewInMenuByText() {
174
+        ArrayList<View> outViews = new ArrayList<>();
175
+        this.toolbar.findViewsWithText(outViews, button.title.get(), View.FIND_VIEWS_WITH_TEXT);
176
+        return outViews;
177
+    }
144 178
 }

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

@@ -51,6 +51,10 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
51 51
         return titleBar.getTitle() != null ? titleBar.getTitle().toString() : "";
52 52
     }
53 53
 
54
+    public void setTestId(String testId) {
55
+        setTag(testId);
56
+    }
57
+
54 58
     public void setTitleTextColor(Color color) {
55 59
         if (color.hasValue()) titleBar.setTitleTextColor(color.get());
56 60
     }

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

@@ -0,0 +1,49 @@
1
+package com.reactnativenavigation.mocks;
2
+
3
+import android.graphics.Canvas;
4
+import android.graphics.ColorFilter;
5
+import android.graphics.drawable.Drawable;
6
+import android.support.annotation.NonNull;
7
+
8
+import com.reactnativenavigation.utils.ImageLoader;
9
+
10
+import org.mockito.Mockito;
11
+
12
+import static org.mockito.ArgumentMatchers.any;
13
+import static org.mockito.ArgumentMatchers.anyString;
14
+import static org.mockito.Mockito.doAnswer;
15
+
16
+public class ImageLoaderMock {
17
+    private static Drawable mockDrawable = new Drawable() {
18
+        @Override
19
+        public void draw(@NonNull Canvas canvas) {
20
+
21
+        }
22
+
23
+        @Override
24
+        public void setAlpha(int alpha) {
25
+
26
+        }
27
+
28
+        @Override
29
+        public void setColorFilter(@android.support.annotation.Nullable ColorFilter colorFilter) {
30
+
31
+        }
32
+
33
+        @Override
34
+        public int getOpacity() {
35
+            return 0;
36
+        }
37
+    };
38
+
39
+    public static ImageLoader mock() {
40
+        ImageLoader imageLoader = Mockito.mock(ImageLoader.class);
41
+        doAnswer(
42
+                invocation -> {
43
+                    ((ImageLoader.ImageLoadingListener) invocation.getArguments()[2]).onComplete(mockDrawable);
44
+                    return null;
45
+                }
46
+        ).when(imageLoader).loadIcon(any(), anyString(), any());
47
+        return imageLoader;
48
+    }
49
+}

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

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

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

@@ -15,9 +15,8 @@ import com.reactnativenavigation.views.TopBar;
15 15
 
16 16
 public class SimpleViewController extends ViewController<FrameLayout> {
17 17
 
18
-    public SimpleViewController(final Activity activity, String id) {
19
-        super(activity, id);
20
-        options = new Options();
18
+    public SimpleViewController(final Activity activity, String id, Options options) {
19
+        super(activity, id, options);
21 20
     }
22 21
 
23 22
     @Override

+ 10
- 12
lib/android/app/src/test/java/com/reactnativenavigation/parse/NavigationOptionsTest.java View File

@@ -27,7 +27,7 @@ public class NavigationOptionsTest extends BaseTest {
27 27
     private static final Options.BooleanOptions TOP_BAR_HIDE_ON_SCROLL = True;
28 28
     private static final Options.BooleanOptions BOTTOM_TABS_ANIMATE_HIDE = True;
29 29
     private static final Options.BooleanOptions BOTTOM_TABS_HIDDEN = True;
30
-    private static final int BOTTOM_TABS_BADGE = 3;
30
+    private static final String BOTTOM_TABS_BADGE = "3";
31 31
     private static final String BOTTOM_TABS_CURRENT_TAB_ID = "ComponentId";
32 32
     private static final int BOTTOM_TABS_CURRENT_TAB_INDEX = 1;
33 33
     private TypefaceLoader mockLoader;
@@ -47,7 +47,7 @@ public class NavigationOptionsTest extends BaseTest {
47 47
     public void parsesJson() throws Exception {
48 48
         JSONObject json = new JSONObject()
49 49
                 .put("topBar", createTopBar())
50
-                .put("bottomTabs", createTabBar());
50
+                .put("bottomTabs", createBottomTabs());
51 51
         Options result = Options.parse(mockLoader, json);
52 52
         assertResult(result);
53 53
     }
@@ -62,20 +62,18 @@ public class NavigationOptionsTest extends BaseTest {
62 62
         assertThat(result.topBarOptions.drawBehind).isEqualTo(TOP_BAR_DRAW_BEHIND);
63 63
         assertThat(result.topBarOptions.hideOnScroll).isEqualTo(TOP_BAR_HIDE_ON_SCROLL);
64 64
         assertThat(result.bottomTabsOptions.animateHide).isEqualTo(BOTTOM_TABS_ANIMATE_HIDE);
65
-        assertThat(result.bottomTabsOptions.hidden).isEqualTo(BOTTOM_TABS_HIDDEN);
66
-        assertThat(result.bottomTabsOptions.tabBadge).isEqualTo(BOTTOM_TABS_BADGE);
65
+        assertThat(result.bottomTabsOptions.visible).isEqualTo(BOTTOM_TABS_HIDDEN);
67 66
         assertThat(result.bottomTabsOptions.currentTabId.get()).isEqualTo(BOTTOM_TABS_CURRENT_TAB_ID);
68 67
         assertThat(result.bottomTabsOptions.currentTabIndex).isEqualTo(BOTTOM_TABS_CURRENT_TAB_INDEX);
69 68
     }
70 69
 
71 70
     @NonNull
72
-    private JSONObject createTabBar() throws JSONException {
71
+    private JSONObject createBottomTabs() throws JSONException {
73 72
         return new JSONObject()
74 73
                 .put("currentTabId", BOTTOM_TABS_CURRENT_TAB_ID)
75 74
                 .put("currentTabIndex", BOTTOM_TABS_CURRENT_TAB_INDEX)
76
-                .put("hidden", BOTTOM_TABS_HIDDEN)
77
-                .put("animateHide", BOTTOM_TABS_ANIMATE_HIDE)
78
-                .put("tabBadge", BOTTOM_TABS_BADGE);
75
+                .put("visible", BOTTOM_TABS_HIDDEN)
76
+                .put("animateHide", BOTTOM_TABS_ANIMATE_HIDE);
79 77
     }
80 78
 
81 79
     @NonNull
@@ -99,7 +97,7 @@ public class NavigationOptionsTest extends BaseTest {
99 97
                 .put("textColor", TOP_BAR_TEXT_COLOR)
100 98
                 .put("textFontSize", TOP_BAR_FONT_SIZE)
101 99
                 .put("textFontFamily", TOP_BAR_FONT_FAMILY)
102
-                .put("hidden", TOP_BAR_HIDDEN);
100
+                .put("visible", TOP_BAR_HIDDEN);
103 101
     }
104 102
 
105 103
     @NonNull
@@ -107,7 +105,7 @@ public class NavigationOptionsTest extends BaseTest {
107 105
         return new JSONObject()
108 106
                 .put("currentTabId", BOTTOM_TABS_CURRENT_TAB_ID)
109 107
                 .put("currentTabIndex", BOTTOM_TABS_CURRENT_TAB_INDEX)
110
-                .put("hidden", BOTTOM_TABS_HIDDEN)
108
+                .put("visible", BOTTOM_TABS_HIDDEN)
111 109
                 .put("animateHide", BOTTOM_TABS_ANIMATE_HIDE)
112 110
                 .put("tabBadge", BOTTOM_TABS_BADGE);
113 111
     }
@@ -116,7 +114,7 @@ public class NavigationOptionsTest extends BaseTest {
116 114
     public void mergeDefaultOptions() throws Exception {
117 115
         JSONObject json = new JSONObject();
118 116
         json.put("topBar", createTopBar());
119
-        json.put("bottomTabs", createTabBar());
117
+        json.put("bottomTabs", createBottomTabs());
120 118
         Options defaultOptions = Options.parse(mockLoader, json);
121 119
         Options options = new Options();
122 120
 
@@ -133,7 +131,7 @@ public class NavigationOptionsTest extends BaseTest {
133 131
 
134 132
         JSONObject json = new JSONObject()
135 133
                 .put("topBar", createTopBar())
136
-                .put("bottomTabs", createTabBar());
134
+                .put("bottomTabs", createBottomTabs());
137 135
         Options options = Options.parse(mockLoader, json);
138 136
         options.withDefaultOptions(defaultOptions);
139 137
         assertResult(options);

+ 13
- 0
lib/android/app/src/test/java/com/reactnativenavigation/utils/OptionHelper.java View File

@@ -0,0 +1,13 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+import com.reactnativenavigation.parse.Options;
4
+import com.reactnativenavigation.parse.Text;
5
+
6
+public class OptionHelper {
7
+    public static Options createBottomTabOptions() {
8
+        Options options = new Options();
9
+        options.bottomTabOptions.title = new Text("Tab");
10
+        options.bottomTabOptions.icon = new Text("http://127.0.0.1/icon.png");
11
+        return options;
12
+    }
13
+}

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

@@ -1,21 +1,29 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3
-import android.app.*;
4
-import android.support.annotation.*;
5
-import android.support.design.widget.*;
6
-import android.view.*;
7
-import android.widget.*;
8
-
9
-import com.reactnativenavigation.*;
10
-import com.reactnativenavigation.mocks.*;
11
-
12
-import org.assertj.core.api.iterable.*;
13
-import org.junit.*;
14
-
15
-import java.util.*;
16
-
17
-import static org.assertj.core.api.Java6Assertions.*;
18
-import static org.mockito.Mockito.*;
3
+import android.app.Activity;
4
+import android.support.annotation.NonNull;
5
+import android.widget.RelativeLayout;
6
+
7
+import com.reactnativenavigation.BaseTest;
8
+import com.reactnativenavigation.mocks.ImageLoaderMock;
9
+import com.reactnativenavigation.mocks.MockPromise;
10
+import com.reactnativenavigation.mocks.SimpleViewController;
11
+import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.utils.ImageLoader;
13
+import com.reactnativenavigation.utils.OptionHelper;
14
+import com.reactnativenavigation.views.BottomTabs;
15
+
16
+import org.junit.Test;
17
+
18
+import java.util.Arrays;
19
+import java.util.Collections;
20
+import java.util.List;
21
+
22
+import static org.assertj.core.api.Java6Assertions.assertThat;
23
+import static org.mockito.Mockito.spy;
24
+import static org.mockito.Mockito.times;
25
+import static org.mockito.Mockito.verify;
26
+import static org.mockito.Mockito.when;
19 27
 
20 28
 public class BottomTabsControllerTest extends BaseTest {
21 29
 
@@ -26,38 +34,49 @@ public class BottomTabsControllerTest extends BaseTest {
26 34
     private ViewController child3;
27 35
     private ViewController child4;
28 36
     private ViewController child5;
37
+    private Options tabOptions = OptionHelper.createBottomTabOptions();
38
+    private ImageLoader imageLoaderMock = ImageLoaderMock.mock();
29 39
 
30 40
     @Override
31 41
     public void beforeEach() {
32 42
         super.beforeEach();
33 43
         activity = newActivity();
34
-        uut = new BottomTabsController(activity, "uut");
35
-        child1 = new SimpleViewController(activity, "child1");
36
-        child2 = new SimpleViewController(activity, "child2");
37
-        child3 = new SimpleViewController(activity, "child3");
38
-        child4 = new SimpleViewController(activity, "child4");
39
-        child5 = new SimpleViewController(activity, "child5");
44
+        uut = new BottomTabsController(activity, imageLoaderMock, "uut", new Options());
45
+        child1 = new SimpleViewController(activity, "child1", tabOptions);
46
+        child2 = new SimpleViewController(activity, "child2", tabOptions);
47
+        child3 = new SimpleViewController(activity, "child3", tabOptions);
48
+        child4 = new SimpleViewController(activity, "child4", tabOptions);
49
+        child5 = new SimpleViewController(activity, "child5", tabOptions);
40 50
     }
41 51
 
42 52
     @Test
43 53
     public void containsRelativeLayoutView() throws Exception {
44 54
         assertThat(uut.getView()).isInstanceOf(RelativeLayout.class);
45
-        assertThat(uut.getView().getChildAt(0)).isInstanceOf(BottomNavigationView.class);
55
+        assertThat(uut.getView().getChildAt(0)).isInstanceOf(BottomTabs.class);
46 56
     }
47 57
 
48 58
     @Test(expected = RuntimeException.class)
49 59
     public void setTabs_ThrowWhenMoreThan5() throws Exception {
50 60
         List<ViewController> tabs = createTabs();
51
-        tabs.add(new SimpleViewController(activity, "6"));
61
+        tabs.add(new SimpleViewController(activity, "6", tabOptions));
62
+        uut.setTabs(tabs);
63
+    }
64
+
65
+    @Test
66
+    public void setTab_controllerIsSetAsParent() throws Exception {
67
+        List<ViewController> tabs = createTabs();
52 68
         uut.setTabs(tabs);
69
+        for (ViewController tab : tabs) {
70
+            assertThat(tab.getParentController()).isEqualTo(uut);
71
+        }
53 72
     }
54 73
 
55 74
     @Test
56
-    public void setTabs_AddAllViewsAsGoneExceptFirst() throws Exception {
75
+    public void setTabs_AddAllViews() throws Exception {
57 76
         List<ViewController> tabs = createTabs();
58 77
         uut.setTabs(tabs);
59
-        assertThat(uut.getView().getChildCount()).isEqualTo(6);
60
-        assertThat(uut.getChildControllers()).extracting((Extractor<ViewController, Integer>) input -> input.getView().getVisibility()).containsExactly(View.VISIBLE, View.GONE, View.GONE, View.GONE, View.GONE);
78
+        assertThat(uut.getView().getChildCount()).isEqualTo(2);
79
+        assertThat(((ViewController) ((List) uut.getChildControllers()).get(0)).getView().getParent()).isNotNull();
61 80
     }
62 81
 
63 82
     @Test
@@ -68,14 +87,14 @@ public class BottomTabsControllerTest extends BaseTest {
68 87
         uut.selectTabAtIndex(3);
69 88
 
70 89
         assertThat(uut.getSelectedIndex()).isEqualTo(3);
71
-        assertThat(uut.getChildControllers()).extracting((Extractor<ViewController, Integer>) input -> input.getView().getVisibility()).containsExactly(View.GONE, View.GONE, View.GONE, View.VISIBLE, View.GONE);
90
+        assertThat(((ViewController) ((List) uut.getChildControllers()).get(0)).getView().getParent()).isNull();
72 91
     }
73 92
 
74 93
     @Test
75 94
     public void findControllerById_ReturnsSelfOrChildren() throws Exception {
76 95
         assertThat(uut.findControllerById("123")).isNull();
77 96
         assertThat(uut.findControllerById(uut.getId())).isEqualTo(uut);
78
-        StackController inner = new StackController(activity, "inner");
97
+        StackController inner = new StackController(activity, "inner", tabOptions);
79 98
         inner.animatePush(child1, new MockPromise());
80 99
         assertThat(uut.findControllerById(child1.getId())).isNull();
81 100
         uut.setTabs(Collections.singletonList(inner));

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

@@ -25,7 +25,7 @@ public class ComponentViewControllerTest extends BaseTest {
25 25
         super.beforeEach();
26 26
         Activity activity = newActivity();
27 27
         view = spy(new TestComponentLayout(activity, new TestReactView(activity)));
28
-        ParentController<StackLayout> parentController = new StackController(activity, "stack");
28
+        ParentController<StackLayout> parentController = new StackController(activity, "stack", new Options());
29 29
         uut = new ComponentViewController(activity, "componentId1", "componentName", (activity1, componentId, componentName) -> view, new Options());
30 30
         uut.setParentController(parentController);
31 31
         parentController.ensureViewIsCreated();

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

@@ -0,0 +1,57 @@
1
+package com.reactnativenavigation.viewcontrollers;
2
+
3
+import com.reactnativenavigation.BaseTest;
4
+import com.reactnativenavigation.mocks.MockPromise;
5
+import com.reactnativenavigation.mocks.ModalCreatorMock;
6
+import com.reactnativenavigation.mocks.SimpleViewController;
7
+import com.reactnativenavigation.parse.Options;
8
+import com.reactnativenavigation.viewcontrollers.modal.Modal;
9
+
10
+import org.junit.Test;
11
+
12
+import javax.annotation.Nullable;
13
+
14
+import static org.assertj.core.api.Assertions.assertThat;
15
+import static org.mockito.Mockito.spy;
16
+import static org.mockito.Mockito.times;
17
+import static org.mockito.Mockito.verify;
18
+
19
+public class ModalStackTest extends BaseTest {
20
+    private static final String CONTROLLER_ID = "simpleController";
21
+    private ModalStack uut;
22
+    private SimpleViewController viewController;
23
+
24
+    @Override
25
+    public void beforeEach() {
26
+        uut = spy(new ModalStack(new ModalCreatorMock()));
27
+        viewController = new SimpleViewController(newActivity(), CONTROLLER_ID, new Options());
28
+    }
29
+
30
+    @Test
31
+    public void modalRefIsSaved() throws Exception {
32
+        uut.showModal(viewController, new MockPromise());
33
+        assertThat(findModal()).isNotNull();
34
+    }
35
+
36
+    @Test
37
+    public void modalIsShown() throws Exception {
38
+        uut.showModal(viewController, new MockPromise() {
39
+            @Override
40
+            public void resolve(@Nullable Object value) {
41
+                verify(findModal(), times(1)).show();
42
+            }
43
+        });
44
+    }
45
+
46
+    @Test
47
+    public void modalIsDismissed() throws Exception {
48
+        uut.showModal(viewController, new MockPromise());
49
+        assertThat(findModal()).isNotNull();
50
+        uut.dismissModal(CONTROLLER_ID, new MockPromise());
51
+        assertThat(findModal()).isNull();
52
+    }
53
+
54
+    private Modal findModal() {
55
+        return uut.findModalByComponentId("simpleController");
56
+    }
57
+}

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

@@ -1,45 +1,56 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3
-import android.app.*;
4
-import android.support.annotation.*;
3
+import android.app.Activity;
4
+import android.support.annotation.NonNull;
5 5
 
6
-import com.reactnativenavigation.*;
7
-import com.reactnativenavigation.mocks.*;
8
-import com.reactnativenavigation.parse.*;
9
-import com.reactnativenavigation.utils.*;
6
+import com.reactnativenavigation.BaseTest;
7
+import com.reactnativenavigation.mocks.ImageLoaderMock;
8
+import com.reactnativenavigation.mocks.MockPromise;
9
+import com.reactnativenavigation.mocks.SimpleComponentViewController;
10
+import com.reactnativenavigation.mocks.SimpleViewController;
11
+import com.reactnativenavigation.parse.Options;
12
+import com.reactnativenavigation.parse.Text;
13
+import com.reactnativenavigation.utils.CompatUtils;
14
+import com.reactnativenavigation.utils.ImageLoader;
15
+import com.reactnativenavigation.utils.OptionHelper;
10 16
 
11
-import org.junit.*;
17
+import org.junit.Test;
12 18
 
13
-import java.util.*;
19
+import java.util.Arrays;
14 20
 
15 21
 import javax.annotation.Nullable;
16 22
 
17
-import static org.assertj.core.api.Java6Assertions.*;
18
-import static org.mockito.Mockito.*;
23
+import static org.assertj.core.api.Java6Assertions.assertThat;
24
+import static org.mockito.Mockito.spy;
25
+import static org.mockito.Mockito.times;
26
+import static org.mockito.Mockito.verify;
27
+import static org.mockito.Mockito.when;
19 28
 
20 29
 public class NavigatorTest extends BaseTest {
21 30
     private Activity activity;
22 31
     private Navigator uut;
23
-    private ParentController parentController;
32
+    private StackController parentController;
24 33
     private SimpleViewController child1;
25 34
     private ViewController child2;
26 35
     private ViewController child3;
27 36
     private ViewController child4;
28 37
     private ViewController child5;
29
-
38
+    private Options tabOptions = OptionHelper.createBottomTabOptions();
39
+    private ImageLoader imageLoaderMock;
30 40
 
31 41
     @Override
32 42
     public void beforeEach() {
33 43
         super.beforeEach();
44
+        imageLoaderMock = ImageLoaderMock.mock();
34 45
         activity = newActivity();
35 46
         uut = new Navigator(activity);
36
-        parentController = new StackController(activity, "stack");
47
+        parentController = spy(new StackController(activity, "stack", new Options()));
37 48
         parentController.ensureViewIsCreated();
38
-        child1 = new SimpleViewController(activity, "child1");
39
-        child2 = new SimpleViewController(activity, "child2");
40
-        child3 = new SimpleViewController(activity, "child3");
41
-        child4 = new SimpleViewController(activity, "child4");
42
-        child5 = new SimpleViewController(activity, "child5");
49
+        child1 = new SimpleViewController(activity, "child1", tabOptions);
50
+        child2 = new SimpleViewController(activity, "child2", tabOptions);
51
+        child3 = new SimpleViewController(activity, "child3", tabOptions);
52
+        child4 = new SimpleViewController(activity, "child4", tabOptions);
53
+        child5 = new SimpleViewController(activity, "child5", tabOptions);
43 54
     }
44 55
 
45 56
     @Test
@@ -94,7 +105,7 @@ public class NavigatorTest extends BaseTest {
94 105
         bottomTabsController.setTabs(Arrays.asList(stack1, stack2));
95 106
         uut.setRoot(bottomTabsController, new MockPromise());
96 107
 
97
-        SimpleViewController newChild = new SimpleViewController(activity, "new child");
108
+        SimpleViewController newChild = new SimpleViewController(activity, "new child", tabOptions);
98 109
         uut.push(child2.getId(), newChild, new MockPromise());
99 110
 
100 111
         assertThat(stack1.getChildControllers()).doesNotContain(newChild);
@@ -223,12 +234,12 @@ public class NavigatorTest extends BaseTest {
223 234
 
224 235
     @NonNull
225 236
     private BottomTabsController newTabs() {
226
-        return new BottomTabsController(activity, "tabsController");
237
+        return new BottomTabsController(activity, imageLoaderMock, "tabsController", new Options());
227 238
     }
228 239
 
229 240
     @NonNull
230 241
     private StackController newStack() {
231
-        return new StackController(activity, "stack" + CompatUtils.generateViewId());
242
+        return new StackController(activity, "stack" + CompatUtils.generateViewId(), tabOptions);
232 243
     }
233 244
 
234 245
     @Test
@@ -301,4 +312,27 @@ public class NavigatorTest extends BaseTest {
301 312
         uut.push(stackController.getId(), child2, new MockPromise());
302 313
         assertIsChildById(stackController.getView(), child2.getView());
303 314
     }
315
+
316
+    @Test
317
+    public void pushedStackCanBePopped() throws Exception {
318
+        StackController parent = new StackController(activity, "someStack", new Options());
319
+        parent.ensureViewIsCreated();
320
+        uut.setRoot(parent, new MockPromise());
321
+        parent.push(parentController, new MockPromise());
322
+
323
+        parentController.push(child1, new MockPromise());
324
+        parentController.push(child2, new MockPromise());
325
+        assertThat(parentController.getChildControllers().size()).isEqualTo(2);
326
+        child1.ensureViewIsCreated();
327
+        child2.ensureViewIsCreated();
328
+
329
+        MockPromise promise = new MockPromise() {
330
+            @Override
331
+            public void resolve(@Nullable Object value) {
332
+                assertThat(parentController.getChildControllers().size()).isEqualTo(1);
333
+            }
334
+        };
335
+        uut.popSpecific("child2", promise);
336
+        verify(parentController, times(1)).popSpecific(child2, promise);
337
+    }
304 338
 }

+ 2
- 2
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java View File

@@ -44,7 +44,7 @@ public class OptionsApplyingTest extends BaseTest {
44 44
                 (activity1, componentId, componentName) -> view,
45 45
                 initialNavigationOptions
46 46
         );
47
-        stackController = new StackController(activity, "stack");
47
+        stackController = new StackController(activity, "stack", new Options());
48 48
         stackController.ensureViewIsCreated();
49 49
         uut.setParentController(stackController);
50 50
     }
@@ -62,7 +62,7 @@ public class OptionsApplyingTest extends BaseTest {
62 62
     public void initialOptionsAppliedOnAppear() throws Exception {
63 63
         assertThat(uut.getOptions()).isSameAs(initialNavigationOptions);
64 64
         initialNavigationOptions.topBarOptions.title = new Text("the title");
65
-        StackController stackController = new StackController(activity, "stackId");
65
+        StackController stackController = new StackController(activity, "stackId", new Options());
66 66
         stackController.animatePush(uut, new MockPromise() {});
67 67
         assertThat(stackController.getTopBar().getTitle()).isEmpty();
68 68
 

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

@@ -7,6 +7,7 @@ import android.widget.*;
7 7
 
8 8
 import com.reactnativenavigation.*;
9 9
 import com.reactnativenavigation.mocks.*;
10
+import com.reactnativenavigation.parse.Options;
10 11
 
11 12
 import org.junit.*;
12 13
 
@@ -26,7 +27,7 @@ public class ParentControllerTest extends BaseTest {
26 27
         super.beforeEach();
27 28
         activity = newActivity();
28 29
         children = new ArrayList<>();
29
-        uut = new ParentController(activity, "uut") {
30
+        uut = new ParentController(activity, "uut", new Options()) {
30 31
 
31 32
             @NonNull
32 33
             @Override
@@ -54,8 +55,8 @@ public class ParentControllerTest extends BaseTest {
54 55
 
55 56
     @Test
56 57
     public void findControllerById_ChildById() throws Exception {
57
-        SimpleViewController child1 = new SimpleViewController(activity, "child1");
58
-        SimpleViewController child2 = new SimpleViewController(activity, "child2");
58
+        SimpleViewController child1 = new SimpleViewController(activity, "child1", new Options());
59
+        SimpleViewController child2 = new SimpleViewController(activity, "child2", new Options());
59 60
         children.add(child1);
60 61
         children.add(child2);
61 62
 
@@ -65,9 +66,9 @@ public class ParentControllerTest extends BaseTest {
65 66
 
66 67
     @Test
67 68
     public void findControllerById_Recursive() throws Exception {
68
-        StackController stackController = new StackController(activity, "stack");
69
-        SimpleViewController child1 = new SimpleViewController(activity, "child1");
70
-        SimpleViewController child2 = new SimpleViewController(activity, "child2");
69
+        StackController stackController = new StackController(activity, "stack", new Options());
70
+        SimpleViewController child1 = new SimpleViewController(activity, "child1", new Options());
71
+        SimpleViewController child2 = new SimpleViewController(activity, "child2", new Options());
71 72
         stackController.animatePush(child1, new MockPromise());
72 73
         stackController.animatePush(child2, new MockPromise());
73 74
         children.add(stackController);
@@ -77,7 +78,7 @@ public class ParentControllerTest extends BaseTest {
77 78
 
78 79
     @Test
79 80
     public void destroy_DestroysChildren() throws Exception {
80
-        ViewController child1 = spy(new SimpleViewController(activity, "child1"));
81
+        ViewController child1 = spy(new SimpleViewController(activity, "child1", new Options()));
81 82
         children.add(child1);
82 83
 
83 84
         verify(child1, times(0)).destroy();
@@ -87,8 +88,8 @@ public class ParentControllerTest extends BaseTest {
87 88
 
88 89
     @Test
89 90
     public void optionsAreClearedWhenChildIsAppeared() throws Exception {
90
-        StackController stackController = spy(new StackController(activity, "stack"));
91
-        SimpleViewController child1 = new SimpleViewController(activity, "child1");
91
+        StackController stackController = spy(new StackController(activity, "stack", new Options()));
92
+        SimpleViewController child1 = new SimpleViewController(activity, "child1", new Options());
92 93
         stackController.animatePush(child1, new MockPromise());
93 94
 
94 95
         child1.onViewAppeared();

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

@@ -1,18 +1,22 @@
1 1
 package com.reactnativenavigation.viewcontrollers;
2 2
 
3
-import android.app.*;
3
+import android.app.Activity;
4 4
 import android.view.View;
5 5
 
6
-import com.reactnativenavigation.*;
7
-import com.reactnativenavigation.mocks.*;
6
+import com.reactnativenavigation.BaseTest;
7
+import com.reactnativenavigation.mocks.MockPromise;
8
+import com.reactnativenavigation.mocks.SimpleViewController;
9
+import com.reactnativenavigation.parse.Options;
8 10
 
9
-import org.assertj.core.api.iterable.*;
10
-import org.junit.*;
11
+import org.assertj.core.api.iterable.Extractor;
12
+import org.junit.Test;
11 13
 
12 14
 import javax.annotation.Nullable;
13 15
 
14
-import static org.assertj.core.api.Java6Assertions.*;
15
-import static org.mockito.Mockito.*;
16
+import static org.assertj.core.api.Java6Assertions.assertThat;
17
+import static org.mockito.Mockito.spy;
18
+import static org.mockito.Mockito.times;
19
+import static org.mockito.Mockito.verify;
16 20
 
17 21
 public class StackControllerTest extends BaseTest {
18 22
 
@@ -26,10 +30,10 @@ public class StackControllerTest extends BaseTest {
26 30
     public void beforeEach() {
27 31
         super.beforeEach();
28 32
         activity = newActivity();
29
-        uut = new StackController(activity, "uut");
30
-        child1 = new SimpleViewController(activity, "child1");
31
-        child2 = new SimpleViewController(activity, "child2");
32
-        child3 = new SimpleViewController(activity, "child3");
33
+        uut = new StackController(activity, "uut", new Options());
34
+        child1 = new SimpleViewController(activity, "child1", new Options());
35
+        child2 = new SimpleViewController(activity, "child2", new Options());
36
+        child3 = new SimpleViewController(activity, "child3", new Options());
33 37
     }
34 38
 
35 39
     @Test
@@ -84,7 +88,7 @@ public class StackControllerTest extends BaseTest {
84 88
         uut.animatePush(child1, new MockPromise());
85 89
         assertThat(child1.getParentController()).isEqualTo(uut);
86 90
 
87
-        StackController anotherNavController = new StackController(activity, "another");
91
+        StackController anotherNavController = new StackController(activity, "another", new Options());
88 92
         anotherNavController.animatePush(child2, new MockPromise());
89 93
         assertThat(child2.getParentController()).isEqualTo(anotherNavController);
90 94
     }
@@ -264,7 +268,7 @@ public class StackControllerTest extends BaseTest {
264 268
 
265 269
     @Test
266 270
     public void findControllerById_Deeply() throws Exception {
267
-        StackController stack = new StackController(activity, "stack2");
271
+        StackController stack = new StackController(activity, "stack2", new Options());
268 272
         stack.animatePush(child2, new MockPromise());
269 273
         uut.animatePush(stack, new MockPromise());
270 274
         assertThat(uut.findControllerById(child2.getId())).isEqualTo(child2);
@@ -325,6 +329,15 @@ public class StackControllerTest extends BaseTest {
325 329
         });
326 330
     }
327 331
 
332
+    @Test
333
+    public void stackCanBePushed() throws Exception {
334
+        StackController parent = new StackController(activity, "someStack", new Options());
335
+        parent.ensureViewIsCreated();
336
+        parent.push(uut, new MockPromise());
337
+        uut.onViewAppeared();
338
+        assertThat(parent.getView().getChildAt(1)).isEqualTo(uut.getView());
339
+    }
340
+
328 341
     private void assertContainsOnlyId(String... ids) {
329 342
         assertThat(uut.size()).isEqualTo(ids.length);
330 343
         assertThat(uut.getChildControllers()).extracting((Extractor<ViewController, String>) ViewController::getId).containsOnly(ids);

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

@@ -1,25 +0,0 @@
1
-package com.reactnativenavigation.viewcontrollers;
2
-
3
-import android.app.*;
4
-import android.support.annotation.*;
5
-import android.view.*;
6
-
7
-import java.util.*;
8
-
9
-public class TopTabsControllerMock extends ParentController {
10
-    TopTabsControllerMock(Activity activity, String id) {
11
-        super(activity, id);
12
-    }
13
-
14
-    @NonNull
15
-    @Override
16
-    protected ViewGroup createView() {
17
-        return null;
18
-    }
19
-
20
-    @NonNull
21
-    @Override
22
-    public Collection<? extends ViewController> getChildControllers() {
23
-        return null;
24
-    }
25
-}

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

@@ -50,7 +50,7 @@ public class TopTabsViewControllerTest extends BaseTest {
50 50
         uut = spy(new TopTabsController(activity, "componentId", tabControllers, layoutCreator, options));
51 51
         tabControllers.forEach(viewController -> viewController.setParentController(uut));
52 52
 
53
-        parentController = spy(new StackController(activity, "stackId"));
53
+        parentController = spy(new StackController(activity, "stackId", new Options()));
54 54
         uut.setParentController(parentController);
55 55
     }
56 56
 

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

@@ -9,6 +9,7 @@ import android.widget.LinearLayout;
9 9
 import com.reactnativenavigation.BaseTest;
10 10
 import com.reactnativenavigation.mocks.MockPromise;
11 11
 import com.reactnativenavigation.mocks.SimpleViewController;
12
+import com.reactnativenavigation.parse.Options;
12 13
 
13 14
 import org.assertj.android.api.Assertions;
14 15
 import org.junit.Test;
@@ -31,7 +32,7 @@ public class ViewControllerTest extends BaseTest {
31 32
     public void beforeEach() {
32 33
         super.beforeEach();
33 34
         activity = newActivity();
34
-        uut = new SimpleViewController(activity, "uut");
35
+        uut = new SimpleViewController(activity, "uut", new Options());
35 36
     }
36 37
 
37 38
     @Test
@@ -47,7 +48,7 @@ public class ViewControllerTest extends BaseTest {
47 48
     @Test
48 49
     public void canOverrideViewCreation() throws Exception {
49 50
         final FrameLayout otherView = new FrameLayout(activity);
50
-        ViewController myController = new ViewController(activity, "vc") {
51
+        ViewController myController = new ViewController(activity, "vc", new Options()) {
51 52
             @Override
52 53
             protected FrameLayout createView() {
53 54
                 return otherView;
@@ -59,7 +60,7 @@ public class ViewControllerTest extends BaseTest {
59 60
     @Test
60 61
     public void holdsAReferenceToStackControllerOrNull() throws Exception {
61 62
         assertThat(uut.getParentController()).isNull();
62
-        StackController nav = new StackController(activity, "stack");
63
+        StackController nav = new StackController(activity, "stack", new Options());
63 64
         nav.animatePush(uut, new MockPromise());
64 65
         assertThat(uut.getParentController()).isEqualTo(nav);
65 66
     }
@@ -143,7 +144,7 @@ public class ViewControllerTest extends BaseTest {
143 144
 
144 145
     @Test
145 146
     public void onDestroy_RemovesGlobalLayoutListener() throws Exception {
146
-        new SimpleViewController(activity, "ensureNotNull").destroy();
147
+        new SimpleViewController(activity, "ensureNotNull", new Options()).destroy();
147 148
 
148 149
         ViewController spy = spy(uut);
149 150
         View view = spy.getView();

+ 2
- 0
lib/ios/RNNBottomTabsOptions.h View File

@@ -13,6 +13,8 @@
13 13
 @property (nonatomic, strong) NSNumber* hideShadow;
14 14
 @property (nonatomic, strong) NSNumber* backgroundColor;
15 15
 @property (nonatomic, strong) NSNumber* textColor;
16
+@property (nonatomic, strong) NSNumber* tabColor;
17
+@property (nonatomic, strong) NSNumber* selectedTabColor;
16 18
 @property (nonatomic, strong) NSNumber* selectedTextColor;
17 19
 @property (nonatomic, strong) NSString* fontFamily;
18 20
 @property (nonatomic, strong) NSNumber* fontSize;

+ 4
- 1
lib/ios/RNNControllerFactory.m View File

@@ -97,18 +97,20 @@
97 97
 
98 98
 - (UIViewController<RNNRootViewProtocol> *)createStack:(RNNLayoutNode*)node {
99 99
 	RNNNavigationController* vc = [[RNNNavigationController alloc] init];
100
-	
100
+	RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initWithDict:node.data[@"options"]];
101 101
 	NSMutableArray* controllers = [NSMutableArray new];
102 102
 	for (NSDictionary* child in node.children) {
103 103
 		[controllers addObject:[self fromTree:child]];
104 104
 	}
105 105
 	[vc setViewControllers:controllers];
106
+	[vc setOptions:options];
106 107
 	
107 108
 	return vc;
108 109
 }
109 110
 
110 111
 -(UIViewController<RNNRootViewProtocol> *)createTabs:(RNNLayoutNode*)node {
111 112
 	RNNTabBarController* vc = [[RNNTabBarController alloc] init];
113
+	RNNNavigationOptions* options = [[RNNNavigationOptions alloc] initWithDict:node.data[@"options"]];
112 114
 	
113 115
 	NSMutableArray* controllers = [NSMutableArray new];
114 116
 	for (NSDictionary *child in node.children) {
@@ -119,6 +121,7 @@
119 121
 		[controllers addObject:childVc];
120 122
 	}
121 123
 	[vc setViewControllers:controllers];
124
+	[vc setOptions:options];
122 125
 	
123 126
 	return vc;
124 127
 }

+ 7
- 0
lib/ios/RNNCustomTitleView.h View File

@@ -0,0 +1,7 @@
1
+#import <UIKit/UIKit.h>
2
+
3
+@interface RNNCustomTitleView : UIView
4
+
5
+-(instancetype)initWithFrame:(CGRect)frame subView:(UIView*)subView alignment:(NSString*)alignment;
6
+
7
+@end

+ 47
- 0
lib/ios/RNNCustomTitleView.m View File

@@ -0,0 +1,47 @@
1
+#import "RNNCustomTitleView.h"
2
+
3
+@interface RNNCustomTitleView ()
4
+@property (nonatomic, strong) UIView *subView;
5
+@property (nonatomic, strong) NSString *subViewAlign;
6
+@end
7
+
8
+@implementation RNNCustomTitleView
9
+
10
+
11
+-(instancetype)initWithFrame:(CGRect)frame subView:(UIView*)subView alignment:(NSString*)alignment {
12
+    self = [super initWithFrame:frame];
13
+    
14
+    if (self) {
15
+        self.backgroundColor = [UIColor clearColor];
16
+        self.subView = subView;
17
+        self.subViewAlign = alignment;
18
+        
19
+        subView.frame = self.bounds;
20
+        [self addSubview:subView];
21
+    }
22
+    
23
+    return self;
24
+}
25
+
26
+
27
+-(void)layoutSubviews {
28
+    [super layoutSubviews];
29
+    
30
+    if ([self.subViewAlign isEqualToString:@"fill"]) {
31
+        self.subView.frame = self.bounds;
32
+    }
33
+    else {
34
+        
35
+        CGFloat superViewWidth = self.superview.frame.size.width;
36
+        CGFloat paddingLeftFromCenter = (superViewWidth/2) - self.frame.origin.x;
37
+        CGFloat paddingRightFromCenter = self.frame.size.width - paddingLeftFromCenter;;
38
+        CGRect reactViewFrame = self.subView.bounds;
39
+        CGFloat minPadding = MIN(paddingLeftFromCenter, paddingRightFromCenter);
40
+        
41
+        reactViewFrame.size.width = minPadding*2;
42
+        reactViewFrame.origin.x = paddingLeftFromCenter - minPadding;
43
+        self.subView.frame = reactViewFrame;
44
+    }
45
+}
46
+
47
+@end

+ 5
- 1
lib/ios/RNNNavigationButtons.m View File

@@ -54,6 +54,7 @@
54 54
 -(RNNUIBarButtonItem*)buildButton: (NSDictionary*)dictionary {
55 55
 	NSString* buttonId = dictionary[@"id"];
56 56
 	NSString* title = dictionary[@"title"];
57
+	NSString* component = dictionary[@"component"];
57 58
 	
58 59
 	if (!buttonId) {
59 60
 		@throw [NSException exceptionWithName:@"NSInvalidArgumentException" reason:[@"button id is not specified " stringByAppendingString:title] userInfo:nil];
@@ -66,7 +67,10 @@
66 67
 	}
67 68
 	
68 69
 	RNNUIBarButtonItem *barButtonItem;
69
-	if (iconImage) {
70
+	if (component) {
71
+		RCTRootView *view = (RCTRootView*)[self.viewController.creator createRootView:component rootViewId:buttonId];
72
+		barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withCustomView:view];
73
+	} else if (iconImage) {
70 74
 		barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withIcon:iconImage];
71 75
 	} else if (title) {
72 76
 		barButtonItem = [[RNNUIBarButtonItem alloc] init:buttonId withTitle:title];

+ 4
- 0
lib/ios/RNNNavigationController.m View File

@@ -16,6 +16,10 @@
16 16
 	return rootVC.isAnimated;
17 17
 }
18 18
 
19
+- (void)setOptions:(RNNNavigationOptions *)options {
20
+	((UIViewController<RNNRootViewProtocol>*)self.topViewController).options = options;
21
+}
22
+
19 23
 - (NSString *)componentId {
20 24
 	return ((UIViewController<RNNRootViewProtocol>*)self.topViewController).componentId;
21 25
 }

+ 1
- 0
lib/ios/RNNRootViewController.h View File

@@ -15,6 +15,7 @@
15 15
 @property (nonatomic, strong) RNNEventEmitter *eventEmitter;
16 16
 @property (nonatomic, strong) NSString* componentId;
17 17
 @property (nonatomic, strong) RNNTopTabsViewController* topTabsViewController;
18
+@property (nonatomic) id<RNNRootViewCreator> creator;
18 19
 @property (nonatomic, strong) RNNAnimator* animator;
19 20
 
20 21
 -(instancetype)initWithName:(NSString*)name

+ 22
- 1
lib/ios/RNNRootViewController.m View File

@@ -2,11 +2,11 @@
2 2
 #import "RNNRootViewController.h"
3 3
 #import <React/RCTConvert.h>
4 4
 #import "RNNAnimator.h"
5
+#import "RNNCustomTitleView.h"
5 6
 
6 7
 @interface RNNRootViewController()
7 8
 @property (nonatomic, strong) NSString* componentName;
8 9
 @property (nonatomic) BOOL _statusBarHidden;
9
-
10 10
 @end
11 11
 
12 12
 @implementation RNNRootViewController
@@ -22,6 +22,7 @@
22 22
 	self.options = options;
23 23
 	self.eventEmitter = eventEmitter;
24 24
 	self.animator = [[RNNAnimator alloc] initWithTransitionOptions:self.options.customTransition];
25
+	self.creator = creator;
25 26
 	self.view = [creator createRootView:self.componentName rootViewId:self.componentId];
26 27
 	
27 28
 	[[NSNotificationCenter defaultCenter] addObserver:self
@@ -37,6 +38,8 @@
37 38
 -(void)viewWillAppear:(BOOL)animated{
38 39
 	[super viewWillAppear:animated];
39 40
 	[self.options applyOn:self];
41
+	[self setCustomNavigationTitleView];
42
+	[self setCustomNavigationBarView];
40 43
 }
41 44
 
42 45
 -(void)viewDidAppear:(BOOL)animated {
@@ -57,6 +60,24 @@
57 60
 	[super viewDidLoad];
58 61
 }
59 62
 
63
+- (void)setCustomNavigationTitleView {
64
+	if (self.options.topBar.customTitleViewName) {
65
+		UIView *reactView = [_creator createRootView:self.options.topBar.customTitleViewName rootViewId:self.options.topBar.customTitleViewName];
66
+		
67
+		RNNCustomTitleView *titleView = [[RNNCustomTitleView alloc] initWithFrame:self.navigationController.navigationBar.bounds subView:reactView alignment:nil];
68
+		self.navigationItem.titleView = titleView;
69
+	}
70
+}
71
+
72
+- (void)setCustomNavigationBarView {
73
+	if (self.options.topBar.customViewName) {
74
+		UIView *reactView = [_creator createRootView:self.options.topBar.customViewName rootViewId:@"navBar"];
75
+		
76
+		RNNCustomTitleView *titleView = [[RNNCustomTitleView alloc] initWithFrame:self.navigationController.navigationBar.bounds subView:reactView alignment:nil];
77
+		[self.navigationController.navigationBar addSubview:titleView];
78
+	}
79
+}
80
+
60 81
 -(BOOL)isCustomTransitioned {
61 82
 	return self.options.customTransition != nil;
62 83
 }

+ 3
- 0
lib/ios/RNNRootViewProtocol.h View File

@@ -2,6 +2,9 @@
2 2
 
3 3
 @protocol RNNRootViewProtocol <NSObject, UINavigationControllerDelegate>
4 4
 
5
+@optional
6
+- (void)setOptions:(RNNNavigationOptions*)options;
7
+
5 8
 @required
6 9
 
7 10
 - (BOOL)isCustomTransitioned;

+ 4
- 0
lib/ios/RNNTabBarController.m View File

@@ -41,6 +41,10 @@
41 41
 	return YES;
42 42
 }
43 43
 
44
+- (void)setOptions:(RNNNavigationOptions *)options {
45
+	[((UIViewController<RNNRootViewProtocol>*)self.selectedViewController) setOptions:options];
46
+}
47
+
44 48
 - (NSString *)componentId {
45 49
 	return ((UIViewController<RNNRootViewProtocol>*)self.selectedViewController).componentId;
46 50
 }

+ 3
- 0
lib/ios/RNNTopBarOptions.h View File

@@ -21,4 +21,7 @@
21 21
 @property (nonatomic, strong) NSNumber* largeTitle;
22 22
 @property (nonatomic, strong) NSString* testID;
23 23
 
24
+@property (nonatomic, strong) NSString* customTitleViewName;
25
+@property (nonatomic, strong) NSString* customViewName;
26
+
24 27
 @end

+ 1
- 0
lib/ios/RNNTopBarOptions.m View File

@@ -1,5 +1,6 @@
1 1
 #import "RNNTopBarOptions.h"
2 2
 #import "RNNNavigationButtons.h"
3
+#import "RNNCustomTitleView.h"
3 4
 
4 5
 extern const NSInteger BLUR_TOPBAR_TAG;
5 6
 

+ 4
- 1
lib/ios/RNNUIBarButtonItem.h View File

@@ -1,11 +1,14 @@
1 1
 #import <Foundation/Foundation.h>
2
+#import <React/RCTRootView.h>
3
+#import <React/RCTRootViewDelegate.h>
2 4
 
3
-@interface RNNUIBarButtonItem : UIBarButtonItem
5
+@interface RNNUIBarButtonItem : UIBarButtonItem <RCTRootViewDelegate>
4 6
 
5 7
 @property (nonatomic, strong) NSString* buttonId;
6 8
 
7 9
 -(instancetype)init:(NSString*)buttonId withIcon:(UIImage*)iconImage;
8 10
 -(instancetype)init:(NSString*)buttonId withTitle:(NSString*)title;
11
+-(instancetype)init:(NSString*)buttonId withCustomView:(RCTRootView*)reactView;
9 12
 
10 13
 @end
11 14
 

+ 16
- 0
lib/ios/RNNUIBarButtonItem.m View File

@@ -16,4 +16,20 @@
16 16
 	return self;
17 17
 }
18 18
 
19
+-(instancetype)init:(NSString*)buttonId withCustomView:(RCTRootView *)reactView {
20
+	self = [super initWithCustomView:reactView];
21
+	
22
+	reactView.sizeFlexibility = RCTRootViewSizeFlexibilityWidthAndHeight;
23
+	reactView.delegate = self;
24
+	reactView.backgroundColor = [UIColor clearColor];
25
+	self.buttonId = buttonId;
26
+	return self;
27
+}
28
+
29
+- (void)rootViewDidChangeIntrinsicSize:(RCTRootView *)rootView {
30
+	CGSize size = rootView.intrinsicContentSize;
31
+	rootView.frame = CGRectMake(0, 0, size.width, size.height);
32
+	self.width = size.width;
33
+}
34
+
19 35
 @end

+ 8
- 0
lib/ios/ReactNativeNavigation.xcodeproj/project.pbxproj View File

@@ -61,6 +61,8 @@
61 61
 		2DCD9196200014A900EDC75D /* RNNBridgeManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DCD9194200014A900EDC75D /* RNNBridgeManager.m */; };
62 62
 		390AD477200F499D00A8250D /* RNNSwizzles.h in Headers */ = {isa = PBXBuildFile; fileRef = 390AD475200F499D00A8250D /* RNNSwizzles.h */; };
63 63
 		390AD478200F499D00A8250D /* RNNSwizzles.m in Sources */ = {isa = PBXBuildFile; fileRef = 390AD476200F499D00A8250D /* RNNSwizzles.m */; };
64
+		5016E8EF20209690009D4F7C /* RNNCustomTitleView.h in Headers */ = {isa = PBXBuildFile; fileRef = 5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */; };
65
+		5016E8F020209690009D4F7C /* RNNCustomTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5016E8EE2020968F009D4F7C /* RNNCustomTitleView.m */; };
64 66
 		5032774E2015E86D00ECD75D /* RNNNavigationEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 5032774C2015E86D00ECD75D /* RNNNavigationEvent.h */; };
65 67
 		5032774F2015E86D00ECD75D /* RNNNavigationEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 5032774D2015E86D00ECD75D /* RNNNavigationEvent.m */; };
66 68
 		503277602016302900ECD75D /* RNNComponentLifecycleEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 5032775E2016302900ECD75D /* RNNComponentLifecycleEvent.h */; };
@@ -238,6 +240,8 @@
238 240
 		2DCD9194200014A900EDC75D /* RNNBridgeManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNBridgeManager.m; sourceTree = "<group>"; };
239 241
 		390AD475200F499D00A8250D /* RNNSwizzles.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNSwizzles.h; sourceTree = "<group>"; };
240 242
 		390AD476200F499D00A8250D /* RNNSwizzles.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNSwizzles.m; sourceTree = "<group>"; };
243
+		5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNNCustomTitleView.h; sourceTree = "<group>"; };
244
+		5016E8EE2020968F009D4F7C /* RNNCustomTitleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNNCustomTitleView.m; sourceTree = "<group>"; };
241 245
 		5032774C2015E86D00ECD75D /* RNNNavigationEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNNavigationEvent.h; sourceTree = "<group>"; };
242 246
 		5032774D2015E86D00ECD75D /* RNNNavigationEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNNNavigationEvent.m; sourceTree = "<group>"; };
243 247
 		5032775E2016302900ECD75D /* RNNComponentLifecycleEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNNComponentLifecycleEvent.h; sourceTree = "<group>"; };
@@ -657,6 +661,8 @@
657 661
 		E8AEDB471F584175000F5A6A /* Components */ = {
658 662
 			isa = PBXGroup;
659 663
 			children = (
664
+				5016E8ED2020968F009D4F7C /* RNNCustomTitleView.h */,
665
+				5016E8EE2020968F009D4F7C /* RNNCustomTitleView.m */,
660 666
 				E8A5CD601F49114F00E89D0D /* RNNElement.h */,
661 667
 				E8A5CD611F49114F00E89D0D /* RNNElement.m */,
662 668
 				E8AEDB3A1F55A1C2000F5A6A /* RNNElementView.h */,
@@ -694,6 +700,7 @@
694 700
 				50F5DFC11F407A8C001A00BC /* RNNTabBarController.h in Headers */,
695 701
 				50CB3B691FDE911400AA153B /* RNNSideMenuOptions.h in Headers */,
696 702
 				263905BD1E4C6F440023D7D3 /* RCCDrawerProtocol.h in Headers */,
703
+				5016E8EF20209690009D4F7C /* RNNCustomTitleView.h in Headers */,
697 704
 				263905C21E4C6F440023D7D3 /* SidebarAnimation.h in Headers */,
698 705
 				E8E518361F83B94A000467AC /* RNNViewLocation.h in Headers */,
699 706
 				263905B51E4C6F440023D7D3 /* MMExampleDrawerVisualStateManager.h in Headers */,
@@ -856,6 +863,7 @@
856 863
 				E8A5CD531F464F0400E89D0D /* RNNAnimator.m in Sources */,
857 864
 				50CB3B6A1FDE911400AA153B /* RNNSideMenuOptions.m in Sources */,
858 865
 				261F0E6B1E6F028A00989DE2 /* RNNNavigationStackManager.m in Sources */,
866
+				5016E8F020209690009D4F7C /* RNNCustomTitleView.m in Sources */,
859 867
 				E8DA24411F97459B00CD552B /* RNNElementFinder.m in Sources */,
860 868
 				263905BF1E4C6F440023D7D3 /* RCCTheSideBarManagerViewController.m in Sources */,
861 869
 				7B1126A01E2D263F00F9B03B /* RNNEventEmitter.m in Sources */,

+ 7
- 2
lib/src/commands/LayoutTreeCrawler.ts View File

@@ -2,10 +2,15 @@ import * as _ from 'lodash';
2 2
 import { OptionsProcessor } from './OptionsProcessor';
3 3
 import { LayoutType, isLayoutType } from './LayoutType';
4 4
 
5
+export interface Data {
6
+  name?: string;
7
+  options?: any;
8
+  passProps?: any;
9
+}
5 10
 export interface LayoutNode {
6 11
   id?: string;
7 12
   type: LayoutType;
8
-  data: object;
13
+  data: Data;
9 14
   children: LayoutNode[];
10 15
 }
11 16
 
@@ -24,6 +29,7 @@ export class LayoutTreeCrawler {
24 29
     if (node.type === LayoutType.Component) {
25 30
       this._handleComponent(node);
26 31
     }
32
+    OptionsProcessor.processOptions(node.data.options);
27 33
     _.forEach(node.children, this.crawl);
28 34
   }
29 35
 
@@ -31,7 +37,6 @@ export class LayoutTreeCrawler {
31 37
     this._assertComponentDataName(node);
32 38
     this._savePropsToStore(node);
33 39
     this._applyStaticOptions(node);
34
-    OptionsProcessor.processOptions(node.data.options);
35 40
   }
36 41
 
37 42
   _savePropsToStore(node) {

+ 13
- 0
package.json View File

@@ -38,6 +38,7 @@
38 38
     "test-unit-ios": "node ./scripts/test.unit.ios.js",
39 39
     "pretest-e2e-android": "npm run build",
40 40
     "test-e2e-android": "node ./scripts/test.e2e.android.js",
41
+    "test-e2e-android-detox": "node ./scripts/test.e2e.android.detox.js",
41 42
     "pretest-e2e-ios": "npm run build",
42 43
     "test-e2e-ios": "node ./scripts/test.e2e.ios.js",
43 44
     "test-all": "node ./scripts/test.all.js",
@@ -123,6 +124,18 @@
123 124
         "build": "RCT_NO_LAUNCH_PACKAGER=true xcodebuild build -scheme playground_release -project playground/ios/playground.xcodeproj -sdk iphonesimulator -configuration Release -derivedDataPath playground/ios/DerivedData/playground ONLY_ACTIVE_ARCH=YES -quiet",
124 125
         "type": "ios.simulator",
125 126
         "name": "iPhone SE"
127
+      },
128
+      "android.emu.debug": {
129
+        "binaryPath": "playground/android/app/build/outputs/apk/debug/app-debug.apk",
130
+        "build": "cd playground/android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ../..",
131
+        "type": "android.emulator",
132
+        "name": "Nexus_5X_API_26"
133
+      },
134
+      "android.emu.release": {
135
+        "binaryPath": "playground/android/app/build/outputs/apk/release/app-release.apk",
136
+        "build": "cd playground/android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ../..",
137
+        "type": "android.emulator",
138
+        "name": "Nexus_5X_API_26"
126 139
       }
127 140
     }
128 141
   }

+ 9
- 0
playground/android/app/build.gradle View File

@@ -25,6 +25,10 @@ android {
25 25
         ndk {
26 26
             abiFilters "armeabi-v7a", "x86"
27 27
         }
28
+
29
+        testBuildType System.getProperty('testBuildType', 'debug')  //this will later be used to control the test apk build type
30
+        missingDimensionStrategy "minReactNative", "minReactNative46" //read note
31
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
28 32
     }
29 33
     signingConfigs {
30 34
         release {
@@ -47,4 +51,9 @@ dependencies {
47 51
     implementation 'com.android.support:design:25.4.0'
48 52
     implementation "com.android.support:appcompat-v7:25.4.0"
49 53
     implementation project(':react-native-navigation')
54
+
55
+    androidTestImplementation(project(path: ":detox"))
56
+    androidTestImplementation 'junit:junit:4.12'
57
+    androidTestImplementation 'com.android.support.test:runner:1.0.1'
58
+    androidTestImplementation 'com.android.support.test:rules:1.0.1'
50 59
 }

+ 24
- 0
playground/android/app/src/androidTest/java/com/reactnativenavigation/playground/DetoxTest.java View File

@@ -0,0 +1,24 @@
1
+package com.reactnativenavigation.playground;
2
+
3
+import android.support.test.filters.LargeTest;
4
+import android.support.test.rule.ActivityTestRule;
5
+import android.support.test.runner.AndroidJUnit4;
6
+
7
+import com.wix.detox.Detox;
8
+
9
+import org.junit.Rule;
10
+import org.junit.Test;
11
+import org.junit.runner.RunWith;
12
+
13
+@RunWith(AndroidJUnit4.class)
14
+@LargeTest
15
+public class DetoxTest {
16
+
17
+    @Rule
18
+    public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
19
+
20
+    @Test
21
+    public void runDetoxTests() throws InterruptedException {
22
+        Detox.runTests(mActivityRule);
23
+    }
24
+}

+ 3
- 0
playground/android/settings.gradle View File

@@ -3,3 +3,6 @@ rootProject.name = 'playground'
3 3
 include ':app'
4 4
 include ':react-native-navigation'
5 5
 project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../../lib/android/app/')
6
+
7
+include ':detox'
8
+project(':detox').projectDir = new File(rootProject.projectDir, '../../node_modules/detox/android/detox')

BIN
playground/src/images/one@2x.png View File


BIN
playground/src/images/one_selected@2x.png View File


BIN
playground/src/images/three@2x.png View File


BIN
playground/src/images/three_selected@2x.png View File


BIN
playground/src/images/two@2x.png View File


BIN
playground/src/images/two_selected@2x.png View File


+ 47
- 0
playground/src/screens/CustomTopBar.js View File

@@ -0,0 +1,47 @@
1
+const React = require('react');
2
+const { Component } = require('react');
3
+const {
4
+  StyleSheet,
5
+  View,
6
+  TouchableOpacity,
7
+  Text,
8
+  Alert,
9
+  Platform
10
+} = require('react-native');
11
+
12
+class CustomTopBar extends Component {
13
+
14
+  constructor(props) {
15
+    super(props);
16
+    this.state = {};
17
+  }
18
+
19
+  render() {
20
+    return (
21
+      <View style={styles.container}>
22
+        <TouchableOpacity stye={styles.button} onPress={() => Alert.alert(this.props.title, 'Thanks for that :)')}>
23
+          <Text style={styles.text}>Press Me</Text>
24
+        </TouchableOpacity>
25
+      </View>
26
+    );
27
+  }
28
+}
29
+
30
+module.exports = CustomTopBar;
31
+
32
+const styles = StyleSheet.create({
33
+  container: {
34
+    flex: 1,
35
+    justifyContent: 'center',
36
+    alignItems: 'center',
37
+    backgroundColor: 'white'
38
+  },
39
+  button: {
40
+    alignSelf: 'center',
41
+    backgroundColor: 'green'
42
+  },
43
+  text: {
44
+    alignSelf: 'center',
45
+    color: Platform.OS === 'ios' ? 'black' : 'white'
46
+  }
47
+});

+ 5
- 0
playground/src/screens/LifecycleScreen.js View File

@@ -37,6 +37,7 @@ class LifecycleScreen extends Component {
37 37
         <Text style={styles.h1}>{`Lifecycle Screen`}</Text>
38 38
         <Text style={styles.h1}>{this.state.text}</Text>
39 39
         <Button title='Push to test didDisappear' testID={testIDs.PUSH_TO_TEST_DID_DISAPPEAR_BUTTON} onPress={this.onClickPush} />
40
+        <Button title='Pop' testID={testIDs.POP_BUTTON} onPress={() => this.onClickPop()} />
40 41
         <Text style={styles.footer}>{`this.props.componentId = ${this.props.componentId}`}</Text>
41 42
       </View>
42 43
     );
@@ -45,6 +46,10 @@ class LifecycleScreen extends Component {
45 46
   onClickPush() {
46 47
     Navigation.push(this.props.componentId, { component: { name: 'navigation.playground.TextScreen' } });
47 48
   }
49
+
50
+  onClickPop() {
51
+    Navigation.pop(this.props.componentId);
52
+  }
48 53
 }
49 54
 module.exports = LifecycleScreen;
50 55
 

+ 1
- 1
playground/src/screens/OptionsScreen.js View File

@@ -63,7 +63,7 @@ class OptionsScreen extends Component {
63 63
         <Button title='Top Bar Transparent' onPress={this.onClickTopBarTransparent} />
64 64
         <Button title='Top Bar Opaque' onPress={this.onClickTopBarOpaque} />
65 65
         <Button title='scrollView Screen' testID={testIDs.SCROLLVIEW_SCREEN_BUTTON} onPress={this.onClickScrollViewScreen} />
66
-        <Button title='Custom Transition' onPress={this.onClickCustomTranstition} />
66
+        <Button title='Custom Transition' testID={testIDs.CUSTOM_TRANSITION_BUTTON} onPress={this.onClickCustomTranstition} />
67 67
         <Button title='Show custom alert' testID={testIDs.SHOW_CUSTOM_ALERT_BUTTON} onPress={this.onClickAlert} />
68 68
         <Button title='Show snackbar' testID={testIDs.SHOW_SNACKBAR_BUTTON} onPress={this.onClickSnackbar} />
69 69
         <Button title='Show overlay' testID={testIDs.SHOW_OVERLAY_BUTTON} onPress={() => this.onClickShowOverlay(true)} />

+ 3
- 1
playground/src/screens/OrientationDetectScreen.js View File

@@ -23,7 +23,9 @@ class OrientationDetectScreen extends Component {
23 23
         <Text style={styles.h1}>{`Orientation Screen`}</Text>
24 24
         <Button title='Dismiss' testID={testIDs.DISMISS_BUTTON} onPress={() => Navigation.dismissModal(this.props.componentId)} />
25 25
         <Text style={styles.footer}>{`this.props.componentId = ${this.props.componentId}`}</Text>
26
-        <Text style={styles.footer} testID='currentOrientation'>{this.state.horizontal ? 'Landscape' : 'Portrait'}</Text>
26
+        {this.state.horizontal ?
27
+        <Text style={styles.footer} testID={testIDs.LANDSCAPE_ELEMENT}>Landscape</Text> :
28
+        <Text style={styles.footer} testID={testIDs.PORTRAIT_ELEMENT}>Portrait</Text>}
27 29
       </View>
28 30
     );
29 31
   }

+ 4
- 8
playground/src/screens/OrientationSelectScreen.js View File

@@ -1,12 +1,12 @@
1 1
 const React = require('react');
2 2
 const { Component } = require('react');
3
+
3 4
 const { View, Text, Button } = require('react-native');
4 5
 
5 6
 const { Navigation } = require('react-native-navigation');
6 7
 const testIDs = require('../testIDs');
7 8
 
8 9
 class OrientationSelectScreen extends Component {
9
-
10 10
   render() {
11 11
     return (
12 12
       <View style={styles.root}>
@@ -37,16 +37,12 @@ const styles = {
37 37
   root: {
38 38
     flexGrow: 1,
39 39
     justifyContent: 'center',
40
-    alignItems: 'center'
40
+    alignItems: 'center',
41
+    backgroundColor: '#f5fcff'
41 42
   },
42 43
   h1: {
43 44
     fontSize: 24,
44 45
     textAlign: 'center',
45
-    margin: 30
46
-  },
47
-  footer: {
48
-    fontSize: 10,
49
-    color: '#888',
50
-    marginTop: 10
46
+    margin: 10
51 47
   }
52 48
 };

+ 1
- 1
playground/src/screens/StaticLifecycleOverlay.js View File

@@ -1,7 +1,7 @@
1 1
 const React = require('react');
2 2
 const { Component } = require('react');
3 3
 const { View, Text } = require('react-native');
4
-const Navigation = require('react-native-navigation');
4
+const { Navigation } = require('react-native-navigation');
5 5
 
6 6
 class StaticLifecycleOverlay extends Component {
7 7
   constructor(props) {

+ 10
- 0
playground/src/screens/TextScreen.js View File

@@ -20,6 +20,7 @@ class TextScreen extends Component {
20 20
   constructor(props) {
21 21
     super(props);
22 22
     globalFirstComponentID = (props.text === 'This is tab 1') ? props.componentId : globalFirstComponentID;
23
+    this.onClickPop = this.onClickPop.bind(this);
23 24
   }
24 25
 
25 26
   render() {
@@ -35,10 +36,15 @@ class TextScreen extends Component {
35 36
         <Button title='Show Tab Bar' testID={testIDs.SHOW_BOTTOM_TABS_BUTTON} onPress={() => this.hideTabBar(false)} />
36 37
         <Button title='Show Left Side Menu' testID={testIDs.SHOW_LEFT_SIDE_MENU_BUTTON} onPress={() => this.showSideMenu('left')} />
37 38
         <Button title='Show Right Side Menu' testID={testIDs.SHOW_RIGHT_SIDE_MENU_BUTTON} onPress={() => this.showSideMenu('right')} />
39
+        <Button title='Pop' testID={testIDs.POP_BUTTON} onPress={this.onClickPop} />
38 40
       </View>
39 41
     );
40 42
   }
41 43
 
44
+  async onClickPop() {
45
+    await Navigation.pop(this.props.componentId);
46
+  }
47
+
42 48
   renderTextFromFunctionInProps() {
43 49
     if (!this.props.myFunction) {
44 50
       return undefined;
@@ -91,6 +97,10 @@ class TextScreen extends Component {
91 97
       }
92 98
     });
93 99
   }
100
+
101
+  onClickPop() {
102
+    Navigation.pop(this.props.componentId);
103
+  }
94 104
 }
95 105
 
96 106
 module.exports = TextScreen;

+ 59
- 25
playground/src/screens/WelcomeScreen.js View File

@@ -59,22 +59,17 @@ class WelcomeScreen extends Component {
59 59
                     passProps: {
60 60
                       text: 'This is tab 1',
61 61
                       myFunction: () => 'Hello from a function!'
62
-                    },
63
-                    options: {
64
-                      bottomTab: {
65
-                        title: 'Tab 1',
66
-                        testID: testIDs.FIRST_TAB_BAR_BUTTON
67
-                      },
68
-                      bottomTabs: {
69
-                        textColor: '#12766b',
70
-                        selectedTextColor: 'red',
71
-                        fontFamily: 'HelveticaNeue-Italic',
72
-                        fontSize: 13
73
-                      }
74 62
                     }
75 63
                   }
76 64
                 }
77
-              ]
65
+              ],
66
+              options: {
67
+                bottomTab: {
68
+                  title: 'Tab 1',
69
+                  icon: require('../images/one.png'),
70
+                  testID: testIDs.FIRST_TAB_BAR_BUTTON
71
+                }
72
+              }
78 73
             }
79 74
           },
80 75
           {
@@ -85,19 +80,29 @@ class WelcomeScreen extends Component {
85 80
                     name: 'navigation.playground.TextScreen',
86 81
                     passProps: {
87 82
                       text: 'This is tab 2'
88
-                    },
89
-                    options: {
90
-                      bottomTab: {
91
-                        title: 'Tab 2',
92
-                        testID: testIDs.SECOND_TAB_BAR_BUTTON
93
-                      }
94 83
                     }
95 84
                   }
96 85
                 }
97
-              ]
86
+              ],
87
+              options: {
88
+                bottomTab: {
89
+                  title: 'Tab 2',
90
+                  icon: require('../images/two.png'),
91
+                  testID: testIDs.SECOND_TAB_BAR_BUTTON
92
+                }
93
+              }
98 94
             }
99 95
           }
100
-        ]
96
+        ],
97
+        options: {
98
+          bottomTabs: {
99
+            tabColor: 'red',
100
+            selectedTabColor: 'blue',
101
+            fontFamily: 'HelveticaNeue-Italic',
102
+            fontSize: 13,
103
+            testID: testIDs.BOTTOM_TABS_ELEMENT
104
+          }
105
+        }
101 106
       }
102 107
     });
103 108
   }
@@ -127,7 +132,14 @@ class WelcomeScreen extends Component {
127 132
                         }
128 133
                       }
129 134
                     }
130
-                  ]
135
+                  ],
136
+                  options: {
137
+                    bottomTab: {
138
+                      title: 'Tab 1',
139
+                      icon: require('../images/one.png'),
140
+                      testID: testIDs.FIRST_TAB_BAR_BUTTON
141
+                    }
142
+                  }
131 143
                 }
132 144
               },
133 145
               {
@@ -141,7 +153,14 @@ class WelcomeScreen extends Component {
141 153
                         }
142 154
                       }
143 155
                     }
144
-                  ]
156
+                  ],
157
+                  options: {
158
+                    bottomTab: {
159
+                      title: 'Tab 2',
160
+                      icon: require('../images/two.png'),
161
+                      testID: testIDs.SECOND_TAB_BAR_BUTTON
162
+                    }
163
+                  }
145 164
                 }
146 165
               },
147 166
               {
@@ -155,10 +174,25 @@ class WelcomeScreen extends Component {
155 174
                         }
156 175
                       }
157 176
                     }
158
-                  ]
177
+                  ],
178
+                  options: {
179
+                    bottomTab: {
180
+                      title: 'Tab 3',
181
+                      icon: require('../images/three.png'),
182
+                      testID: testIDs.SECOND_TAB_BAR_BUTTON
183
+                    }
184
+                  }
159 185
                 }
160 186
               }
161
-            ]
187
+            ],
188
+            options: {
189
+              bottomTabs: {
190
+                tabColor: 'red',
191
+                selectedTabColor: 'blue',
192
+                fontFamily: 'HelveticaNeue-Italic',
193
+                fontSize: 13
194
+              }
195
+            }
162 196
           }
163 197
         },
164 198
         right: {

+ 2
- 0
playground/src/screens/index.js View File

@@ -16,6 +16,7 @@ const BandHandlerScreen = require('./BackHandlerScreen');
16 16
 const SideMenuScreen = require('./SideMenuScreen');
17 17
 const TopTabScreen = require('./TopTabScreen');
18 18
 const TopTabOptionsScreen = require('./TopTabOptionsScreen');
19
+const CustomTopBar = require('./CustomTopBar');
19 20
 
20 21
 function registerScreens() {
21 22
   Navigation.registerComponent(`navigation.playground.CustomTransitionDestination`, () => CustomTransitionDestination);
@@ -35,6 +36,7 @@ function registerScreens() {
35 36
   Navigation.registerComponent('navigation.playground.SideMenuScreen', () => SideMenuScreen);
36 37
   Navigation.registerComponent('navigation.playground.TopTabScreen', () => TopTabScreen);
37 38
   Navigation.registerComponent('navigation.playground.TopTabOptionsScreen', () => TopTabOptionsScreen);
39
+  Navigation.registerComponent('navigation.playground.CustomTopBar', () => CustomTopBar);
38 40
 }
39 41
 
40 42
 module.exports = {

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

@@ -51,11 +51,14 @@ module.exports = {
51 51
   SHOW_TOUCH_THROUGH_OVERLAY_BUTTON: `SHOW_TOUCH_THROUGH_OVERLAY_BUTTON`,
52 52
   OK_BUTTON: `OK_BUTTON`,
53 53
   MODAL_WITH_STACK_BUTTON: `MODAL_WITH_STACK_BUTTON`,
54
+  CUSTOM_TRANSITION_BUTTON: `CUSTOM_TRANSITION_BUTTON`,
54 55
 
55 56
   // Elements
56 57
   SCROLLVIEW_ELEMENT: `SCROLLVIEW_ELEMENT`,
57 58
   BOTTOM_TABS_ELEMENT: `BOTTOM_TABS_ELEMENT`,
58 59
   TOP_BAR_ELEMENT: `TOP_BAR_ELEMENT`,
60
+  LANDSCAPE_ELEMENT: `LANDSCAPE_ELEMENT`,
61
+  PORTRAIT_ELEMENT: `PORTRAIT_ELEMENT`,
59 62
 
60 63
   // Headers
61 64
   WELCOME_SCREEN_HEADER: `WELCOME_SCREEN_HEADER`,

+ 12
- 0
scripts/test.e2e.android.detox.js View File

@@ -0,0 +1,12 @@
1
+const _ = require('lodash');
2
+const exec = require('shell-utils').exec;
3
+
4
+const release = _.includes(process.argv, '--release');
5
+
6
+run();
7
+
8
+function run() {
9
+  const conf = release ? `release` : `debug`;
10
+  exec.execSync(`detox build --configuration android.emu.${conf}`);
11
+  exec.execSync(`detox test --configuration android.emu.${conf} ${process.env.CI ? '--cleanup' : ''}`);
12
+}