Browse Source

Android buttons (#2256)

* upstream merge

* Fixed bad merge

* More bad merges

* Mostly done, however a few properties missing and tests remain.

* async icons loading

* Left button

* Whoops!

* Added button size

* updated docs

* refactor

* Rebase fixes

* Rebase fixes

* rebase fix

* Rebase fixes

* rebase fixes

* reset contributing
Johan 6 years ago
parent
commit
2c26d4f7c3
27 changed files with 10208 additions and 38 deletions
  1. 7
    0
      AndroidE2E/app/src/androidTest/java/com/reactnativenavigation/e2e/androide2e/NavigationOptionsTest.java
  2. 7
    8
      README.md
  3. 77
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Button.java
  4. 19
    5
      lib/android/app/src/main/java/com/reactnativenavigation/parse/NavigationOptions.java
  5. 6
    4
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/OptionsPresenter.java
  6. 9
    2
      lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationEvent.java
  7. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/react/ReactView.java
  8. 66
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ImageUtils.java
  9. 13
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/UiUtils.java
  10. 2
    1
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ContainerViewController.java
  11. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/toptabs/TopTabsViewPager.java
  12. 2
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/Container.java
  13. 6
    1
      lib/android/app/src/main/java/com/reactnativenavigation/views/ContainerLayout.java
  14. 146
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/TitleBarButton.java
  15. 50
    2
      lib/android/app/src/main/java/com/reactnativenavigation/views/TopBar.java
  16. 5
    0
      lib/android/app/src/main/java/com/reactnativenavigation/views/TopTab.java
  17. 7
    2
      lib/android/app/src/main/java/com/reactnativenavigation/views/TopTabsLayout.java
  18. 6
    1
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java
  19. 56
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerView.java
  20. 5
    0
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopTabLayoutMock.java
  21. 1
    1
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/OptionsApplyingTest.java
  22. 2
    1
      lib/android/app/src/test/java/com/reactnativenavigation/views/TopBarTest.java
  23. 11
    2
      lib/src/params/Button.js
  24. 12
    2
      lib/src/params/Button.test.js
  25. 2
    2
      lib/src/params/NavigationOptions.test.js
  26. 9666
    0
      package-lock.json
  27. 15
    4
      playground/src/containers/OptionsScreen.js

+ 7
- 0
AndroidE2E/app/src/androidTest/java/com/reactnativenavigation/e2e/androide2e/NavigationOptionsTest.java View File

@@ -32,4 +32,11 @@ public class NavigationOptionsTest extends BaseTest {
32 32
 //		elementByText("SHOW TOP BAR").click();
33 33
 //		assertExists(By.text("Static Title"));
34 34
 //	}
35
+
36
+    @Test
37
+    public void testRightButtons() throws Exception {
38
+        elementByText("PUSH OPTIONS SCREEN").click();
39
+        assertExists(By.text("ONE"));
40
+        elementByText("ONE").click();
41
+    }
35 42
 }

+ 7
- 8
README.md View File

@@ -103,14 +103,13 @@ v2 is written in Test Driven Development. We have a test for every feature inclu
103 103
 
104 104
 |       buttons        | iOS  | Android | contributors|
105 105
 |--------------------|-----|----|-----|
106
-| id    |   ✅      |      [Contribute](/docs/docs/CONTRIBUTING.md)    |@Johan-dutoit|
107
-| testID   |     ✅   |    [Contribute](/docs/docs/CONTRIBUTING.md)      | @Johan-dutoit|
108
-| color             |    ✅     |      [Contribute](/docs/docs/CONTRIBUTING.md)       | @Johan-dutoit|
109
-| icon          |   ✅     |     [Contribute](/docs/docs/CONTRIBUTING.md)       | @Johan-dutoit|
110
-| disableTint        |    ✅       |     [Contribute](/docs/docs/CONTRIBUTING.md)      | @Johan-dutoit|
111
-| fontSize        |    ✅       |     [Contribute](/docs/docs/CONTRIBUTING.md)      | @Johan-dutoit |
112
-| fontWeight        |    ✅       |     [Contribute](/docs/docs/CONTRIBUTING.md)      | @Johan-dutoit |
113
-| fontWeight        |    ✅       |     [Contribute](/docs/docs/CONTRIBUTING.md)      | @Johan-dutoit |
106
+| id    |   ✅      |     @Johan-dutoit  |@Johan-dutoit|
107
+| testID   |     ✅   |   [Contribute](/docs/docs/CONTRIBUTING.md)  | @Johan-dutoit|
108
+| color             |    ✅     |     @Johan-dutoit     | @Johan-dutoit|
109
+| icon          |   ✅     |    @Johan-dutoit     | @Johan-dutoit|
110
+| disableTint        |    ✅       |    @Johan-dutoit    | @Johan-dutoit|
111
+| fontSize        |    ✅       |    @Johan-dutoit    | @Johan-dutoit |
112
+| fontWeight        |    ✅       |    [Contribute](/docs/docs/CONTRIBUTING.md)   |  [Contribute](/docs/docs/CONTRIBUTING.md) |
114 113
 
115 114
 |       statusBar        | iOS  | Android | contributors|
116 115
 |--------------------|-----|----|-----|

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

@@ -0,0 +1,77 @@
1
+package com.reactnativenavigation.parse;
2
+
3
+import android.support.annotation.ColorInt;
4
+import android.view.MenuItem;
5
+
6
+import org.json.JSONArray;
7
+import org.json.JSONObject;
8
+
9
+import java.util.ArrayList;
10
+
11
+import static com.reactnativenavigation.parse.DEFAULT_VALUES.NO_VALUE;
12
+import static com.reactnativenavigation.parse.NavigationOptions.NO_INT_VALUE;
13
+
14
+public class Button {
15
+	public String id;
16
+	public String title;
17
+	public NavigationOptions.BooleanOptions disabled;
18
+	public NavigationOptions.BooleanOptions disableIconTint;
19
+	public int showAsAction;
20
+	@ColorInt public int buttonColor;
21
+	public int buttonFontSize;
22
+	public String buttonFontWeight;
23
+	public String icon;
24
+
25
+	private static Button parseJson(JSONObject json)  {
26
+		Button button = new Button();
27
+		button.id = json.optString("id");
28
+		button.title = json.optString("title", NO_VALUE);
29
+		button.disabled = NavigationOptions.BooleanOptions.parse(json.optString("disabled", NO_VALUE));
30
+		button.disableIconTint = NavigationOptions.BooleanOptions.parse(json.optString("disableIconTint", NO_VALUE));
31
+		button.showAsAction = parseShowAsAction(json.optString("showAsAction", NO_VALUE));
32
+		button.buttonColor = json.optInt("buttonColor", NO_INT_VALUE);
33
+		button.buttonFontSize = json.optInt("buttonFontSize", NO_INT_VALUE);
34
+		button.buttonFontWeight = json.optString("buttonFontWeight", NO_VALUE);
35
+
36
+		JSONObject iconObject = json.optJSONObject("icon");
37
+		if (iconObject != null) {
38
+			button.icon = iconObject.optString("uri", NO_VALUE);
39
+		}
40
+
41
+		return button;
42
+	}
43
+
44
+	public static ArrayList<Button> parseJsonArray(JSONArray jsonArray) {
45
+		ArrayList<Button> buttons = new ArrayList<>();
46
+
47
+		if (jsonArray == null) {
48
+			return null;
49
+		}
50
+
51
+		for (int i = 0; i < jsonArray.length(); i++) {
52
+			JSONObject json = jsonArray.optJSONObject(i);
53
+			Button button =	Button.parseJson(json);
54
+			buttons.add(button);
55
+		}
56
+
57
+		return buttons;
58
+	}
59
+
60
+	private static int parseShowAsAction(String showAsAction) {
61
+		if (NO_VALUE.equals(showAsAction)) {
62
+			return MenuItem.SHOW_AS_ACTION_IF_ROOM;
63
+		}
64
+
65
+		switch (showAsAction) {
66
+			case "always":
67
+				return MenuItem.SHOW_AS_ACTION_ALWAYS;
68
+			case "never":
69
+				return MenuItem.SHOW_AS_ACTION_NEVER;
70
+			case "withText":
71
+				return MenuItem.SHOW_AS_ACTION_WITH_TEXT;
72
+			case "ifRoom":
73
+			default:
74
+				return MenuItem.SHOW_AS_ACTION_IF_ROOM;
75
+		}
76
+	}
77
+}

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

@@ -4,6 +4,8 @@ import android.support.annotation.NonNull;
4 4
 
5 5
 import org.json.JSONObject;
6 6
 
7
+import java.util.ArrayList;
8
+
7 9
 public class NavigationOptions implements DEFAULT_VALUES {
8 10
 
9 11
     public enum BooleanOptions {
@@ -33,20 +35,32 @@ public class NavigationOptions implements DEFAULT_VALUES {
33 35
 		result.topTabsOptions = TopTabsOptions.parse(json.optJSONObject("topTabs"));
34 36
         result.topTabOptions = TopTabOptions.parse(json.optJSONObject("topTab"));
35 37
 		result.bottomTabsOptions = BottomTabsOptions.parse(json.optJSONObject("bottomTabs"));
38
+		result.rightButtons = Button.parseJsonArray(json.optJSONArray("rightButtons"));
39
+        result.leftButtons = Button.parseJsonArray(json.optJSONArray("leftButtons"));
36 40
 
37 41
 		return result.withDefaultOptions(defaultOptions);
38 42
 	}
39 43
 
40
-	public TopBarOptions topBarOptions = new TopBarOptions();
41
-    public TopTabsOptions topTabsOptions = new TopTabsOptions();
42
-    public TopTabOptions topTabOptions = new TopTabOptions();
43
-    public BottomTabsOptions bottomTabsOptions = new BottomTabsOptions();
44
+    @NonNull public TopBarOptions topBarOptions = new TopBarOptions();
45
+    @NonNull private TopTabsOptions topTabsOptions = new TopTabsOptions();
46
+    @NonNull public TopTabOptions topTabOptions = new TopTabOptions();
47
+    @NonNull public BottomTabsOptions bottomTabsOptions = new BottomTabsOptions();
48
+    public ArrayList<Button> leftButtons;
49
+    public ArrayList<Button> rightButtons;
44 50
 
45 51
 	public void mergeWith(final NavigationOptions other) {
46 52
         topBarOptions.mergeWith(other.topBarOptions);
47 53
         topTabsOptions.mergeWith(other.topTabsOptions);
48 54
         bottomTabsOptions.mergeWith(other.bottomTabsOptions);
49
-	}
55
+
56
+        if(other.leftButtons != null) {
57
+            leftButtons = other.leftButtons;
58
+        }
59
+
60
+        if(other.rightButtons != null) {
61
+            rightButtons = other.rightButtons;
62
+        }
63
+    }
50 64
 
51 65
     NavigationOptions withDefaultOptions(final NavigationOptions other) {
52 66
         topBarOptions.mergeWithDefault(other.topBarOptions);

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

@@ -3,12 +3,14 @@ package com.reactnativenavigation.presentation;
3 3
 import android.view.View;
4 4
 
5 5
 import com.reactnativenavigation.anim.StackAnimator;
6
+import com.reactnativenavigation.parse.Button;
6 7
 import com.reactnativenavigation.parse.NavigationOptions;
7 8
 import com.reactnativenavigation.parse.TopBarOptions;
8
-import com.reactnativenavigation.parse.TopTabsOptions;
9 9
 import com.reactnativenavigation.utils.TypefaceLoader;
10 10
 import com.reactnativenavigation.views.TopBar;
11 11
 
12
+import java.util.ArrayList;
13
+
12 14
 public class OptionsPresenter {
13 15
 
14 16
 	private final StackAnimator animator;
@@ -23,7 +25,7 @@ public class OptionsPresenter {
23 25
 
24 26
 	public void applyOptions(NavigationOptions options) {
25 27
         applyTopBarOptions(options.topBarOptions);
26
-        applyTopTabsOptions(options.topTabsOptions);
28
+        applyButtons(options.leftButtons, options.rightButtons);
27 29
 	}
28 30
 
29 31
     private void applyTopBarOptions(TopBarOptions options) {
@@ -64,7 +66,7 @@ public class OptionsPresenter {
64 66
 		}
65 67
 	}
66 68
 
67
-    private void applyTopTabsOptions(TopTabsOptions topTabsOptions) {
68
-        // TODO: -guyca
69
+    private void applyButtons(ArrayList<Button> leftButtons, ArrayList<Button> rightButtons) {
70
+        topBar.setButtons(leftButtons, rightButtons);
69 71
     }
70 72
 }

+ 9
- 2
lib/android/app/src/main/java/com/reactnativenavigation/react/NavigationEvent.java View File

@@ -3,8 +3,12 @@ package com.reactnativenavigation.react;
3 3
 import com.facebook.react.bridge.Arguments;
4 4
 import com.facebook.react.bridge.ReactContext;
5 5
 import com.facebook.react.bridge.WritableMap;
6
+import com.facebook.react.bridge.WritableNativeArray;
6 7
 import com.facebook.react.modules.core.DeviceEventManagerModule;
7 8
 
9
+import org.json.JSONException;
10
+import org.json.JSONObject;
11
+
8 12
 import static com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
9 13
 
10 14
 public class NavigationEvent {
@@ -32,8 +36,11 @@ public class NavigationEvent {
32 36
 	}
33 37
 
34 38
 	public void sendOnNavigationButtonPressed(String id, String buttonId) {
35
-		//TODO!
36
-		//emit(onNavigationButtonPressed, id);
39
+		WritableMap map = Arguments.createMap();
40
+		map.putString("containerId", id);
41
+		map.putString("buttonId", buttonId);
42
+
43
+		emit(onNavigationButtonPressed, map);
37 44
 	}
38 45
 
39 46
 	private void emit(String eventName) {

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

@@ -59,4 +59,9 @@ public class ReactView extends ReactRootView implements ContainerViewController.
59 59
 	public void sendContainerStop() {
60 60
 		new NavigationEvent(reactInstanceManager.getCurrentReactContext()).containerDidDisappear(containerId);
61 61
 	}
62
+
63
+    @Override
64
+	public void sendOnNavigationButtonPressed(String buttonId) {
65
+		new NavigationEvent(reactInstanceManager.getCurrentReactContext()).sendOnNavigationButtonPressed(containerId, buttonId);
66
+	}
62 67
 }

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

@@ -0,0 +1,66 @@
1
+package com.reactnativenavigation.utils;
2
+
3
+import android.content.Context;
4
+import android.graphics.Bitmap;
5
+import android.graphics.BitmapFactory;
6
+import android.graphics.drawable.BitmapDrawable;
7
+import android.graphics.drawable.Drawable;
8
+import android.net.Uri;
9
+import android.os.StrictMode;
10
+import android.support.annotation.NonNull;
11
+
12
+import java.io.IOException;
13
+import java.io.InputStream;
14
+import java.net.URL;
15
+
16
+public class ImageUtils {
17
+
18
+	public interface ImageLoadingListener {
19
+		void onComplete(@NonNull Drawable drawable);
20
+
21
+		void onError(Throwable error);
22
+	}
23
+
24
+	public static void tryLoadIcon(final Context context, final String uri, final ImageLoadingListener listener) {
25
+		if (uri == null) {
26
+			if (listener != null) {
27
+				listener.onError(new IllegalArgumentException("Uri is null"));
28
+			}
29
+			return;
30
+		}
31
+		runWorkerThread(new Runnable() {
32
+			@Override
33
+			public void run() {
34
+				loadIcon(context, uri, listener);
35
+			}
36
+		});
37
+	}
38
+
39
+	private static void loadIcon(Context context, String uri, final ImageLoadingListener listener) {
40
+		try {
41
+			InputStream is = null;
42
+			if (uri.contains("http")) {
43
+				URL url = new URL(uri);
44
+				is = url.openStream();
45
+			} else {
46
+				is = context.getContentResolver().openInputStream(Uri.parse(uri));
47
+			}
48
+
49
+			Bitmap bitmap = BitmapFactory.decodeStream(is);
50
+			Drawable drawable = new BitmapDrawable(context.getResources(), bitmap);
51
+			if (listener != null) {
52
+				listener.onComplete(drawable);
53
+			}
54
+		} catch (IOException e) {
55
+			if (listener != null) {
56
+				listener.onError(e);
57
+			} else {
58
+				e.printStackTrace();
59
+			}
60
+		}
61
+	}
62
+
63
+	private static void runWorkerThread(Runnable runnable) {
64
+		new Thread(runnable).start();
65
+	}
66
+}

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

@@ -1,5 +1,10 @@
1 1
 package com.reactnativenavigation.utils;
2 2
 
3
+import android.graphics.PorterDuff;
4
+import android.graphics.PorterDuffColorFilter;
5
+import android.graphics.drawable.Drawable;
6
+import android.os.Handler;
7
+import android.os.Looper;
3 8
 import android.view.View;
4 9
 import android.view.ViewTreeObserver;
5 10
 
@@ -14,4 +19,12 @@ public class UiUtils {
14 19
 			}
15 20
 		});
16 21
 	}
22
+
23
+	public static void tintDrawable(Drawable drawable, int tint) {
24
+		drawable.setColorFilter(new PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN));
25
+	}
26
+
27
+	public static void runOnMainThread(Runnable runnable) {
28
+		new Handler(Looper.getMainLooper()).post(runnable);
29
+	}
17 30
 }

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

@@ -29,7 +29,8 @@ public class ContainerViewController extends ViewController implements Navigatio
29 29
 
30 30
 		void sendContainerStop();
31 31
 
32
-	}
32
+        void sendOnNavigationButtonPressed(String buttonId);
33
+    }
33 34
 
34 35
 	private final String containerName;
35 36
 

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

@@ -5,6 +5,7 @@ import android.content.Context;
5 5
 import android.support.v4.view.ViewPager;
6 6
 import android.view.ViewGroup;
7 7
 
8
+import com.reactnativenavigation.viewcontrollers.ContainerViewController;
8 9
 import com.reactnativenavigation.viewcontrollers.ViewController;
9 10
 
10 11
 import java.util.List;
@@ -36,4 +37,8 @@ public class TopTabsViewPager extends ViewPager {
36 37
             tab.destroy();
37 38
         }
38 39
     }
40
+
41
+    public void sendOnNavigationButtonPressed(String id) {
42
+        ((ContainerViewController.IReactView) tabs.get(getCurrentItem()).getView()).sendOnNavigationButtonPressed(id);
43
+    }
39 44
 }

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

@@ -7,6 +7,8 @@ import com.reactnativenavigation.parse.NavigationOptions;
7 7
 public interface Container {
8 8
     void applyOptions(NavigationOptions options);
9 9
 
10
+    void sendOnNavigationButtonPressed(String id);
11
+
10 12
     @RestrictTo(RestrictTo.Scope.TESTS)
11 13
     TopBar getTopBar();
12 14
 }

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

@@ -19,7 +19,7 @@ public class ContainerLayout extends LinearLayout implements ReactContainer {
19 19
 
20 20
 	public ContainerLayout(Context context, IReactView reactView) {
21 21
 		super(context);
22
-		this.topBar = new TopBar(context);
22
+		this.topBar = new TopBar(context, this);
23 23
 		this.reactView = reactView;
24 24
         optionsPresenter = new OptionsPresenter(topBar, reactView.asView());
25 25
         initViews();
@@ -62,6 +62,11 @@ public class ContainerLayout extends LinearLayout implements ReactContainer {
62 62
     }
63 63
 
64 64
 	@Override
65
+    public void sendOnNavigationButtonPressed(String buttonId) {
66
+        reactView.sendOnNavigationButtonPressed(buttonId);
67
+    }
68
+
69
+    @Override
65 70
     @RestrictTo(RestrictTo.Scope.TESTS)
66 71
     public TopBar getTopBar() {
67 72
         return topBar;

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

@@ -0,0 +1,146 @@
1
+package com.reactnativenavigation.views;
2
+
3
+import android.content.Context;
4
+import android.graphics.Color;
5
+import android.graphics.drawable.Drawable;
6
+import android.support.annotation.NonNull;
7
+import android.support.v7.widget.Toolbar;
8
+import android.text.Spannable;
9
+import android.text.SpannableString;
10
+import android.text.style.AbsoluteSizeSpan;
11
+import android.util.Log;
12
+import android.view.Menu;
13
+import android.view.MenuItem;
14
+import android.view.View;
15
+import android.widget.TextView;
16
+
17
+import com.reactnativenavigation.parse.Button;
18
+import com.reactnativenavigation.parse.NavigationOptions;
19
+import com.reactnativenavigation.utils.ImageUtils;
20
+import com.reactnativenavigation.utils.UiUtils;
21
+
22
+import java.util.ArrayList;
23
+
24
+public class TitleBarButton implements MenuItem.OnMenuItemClickListener {
25
+	private Toolbar toolbar;
26
+	private final Button button;
27
+	private Container container;
28
+	private Drawable icon;
29
+
30
+	TitleBarButton(Container container, Toolbar toolbar, Button button) {
31
+		this.container = container;
32
+		this.toolbar = toolbar;
33
+		this.button = button;
34
+	}
35
+
36
+	void addToMenu(Context context, final Menu menu) {
37
+		MenuItem menuItem = menu.add(button.title);
38
+		menuItem.setShowAsAction(button.showAsAction);
39
+		menuItem.setEnabled(button.disabled != NavigationOptions.BooleanOptions.True);
40
+		menuItem.setOnMenuItemClickListener(this);
41
+
42
+		if (hasIcon()) {
43
+			applyIcon(context, menuItem);
44
+		} else {
45
+			setTextColor();
46
+			setFontSize(menuItem);
47
+		}
48
+	}
49
+
50
+	void applyNavigationIcon(Context context) {
51
+		if (!hasIcon()) {
52
+			Log.w("RNN", "Left button needs to have an icon");
53
+			return;
54
+		}
55
+
56
+		ImageUtils.tryLoadIcon(context, button.icon, new ImageUtils.ImageLoadingListener() {
57
+			@Override
58
+			public void onComplete(@NonNull Drawable drawable) {
59
+				icon = drawable;
60
+				UiUtils.runOnMainThread(() -> {
61
+                    setIconColor();
62
+                    setNavigationClickListener();
63
+                    toolbar.setNavigationIcon(icon);
64
+                });
65
+			}
66
+
67
+			@Override
68
+			public void onError(Throwable error) {
69
+				//TODO: handle
70
+				error.printStackTrace();
71
+			}
72
+		});
73
+	}
74
+
75
+	private void applyIcon(Context context, final MenuItem menuItem) {
76
+		ImageUtils.tryLoadIcon(context, button.icon, new ImageUtils.ImageLoadingListener() {
77
+			@Override
78
+			public void onComplete(@NonNull Drawable drawable) {
79
+				icon = drawable;
80
+				UiUtils.runOnMainThread(() -> {
81
+                    menuItem.setIcon(icon);
82
+                    setIconColor();
83
+                });
84
+			}
85
+
86
+			@Override
87
+			public void onError(Throwable error) {
88
+				//TODO: handle
89
+				error.printStackTrace();
90
+			}
91
+		});
92
+	}
93
+
94
+	private void setIconColor() {
95
+		if (button.disabled == NavigationOptions.BooleanOptions.False || button.disabled == NavigationOptions.BooleanOptions.NoValue) {
96
+			UiUtils.tintDrawable(icon, button.buttonColor);
97
+			return;
98
+		}
99
+
100
+		if (button.disableIconTint == NavigationOptions.BooleanOptions.True) {
101
+			UiUtils.tintDrawable(icon, button.buttonColor);
102
+		} else {
103
+			UiUtils.tintDrawable(icon, Color.LTGRAY);
104
+		}
105
+	}
106
+
107
+	private void setTextColor() {
108
+		UiUtils.runOnPreDrawOnce(this.toolbar, () -> {
109
+            ArrayList<View> outViews = findActualTextViewInMenuByLabel();
110
+            setTextColorForFoundButtonViews(outViews);
111
+        });
112
+	}
113
+
114
+	private void setFontSize(MenuItem menuItem) {
115
+		SpannableString spanString = new SpannableString(button.title);
116
+		spanString.setSpan(new AbsoluteSizeSpan(button.buttonFontSize, true), 0, button.title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
117
+		menuItem.setTitleCondensed(spanString);
118
+	}
119
+
120
+	private void setNavigationClickListener() {
121
+		toolbar.setNavigationOnClickListener(view -> container.sendOnNavigationButtonPressed(button.id));
122
+	}
123
+
124
+	@Override
125
+	public boolean onMenuItemClick(MenuItem menuItem) {
126
+		this.container.sendOnNavigationButtonPressed(button.id);
127
+		return true;
128
+	}
129
+
130
+	@NonNull
131
+	private ArrayList<View> findActualTextViewInMenuByLabel() {
132
+		ArrayList<View> outViews = new ArrayList<>();
133
+		this.toolbar.findViewsWithText(outViews, button.title, View.FIND_VIEWS_WITH_TEXT);
134
+		return outViews;
135
+	}
136
+
137
+	private void setTextColorForFoundButtonViews(ArrayList<View> buttons) {
138
+		for (View button : buttons) {
139
+			((TextView) button).setTextColor(this.button.buttonColor);
140
+		}
141
+	}
142
+
143
+	private boolean hasIcon() {
144
+		return button.icon != null;
145
+	}
146
+}

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

@@ -1,24 +1,33 @@
1 1
 package com.reactnativenavigation.views;
2 2
 
3
+import android.annotation.SuppressLint;
3 4
 import android.content.Context;
4 5
 import android.graphics.Typeface;
5 6
 import android.support.annotation.ColorInt;
6 7
 import android.support.annotation.Nullable;
7 8
 import android.support.design.widget.AppBarLayout;
8 9
 import android.support.v7.widget.Toolbar;
10
+import android.util.Log;
11
+import android.view.Menu;
9 12
 import android.view.View;
10 13
 import android.view.ViewGroup;
11 14
 import android.widget.TextView;
12 15
 
16
+import com.reactnativenavigation.parse.Button;
13 17
 import com.reactnativenavigation.viewcontrollers.toptabs.TopTabsViewPager;
14 18
 
19
+import java.util.ArrayList;
20
+
21
+@SuppressLint("ViewConstructor")
15 22
 public class TopBar extends AppBarLayout {
16 23
 	private final Toolbar titleBar;
17
-	private TopTabs topTabs;
24
+    private Container container;
25
+    private TopTabs topTabs;
18 26
 
19
-    public TopBar(final Context context) {
27
+    public TopBar(final Context context, Container container) {
20 28
         super(context);
21 29
         titleBar = new Toolbar(context);
30
+        this.container = container;
22 31
         addView(titleBar);
23 32
     }
24 33
 
@@ -48,6 +57,11 @@ public class TopBar extends AppBarLayout {
48 57
 		}
49 58
 	}
50 59
 
60
+	public void setButtons(ArrayList<Button> leftButtons, ArrayList<Button> rightButtons) {
61
+		setLeftButtons(leftButtons);
62
+		setRightButtons(rightButtons);
63
+    }
64
+
51 65
 	public TextView getTitleTextView() {
52 66
 		return findTextView(titleBar);
53 67
 	}
@@ -71,6 +85,40 @@ public class TopBar extends AppBarLayout {
71 85
 		return null;
72 86
 	}
73 87
 
88
+	private void setLeftButtons(ArrayList<Button> leftButtons) {
89
+		if(leftButtons == null || leftButtons.isEmpty()) {
90
+			titleBar.setNavigationIcon(null);
91
+			return;
92
+		}
93
+
94
+		if(leftButtons.size() > 1) {
95
+			Log.w("RNN", "Use a custom TopBar to have more than one left button");
96
+		}
97
+
98
+		Button leftButton = leftButtons.get(0);
99
+		setLeftButton(leftButton);
100
+	}
101
+
102
+	private void setLeftButton(final Button button) {
103
+		TitleBarButton leftBarButton = new TitleBarButton(container, this.titleBar, button);
104
+		leftBarButton.applyNavigationIcon(getContext());
105
+	}
106
+
107
+	private void setRightButtons(ArrayList<Button> rightButtons) {
108
+		if(rightButtons == null || rightButtons.size() == 0) {
109
+			return;
110
+		}
111
+
112
+		Menu menu = getTitleBar().getMenu();
113
+		menu.clear();
114
+
115
+		for (int i = 0; i < rightButtons.size(); i++){
116
+	   		Button button = rightButtons.get(i);
117
+			TitleBarButton titleBarButton = new TitleBarButton(container, this.titleBar, button);
118
+			titleBarButton.addToMenu(getContext(), menu);
119
+       }
120
+    }
121
+
74 122
 	public Toolbar getTitleBar() {
75 123
 		return titleBar;
76 124
 	}

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

@@ -42,4 +42,9 @@ public class TopTab extends FrameLayout implements IReactView {
42 42
 	public void sendContainerStop() {
43 43
 		reactView.sendContainerStop();
44 44
 	}
45
+
46
+    @Override
47
+    public void sendOnNavigationButtonPressed(String buttonId) {
48
+        reactView.sendOnNavigationButtonPressed(buttonId);
49
+    }
45 50
 }

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

@@ -23,9 +23,9 @@ public class TopTabsLayout extends LinearLayout implements Container {
23 23
 
24 24
     public TopTabsLayout(Context context, List<TopTabController> tabs, TopTabsAdapter adapter) {
25 25
         super(context);
26
-        topBar = new TopBar(context);
27
-        optionsPresenter = new OptionsPresenter(topBar, viewPager);
26
+        topBar = new TopBar(context, this);
28 27
         viewPager = new TopTabsViewPager(context, tabs, adapter);
28
+        optionsPresenter = new OptionsPresenter(topBar, viewPager);
29 29
         initViews();
30 30
     }
31 31
 
@@ -41,6 +41,11 @@ public class TopTabsLayout extends LinearLayout implements Container {
41 41
         optionsPresenter.applyOptions(options);
42 42
     }
43 43
 
44
+    @Override
45
+    public void sendOnNavigationButtonPressed(String id) {
46
+        viewPager.sendOnNavigationButtonPressed(id);
47
+    }
48
+
44 49
     @Override
45 50
     @RestrictTo(RestrictTo.Scope.TESTS)
46 51
     public TopBar getTopBar() {

+ 6
- 1
lib/android/app/src/test/java/com/reactnativenavigation/mocks/TestContainerLayout.java View File

@@ -15,7 +15,7 @@ public class TestContainerLayout extends View implements ReactContainer {
15 15
 
16 16
     public TestContainerLayout(final Context context) {
17 17
 		super(context);
18
-        topBar = new TopBar(context);
18
+        topBar = new TopBar(context, this);
19 19
         optionsPresenter = new OptionsPresenter(topBar, new View(context));
20 20
     }
21 21
 
@@ -49,4 +49,9 @@ public class TestContainerLayout extends View implements ReactContainer {
49 49
     public void applyOptions(NavigationOptions options) {
50 50
         optionsPresenter.applyOptions(options);
51 51
     }
52
+
53
+    @Override
54
+    public void sendOnNavigationButtonPressed(String id) {
55
+
56
+    }
52 57
 }

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

@@ -0,0 +1,56 @@
1
+package com.reactnativenavigation.mocks;
2
+
3
+import android.content.Context;
4
+import android.view.View;
5
+
6
+import com.reactnativenavigation.parse.NavigationOptions;
7
+import com.reactnativenavigation.views.ReactContainer;
8
+import com.reactnativenavigation.views.TopBar;
9
+
10
+public class TestContainerView extends View implements ReactContainer {
11
+
12
+	private TopBar topBar;
13
+
14
+	public TestContainerView(final Context context) {
15
+		super(context);
16
+		topBar = new TopBar(context, this);
17
+
18
+	}
19
+
20
+	@Override
21
+	public boolean isReady() {
22
+		return false;
23
+	}
24
+
25
+	@Override
26
+	public View asView() {
27
+		return this;
28
+	}
29
+
30
+	@Override
31
+	public void destroy() {
32
+	}
33
+
34
+	@Override
35
+	public void sendContainerStart() {
36
+	}
37
+
38
+	@Override
39
+	public void sendContainerStop() {
40
+	}
41
+
42
+    @Override
43
+    public void applyOptions(NavigationOptions options) {
44
+
45
+    }
46
+
47
+    @Override
48
+    public void sendOnNavigationButtonPressed(String id) {
49
+
50
+    }
51
+
52
+    @Override
53
+    public TopBar getTopBar() {
54
+        return topBar;
55
+    }
56
+}

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

@@ -35,4 +35,9 @@ public class TopTabLayoutMock extends View implements ContainerViewController.IR
35 35
     public void sendContainerStop() {
36 36
 
37 37
     }
38
+
39
+    @Override
40
+    public void sendOnNavigationButtonPressed(String buttonId) {
41
+
42
+    }
38 43
 }

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

@@ -134,4 +134,4 @@ public class OptionsApplyingTest extends BaseTest {
134 134
 
135 135
 		assertThat(uut.getTopBar().getVisibility()).isEqualTo(View.GONE);
136 136
 	}
137
-}
137
+}

+ 2
- 1
lib/android/app/src/test/java/com/reactnativenavigation/views/TopBarTest.java View File

@@ -9,7 +9,8 @@ import static org.assertj.core.api.Java6Assertions.assertThat;
9 9
 public class TopBarTest extends BaseTest {
10 10
 	@Test
11 11
 	public void title() throws Exception {
12
-		TopBar topBar = new TopBar(newActivity());
12
+		//TODO:  fix null
13
+		TopBar topBar = new TopBar(newActivity(), null);
13 14
 		assertThat(topBar.getTitle()).isEmpty();
14 15
 
15 16
 		topBar.setTitle("new title");

+ 11
- 2
lib/src/params/Button.js View File

@@ -3,13 +3,22 @@ class Button {
3 3
    * @property {String} id
4 4
    * @property {string} [testID]
5 5
    * @property {string} [title]
6
-   * @property {string} [color]
6
+   * @property {string} [buttonColor]
7
+   * @property {string} [showAsAction]
8
+   * @property {int} [buttonFontWeight]
9
+   * @property {boolean} [disableIconTint]
10
+   * @property {boolean} [disabled]
7 11
    */
8 12
   constructor(params) {
9 13
     this.id = params.id;
10 14
     this.testID = params.testID;
11 15
     this.title = params.title;
12
-    this.color = params.color;
16
+    this.buttonColor = params.buttonColor;
17
+    this.showAsAction = params.showAsAction;
18
+    this.buttonFontWeight = params.buttonFontWeight;
19
+    this.buttonFontSize = params.buttonFontSize;
20
+    this.disableIconTint = params.disableIconTint;
21
+    this.disabled = params.disabled;
13 22
   }
14 23
 }
15 24
 

+ 12
- 2
lib/src/params/Button.test.js View File

@@ -4,7 +4,12 @@ const BUTTON = {
4 4
   id: 'myBtn',
5 5
   testID: 'BTN',
6 6
   title: 'My Button',
7
-  color: 'red'
7
+  buttonColor: 'red',
8
+  buttonFontSize: 16,
9
+  buttonFontWeight: 300,
10
+  showAsAction: 'never',
11
+  disableIconTint: true,
12
+  disabled: true
8 13
 };
9 14
 
10 15
 describe('Button', () => {
@@ -13,6 +18,11 @@ describe('Button', () => {
13 18
     expect(uut.id).toEqual('myBtn');
14 19
     expect(uut.testID).toEqual('BTN');
15 20
     expect(uut.title).toEqual('My Button');
16
-    expect(uut.color).toEqual('red');
21
+    expect(uut.buttonColor).toEqual('red');
22
+    expect(uut.showAsAction).toEqual('never');
23
+    expect(uut.buttonFontWeight).toEqual(300);
24
+    expect(uut.buttonFontSize).toEqual(16);
25
+    expect(uut.disableIconTint).toEqual(true);
26
+    expect(uut.disabled).toEqual(true);
17 27
   });
18 28
 });

+ 2
- 2
lib/src/params/NavigationOptions.test.js View File

@@ -9,7 +9,7 @@ const RIGHT_BUTTONS = [
9 9
     id: 'myBtn',
10 10
     testID: 'BTN',
11 11
     title: 'My Button',
12
-    color: 'red'
12
+    buttonColor: 'red'
13 13
   }
14 14
 ];
15 15
 const LEFT_BUTTONS = [
@@ -17,7 +17,7 @@ const LEFT_BUTTONS = [
17 17
     id: 'myBtn',
18 18
     testID: 'BTN',
19 19
     title: 'My Button',
20
-    color: 'red'
20
+    buttonColor: 'red'
21 21
   }
22 22
 ];
23 23
 const NAVIGATION_OPTIONS = {

+ 9666
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 15
- 4
playground/src/containers/OptionsScreen.js View File

@@ -26,11 +26,13 @@ class OptionsScreen extends Component {
26 26
         id: BUTTON_ONE,
27 27
         testID: BUTTON_ONE,
28 28
         title: 'One',
29
+        buttonFontSize: 28,
29 30
         buttonColor: 'red'
30 31
       }],
31 32
       leftButtons: [{
32 33
         id: BUTTON_LEFT,
33 34
         testID: BUTTON_LEFT,
35
+        icon: require('../../img/navicon_add.png'),
34 36
         title: 'Left',
35 37
         buttonColor: 'purple'
36 38
       }]
@@ -73,13 +75,15 @@ class OptionsScreen extends Component {
73 75
           id: BUTTON_TWO,
74 76
           testID: BUTTON_TWO,
75 77
           title: 'Two',
76
-          // icon: require('../../img/navicon_add.png'),
77
-          // disableIconTint: true,
78
-          // disabled: true
78
+          icon: require('../../img/navicon_add.png'),
79
+          disableIconTint: true,
80
+          // disabled: true,
81
+          showAsAction: 'ifRoom',
79 82
           buttonColor: 'green',
80 83
           buttonFontSize: 28,
81 84
           buttonFontWeight: '800'
82
-        }]
85
+        }],
86
+        leftButtons: []
83 87
       });
84 88
     } else if (id === BUTTON_TWO) {
85 89
       Navigation.setOptions(this.props.containerId, {
@@ -88,6 +92,13 @@ class OptionsScreen extends Component {
88 92
           testID: BUTTON_ONE,
89 93
           title: 'One',
90 94
           buttonColor: 'red'
95
+        }],
96
+        leftButtons: [{
97
+          id: BUTTON_LEFT,
98
+          testID: BUTTON_LEFT,
99
+          icon: require('../../img/navicon_add.png'),
100
+          title: 'Left',
101
+          buttonColor: 'purple'
91 102
         }]
92 103
       });
93 104
     } else if (id === BUTTON_LEFT) {