Browse Source

Bottom tabs android (#2649)

* Use AHBottomNavigation for BottomTabs

* Process passed options for all layout types

Static options are still processed only for components

* ios e2e fix

* Update RNNNavigationController.m
Guy Carmeli 6 years ago
parent
commit
990e0594d1
No account linked to committer's email address
45 changed files with 417 additions and 211 deletions
  1. 2
    1
      lib/android/app/build.gradle
  2. 39
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabOptions.java
  3. 24
    16
      lib/android/app/src/main/java/com/reactnativenavigation/parse/BottomTabsOptions.java
  4. 8
    4
      lib/android/app/src/main/java/com/reactnativenavigation/parse/LayoutFactory.java
  5. 5
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Options.java
  6. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/TopTabsOptions.java
  7. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/utils/CompatUtils.java
  8. 4
    4
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageLoader.java
  9. 51
    29
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/BottomTabsController.java
  10. 2
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ComponentViewController.java
  11. 1
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/Navigator.java
  12. 2
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ParentController.java
  13. 4
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/SideMenuController.java
  14. 2
    2
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/StackController.java
  15. 2
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java
  16. 1
    3
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsController.java
  17. 13
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/BottomTabs.java
  18. 3
    3
      lib/android/app/src/main/java/com/reactnativenavigation/views/TitleBarButton.java
  19. 49
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/ImageLoaderMock.java
  20. 2
    3
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/SimpleViewController.java
  21. 10
    12
      lib/android/app/src/test/java/com/reactnativenavigation/parse/NavigationOptionsTest.java
  22. 13
    0
      lib/android/app/src/test/java/com/reactnativenavigation/utils/OptionHelper.java
  23. 37
    25
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/BottomTabsControllerTest.java
  24. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ComponentViewControllerTest.java
  25. 31
    20
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/NavigatorTest.java
  26. 2
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java
  27. 10
    9
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ParentControllerTest.java
  28. 7
    6
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackControllerTest.java
  29. 0
    25
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsControllerMock.java
  30. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopTabsViewControllerTest.java
  31. 5
    4
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/ViewControllerTest.java
  32. 2
    0
      lib/ios/RNNBottomTabsOptions.h
  33. 4
    1
      lib/ios/RNNControllerFactory.m
  34. 4
    0
      lib/ios/RNNNavigationController.m
  35. 3
    0
      lib/ios/RNNRootViewProtocol.h
  36. 4
    0
      lib/ios/RNNTabBarController.m
  37. 7
    2
      lib/src/commands/LayoutTreeCrawler.ts
  38. BIN
      playground/src/images/one@2x.png
  39. BIN
      playground/src/images/one_selected@2x.png
  40. BIN
      playground/src/images/three@2x.png
  41. BIN
      playground/src/images/three_selected@2x.png
  42. BIN
      playground/src/images/two@2x.png
  43. BIN
      playground/src/images/two_selected@2x.png
  44. 1
    1
      playground/src/screens/StaticLifecycleOverlay.js
  45. 59
    25
      playground/src/screens/WelcomeScreen.js

+ 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

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

@@ -0,0 +1,39 @@
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
+            throw new RuntimeException("BottomTab must have an icon");
14
+        }
15
+        options.icon = TextParser.parse(json.optJSONObject("icon"), "uri");
16
+        return options;
17
+    }
18
+
19
+    public Text title = new NullText();
20
+    public Text icon = new NullText();
21
+
22
+    void mergeWith(final BottomTabOptions other) {
23
+        if (other.title.hasValue()) {
24
+            title = other.title;
25
+        }
26
+        if (other.icon.hasValue()) {
27
+            icon = other.icon;
28
+        }
29
+    }
30
+
31
+    void mergeWithDefault(final BottomTabOptions defaultOptions) {
32
+        if (!title.hasValue()) {
33
+            title = defaultOptions.title;
34
+        }
35
+        if (!icon.hasValue()) {
36
+            icon = defaultOptions.icon;
37
+        }
38
+    }
39
+}

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

@@ -11,17 +11,19 @@ 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"));
19 20
 
20 21
 		return options;
21 22
 	}
22 23
 
23
-	int tabBadge = NO_INT_VALUE;
24
-	BooleanOptions hidden = BooleanOptions.False;
24
+    public Color color = new NullColor();
25
+    public Color selectedColor = new NullColor();
26
+	BooleanOptions visible = BooleanOptions.False;
25 27
 	BooleanOptions animateHide = BooleanOptions.False;
26 28
 	public int currentTabIndex = NO_INT_VALUE;
27 29
 	public Text currentTabId = new NullText();
@@ -33,16 +35,19 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
33 35
 		if (NO_INT_VALUE != other.currentTabIndex) {
34 36
             currentTabIndex = other.currentTabIndex;
35 37
 		}
36
-		if (NO_INT_VALUE != other.tabBadge) {
37
-			tabBadge = other.tabBadge;
38
-		}
39
-		if (other.hidden != BooleanOptions.NoValue) {
40
-			hidden = other.hidden;
38
+		if (other.visible != BooleanOptions.NoValue) {
39
+			visible = other.visible;
41 40
 		}
42 41
 		if (other.animateHide != BooleanOptions.NoValue) {
43 42
 			animateHide = other.animateHide;
44 43
 		}
45
-	}
44
+        if (other.color.hasValue()) {
45
+            color = other.color;
46
+        }
47
+        if (other.selectedColor.hasValue()) {
48
+            selectedColor = other.selectedColor;
49
+        }
50
+    }
46 51
 
47 52
     void mergeWithDefault(final BottomTabsOptions defaultOptions) {
48 53
         if (!currentTabId.hasValue()) {
@@ -51,14 +56,17 @@ public class BottomTabsOptions implements DEFAULT_VALUES {
51 56
         if (NO_INT_VALUE == currentTabIndex) {
52 57
             currentTabIndex = defaultOptions.currentTabIndex;
53 58
         }
54
-        if (NO_INT_VALUE == tabBadge) {
55
-            tabBadge = defaultOptions.tabBadge;
56
-        }
57
-        if (hidden == BooleanOptions.NoValue) {
58
-            hidden = defaultOptions.hidden;
59
+        if (visible == BooleanOptions.NoValue) {
60
+            visible = defaultOptions.visible;
59 61
         }
60 62
         if (animateHide == BooleanOptions.NoValue) {
61 63
             animateHide = defaultOptions.animateHide;
62 64
         }
65
+        if (!color.hasValue()) {
66
+            color = defaultOptions.color;
67
+        }
68
+        if (!selectedColor.hasValue()) {
69
+            selectedColor = defaultOptions.selectedColor;
70
+        }
63 71
     }
64 72
 }

+ 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
     }

+ 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;

+ 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
         }

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

@@ -2,18 +2,22 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4 4
 import android.graphics.Color;
5
+import android.graphics.drawable.Drawable;
5 6
 import android.support.annotation.NonNull;
6
-import android.support.design.widget.BottomNavigationView;
7
-import android.view.Menu;
8
-import android.view.MenuItem;
9 7
 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;
15 17
 import com.reactnativenavigation.presentation.NavigationOptionsListener;
16
-import com.reactnativenavigation.utils.CompatUtils;
18
+import com.reactnativenavigation.utils.ImageLoader;
19
+import com.reactnativenavigation.utils.UiUtils;
20
+import com.reactnativenavigation.views.BottomTabs;
17 21
 
18 22
 import java.util.ArrayList;
19 23
 import java.util.Collection;
@@ -25,27 +29,26 @@ import static android.widget.RelativeLayout.ABOVE;
25 29
 import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
26 30
 import static com.reactnativenavigation.parse.DEFAULT_VALUES.NO_INT_VALUE;
27 31
 
28
-public class BottomTabsController extends ParentController
29
-		implements BottomNavigationView.OnNavigationItemSelectedListener, NavigationOptionsListener {
30
-	private BottomNavigationView bottomNavigationView;
32
+public class BottomTabsController extends ParentController implements AHBottomNavigation.OnTabSelectedListener, NavigationOptionsListener {
33
+	private BottomTabs bottomTabs;
31 34
 	private List<ViewController> tabs = new ArrayList<>();
32 35
 	private int selectedIndex = 0;
36
+    private ImageLoader imageLoader;
33 37
 
34
-	public BottomTabsController(final Activity activity, final String id) {
35
-		super(activity, id);
36
-	}
38
+    public BottomTabsController(final Activity activity, ImageLoader imageLoader, final String id, Options initialOptions) {
39
+		super(activity, id, initialOptions);
40
+        this.imageLoader = imageLoader;
41
+    }
37 42
 
38 43
 	@NonNull
39 44
 	@Override
40 45
 	protected ViewGroup createView() {
41 46
 		RelativeLayout root = new RelativeLayout(getActivity());
42
-		bottomNavigationView = new BottomNavigationView(getActivity());
43
-		bottomNavigationView.setId(CompatUtils.generateViewId());
44
-		bottomNavigationView.setBackgroundColor(Color.DKGRAY);
45
-		bottomNavigationView.setOnNavigationItemSelectedListener(this);
47
+		bottomTabs = new BottomTabs(getActivity());
48
+        bottomTabs.setOnTabSelectedListener(this);
46 49
 		RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT);
47 50
 		lp.addRule(ALIGN_PARENT_BOTTOM);
48
-		root.addView(bottomNavigationView, lp);
51
+		root.addView(bottomTabs, lp);
49 52
 		return root;
50 53
 	}
51 54
 
@@ -54,11 +57,11 @@ public class BottomTabsController extends ParentController
54 57
 		return !tabs.isEmpty() && tabs.get(selectedIndex).handleBack();
55 58
 	}
56 59
 
57
-	@Override
58
-	public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
59
-		selectTabAtIndex(item.getItemId());
60
-		return true;
61
-	}
60
+    @Override
61
+    public boolean onTabSelected(int index, boolean wasSelected) {
62
+        selectTabAtIndex(index);
63
+        return true;
64
+    }
62 65
 
63 66
 	void selectTabAtIndex(final int newIndex) {
64 67
 		tabs.get(selectedIndex).getView().setVisibility(View.GONE);
@@ -73,21 +76,40 @@ public class BottomTabsController extends ParentController
73 76
 		this.tabs = tabs;
74 77
 		getView();
75 78
 		for (int i = 0; i < tabs.size(); i++) {
76
-			String title = String.valueOf(i);
77
-			createTab(tabs.get(i), i, title);
79
+			createTab(tabs.get(i), tabs.get(i).options.bottomTabOptions, tabs.get(i).options.bottomTabsOptions);
78 80
 		}
79 81
 		selectTabAtIndex(0);
80 82
 	}
81 83
 
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);
84
+	private void createTab(ViewController tab, final BottomTabOptions tabOptions, final BottomTabsOptions bottomTabsOptions) {
85
+	    if (!tabOptions.icon.hasValue()) {
86
+            throw new RuntimeException("BottomTab must have an icon");
87
+        }
88
+        imageLoader.loadIcon(getActivity(), tabOptions.icon.get(), new ImageLoader.ImageLoadingListener() {
89
+            @Override
90
+            public void onComplete(@NonNull Drawable drawable) {
91
+                setIconColor(drawable, bottomTabsOptions);
92
+                AHBottomNavigationItem item = new AHBottomNavigationItem(tabOptions.title.get(""), drawable);
93
+                bottomTabs.addItem(item);
94
+            }
95
+
96
+            @Override
97
+            public void onError(Throwable error) {
98
+                error.printStackTrace();
99
+            }
100
+        });
101
+
102
+        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
103
+        params.addRule(ABOVE, bottomTabs.getId());
104
+        tab.getView().setVisibility(View.GONE);
105
+        getView().addView(tab.getView(), params);
88 106
 	}
89 107
 
90
-	int getSelectedIndex() {
108
+    private void setIconColor(Drawable drawable, BottomTabsOptions options) {
109
+        UiUtils.tintDrawable(drawable, Color.RED);
110
+    }
111
+
112
+    int getSelectedIndex() {
91 113
 		return selectedIndex;
92 114
 	}
93 115
 

+ 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

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

@@ -25,7 +25,7 @@ public class Navigator extends ParentController {
25 25
     private Options defaultOptions = new Options();
26 26
 
27 27
     public Navigator(final Activity activity) {
28
-		super(activity, "navigator" + CompatUtils.generateViewId());
28
+		super(activity, "navigator" + CompatUtils.generateViewId(), new Options());
29 29
 	}
30 30
 
31 31
     @NonNull

+ 2
- 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

+ 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

+ 2
- 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
 

+ 2
- 1
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();

+ 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);

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

@@ -0,0 +1,13 @@
1
+package com.reactnativenavigation.views;
2
+
3
+import android.content.Context;
4
+
5
+import com.aurelhubert.ahbottomnavigation.AHBottomNavigation;
6
+import com.reactnativenavigation.utils.CompatUtils;
7
+
8
+public class BottomTabs extends AHBottomNavigation {
9
+    public BottomTabs(Context context) {
10
+        super(context);
11
+        setId(CompatUtils.generateViewId());
12
+    }
13
+}

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

@@ -16,7 +16,7 @@ import android.widget.TextView;
16 16
 
17 17
 import com.reactnativenavigation.parse.Button;
18 18
 import com.reactnativenavigation.parse.Options;
19
-import com.reactnativenavigation.utils.ImageUtils;
19
+import com.reactnativenavigation.utils.ImageLoader;
20 20
 import com.reactnativenavigation.utils.UiUtils;
21 21
 
22 22
 import java.util.ArrayList;
@@ -57,7 +57,7 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
57 57
 			return;
58 58
 		}
59 59
 
60
-		ImageUtils.loadIcon(context, button.icon.get(), new ImageUtils.ImageLoadingListener() {
60
+		new ImageLoader().loadIcon(context, button.icon.get(), new ImageLoader.ImageLoadingListener() {
61 61
 			@Override
62 62
 			public void onComplete(@NonNull Drawable drawable) {
63 63
 				icon = drawable;
@@ -74,7 +74,7 @@ public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
74 74
 	}
75 75
 
76 76
 	private void applyIcon(Context context, final MenuItem menuItem) {
77
-		ImageUtils.loadIcon(context, button.icon.get(), new ImageUtils.ImageLoadingListener() {
77
+        new ImageLoader().loadIcon(context, button.icon.get(), new ImageLoader.ImageLoadingListener() {
78 78
 			@Override
79 79
 			public void onComplete(@NonNull Drawable drawable) {
80 80
 				icon = drawable;

+ 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
+}

+ 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
+}

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

@@ -1,21 +1,31 @@
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.view.View;
6
+import android.widget.RelativeLayout;
7
+
8
+import com.reactnativenavigation.BaseTest;
9
+import com.reactnativenavigation.mocks.ImageLoaderMock;
10
+import com.reactnativenavigation.mocks.MockPromise;
11
+import com.reactnativenavigation.mocks.SimpleViewController;
12
+import com.reactnativenavigation.parse.Options;
13
+import com.reactnativenavigation.utils.ImageLoader;
14
+import com.reactnativenavigation.utils.OptionHelper;
15
+import com.reactnativenavigation.views.BottomTabs;
16
+
17
+import org.assertj.core.api.iterable.Extractor;
18
+import org.junit.Test;
19
+
20
+import java.util.Arrays;
21
+import java.util.Collections;
22
+import java.util.List;
23
+
24
+import static org.assertj.core.api.Java6Assertions.assertThat;
25
+import static org.mockito.Mockito.spy;
26
+import static org.mockito.Mockito.times;
27
+import static org.mockito.Mockito.verify;
28
+import static org.mockito.Mockito.when;
19 29
 
20 30
 public class BottomTabsControllerTest extends BaseTest {
21 31
 
@@ -26,29 +36,31 @@ public class BottomTabsControllerTest extends BaseTest {
26 36
     private ViewController child3;
27 37
     private ViewController child4;
28 38
     private ViewController child5;
39
+    private Options tabOptions = OptionHelper.createBottomTabOptions();
40
+    private ImageLoader imageLoaderMock = ImageLoaderMock.mock();
29 41
 
30 42
     @Override
31 43
     public void beforeEach() {
32 44
         super.beforeEach();
33 45
         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");
46
+        uut = new BottomTabsController(activity, imageLoaderMock, "uut", new Options());
47
+        child1 = new SimpleViewController(activity, "child1", tabOptions);
48
+        child2 = new SimpleViewController(activity, "child2", tabOptions);
49
+        child3 = new SimpleViewController(activity, "child3", tabOptions);
50
+        child4 = new SimpleViewController(activity, "child4", tabOptions);
51
+        child5 = new SimpleViewController(activity, "child5", tabOptions);
40 52
     }
41 53
 
42 54
     @Test
43 55
     public void containsRelativeLayoutView() throws Exception {
44 56
         assertThat(uut.getView()).isInstanceOf(RelativeLayout.class);
45
-        assertThat(uut.getView().getChildAt(0)).isInstanceOf(BottomNavigationView.class);
57
+        assertThat(uut.getView().getChildAt(0)).isInstanceOf(BottomTabs.class);
46 58
     }
47 59
 
48 60
     @Test(expected = RuntimeException.class)
49 61
     public void setTabs_ThrowWhenMoreThan5() throws Exception {
50 62
         List<ViewController> tabs = createTabs();
51
-        tabs.add(new SimpleViewController(activity, "6"));
63
+        tabs.add(new SimpleViewController(activity, "6", tabOptions));
52 64
         uut.setTabs(tabs);
53 65
     }
54 66
 
@@ -75,7 +87,7 @@ public class BottomTabsControllerTest extends BaseTest {
75 87
     public void findControllerById_ReturnsSelfOrChildren() throws Exception {
76 88
         assertThat(uut.findControllerById("123")).isNull();
77 89
         assertThat(uut.findControllerById(uut.getId())).isEqualTo(uut);
78
-        StackController inner = new StackController(activity, "inner");
90
+        StackController inner = new StackController(activity, "inner", tabOptions);
79 91
         inner.animatePush(child1, new MockPromise());
80 92
         assertThat(uut.findControllerById(child1.getId())).isNull();
81 93
         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();

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

@@ -1,21 +1,30 @@
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;
@@ -26,20 +35,22 @@ public class NavigatorTest extends BaseTest {
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 = 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

+ 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();

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

@@ -5,6 +5,7 @@ import android.view.View;
5 5
 
6 6
 import com.reactnativenavigation.*;
7 7
 import com.reactnativenavigation.mocks.*;
8
+import com.reactnativenavigation.parse.Options;
8 9
 
9 10
 import org.assertj.core.api.iterable.*;
10 11
 import org.junit.*;
@@ -26,10 +27,10 @@ public class StackControllerTest extends BaseTest {
26 27
     public void beforeEach() {
27 28
         super.beforeEach();
28 29
         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");
30
+        uut = new StackController(activity, "uut", new Options());
31
+        child1 = new SimpleViewController(activity, "child1", new Options());
32
+        child2 = new SimpleViewController(activity, "child2", new Options());
33
+        child3 = new SimpleViewController(activity, "child3", new Options());
33 34
     }
34 35
 
35 36
     @Test
@@ -84,7 +85,7 @@ public class StackControllerTest extends BaseTest {
84 85
         uut.animatePush(child1, new MockPromise());
85 86
         assertThat(child1.getParentController()).isEqualTo(uut);
86 87
 
87
-        StackController anotherNavController = new StackController(activity, "another");
88
+        StackController anotherNavController = new StackController(activity, "another", new Options());
88 89
         anotherNavController.animatePush(child2, new MockPromise());
89 90
         assertThat(child2.getParentController()).isEqualTo(anotherNavController);
90 91
     }
@@ -264,7 +265,7 @@ public class StackControllerTest extends BaseTest {
264 265
 
265 266
     @Test
266 267
     public void findControllerById_Deeply() throws Exception {
267
-        StackController stack = new StackController(activity, "stack2");
268
+        StackController stack = new StackController(activity, "stack2", new Options());
268 269
         stack.animatePush(child2, new MockPromise());
269 270
         uut.animatePush(stack, new MockPromise());
270 271
         assertThat(uut.findControllerById(child2.getId())).isEqualTo(child2);

+ 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
 }

+ 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
 }

+ 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
 }

+ 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) {

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


+ 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) {

+ 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: {