Browse Source

Refactor right buttons (#6090)

Refactor right buttons

This pr started with an attempt to eliminate button flickering entirely when updating buttons which contain react components via mergeOptions. While the attempt wasn't 100% successful, I dit get some insights in the process.

* It's not possible to add buttons at specific indices. Buttons contain an order property which determines the order of the button in the TopBar. To somewhat overcome this limitation, we can let users control button order via options.

* When a right most component button is replaced with another component button, the rest of the buttons shift to the right since the newly added component isn't measured when it's added to the menu and its width is zero. 
We usually handle this situation with the `waitForRender` option but since buttons are measured with `MeasureSpec.UNSPECIFIED`, their dimensions are zero. Because of this, I've added options to set button dimensions.

* When updating buttons via mergeOptions, if a component button is already added to the menu with the same order we will not remove and added it again. This mitigates flickering in some situations.

* Textual button style properties were applied by traversing the view hierarchy and searching for the TextView corresponding to the button and updating its styles directly.
There was an inherent bug in this logic where if two buttons contained the same text, styles could have been applied to the wrong TextView. We now apply styles directly on the button using spans.
Guy Carmeli 4 years ago
parent
commit
42a6917eee
No account linked to committer's email address
29 changed files with 652 additions and 378 deletions
  1. 14
    1
      lib/android/app/src/main/java/com/reactnativenavigation/parse/Component.java
  2. 12
    5
      lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Button.java
  3. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Text.java
  4. 54
    47
      lib/android/app/src/main/java/com/reactnativenavigation/presentation/StackPresenter.java
  5. 6
    66
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ButtonPresenter.java
  6. 27
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ButtonSpan.kt
  7. 42
    2
      lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java
  8. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/Functions.java
  9. 17
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/IdFactory.kt
  10. 4
    0
      lib/android/app/src/main/java/com/reactnativenavigation/utils/ObjectUtils.java
  11. 32
    15
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java
  12. 37
    4
      lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java
  13. 37
    14
      lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java
  14. 4
    5
      lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBarButtonCreator.java
  15. 20
    3
      lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBarReactButtonView.java
  16. 28
    11
      lib/android/app/src/main/java/com/reactnativenavigation/views/topbar/TopBar.java
  17. 2
    2
      lib/android/app/src/test/java/com/reactnativenavigation/TestUtils.java
  18. 9
    4
      lib/android/app/src/test/java/com/reactnativenavigation/mocks/TitleBarButtonCreatorMock.java
  19. 4
    12
      lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleBarHelper.java
  20. 63
    37
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/StackPresenterTest.java
  21. 61
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonControllerTest.java
  22. 0
    77
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TitleBarTest.java
  23. 5
    68
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarButtonControllerTest.java
  24. 144
    0
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/TopBarControllerTest.java
  25. 2
    2
      lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerTest.java
  26. 8
    0
      lib/src/interfaces/Options.ts
  27. 8
    2
      playground/src/screens/OptionsScreen.js
  28. 2
    1
      playground/src/screens/RoundedButton.js
  29. 2
    0
      playground/src/testIDs.js

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

@@ -2,9 +2,12 @@ package com.reactnativenavigation.parse;
2 2
 
3 3
 import com.reactnativenavigation.parse.params.Bool;
4 4
 import com.reactnativenavigation.parse.params.NullBool;
5
+import com.reactnativenavigation.parse.params.NullNumber;
5 6
 import com.reactnativenavigation.parse.params.NullText;
7
+import com.reactnativenavigation.parse.params.Number;
6 8
 import com.reactnativenavigation.parse.params.Text;
7 9
 import com.reactnativenavigation.parse.parsers.BoolParser;
10
+import com.reactnativenavigation.parse.parsers.NumberParser;
8 11
 import com.reactnativenavigation.parse.parsers.TextParser;
9 12
 
10 13
 import org.json.JSONObject;
@@ -18,6 +21,8 @@ public class Component {
18 21
         result.componentId = TextParser.parse(json, "componentId");
19 22
         result.alignment = Alignment.fromString(TextParser.parse(json, "alignment").get(""));
20 23
         result.waitForRender = BoolParser.parse(json, "waitForRender");
24
+        result.width = NumberParser.parse(json, "width");
25
+        result.height = NumberParser.parse(json, "height");
21 26
 
22 27
         return result;
23 28
     }
@@ -26,12 +31,16 @@ public class Component {
26 31
     public Text componentId = new NullText();
27 32
     public Alignment alignment = Alignment.Default;
28 33
     public Bool waitForRender = new NullBool();
34
+    public Number width = new NullNumber();
35
+    public Number height = new NullNumber();
29 36
 
30 37
     void mergeWith(Component other) {
31 38
         if (other.componentId.hasValue()) componentId = other.componentId;
32 39
         if (other.name.hasValue()) name = other.name;
33 40
         if (other.waitForRender.hasValue()) waitForRender = other.waitForRender;
34 41
         if (other.alignment != Alignment.Default) alignment = other.alignment;
42
+        if (other.width.hasValue()) width = other.width;
43
+        if (other.height.hasValue()) height = other.height;
35 44
     }
36 45
 
37 46
     public void mergeWithDefault(Component defaultOptions) {
@@ -39,6 +48,8 @@ public class Component {
39 48
         if (!name.hasValue()) name = defaultOptions.name;
40 49
         if (!waitForRender.hasValue()) waitForRender = defaultOptions.waitForRender;
41 50
         if (alignment == Alignment.Default) alignment = defaultOptions.alignment;
51
+        if (!width.hasValue()) width = defaultOptions.width;
52
+        if (!height.hasValue()) height = defaultOptions.height;
42 53
     }
43 54
 
44 55
     public boolean hasValue() {
@@ -49,6 +60,8 @@ public class Component {
49 60
         return name.equals(other.name) &&
50 61
                componentId.equals(other.componentId) &&
51 62
                alignment.equals(other.alignment) &&
52
-               waitForRender.equals(other.waitForRender);
63
+               waitForRender.equals(other.waitForRender) &&
64
+               width.equals(other.width) &&
65
+               height.equals(other.height);
53 66
     }
54 67
 }

+ 12
- 5
lib/android/app/src/main/java/com/reactnativenavigation/parse/params/Button.java View File

@@ -6,9 +6,10 @@ import android.view.MenuItem;
6 6
 import com.reactnativenavigation.parse.Component;
7 7
 import com.reactnativenavigation.parse.parsers.BoolParser;
8 8
 import com.reactnativenavigation.parse.parsers.ColorParser;
9
-import com.reactnativenavigation.parse.parsers.NumberParser;
9
+import com.reactnativenavigation.parse.parsers.FractionParser;
10 10
 import com.reactnativenavigation.parse.parsers.TextParser;
11 11
 import com.reactnativenavigation.utils.CompatUtils;
12
+import com.reactnativenavigation.utils.IdFactory;
12 13
 import com.reactnativenavigation.utils.TypefaceLoader;
13 14
 
14 15
 import org.json.JSONArray;
@@ -19,10 +20,12 @@ import java.util.Objects;
19 20
 
20 21
 import androidx.annotation.Nullable;
21 22
 
23
+import static com.reactnativenavigation.utils.ObjectUtils.take;
24
+
22 25
 public class Button {
23 26
     public String instanceId = "btn" + CompatUtils.generateViewId();
24 27
 
25
-    @Nullable public String id;
28
+    public String id = "btn" + CompatUtils.generateViewId();
26 29
     public Text accessibilityLabel = new NullText();
27 30
     public Text text = new NullText();
28 31
     public Bool enabled = new NullBool();
@@ -30,7 +33,7 @@ public class Button {
30 33
     public Number showAsAction = new NullNumber();
31 34
     public Colour color = new NullColor();
32 35
     public Colour disabledColor = new NullColor();
33
-    public Number fontSize = new NullNumber();
36
+    public Fraction fontSize = new NullFraction();
34 37
     private Text fontWeight = new NullText();
35 38
     @Nullable public Typeface fontFamily;
36 39
     public Text icon = new NullText();
@@ -56,7 +59,7 @@ public class Button {
56 59
 
57 60
     private static Button parseJson(JSONObject json, TypefaceLoader typefaceManager) {
58 61
         Button button = new Button();
59
-        button.id = json.optString("id");
62
+        button.id = take(json.optString("id"), "btn" + CompatUtils.generateViewId());
60 63
         button.accessibilityLabel = TextParser.parse(json, "accessibilityLabel");
61 64
         button.text = TextParser.parse(json, "text");
62 65
         button.enabled = BoolParser.parse(json, "enabled");
@@ -64,7 +67,7 @@ public class Button {
64 67
         button.showAsAction = parseShowAsAction(json);
65 68
         button.color = ColorParser.parse(json, "color");
66 69
         button.disabledColor = ColorParser.parse(json, "disabledColor");
67
-        button.fontSize = NumberParser.parse(json, "fontSize");
70
+        button.fontSize = FractionParser.parse(json, "fontSize");
68 71
         button.fontFamily = typefaceManager.getTypeFace(json.optString("fontFamily", ""));
69 72
         button.fontWeight = TextParser.parse(json, "fontWeight");
70 73
         button.testId = TextParser.parse(json, "testID");
@@ -116,6 +119,10 @@ public class Button {
116 119
         return icon.hasValue();
117 120
     }
118 121
 
122
+    public int getIntId() {
123
+        return IdFactory.Companion.get(component.componentId.get(id));
124
+    }
125
+
119 126
     private static Number parseShowAsAction(JSONObject json) {
120 127
         final Text showAsAction = TextParser.parse(json, "showAsAction");
121 128
         if (!showAsAction.hasValue()) {

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

@@ -7,6 +7,10 @@ public class Text extends Param<String> {
7 7
         super(value);
8 8
     }
9 9
 
10
+    public int length() {
11
+        return hasValue() ? value.length() : 0;
12
+    }
13
+
10 14
     @NonNull
11 15
     @Override
12 16
     public String toString() {

+ 54
- 47
lib/android/app/src/main/java/com/reactnativenavigation/presentation/StackPresenter.java View File

@@ -25,7 +25,6 @@ import com.reactnativenavigation.utils.ObjectUtils;
25 25
 import com.reactnativenavigation.utils.StatusBarUtils;
26 26
 import com.reactnativenavigation.utils.UiUtils;
27 27
 import com.reactnativenavigation.viewcontrollers.IReactView;
28
-import com.reactnativenavigation.viewcontrollers.ReactViewCreator;
29 28
 import com.reactnativenavigation.viewcontrollers.TitleBarButtonController;
30 29
 import com.reactnativenavigation.viewcontrollers.TitleBarReactViewController;
31 30
 import com.reactnativenavigation.viewcontrollers.ViewController;
@@ -33,6 +32,7 @@ import com.reactnativenavigation.viewcontrollers.button.IconResolver;
33 32
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
34 33
 import com.reactnativenavigation.viewcontrollers.topbar.TopBarBackgroundViewController;
35 34
 import com.reactnativenavigation.viewcontrollers.topbar.TopBarController;
35
+import com.reactnativenavigation.views.titlebar.TitleBarButtonCreator;
36 36
 import com.reactnativenavigation.views.titlebar.TitleBarReactViewCreator;
37 37
 import com.reactnativenavigation.views.topbar.TopBar;
38 38
 import com.reactnativenavigation.views.topbar.TopBarBackgroundViewCreator;
@@ -44,12 +44,14 @@ import java.util.LinkedHashMap;
44 44
 import java.util.List;
45 45
 import java.util.Map;
46 46
 
47
+import androidx.annotation.NonNull;
47 48
 import androidx.annotation.Nullable;
48 49
 import androidx.annotation.RestrictTo;
49 50
 import androidx.appcompat.widget.Toolbar;
50 51
 
51 52
 import static com.reactnativenavigation.utils.CollectionUtils.*;
52 53
 import static com.reactnativenavigation.utils.ObjectUtils.perform;
54
+import static com.reactnativenavigation.utils.ObjectUtils.take;
53 55
 
54 56
 public class StackPresenter {
55 57
     private static final int DEFAULT_TITLE_COLOR = Color.BLACK;
@@ -66,7 +68,7 @@ public class StackPresenter {
66 68
     private TitleBarButtonController.OnClickListener onClickListener;
67 69
     private final RenderChecker renderChecker;
68 70
     private final TopBarBackgroundViewCreator topBarBackgroundViewCreator;
69
-    private final ReactViewCreator buttonCreator;
71
+    private final TitleBarButtonCreator buttonCreator;
70 72
     private Options defaultOptions;
71 73
 
72 74
     private List<TitleBarButtonController> currentRightButtons = new ArrayList<>();
@@ -79,7 +81,7 @@ public class StackPresenter {
79 81
     public StackPresenter(Activity activity,
80 82
                           TitleBarReactViewCreator titleViewCreator,
81 83
                           TopBarBackgroundViewCreator topBarBackgroundViewCreator,
82
-                          ReactViewCreator buttonCreator,
84
+                          TitleBarButtonCreator buttonCreator,
83 85
                           IconResolver iconResolver,
84 86
                           RenderChecker renderChecker,
85 87
                           Options defaultOptions) {
@@ -268,25 +270,24 @@ public class StackPresenter {
268 270
     }
269 271
 
270 272
     private void applyButtons(TopBarOptions options, ViewController child) {
271
-        List<Button> rightButtons = mergeButtonsWithColor(options.buttons.right, options.rightButtonColor, options.rightButtonDisabledColor);
272
-        List<Button> leftButtons = mergeButtonsWithColor(options.buttons.left, options.leftButtonColor, options.leftButtonDisabledColor);
273
-
274
-        if (rightButtons != null) {
275
-            List<TitleBarButtonController> rightButtonControllers = getOrCreateButtonControllers(componentRightButtons.get(child.getView()), rightButtons);
273
+        if (options.buttons.right != null) {
274
+            List<Button> rightButtons = mergeButtonsWithColor(options.buttons.right, options.rightButtonColor, options.rightButtonDisabledColor);
275
+            List<TitleBarButtonController> rightButtonControllers = getOrCreateButtonControllersByInstanceId(componentRightButtons.get(child.getView()), rightButtons);
276 276
             componentRightButtons.put(child.getView(), keyBy(rightButtonControllers, TitleBarButtonController::getButtonInstanceId));
277 277
             if (!CollectionUtils.equals(currentRightButtons, rightButtonControllers)) {
278 278
                 currentRightButtons = rightButtonControllers;
279
-                topBar.setRightButtons(rightButtonControllers);
279
+                topBarController.applyRightButtons(currentRightButtons);
280 280
             }
281 281
         } else {
282 282
             currentRightButtons = null;
283 283
             topBar.clearRightButtons();
284 284
         }
285 285
 
286
-        if (leftButtons != null) {
287
-            List<TitleBarButtonController> leftButtonControllers = getOrCreateButtonControllers(componentLeftButtons.get(child.getView()), leftButtons);
286
+        if (options.buttons.left != null) {
287
+            List<Button> leftButtons = mergeButtonsWithColor(options.buttons.left, options.leftButtonColor, options.leftButtonDisabledColor);
288
+            List<TitleBarButtonController> leftButtonControllers = getOrCreateButtonControllersByInstanceId(componentLeftButtons.get(child.getView()), leftButtons);
288 289
             componentLeftButtons.put(child.getView(), keyBy(leftButtonControllers, TitleBarButtonController::getButtonInstanceId));
289
-            topBar.setLeftButtons(leftButtonControllers);
290
+            topBarController.setLeftButtons(leftButtonControllers);
290 291
         } else {
291 292
             topBar.clearLeftButtons();
292 293
         }
@@ -298,19 +299,25 @@ public class StackPresenter {
298 299
         topBar.setOverflowButtonColor(options.rightButtonColor.get(Color.BLACK));
299 300
     }
300 301
 
301
-    private List<TitleBarButtonController> getOrCreateButtonControllers(@Nullable Map<String, TitleBarButtonController> currentButtons, @Nullable List<Button> buttons) {
302
+    private List<TitleBarButtonController> getOrCreateButtonControllersByInstanceId(@Nullable Map<String, TitleBarButtonController> currentButtons, @Nullable List<Button> buttons) {
302 303
         if (buttons == null) return null;
303 304
         Map<String, TitleBarButtonController> result = new LinkedHashMap<>();
305
+        forEach(buttons, b -> result.put(b.instanceId, getOrDefault(currentButtons, b.instanceId, () -> createButtonController(b))));
306
+        return new ArrayList<>(result.values());
307
+    }
308
+
309
+    private List<TitleBarButtonController> getOrCreateButtonControllersById(@Nullable Map<String, TitleBarButtonController> currentButtons,@NonNull List<Button> buttons) {
310
+        ArrayList result = new ArrayList<TitleBarButtonController>();
304 311
         for (Button b : buttons) {
305
-            result.put(b.instanceId, currentButtons != null && currentButtons.containsKey(b.instanceId) ? currentButtons.get(b.instanceId) : createButtonController(b));
312
+            result.add(take(first(perform(currentButtons, null, Map::values), button -> button.getId().equals(b.id)), createButtonController(b)));
306 313
         }
307
-        return new ArrayList<>(result.values());
314
+        return result;
308 315
     }
309 316
 
310 317
     private TitleBarButtonController createButtonController(Button button) {
311 318
         TitleBarButtonController controller = new TitleBarButtonController(activity,
312 319
                 iconResolver,
313
-                new ButtonPresenter(topBar.getTitleBar(), button),
320
+                new ButtonPresenter(button),
314 321
                 button,
315 322
                 buttonCreator,
316 323
                 onClickListener
@@ -354,28 +361,34 @@ public class StackPresenter {
354 361
     }
355 362
 
356 363
     private void mergeButtons(TopBarOptions options, TopBarButtons buttons, View child) {
357
-        List<Button> rightButtons = mergeButtonsWithColor(buttons.right, options.rightButtonColor, options.rightButtonDisabledColor);
358
-        List<Button> leftButtons = mergeButtonsWithColor(buttons.left, options.leftButtonColor, options.leftButtonDisabledColor);
364
+        mergeRightButtons(options, buttons, child);
365
+        mergeLeftButton(options, buttons, child);
366
+        mergeBackButton(buttons);
367
+    }
359 368
 
360
-        List<TitleBarButtonController> rightButtonControllers = getOrCreateButtonControllers(componentRightButtons.get(child), rightButtons);
361
-        List<TitleBarButtonController> leftButtonControllers = getOrCreateButtonControllers(componentLeftButtons.get(child), leftButtons);
369
+    private void mergeRightButtons(TopBarOptions options, TopBarButtons buttons, View child) {
370
+        if (buttons.right == null) return;
371
+        List<Button> rightButtons = mergeButtonsWithColor(buttons.right, options.rightButtonColor, options.rightButtonDisabledColor);
372
+        List<TitleBarButtonController> toMerge = getOrCreateButtonControllersById(componentRightButtons.get(child), rightButtons);
373
+        List<TitleBarButtonController> toRemove = difference(currentRightButtons, toMerge, TitleBarButtonController::equals);
374
+        forEach(toRemove, TitleBarButtonController::destroy);
362 375
 
363
-        if (rightButtonControllers != null) {
364
-            Map previousRightButtons = componentRightButtons.put(child, keyBy(rightButtonControllers, TitleBarButtonController::getButtonInstanceId));
365
-            if (previousRightButtons != null) forEach(previousRightButtons.values(), TitleBarButtonController::destroy);
366
-        }
367
-        if (leftButtonControllers != null) {
368
-            Map previousLeftButtons = componentLeftButtons.put(child, keyBy(leftButtonControllers, TitleBarButtonController::getButtonInstanceId));
369
-            if (previousLeftButtons != null) forEach(previousLeftButtons.values(), TitleBarButtonController::destroy);
376
+        if (!CollectionUtils.equals(currentRightButtons, toMerge)) {
377
+            topBarController.mergeRightButtons(toMerge, toRemove);
378
+            currentRightButtons = toMerge;
370 379
         }
380
+        if (options.rightButtonColor.hasValue()) topBar.setOverflowButtonColor(options.rightButtonColor.get());
381
+    }
371 382
 
372
-        if (buttons.right != null) {
373
-            if (!CollectionUtils.equals(currentRightButtons, rightButtonControllers)) {
374
-                currentRightButtons = rightButtonControllers;
375
-                topBar.setRightButtons(rightButtonControllers);
376
-            }
377
-        }
378
-        if (buttons.left != null) topBar.setLeftButtons(leftButtonControllers);
383
+    private void mergeLeftButton(TopBarOptions options, TopBarButtons buttons, View child) {
384
+        if (buttons.left == null) return;
385
+        List<Button> leftButtons = mergeButtonsWithColor(buttons.left, options.leftButtonColor, options.leftButtonDisabledColor);
386
+        List<TitleBarButtonController> toMerge = getOrCreateButtonControllersById(componentLeftButtons.get(child), leftButtons);
387
+        componentLeftButtons.put(child, keyBy(toMerge, TitleBarButtonController::getButtonInstanceId));
388
+        topBarController.setLeftButtons(toMerge);
389
+    }
390
+
391
+    private void mergeBackButton(TopBarButtons buttons) {
379 392
         if (buttons.back.hasValue()) {
380 393
             if (buttons.back.visible.isFalse()) {
381 394
                 topBar.clearLeftButtons();
@@ -383,21 +396,15 @@ public class StackPresenter {
383 396
                 topBar.setBackButton(createButtonController(buttons.back));
384 397
             }
385 398
         }
386
-
387
-        if (options.rightButtonColor.hasValue()) topBar.setOverflowButtonColor(options.rightButtonColor.get());
388 399
     }
389 400
 
390
-    @Nullable
391
-    private List<Button> mergeButtonsWithColor(List<Button> buttons, Colour buttonColor, Colour disabledColor) {
392
-        List<Button> result = null;
393
-        if (buttons != null) {
394
-            result = new ArrayList<>();
395
-            for (Button button : buttons) {
396
-                Button copy = button.copy();
397
-                if (!button.color.hasValue()) copy.color = buttonColor;
398
-                if (!button.disabledColor.hasValue()) copy.disabledColor = disabledColor;
399
-                result.add(copy);
400
-            }
401
+    private List<Button> mergeButtonsWithColor(@NonNull List<Button> buttons, Colour buttonColor, Colour disabledColor) {
402
+        List<Button> result = new ArrayList<>();
403
+        for (Button button : buttons) {
404
+            Button copy = button.copy();
405
+            if (!button.color.hasValue()) copy.color = buttonColor;
406
+            if (!button.disabledColor.hasValue()) copy.disabledColor = disabledColor;
407
+            result.add(copy);
401 408
         }
402 409
         return result;
403 410
     }

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

@@ -1,32 +1,18 @@
1 1
 package com.reactnativenavigation.utils;
2 2
 
3
-import android.graphics.Color;
4 3
 import android.graphics.PorterDuff;
5 4
 import android.graphics.PorterDuffColorFilter;
6
-import android.graphics.Typeface;
7 5
 import android.graphics.drawable.Drawable;
8
-import androidx.annotation.NonNull;
9
-import androidx.appcompat.widget.ActionMenuView;
10
-import androidx.appcompat.widget.Toolbar;
11
-import android.text.Spannable;
12 6
 import android.text.SpannableString;
13
-import android.text.style.AbsoluteSizeSpan;
14
-import android.view.MenuItem;
15
-import android.view.View;
16
-import android.widget.TextView;
17 7
 
18 8
 import com.reactnativenavigation.parse.params.Button;
19 9
 
20
-import java.util.ArrayList;
10
+import static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
21 11
 
22 12
 public class ButtonPresenter {
23
-    private final Toolbar toolbar;
24
-    private final ActionMenuView actionMenuView;
25 13
     private Button button;
26 14
 
27
-    public ButtonPresenter(Toolbar toolbar, Button button) {
28
-        this.toolbar = toolbar;
29
-        actionMenuView = ViewUtils.findChildrenByClass(toolbar, ActionMenuView.class).get(0);
15
+    public ButtonPresenter(Button button) {
30 16
         this.button = button;
31 17
     }
32 18
 
@@ -34,55 +20,9 @@ public class ButtonPresenter {
34 20
         drawable.setColorFilter(new PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN));
35 21
     }
36 22
 
37
-    public void setTypeFace(Typeface typeface) {
38
-        UiUtils.runOnPreDrawOnce(toolbar, () -> {
39
-            ArrayList<View> buttons = findActualTextViewInMenu();
40
-            for (View btn : buttons) {
41
-                ((TextView) btn).setTypeface(typeface);
42
-            }
43
-        });
23
+    public SpannableString getStyledText() {
24
+        SpannableString string = new SpannableString(button.text.get(""));
25
+        string.setSpan(new ButtonSpan(button), 0, button.text.length(), SPAN_EXCLUSIVE_INCLUSIVE);
26
+        return string;
44 27
     }
45
-
46
-    public void setFontSize(MenuItem menuItem) {
47
-        SpannableString spanString = new SpannableString(button.text.get());
48
-        if (this.button.fontSize.hasValue())
49
-            spanString.setSpan(
50
-                    new AbsoluteSizeSpan(button.fontSize.get(), true),
51
-                    0,
52
-                    button.text.get().length(),
53
-                    Spannable.SPAN_INCLUSIVE_INCLUSIVE
54
-            );
55
-        menuItem.setTitleCondensed(spanString);
56
-    }
57
-
58
-    public void setTextColor() {
59
-        UiUtils.runOnPreDrawOnce(toolbar, () -> {
60
-            ArrayList<View> buttons = findActualTextViewInMenu();
61
-            for (View btn : buttons) {
62
-                if (button.enabled.isTrueOrUndefined() && button.color.hasValue()) {
63
-                    setEnabledColor((TextView) btn);
64
-                } else if (button.enabled.isFalse()) {
65
-                    setDisabledColor((TextView) btn, button.disabledColor.get(Color.LTGRAY));
66
-                }
67
-            }
68
-        });
69
-    }
70
-
71
-    public void setDisabledColor(TextView btn, int color) {
72
-        btn.setTextColor(color);
73
-    }
74
-
75
-    public void setEnabledColor(TextView btn) {
76
-        btn.setTextColor(button.color.get());
77
-    }
78
-
79
-    @NonNull
80
-    private ArrayList<View> findActualTextViewInMenu() {
81
-        ArrayList<View> outViews = new ArrayList<>();
82
-        if (button.text.hasValue()) {
83
-            actionMenuView.findViewsWithText(outViews, button.text.get(), View.FIND_VIEWS_WITH_TEXT);
84
-        }
85
-        return outViews;
86
-    }
87
-
88 28
 }

+ 27
- 0
lib/android/app/src/main/java/com/reactnativenavigation/utils/ButtonSpan.kt View File

@@ -0,0 +1,27 @@
1
+package com.reactnativenavigation.utils
2
+
3
+import android.graphics.Paint
4
+import android.graphics.Typeface
5
+import android.text.TextPaint
6
+import android.text.style.MetricAffectingSpan
7
+import com.reactnativenavigation.parse.params.Button
8
+import com.reactnativenavigation.parse.params.Colour
9
+import com.reactnativenavigation.parse.params.Fraction
10
+
11
+class ButtonSpan(private val button: Button) : MetricAffectingSpan() {
12
+    override fun updateDrawState(drawState: TextPaint) = apply(drawState)
13
+
14
+    override fun updateMeasureState(paint: TextPaint) = apply(paint)
15
+
16
+    private fun apply(paint: Paint) {
17
+        with(button) {
18
+            val fakeStyle = (paint.typeface?.style ?: 0) and (fontFamily?.style?.inv() ?: 1)
19
+            if (fakeStyle and Typeface.BOLD != 0) paint.isFakeBoldText = true
20
+            if (fakeStyle and Typeface.ITALIC != 0) paint.textSkewX = -0.25f
21
+            if (fontSize.hasValue()) paint.textSize = fontSize.get().toFloat()
22
+            if (color.hasValue()) paint.color = if (enabled.isTrueOrUndefined) color.get() else disabledColor.get()
23
+            paint.typeface = fontFamily
24
+        }
25
+
26
+    }
27
+}

+ 42
- 2
lib/android/app/src/main/java/com/reactnativenavigation/utils/CollectionUtils.java View File

@@ -14,11 +14,16 @@ import androidx.annotation.NonNull;
14 14
 import androidx.annotation.Nullable;
15 15
 import androidx.core.util.Pair;
16 16
 
17
+@SuppressWarnings("WeakerAccess")
17 18
 public class CollectionUtils {
18 19
     public interface Apply<T> {
19 20
         void on(T t);
20 21
     }
21 22
 
23
+    public interface Comparator<T> {
24
+        boolean compare(T a, T b);
25
+    }
26
+
22 27
     public static boolean isNullOrEmpty(Collection collection) {
23 28
         return collection == null || collection.isEmpty();
24 29
     }
@@ -60,18 +65,42 @@ public class CollectionUtils {
60 65
         return result;
61 66
     }
62 67
 
68
+    public static <K, V> V getOrDefault(@Nullable Map<K, V> map, K key, Functions.FuncR<V> defaultValueCreator) {
69
+        if (map == null) return defaultValueCreator.run();
70
+        return map.containsKey(key) ? map.get(key) : defaultValueCreator.run();
71
+    }
72
+
63 73
     public static <T> List<T> merge(@Nullable Collection<T> a, @Nullable Collection<T> b, @NonNull List<T> defaultValue) {
64 74
         List<T> result = merge(a, b);
65 75
         return result == null ? defaultValue : result;
66 76
     }
67 77
 
68
-    public static <T> List<T> merge(@Nullable Collection<T> a, @Nullable Collection<T> b) {
78
+    public static <T> ArrayList<T> merge(@Nullable Collection<T> a, @Nullable Collection<T> b) {
69 79
         if (a == null && b == null) return null;
70
-        List<T> result = new ArrayList<>(get(a));
80
+        ArrayList<T> result = new ArrayList<>(get(a));
71 81
         result.addAll(get(b));
72 82
         return result;
73 83
     }
74 84
 
85
+    /**
86
+     * @return Items in a, that are not in b
87
+     */
88
+    public static <T> List<T> difference(@NonNull Collection<T> a, @Nullable Collection<T> b, Comparator<T> comparator) {
89
+        if (b == null) return new ArrayList<>(a);
90
+        ArrayList<T> results = new ArrayList<>();
91
+        forEach(a, btn -> {
92
+            if (!contains(b, btn, comparator)) results.add(btn);
93
+        });
94
+        return results;
95
+    }
96
+
97
+    private static <T> boolean contains(@NonNull Collection<T> items, T item, Comparator<T> comparator) {
98
+        for (T t : items) {
99
+            if (comparator.compare(t, item)) return true;
100
+        }
101
+        return false;
102
+    }
103
+
75 104
     public static <T> void forEach(@Nullable Collection<T> items, Apply<T> apply) {
76 105
         if (items != null) forEach(new ArrayList<>(items), 0, apply);
77 106
     }
@@ -94,6 +123,13 @@ public class CollectionUtils {
94 123
         }
95 124
     }
96 125
 
126
+    public static <T> void forEachIndexed(@Nullable List<T> items, Functions.Func2<T, Integer> apply) {
127
+        if (items == null) return;
128
+        for (int i = 0; i < items.size(); i++) {
129
+            apply.run(items.get(i), i);
130
+        }
131
+    }
132
+
97 133
     public static @Nullable <T> T first(@Nullable Collection<T> items, Filter<T> by) {
98 134
         if (isNullOrEmpty(items)) return null;
99 135
         for (T item : items) {
@@ -170,4 +206,8 @@ public class CollectionUtils {
170 206
         }
171 207
         return result;
172 208
     }
209
+
210
+    public static @Nullable<T> T safeGet(List<T> items, int index) {
211
+        return index >= 0 && index < items.size() ? items.get(index) : null;
212
+    }
173 213
 }

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

@@ -13,6 +13,10 @@ public class Functions {
13 13
         void run(T param);
14 14
     }
15 15
 
16
+    public interface Func2<T, S> {
17
+        void run(T param1, S param2);
18
+    }
19
+
16 20
     public interface FuncR<T> {
17 21
         T run();
18 22
     }

+ 17
- 0
lib/android/app/src/main/java/com/reactnativenavigation/utils/IdFactory.kt View File

@@ -0,0 +1,17 @@
1
+package com.reactnativenavigation.utils
2
+
3
+
4
+class IdFactory {
5
+    companion object {
6
+        private val stringIdToIntId = HashMap<String, Int>()
7
+        private var count = 0
8
+
9
+        fun get(id: String): Int {
10
+            return if (stringIdToIntId.containsKey(id)) {
11
+                stringIdToIntId[id]!!
12
+            } else {
13
+                (++count).apply { stringIdToIntId[id] = count }
14
+            }
15
+        }
16
+    }
17
+}

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

@@ -19,6 +19,10 @@ public class ObjectUtils {
19 19
         return obj == null ? defaultValue : obj;
20 20
     }
21 21
 
22
+    public static <T> T getOrCreate(@Nullable T obj, @NonNull Functions.FuncR<T> creator) {
23
+        return obj == null ? creator.run() : obj;
24
+    }
25
+
22 26
     public static boolean notNull(Object o) {
23 27
         return o != null;
24 28
     }

+ 32
- 15
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/TitleBarButtonController.java View File

@@ -21,25 +21,30 @@ import com.reactnativenavigation.utils.UiUtils;
21 21
 import com.reactnativenavigation.utils.ViewUtils;
22 22
 import com.reactnativenavigation.viewcontrollers.button.IconResolver;
23 23
 import com.reactnativenavigation.viewcontrollers.viewcontrolleroverlay.ViewControllerOverlay;
24
+import com.reactnativenavigation.views.titlebar.TitleBar;
25
+import com.reactnativenavigation.views.titlebar.TitleBarButtonCreator;
24 26
 import com.reactnativenavigation.views.titlebar.TitleBarReactButtonView;
25 27
 
26 28
 import java.util.List;
27 29
 
28 30
 import androidx.annotation.NonNull;
31
+import androidx.annotation.Nullable;
29 32
 import androidx.annotation.RestrictTo;
30 33
 import androidx.appcompat.widget.ActionMenuView;
31 34
 import androidx.appcompat.widget.Toolbar;
32 35
 import androidx.core.view.MenuItemCompat;
33 36
 
34 37
 public class TitleBarButtonController extends ViewController<TitleBarReactButtonView> implements MenuItem.OnMenuItemClickListener {
38
+    @Nullable private MenuItem menuItem;
39
+
35 40
     public interface OnClickListener {
36 41
         void onPress(String buttonId);
37 42
     }
38 43
 
39 44
     private final IconResolver navigationIconResolver;
40
-    private ButtonPresenter optionsPresenter;
45
+    private ButtonPresenter presenter;
41 46
     private final Button button;
42
-    private final ReactViewCreator viewCreator;
47
+    private final TitleBarButtonCreator viewCreator;
43 48
     private TitleBarButtonController.OnClickListener onPressListener;
44 49
     private Drawable icon;
45 50
 
@@ -52,15 +57,19 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
52 57
         return button.instanceId;
53 58
     }
54 59
 
60
+    public int getButtonIntId() {
61
+        return button.getIntId();
62
+    }
63
+
55 64
     public TitleBarButtonController(Activity activity,
56 65
                                     IconResolver navigationIconResolver,
57
-                                    ButtonPresenter optionsPresenter,
66
+                                    ButtonPresenter presenter,
58 67
                                     Button button,
59
-                                    ReactViewCreator viewCreator,
68
+                                    TitleBarButtonCreator viewCreator,
60 69
                                     OnClickListener onClickListener) {
61 70
         super(activity, button.id, new YellowBoxDelegate(), new Options(), new ViewControllerOverlay(activity));
62 71
         this.navigationIconResolver = navigationIconResolver;
63
-        this.optionsPresenter = optionsPresenter;
72
+        this.presenter = presenter;
64 73
         this.button = button;
65 74
         this.viewCreator = viewCreator;
66 75
         this.onPressListener = onClickListener;
@@ -96,7 +105,7 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
96 105
     @NonNull
97 106
     @Override
98 107
     protected TitleBarReactButtonView createView() {
99
-        view = (TitleBarReactButtonView) viewCreator.create(getActivity(), button.component.componentId.get(), button.component.name.get());
108
+        view = viewCreator.create(getActivity(), button.component);
100 109
         return (TitleBarReactButtonView) view.asView();
101 110
     }
102 111
 
@@ -106,6 +115,12 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
106 115
         return true;
107 116
     }
108 117
 
118
+    public boolean equals(TitleBarButtonController other) {
119
+        if (other == this) return true;
120
+        if (!other.getId().equals(getId())) return false;
121
+        return button.equals(other.button);
122
+    }
123
+
109 124
     public void applyNavigationIcon(Toolbar toolbar) {
110 125
         navigationIconResolver.resolve(button, icon -> {
111 126
             setIconColor(icon);
@@ -126,8 +141,14 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
126 141
         });
127 142
     }
128 143
 
129
-    public void addToMenu(Toolbar toolbar, int position) {
130
-        MenuItem menuItem = toolbar.getMenu().add(Menu.NONE, position, position, button.text.get(""));
144
+    public void addToMenu(TitleBar titleBar, int order) {
145
+        if (button.component.hasValue() && titleBar.containsRightButton(menuItem, order)) return;
146
+        titleBar.getMenu().removeItem(button.getIntId());
147
+        menuItem = titleBar.getMenu().add(Menu.NONE, button.getIntId(), order, presenter.getStyledText());
148
+        applyButtonOptions(titleBar, menuItem);
149
+    }
150
+
151
+    private void applyButtonOptions(TitleBar titleBar, MenuItem menuItem) {
131 152
         if (button.showAsAction.hasValue()) menuItem.setShowAsAction(button.showAsAction.get());
132 153
         menuItem.setEnabled(button.enabled.isTrueOrUndefined());
133 154
         menuItem.setOnMenuItemClickListener(this);
@@ -145,13 +166,9 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
145 166
                         menuItem.setIcon(icon);
146 167
                     }
147 168
                 });
148
-            } else {
149
-                optionsPresenter.setTextColor();
150
-                if (button.fontSize.hasValue()) optionsPresenter.setFontSize(menuItem);
151
-                optionsPresenter.setTypeFace(button.fontFamily);
152 169
             }
153 170
         }
154
-        setTestId(toolbar, button.testId);
171
+        setTestId(titleBar, button.testId);
155 172
     }
156 173
 
157 174
     private void loadIcon(ImageLoader.ImagesLoadingListener callback) {
@@ -161,9 +178,9 @@ public class TitleBarButtonController extends ViewController<TitleBarReactButton
161 178
     private void setIconColor(Drawable icon) {
162 179
         if (button.disableIconTint.isTrue()) return;
163 180
         if (button.enabled.isTrueOrUndefined() && button.color.hasValue()) {
164
-            optionsPresenter.tint(icon, button.color.get());
181
+            presenter.tint(icon, button.color.get());
165 182
         } else if (button.enabled.isFalse()) {
166
-            optionsPresenter.tint(icon, button.disabledColor.get(Color.LTGRAY));
183
+            presenter.tint(icon, button.disabledColor.get(Color.LTGRAY));
167 184
         }
168 185
     }
169 186
 

+ 37
- 4
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/topbar/TopBarController.java View File

@@ -1,28 +1,35 @@
1 1
 package com.reactnativenavigation.viewcontrollers.topbar;
2 2
 
3 3
 import android.content.Context;
4
+import android.graphics.drawable.Drawable;
5
+import android.view.MenuItem;
4 6
 import android.view.View;
5 7
 
6 8
 import com.reactnativenavigation.anim.TopBarAnimator;
7 9
 import com.reactnativenavigation.parse.AnimationOptions;
8
-import com.reactnativenavigation.utils.CompatUtils;
10
+import com.reactnativenavigation.viewcontrollers.TitleBarButtonController;
9 11
 import com.reactnativenavigation.viewcontrollers.TitleBarReactViewController;
10 12
 import com.reactnativenavigation.views.StackLayout;
13
+import com.reactnativenavigation.views.titlebar.TitleBar;
11 14
 import com.reactnativenavigation.views.topbar.TopBar;
12 15
 
16
+import java.util.List;
17
+
13 18
 import androidx.annotation.VisibleForTesting;
14 19
 import androidx.viewpager.widget.ViewPager;
15 20
 
21
+import static com.reactnativenavigation.utils.CollectionUtils.*;
16 22
 import static com.reactnativenavigation.utils.ObjectUtils.perform;
17 23
 import static com.reactnativenavigation.utils.ViewUtils.isVisible;
18 24
 
19 25
 
20 26
 public class TopBarController {
21 27
     private TopBar topBar;
28
+    private TitleBar titleBar;
22 29
     private TopBarAnimator animator;
23 30
 
24
-    public TopBarController() {
25
-        animator = new TopBarAnimator();
31
+    public MenuItem getRightButton(int index) {
32
+        return titleBar.getRightButton(index);
26 33
     }
27 34
 
28 35
     public TopBar getView() {
@@ -33,15 +40,27 @@ public class TopBarController {
33 40
         return perform(topBar, 0, View::getHeight);
34 41
     }
35 42
 
43
+    public int getRightButtonsCount() {
44
+        return topBar.getRightButtonsCount();
45
+    }
46
+
47
+    public Drawable getLeftButton() {
48
+        return titleBar.getNavigationIcon();
49
+    }
50
+
36 51
     @VisibleForTesting
37 52
     public void setAnimator(TopBarAnimator animator) {
38 53
         this.animator = animator;
39 54
     }
40 55
 
56
+    public TopBarController() {
57
+        animator = new TopBarAnimator();
58
+    }
59
+
41 60
     public TopBar createView(Context context, StackLayout parent) {
42 61
         if (topBar == null) {
43 62
             topBar = createTopBar(context, parent);
44
-            topBar.setId(CompatUtils.generateViewId());
63
+            titleBar = topBar.getTitleBar();
45 64
             animator.bindView(topBar, parent);
46 65
         }
47 66
         return topBar;
@@ -98,4 +117,18 @@ public class TopBarController {
98 117
     public void setTitleComponent(TitleBarReactViewController component) {
99 118
         topBar.setTitleComponent(component.getView());
100 119
     }
120
+
121
+    public void applyRightButtons(List<TitleBarButtonController> toAdd) {
122
+        topBar.clearRightButtons();
123
+        forEachIndexed(toAdd, (b, i) -> b.addToMenu(titleBar, (toAdd.size() - i) * 10));
124
+    }
125
+
126
+    public void mergeRightButtons(List<TitleBarButtonController> toAdd, List<TitleBarButtonController> toRemove) {
127
+        forEach(toRemove, btn -> topBar.removeRightButton(btn));
128
+        forEachIndexed(toAdd, (b, i) -> b.addToMenu(titleBar, (toAdd.size() - i) * 10));
129
+    }
130
+
131
+    public void setLeftButtons(List<TitleBarButtonController> leftButtons) {
132
+        titleBar.setLeftButtons(leftButtons);
133
+    }
101 134
 }

+ 37
- 14
lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBar.java View File

@@ -7,6 +7,7 @@ import android.graphics.PorterDuffColorFilter;
7 7
 import android.graphics.Typeface;
8 8
 import android.graphics.drawable.Drawable;
9 9
 import android.util.Log;
10
+import android.view.MenuItem;
10 11
 import android.view.View;
11 12
 import android.view.ViewGroup;
12 13
 import android.widget.TextView;
@@ -18,10 +19,10 @@ import com.reactnativenavigation.utils.UiUtils;
18 19
 import com.reactnativenavigation.utils.ViewUtils;
19 20
 import com.reactnativenavigation.viewcontrollers.TitleBarButtonController;
20 21
 
22
+import java.util.ArrayList;
21 23
 import java.util.List;
22 24
 
23
-import javax.annotation.Nullable;
24
-
25
+import androidx.annotation.Nullable;
25 26
 import androidx.appcompat.widget.ActionMenuView;
26 27
 import androidx.appcompat.widget.Toolbar;
27 28
 
@@ -42,6 +43,22 @@ public class TitleBar extends Toolbar {
42 43
     private Boolean isTitleChanged = false;
43 44
     private Boolean isSubtitleChanged = false;
44 45
 
46
+    public MenuItem getRightButton(int index) {
47
+        return getMenu().getItem(index);
48
+    }
49
+
50
+    public int getRightButtonsCount() {
51
+        return getMenu().size();
52
+    }
53
+
54
+    public List<MenuItem> getRightButtons() {
55
+        List<MenuItem> items = new ArrayList<>();
56
+        for (int i = 0; i < getRightButtonsCount(); i++) {
57
+            items.add(i, getRightButton(i));
58
+        }
59
+        return items;
60
+    }
61
+
45 62
     public TitleBar(Context context) {
46 63
         super(context);
47 64
         getMenu();
@@ -110,13 +127,19 @@ public class TitleBar extends Toolbar {
110 127
         subtitleAlignment = alignment;
111 128
     }
112 129
 
130
+    public boolean containsRightButton(@Nullable MenuItem menuItem, int order) {
131
+        return menuItem != null &&
132
+               getMenu().findItem(menuItem.getItemId()) != null &&
133
+               menuItem.getOrder() == order;
134
+    }
135
+
113 136
     public void alignTextView(Alignment alignment, TextView view) {
114 137
         if (StringUtils.isEmpty(view.getText())) return;
115 138
         int direction = view.getParent().getLayoutDirection();
116 139
         boolean isRTL = direction == View.LAYOUT_DIRECTION_RTL;
117 140
 
118 141
         if (alignment == Alignment.Center) {
119
-            view.setX((getWidth() - view.getWidth()) / 2);
142
+            view.setX((getWidth() - view.getWidth()) / 2f);
120 143
         } else if (leftButtonController != null) {
121 144
             view.setX(isRTL ? (getWidth() - view.getWidth()) - getContentInsetStartWithNavigation() : getContentInsetStartWithNavigation());
122 145
         } else {
@@ -128,13 +151,13 @@ public class TitleBar extends Toolbar {
128 151
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
129 152
         super.onLayout(changed, l, t, r, b);
130 153
 
131
-        if(changed || isTitleChanged) {
154
+        if (changed || isTitleChanged) {
132 155
             TextView title = findTitleTextView();
133 156
             if (title != null) this.alignTextView(titleAlignment, title);
134 157
             isTitleChanged = false;
135 158
         }
136 159
 
137
-        if(changed || isSubtitleChanged) {
160
+        if (changed || isSubtitleChanged) {
138 161
             TextView subtitle = findSubtitleTextView();
139 162
             if (subtitle != null) this.alignTextView(subtitleAlignment, subtitle);
140 163
             isSubtitleChanged = false;
@@ -190,7 +213,7 @@ public class TitleBar extends Toolbar {
190 213
         }
191 214
     }
192 215
 
193
-    private void clearRightButtons() {
216
+    public void clearRightButtons() {
194 217
         if (getMenu().size() > 0) getMenu().clear();
195 218
     }
196 219
 
@@ -216,14 +239,6 @@ public class TitleBar extends Toolbar {
216 239
         button.applyNavigationIcon(this);
217 240
     }
218 241
 
219
-    public void setRightButtons(List<TitleBarButtonController> rightButtons) {
220
-        if (rightButtons == null) return;
221
-        clearRightButtons();
222
-        for (int i = 0; i < rightButtons.size(); i++) {
223
-            rightButtons.get(i).addToMenu(this, rightButtons.size() - i - 1);
224
-        }
225
-    }
226
-
227 242
     public void setHeight(int height) {
228 243
         int pixelHeight = UiUtils.dpToPx(getContext(), height);
229 244
         if (pixelHeight == getLayoutParams().height) return;
@@ -257,4 +272,12 @@ public class TitleBar extends Toolbar {
257 272
             ((ViewGroup) child).setClipChildren(false);
258 273
         }
259 274
     }
275
+
276
+    public void removeRightButton(int buttonId) {
277
+        getMenu().removeItem(buttonId);
278
+    }
279
+
280
+    public boolean containsRightButton(TitleBarButtonController button) {
281
+        return getMenu().findItem(button.getButtonIntId()) != null;
282
+    }
260 283
 }

+ 4
- 5
lib/android/app/src/main/java/com/reactnativenavigation/views/titlebar/TitleBarButtonCreator.java View File

@@ -3,9 +3,9 @@ package com.reactnativenavigation.views.titlebar;
3 3
 import android.app.Activity;
4 4
 
5 5
 import com.facebook.react.ReactInstanceManager;
6
-import com.reactnativenavigation.viewcontrollers.ReactViewCreator;
6
+import com.reactnativenavigation.parse.Component;
7 7
 
8
-public class TitleBarButtonCreator implements ReactViewCreator {
8
+public class TitleBarButtonCreator {
9 9
 
10 10
     private ReactInstanceManager instanceManager;
11 11
 
@@ -13,8 +13,7 @@ public class TitleBarButtonCreator implements ReactViewCreator {
13 13
         this.instanceManager = instanceManager;
14 14
 	}
15 15
 
16
-	@Override
17
-	public TitleBarReactButtonView create(Activity activity, String componentId, String componentName) {
18
-        return new TitleBarReactButtonView(activity, instanceManager, componentId, componentName);
16
+	public TitleBarReactButtonView create(Activity activity, Component component) {
17
+        return new TitleBarReactButtonView(activity, instanceManager, component);
19 18
     }
20 19
 }

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

@@ -4,17 +4,34 @@ import android.annotation.SuppressLint;
4 4
 import android.content.Context;
5 5
 
6 6
 import com.facebook.react.ReactInstanceManager;
7
+import com.reactnativenavigation.parse.Component;
8
+import com.reactnativenavigation.parse.params.Number;
7 9
 import com.reactnativenavigation.react.ReactView;
8 10
 
11
+import static android.view.View.MeasureSpec.EXACTLY;
12
+import static android.view.View.MeasureSpec.UNSPECIFIED;
13
+import static android.view.View.MeasureSpec.makeMeasureSpec;
14
+import static com.reactnativenavigation.utils.UiUtils.dpToPx;
15
+
9 16
 @SuppressLint("ViewConstructor")
10 17
 public class TitleBarReactButtonView extends ReactView {
18
+    private final Component component;
11 19
 
12
-    public TitleBarReactButtonView(Context context, ReactInstanceManager reactInstanceManager, String componentId, String componentName) {
13
-        super(context, reactInstanceManager, componentId, componentName);
20
+    public TitleBarReactButtonView(Context context, ReactInstanceManager reactInstanceManager, Component component) {
21
+        super(context, reactInstanceManager, component.componentId.get(), component.name.get());
22
+        this.component = component;
14 23
     }
15 24
 
16 25
     @Override
17 26
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
18
-        super.onMeasure(MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED), heightMeasureSpec);
27
+        super.onMeasure(createSpec(widthMeasureSpec, component.width), createSpec(widthMeasureSpec, component.height));
28
+    }
29
+
30
+    private int createSpec(int measureSpec, Number dimension) {
31
+        if (dimension.hasValue()) {
32
+            return makeMeasureSpec(MeasureSpec.getSize(dpToPx(getContext(), dimension.get())), EXACTLY);
33
+        } else {
34
+            return makeMeasureSpec(MeasureSpec.getSize(measureSpec), UNSPECIFIED);
35
+        }
19 36
     }
20 37
 }

+ 28
- 11
lib/android/app/src/main/java/com/reactnativenavigation/views/topbar/TopBar.java View File

@@ -6,6 +6,7 @@ import android.graphics.Color;
6 6
 import android.graphics.Typeface;
7 7
 import android.os.Build;
8 8
 import android.view.Gravity;
9
+import android.view.MenuItem;
9 10
 import android.view.View;
10 11
 import android.view.ViewGroup;
11 12
 import android.widget.FrameLayout;
@@ -32,7 +33,6 @@ import java.util.List;
32 33
 import androidx.annotation.ColorInt;
33 34
 import androidx.annotation.NonNull;
34 35
 import androidx.annotation.VisibleForTesting;
35
-import androidx.appcompat.widget.Toolbar;
36 36
 import androidx.viewpager.widget.ViewPager;
37 37
 
38 38
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@@ -48,9 +48,14 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
48 48
     private View component;
49 49
     private float elevation = -1;
50 50
 
51
+    public int getRightButtonsCount() {
52
+        return titleBar.getRightButtonsCount();
53
+    }
54
+
51 55
     public TopBar(final Context context) {
52 56
         super(context);
53 57
         context.setTheme(R.style.TopBar);
58
+        setId(CompatUtils.generateViewId());
54 59
         collapsingBehavior = new TopBarCollapseBehavior(this);
55 60
         topTabs = new TopTabs(getContext());
56 61
         createLayout();
@@ -204,20 +209,12 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
204 209
         titleBar.setBackButton(backButton);
205 210
     }
206 211
 
207
-    public void setLeftButtons(List<TitleBarButtonController> leftButtons) {
208
-        titleBar.setLeftButtons(leftButtons);
209
-    }
210
-
211 212
     public void clearLeftButtons() {
212 213
         titleBar.setLeftButtons(Collections.emptyList());
213 214
     }
214 215
 
215
-    public void setRightButtons(List<TitleBarButtonController> rightButtons) {
216
-        titleBar.setRightButtons(rightButtons);
217
-    }
218
-
219 216
     public void clearRightButtons() {
220
-        titleBar.setRightButtons(Collections.emptyList());
217
+        titleBar.clearRightButtons();
221 218
     }
222 219
 
223 220
     public void setElevation(Double elevation) {
@@ -234,10 +231,18 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
234 231
         }
235 232
     }
236 233
 
237
-    public Toolbar getTitleBar() {
234
+    public TitleBar getTitleBar() {
238 235
         return titleBar;
239 236
     }
240 237
 
238
+    public List<MenuItem> getRightButtons() {
239
+        return titleBar.getRightButtons();
240
+    }
241
+
242
+    public MenuItem getRightButton(int index) {
243
+        return titleBar.getRightButton(getRightButtonsCount() - index - 1);
244
+    }
245
+
241 246
     public void initTopTabs(ViewPager viewPager) {
242 247
         topTabs.setVisibility(VISIBLE);
243 248
         topTabs.init(viewPager);
@@ -284,4 +289,16 @@ public class TopBar extends AppBarLayout implements ScrollEventListener.ScrollAw
284 289
     public void setLayoutDirection(LayoutDirection direction) {
285 290
         titleBar.setLayoutDirection(direction.get());
286 291
     }
292
+
293
+    public void removeRightButton(TitleBarButtonController button) {
294
+        removeRightButton(button.getButtonIntId());
295
+    }
296
+
297
+    public void removeRightButton(int buttonId) {
298
+        titleBar.removeRightButton(buttonId);
299
+    }
300
+
301
+    public boolean containsRightButton(TitleBarButtonController button) {
302
+        return titleBar.containsRightButton(button);
303
+    }
287 304
 }

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

@@ -7,7 +7,7 @@ import android.view.ViewGroup;
7 7
 
8 8
 import com.reactnativenavigation.mocks.TitleBarReactViewCreatorMock;
9 9
 import com.reactnativenavigation.mocks.TopBarBackgroundViewCreatorMock;
10
-import com.reactnativenavigation.mocks.TopBarButtonCreatorMock;
10
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
11 11
 import com.reactnativenavigation.parse.Options;
12 12
 import com.reactnativenavigation.parse.params.Bool;
13 13
 import com.reactnativenavigation.presentation.RenderChecker;
@@ -39,7 +39,7 @@ public class TestUtils {
39 39
                 .setId("stack")
40 40
                 .setChildRegistry(new ChildControllersRegistry())
41 41
                 .setTopBarController(topBarController)
42
-                .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), new IconResolver(activity, new ImageLoader()), new RenderChecker(), new Options()))
42
+                .setStackPresenter(new StackPresenter(activity, new TitleBarReactViewCreatorMock(), new TopBarBackgroundViewCreatorMock(), new TitleBarButtonCreatorMock(), new IconResolver(activity, new ImageLoader()), new RenderChecker(), new Options()))
43 43
                 .setInitialOptions(new Options());
44 44
     }
45 45
 

lib/android/app/src/test/java/com/reactnativenavigation/mocks/TopBarButtonCreatorMock.java → lib/android/app/src/test/java/com/reactnativenavigation/mocks/TitleBarButtonCreatorMock.java View File

@@ -3,18 +3,23 @@ package com.reactnativenavigation.mocks;
3 3
 import android.app.Activity;
4 4
 
5 5
 import com.facebook.react.ReactInstanceManager;
6
+import com.reactnativenavigation.parse.Component;
6 7
 import com.reactnativenavigation.react.events.ComponentType;
7
-import com.reactnativenavigation.viewcontrollers.ReactViewCreator;
8
+import com.reactnativenavigation.views.titlebar.TitleBarButtonCreator;
8 9
 import com.reactnativenavigation.views.titlebar.TitleBarReactButtonView;
9 10
 
10 11
 import static org.mockito.Mockito.mock;
11 12
 
12
-public class TopBarButtonCreatorMock implements ReactViewCreator {
13
+public class TitleBarButtonCreatorMock extends TitleBarButtonCreator {
14
+
15
+    public TitleBarButtonCreatorMock() {
16
+        super(null);
17
+    }
13 18
 
14 19
     @Override
15
-    public TitleBarReactButtonView create(Activity activity, String componentId, String componentName) {
20
+    public TitleBarReactButtonView create(Activity activity, Component component) {
16 21
         final ReactInstanceManager reactInstanceManager = mock(ReactInstanceManager.class);
17
-        return new TitleBarReactButtonView(activity, reactInstanceManager, componentId, componentName) {
22
+        return new TitleBarReactButtonView(activity, reactInstanceManager, component) {
18 23
             @Override
19 24
             public void sendComponentStart(ComponentType type) {
20 25
 

+ 4
- 12
lib/android/app/src/test/java/com/reactnativenavigation/utils/TitleBarHelper.java View File

@@ -3,22 +3,14 @@ package com.reactnativenavigation.utils;
3 3
 import android.app.Activity;
4 4
 
5 5
 import com.reactnativenavigation.mocks.ImageLoaderMock;
6
-import com.reactnativenavigation.mocks.TopBarButtonCreatorMock;
6
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
7 7
 import com.reactnativenavigation.parse.Component;
8 8
 import com.reactnativenavigation.parse.params.Button;
9 9
 import com.reactnativenavigation.parse.params.Text;
10 10
 import com.reactnativenavigation.viewcontrollers.TitleBarButtonController;
11 11
 import com.reactnativenavigation.viewcontrollers.button.IconResolver;
12
-import com.reactnativenavigation.views.titlebar.TitleBar;
13
-
14
-import androidx.appcompat.view.menu.ActionMenuItemView;
15
-import androidx.appcompat.widget.Toolbar;
16 12
 
17 13
 public class TitleBarHelper {
18
-    public static ActionMenuItemView getRightButton(Toolbar toolbar, int index) {
19
-        return (ActionMenuItemView) ViewUtils.findChildrenByClassRecursive(toolbar, ActionMenuItemView.class).get(toolbar.getMenu().size() - index - 1);
20
-    }
21
-
22 14
     public static Button textualButton(String text) {
23 15
         Button button = new Button();
24 16
         button.id = text + CompatUtils.generateViewId();
@@ -50,12 +42,12 @@ public class TitleBarHelper {
50 42
     }
51 43
 
52 44
 
53
-    public static TitleBarButtonController createButtonController(Activity activity, TitleBar titleBar, Button button) {
45
+    public static TitleBarButtonController createButtonController(Activity activity, Button button) {
54 46
         return new TitleBarButtonController(activity,
55 47
                 new IconResolver(activity, ImageLoaderMock.mock()),
56
-                new ButtonPresenter(titleBar, button),
48
+                new ButtonPresenter(button),
57 49
                 button,
58
-                new TopBarButtonCreatorMock(),
50
+                new TitleBarButtonCreatorMock(),
59 51
                 buttonId -> {}
60 52
         );
61 53
     }

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

@@ -14,7 +14,7 @@ import com.reactnativenavigation.mocks.Mocks;
14 14
 import com.reactnativenavigation.mocks.SimpleViewController;
15 15
 import com.reactnativenavigation.mocks.TitleBarReactViewCreatorMock;
16 16
 import com.reactnativenavigation.mocks.TopBarBackgroundViewCreatorMock;
17
-import com.reactnativenavigation.mocks.TopBarButtonCreatorMock;
17
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
18 18
 import com.reactnativenavigation.parse.Alignment;
19 19
 import com.reactnativenavigation.parse.Component;
20 20
 import com.reactnativenavigation.parse.Options;
@@ -57,6 +57,9 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
57 57
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
58 58
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
59 59
 import static com.reactnativenavigation.utils.CollectionUtils.*;
60
+import static java.util.Arrays.asList;
61
+import static java.util.Collections.singletonList;
62
+import static java.util.Objects.requireNonNull;
60 63
 import static org.assertj.core.api.Java6Assertions.assertThat;
61 64
 import static org.mockito.ArgumentMatchers.any;
62 65
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -99,8 +102,7 @@ public class StackPresenterTest extends BaseTest {
99 102
             }
100 103
         };
101 104
         renderChecker = spy(new RenderChecker());
102
-        IconResolver iconResolver = new IconResolver(activity, ImageLoaderMock.mock());
103
-        uut = spy(new StackPresenter(activity, titleViewCreator, new TopBarBackgroundViewCreatorMock(), new TopBarButtonCreatorMock(), iconResolver, renderChecker, new Options()));
105
+        uut = spy(new StackPresenter(activity, titleViewCreator, new TopBarBackgroundViewCreatorMock(), new TitleBarButtonCreatorMock(), new IconResolver(activity, ImageLoaderMock.mock()), renderChecker, new Options()));
104 106
         createTopBarController();
105 107
 
106 108
         parent = TestUtils.newStackController(activity)
@@ -119,7 +121,7 @@ public class StackPresenterTest extends BaseTest {
119 121
         Options o1 = new Options();
120 122
         o1.topBar.title.component = component(Alignment.Default);
121 123
         o1.topBar.background.component = component(Alignment.Default);
122
-        o1.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
124
+        o1.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
123 125
         uut.applyChildOptions(o1, parent, child);
124 126
 
125 127
         uut.isRendered(child.getView());
@@ -192,8 +194,8 @@ public class StackPresenterTest extends BaseTest {
192 194
     @Test
193 195
     public void mergeButtons() {
194 196
         uut.mergeChildOptions(EMPTY_OPTIONS, EMPTY_OPTIONS, parent, child);
195
-        verify(topBar, times(0)).setRightButtons(any());
196
-        verify(topBar, times(0)).setLeftButtons(any());
197
+        verify(topBarController, times(0)).applyRightButtons(any());
198
+        verify(topBarController, times(0)).setLeftButtons(any());
197 199
 
198 200
         Options options = new Options();
199 201
 
@@ -201,22 +203,22 @@ public class StackPresenterTest extends BaseTest {
201 203
         button.text = new Text("btn");
202 204
         options.topBar.buttons.right = new ArrayList<>(Collections.singleton(button));
203 205
         uut.mergeChildOptions(options, EMPTY_OPTIONS, parent, child);
204
-        verify(topBar, times(1)).setRightButtons(any());
206
+        verify(topBarController).mergeRightButtons(any(), any());
205 207
 
206 208
         options.topBar.buttons.left = new ArrayList<>();
207 209
         uut.mergeChildOptions(options, EMPTY_OPTIONS, parent, child);
208
-        verify(topBar, times(1)).setLeftButtons(any());
210
+        verify(topBarController).setLeftButtons(any());
209 211
     }
210 212
 
211 213
     @Test
212 214
     public void mergeButtons_previousRightButtonsAreDestroyed() {
213 215
         Options options = new Options();
214
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
216
+        options.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
215 217
         uut.applyChildOptions(options, parent, child);
216 218
         List<TitleBarButtonController> initialButtons = uut.getComponentButtons(child.getView());
217 219
         forEach(initialButtons, ViewController::ensureViewIsCreated);
218 220
 
219
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn2));
221
+        options.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn2));
220 222
         uut.mergeChildOptions(options, Options.EMPTY, parent, child);
221 223
         for (TitleBarButtonController button : initialButtons) {
222 224
             assertThat(button.isDestroyed()).isTrue();
@@ -224,35 +226,59 @@ public class StackPresenterTest extends BaseTest {
224 226
     }
225 227
 
226 228
     @Test
227
-    public void mergeButtons_mergingRightButtonsOnlyDestroysRightButtons() {
229
+    public void mergeRightButtons_mergingButtonsOnlyDestroysRightButtons() {
228 230
         Options a = new Options();
229
-        a.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
230
-        a.topBar.buttons.left = new ArrayList<>(Collections.singletonList(componentBtn2));
231
+        a.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
232
+        a.topBar.buttons.left = new ArrayList<>(singletonList(componentBtn2));
231 233
         uut.applyChildOptions(a, parent, child);
232 234
         List<TitleBarButtonController> initialButtons = uut.getComponentButtons(child.getView());
233 235
         forEach(initialButtons, ViewController::ensureViewIsCreated);
234 236
 
235 237
         Options b = new Options();
236
-        b.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn2));
238
+        b.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn2));
237 239
         uut.mergeChildOptions(b, Options.EMPTY, parent, child);
238 240
         assertThat(initialButtons.get(0).isDestroyed()).isTrue();
239 241
         assertThat(initialButtons.get(1).isDestroyed()).isFalse();
240 242
     }
241 243
 
242 244
     @Test
243
-    public void mergeButtons_mergingLeftButtonsOnlyDestroysLeftButtons() {
245
+    public void mergeRightButtons_buttonsAreCreatedOnlyIfNeeded() {
246
+        Options toApply = new Options();
247
+        toApply.topBar.buttons.right = new ArrayList<>(asList(textBtn1, componentBtn1));
248
+        uut.applyChildOptions(toApply, parent, child);
249
+
250
+        ArgumentCaptor<List<TitleBarButtonController>> captor1 = ArgumentCaptor.forClass(List.class);
251
+        verify(topBarController).applyRightButtons(captor1.capture());
252
+        assertThat(topBar.getTitleBar().getMenu().size()).isEqualTo(2);
253
+        List<TitleBarButtonController> appliedButtons = captor1.getValue();
254
+
255
+        Options toMerge = new Options();
256
+        toMerge.topBar.buttons.right = new ArrayList(requireNonNull(map(toApply.topBar.buttons.right, Button::copy)));
257
+        toMerge.topBar.buttons.right.add(1, componentBtn2);
258
+        uut.mergeChildOptions(toMerge, Options.EMPTY, parent, child);
259
+
260
+        assertThat(topBar.getTitleBar().getMenu().size()).isEqualTo(3);
261
+        ArgumentCaptor<List<TitleBarButtonController>> captor2 = ArgumentCaptor.forClass(List.class);
262
+        verify(topBarController).mergeRightButtons(captor2.capture(), any());
263
+        List<TitleBarButtonController> mergedButtons = captor2.getValue();
264
+        assertThat(mergedButtons).hasSize(3);
265
+        assertThat(appliedButtons.get(0)).isEqualTo(mergedButtons.get(0));
266
+        assertThat(appliedButtons.get(1)).isEqualTo(mergedButtons.get(2));
267
+    }
268
+
269
+    @Test
270
+    public void mergeButtons_mergingLeftButtonsDoesNotDestroyRightButtons() {
244 271
         Options a = new Options();
245
-        a.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
246
-        a.topBar.buttons.left = new ArrayList<>(Collections.singletonList(componentBtn2));
272
+        a.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
273
+        a.topBar.buttons.left = new ArrayList<>(singletonList(componentBtn2));
247 274
         uut.applyChildOptions(a, parent, child);
248 275
         List<TitleBarButtonController> initialButtons = uut.getComponentButtons(child.getView());
249 276
         forEach(initialButtons, ViewController::ensureViewIsCreated);
250 277
 
251 278
         Options b = new Options();
252
-        b.topBar.buttons.left = new ArrayList<>(Collections.singletonList(componentBtn2));
279
+        b.topBar.buttons.left = new ArrayList<>(singletonList(componentBtn2));
253 280
         uut.mergeChildOptions(b, Options.EMPTY, parent, child);
254 281
         assertThat(initialButtons.get(0).isDestroyed()).isFalse();
255
-        assertThat(initialButtons.get(1).isDestroyed()).isTrue();
256 282
     }
257 283
 
258 284
     @Test
@@ -423,14 +449,14 @@ public class StackPresenterTest extends BaseTest {
423 449
 
424 450
         uut.applyChildOptions(options, parent, child);
425 451
         ArgumentCaptor<List<TitleBarButtonController>> rightCaptor = ArgumentCaptor.forClass(List.class);
426
-        verify(topBar).setRightButtons(rightCaptor.capture());
452
+        verify(topBarController).applyRightButtons(rightCaptor.capture());
427 453
         assertThat(rightCaptor.getValue().get(0).getButton().color.get()).isEqualTo(options.topBar.rightButtonColor.get());
428 454
         assertThat(rightCaptor.getValue().get(1).getButton().color.get()).isEqualTo(options.topBar.rightButtonColor.get());
429 455
         assertThat(rightCaptor.getValue().get(0)).isNotEqualTo(rightButton1);
430 456
         assertThat(rightCaptor.getValue().get(1)).isNotEqualTo(rightButton2);
431 457
 
432 458
         ArgumentCaptor<List<TitleBarButtonController>> leftCaptor = ArgumentCaptor.forClass(List.class);
433
-        verify(topBar).setLeftButtons(leftCaptor.capture());
459
+        verify(topBarController).setLeftButtons(leftCaptor.capture());
434 460
         assertThat(leftCaptor.getValue().get(0).getButton().color).isEqualTo(options.topBar.leftButtonColor);
435 461
         assertThat(leftCaptor.getValue().get(0)).isNotEqualTo(leftButton);
436 462
     }
@@ -468,14 +494,14 @@ public class StackPresenterTest extends BaseTest {
468 494
 
469 495
         uut.mergeChildOptions(options2, appliedOptions, parent, child);
470 496
         ArgumentCaptor<List<TitleBarButtonController>> rightCaptor = ArgumentCaptor.forClass(List.class);
471
-        verify(topBar, times(1)).setRightButtons(rightCaptor.capture());
497
+        verify(topBarController).mergeRightButtons(rightCaptor.capture(), any());
472 498
         assertThat(rightCaptor.getValue().get(0).getButton().color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get());
473 499
         assertThat(rightCaptor.getValue().get(1).getButton().color.get()).isEqualTo(appliedOptions.topBar.rightButtonColor.get());
474 500
         assertThat(rightCaptor.getValue().get(0)).isNotEqualTo(rightButton1);
475 501
         assertThat(rightCaptor.getValue().get(1)).isNotEqualTo(rightButton2);
476 502
 
477 503
         ArgumentCaptor<List<TitleBarButtonController>> leftCaptor = ArgumentCaptor.forClass(List.class);
478
-        verify(topBar, times(1)).setLeftButtons(leftCaptor.capture());
504
+        verify(topBarController).setLeftButtons(leftCaptor.capture());
479 505
         assertThat(leftCaptor.getValue().get(0).getButton().color.get()).isEqualTo(appliedOptions.topBar.leftButtonColor.get());
480 506
         assertThat(leftCaptor.getValue().get(0)).isNotEqualTo(leftButton);
481 507
     }
@@ -500,14 +526,14 @@ public class StackPresenterTest extends BaseTest {
500 526
 
501 527
         uut.mergeChildOptions(options2, resolvedOptions, parent, child);
502 528
         ArgumentCaptor<List<TitleBarButtonController>> rightCaptor = ArgumentCaptor.forClass(List.class);
503
-        verify(topBar).setRightButtons(rightCaptor.capture());
529
+        verify(topBarController).mergeRightButtons(rightCaptor.capture(), any());
504 530
         assertThat(rightCaptor.getValue().get(0).getButton().color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get());
505 531
         assertThat(rightCaptor.getValue().get(1).getButton().color.get()).isEqualTo(resolvedOptions.topBar.rightButtonColor.get());
506 532
         assertThat(rightCaptor.getValue().get(0)).isNotEqualTo(rightButton1);
507 533
         assertThat(rightCaptor.getValue().get(1)).isNotEqualTo(rightButton2);
508 534
 
509 535
         ArgumentCaptor<List<TitleBarButtonController>> leftCaptor = ArgumentCaptor.forClass(List.class);
510
-        verify(topBar).setLeftButtons(leftCaptor.capture());
536
+        verify(topBarController).setLeftButtons(leftCaptor.capture());
511 537
         assertThat(leftCaptor.getValue().get(0).getButton().color.get()).isEqualTo(resolvedOptions.topBar.leftButtonColor.get());
512 538
         assertThat(leftCaptor.getValue().get(0)).isNotEqualTo(leftButton);
513 539
     }
@@ -515,14 +541,14 @@ public class StackPresenterTest extends BaseTest {
515 541
     @Test
516 542
     public void getButtonControllers_buttonControllersArePassedToTopBar() {
517 543
         Options options = new Options();
518
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(textBtn1));
519
-        options.topBar.buttons.left = new ArrayList<>(Collections.singletonList(textBtn1));
544
+        options.topBar.buttons.right = new ArrayList<>(singletonList(textBtn1));
545
+        options.topBar.buttons.left = new ArrayList<>(singletonList(textBtn1));
520 546
         uut.applyChildOptions(options, parent, child);
521 547
 
522 548
         ArgumentCaptor<List<TitleBarButtonController>> rightCaptor = ArgumentCaptor.forClass(List.class);
523 549
         ArgumentCaptor<List<TitleBarButtonController>> leftCaptor = ArgumentCaptor.forClass(List.class);
524
-        verify(topBar).setRightButtons(rightCaptor.capture());
525
-        verify(topBar).setLeftButtons(leftCaptor.capture());
550
+        verify(topBarController).applyRightButtons(rightCaptor.capture());
551
+        verify(topBarController).setLeftButtons(leftCaptor.capture());
526 552
 
527 553
         assertThat(rightCaptor.getValue().size()).isOne();
528 554
         assertThat(leftCaptor.getValue().size()).isOne();
@@ -531,8 +557,8 @@ public class StackPresenterTest extends BaseTest {
531 557
     @Test
532 558
     public void getButtonControllers_storesButtonsByComponent() {
533 559
         Options options = new Options();
534
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(textBtn1));
535
-        options.topBar.buttons.left = new ArrayList<>(Collections.singletonList(textBtn2));
560
+        options.topBar.buttons.right = new ArrayList<>(singletonList(textBtn1));
561
+        options.topBar.buttons.left = new ArrayList<>(singletonList(textBtn2));
536 562
         uut.applyChildOptions(options, parent, child);
537 563
 
538 564
         List<TitleBarButtonController> componentButtons = uut.getComponentButtons(child.getView());
@@ -544,8 +570,8 @@ public class StackPresenterTest extends BaseTest {
544 570
     @Test
545 571
     public void getButtonControllers_createdOnce() {
546 572
         Options options = new Options();
547
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(textBtn1));
548
-        options.topBar.buttons.left = new ArrayList<>(Collections.singletonList(textBtn2));
573
+        options.topBar.buttons.right = new ArrayList<>(singletonList(textBtn1));
574
+        options.topBar.buttons.left = new ArrayList<>(singletonList(textBtn2));
549 575
 
550 576
         uut.applyChildOptions(options, parent, child);
551 577
         List<TitleBarButtonController> buttons1 = uut.getComponentButtons(child.getView());
@@ -560,8 +586,8 @@ public class StackPresenterTest extends BaseTest {
560 586
     @Test
561 587
     public void applyButtons_doesNotDestroyOtherComponentButtons() {
562 588
         Options options = new Options();
563
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
564
-        options.topBar.buttons.left = new ArrayList<>(Collections.singletonList(componentBtn2));
589
+        options.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
590
+        options.topBar.buttons.left = new ArrayList<>(singletonList(componentBtn2));
565 591
         uut.applyChildOptions(options, parent, child);
566 592
         List<TitleBarButtonController> buttons = uut.getComponentButtons(child.getView());
567 593
         forEach(buttons, ViewController::ensureViewIsCreated);
@@ -575,8 +601,8 @@ public class StackPresenterTest extends BaseTest {
575 601
     @Test
576 602
     public void onChildDestroyed_destroyedButtons() {
577 603
         Options options = new Options();
578
-        options.topBar.buttons.right = new ArrayList<>(Collections.singletonList(componentBtn1));
579
-        options.topBar.buttons.left = new ArrayList<>(Collections.singletonList(componentBtn2));
604
+        options.topBar.buttons.right = new ArrayList<>(singletonList(componentBtn1));
605
+        options.topBar.buttons.left = new ArrayList<>(singletonList(componentBtn2));
580 606
         uut.applyChildOptions(options, parent, child);
581 607
         List<TitleBarButtonController> buttons = uut.getComponentButtons(child.getView());
582 608
         forEach(buttons, ViewController::ensureViewIsCreated);

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

@@ -0,0 +1,61 @@
1
+package com.reactnativenavigation.viewcontrollers;
2
+
3
+import android.app.Activity;
4
+import android.view.MenuItem;
5
+
6
+import com.reactnativenavigation.BaseTest;
7
+import com.reactnativenavigation.mocks.ImageLoaderMock;
8
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
9
+import com.reactnativenavigation.parse.params.Button;
10
+import com.reactnativenavigation.parse.params.Text;
11
+import com.reactnativenavigation.utils.ButtonPresenter;
12
+import com.reactnativenavigation.viewcontrollers.button.IconResolver;
13
+import com.reactnativenavigation.views.titlebar.TitleBar;
14
+
15
+import org.junit.Test;
16
+import org.mockito.Mockito;
17
+
18
+import static org.assertj.core.api.Java6Assertions.assertThat;
19
+
20
+public class TitleBarButtonControllerTest extends BaseTest {
21
+    private TitleBarButtonController uut;
22
+    private TitleBar titleBar;
23
+
24
+    @Override
25
+    public void beforeEach() {
26
+        Activity activity = newActivity();
27
+        titleBar = new TitleBar(activity);
28
+
29
+        Button button = createComponentButton();
30
+        uut = new TitleBarButtonController(
31
+                activity,
32
+                new IconResolver(activity, ImageLoaderMock.mock()),
33
+                new ButtonPresenter(button),
34
+                button,
35
+                new TitleBarButtonCreatorMock(),
36
+                Mockito.mock(TitleBarButtonController.OnClickListener.class)
37
+        );
38
+    }
39
+
40
+    @Test
41
+    public void addToMenu_componentButtonIsNotRecreatedIfAlreadyAddedWithSameOrder() {
42
+        uut.addToMenu(titleBar, 0);
43
+        MenuItem first = titleBar.getRightButton(0);
44
+
45
+        uut.addToMenu(titleBar, 0);
46
+        MenuItem second = titleBar.getRightButton(0);
47
+        assertThat(first).isEqualTo(second);
48
+
49
+        uut.addToMenu(titleBar, 1);
50
+        MenuItem third = titleBar.getRightButton(0);
51
+        assertThat(third).isNotEqualTo(second);
52
+    }
53
+
54
+    private Button createComponentButton() {
55
+        Button componentButton = new Button();
56
+        componentButton.id = "customBtn";
57
+        componentButton.component.name = new Text("com.rnn.customBtn");
58
+        componentButton.component.componentId = new Text("component4");
59
+        return componentButton;
60
+    }
61
+}

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

@@ -6,25 +6,16 @@ import android.widget.TextView;
6 6
 
7 7
 import com.reactnativenavigation.BaseTest;
8 8
 import com.reactnativenavigation.TestUtils;
9
-import com.reactnativenavigation.parse.params.Button;
10
-import com.reactnativenavigation.parse.params.Text;
11
-import com.reactnativenavigation.react.Constants;
12
-import com.reactnativenavigation.react.ReactView;
13
-import com.reactnativenavigation.utils.CollectionUtils;
14 9
 import com.reactnativenavigation.views.titlebar.TitleBar;
15 10
 
16 11
 import org.junit.Test;
17 12
 import org.mockito.Mockito;
18 13
 
19
-import java.util.ArrayList;
20
-import java.util.Arrays;
21 14
 import java.util.Collections;
22
-import java.util.List;
23 15
 
24 16
 import androidx.appcompat.widget.ActionMenuView;
25 17
 
26 18
 import static com.reactnativenavigation.utils.Assertions.assertNotNull;
27
-import static com.reactnativenavigation.utils.TitleBarHelper.createButtonController;
28 19
 import static com.reactnativenavigation.utils.ViewUtils.findChildByClass;
29 20
 import static org.assertj.core.api.Java6Assertions.assertThat;
30 21
 import static org.mockito.Mockito.any;
@@ -37,67 +28,14 @@ import static org.mockito.Mockito.when;
37 28
 public class TitleBarTest extends BaseTest {
38 29
 
39 30
     private TitleBar uut;
40
-    private Button leftButton;
41
-    private Button textButton;
42
-    private Button customButton;
43 31
     private Activity activity;
44 32
 
45 33
     @Override
46 34
     public void beforeEach() {
47 35
         activity = newActivity();
48
-        createButtons();
49 36
         uut = spy(new TitleBar(activity));
50 37
     }
51 38
 
52
-    private void createButtons() {
53
-        leftButton = new Button();
54
-        leftButton.id = Constants.BACK_BUTTON_ID;
55
-
56
-        textButton = new Button();
57
-        textButton.id = "textButton";
58
-        textButton.text = new Text("Btn");
59
-
60
-        customButton = new Button();
61
-        customButton.id = "customBtn";
62
-        customButton.component.name = new Text("com.rnn.customBtn");
63
-        customButton.component.componentId = new Text("component4");
64
-    }
65
-
66
-    @Test
67
-    public void setButton_setsTextButton() {
68
-        uut.setRightButtons(rightButtons(textButton));
69
-        uut.setLeftButtons(leftButton(leftButton));
70
-        assertThat(uut.getMenu().getItem(0).getTitle()).isEqualTo(textButton.text.get());
71
-    }
72
-
73
-    @Test
74
-    public void setButton_setsCustomButton() {
75
-        uut.setLeftButtons(leftButton(leftButton));
76
-        uut.setRightButtons(rightButtons(customButton));
77
-        ReactView btnView = (ReactView) uut.getMenu().getItem(0).getActionView();
78
-        assertThat(btnView.getComponentName()).isEqualTo(customButton.component.name.get());
79
-    }
80
-
81
-    @Test
82
-    public void setRightButtons_emptyButtonsListClearsRightButtons() {
83
-        uut.setLeftButtons(new ArrayList<>());
84
-        uut.setRightButtons(rightButtons(customButton, textButton));
85
-        uut.setLeftButtons(new ArrayList<>());
86
-        uut.setRightButtons(new ArrayList<>());
87
-        assertThat(uut.getMenu().size()).isEqualTo(0);
88
-    }
89
-
90
-    @Test
91
-    public void setLeftButtons_emptyButtonsListClearsLeftButton() {
92
-        uut.setLeftButtons(leftButton(leftButton));
93
-        uut.setRightButtons(rightButtons(customButton));
94
-        assertThat(uut.getNavigationIcon()).isNotNull();
95
-
96
-        uut.setLeftButtons(new ArrayList<>());
97
-        uut.setRightButtons(rightButtons(textButton));
98
-        assertThat(uut.getNavigationIcon()).isNull();
99
-    }
100
-
101 39
     @Test
102 40
     public void setLeftButton_titleIsAligned() {
103 41
         uut.setTitle("Title");
@@ -110,13 +48,6 @@ public class TitleBarTest extends BaseTest {
110 48
         verify(uut).alignTextView(any(), eq(title));
111 49
     }
112 50
 
113
-    @Test
114
-    public void setRightButtons_buttonsAreAddedInReverseOrderToMatchOrderOnIOs() {
115
-        uut.setLeftButtons(new ArrayList<>());
116
-        uut.setRightButtons(rightButtons(textButton, customButton));
117
-        assertThat(uut.getMenu().getItem(1).getTitle()).isEqualTo(textButton.text.get());
118
-    }
119
-
120 51
     @Test
121 52
     public void setComponent_addsComponentToTitleBar() {
122 53
         View component = new View(activity);
@@ -161,12 +92,4 @@ public class TitleBarTest extends BaseTest {
161 92
         uut.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
162 93
         verify(spy).setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
163 94
     }
164
-
165
-    private List<TitleBarButtonController> leftButton(Button leftButton) {
166
-        return Collections.singletonList(createButtonController(activity, uut, leftButton));
167
-    }
168
-
169
-    private List<TitleBarButtonController> rightButtons(Button... buttons) {
170
-        return CollectionUtils.map(Arrays.asList(buttons), button -> createButtonController(activity, uut, button));
171
-    }
172 95
 }

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

@@ -2,13 +2,12 @@ package com.reactnativenavigation.viewcontrollers;
2 2
 
3 3
 import android.app.Activity;
4 4
 import android.graphics.Color;
5
-import android.graphics.Typeface;
6 5
 import android.view.MenuItem;
7 6
 
8 7
 import com.reactnativenavigation.BaseTest;
9 8
 import com.reactnativenavigation.TestUtils;
10 9
 import com.reactnativenavigation.mocks.ImageLoaderMock;
11
-import com.reactnativenavigation.mocks.TopBarButtonCreatorMock;
10
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
12 11
 import com.reactnativenavigation.parse.params.Bool;
13 12
 import com.reactnativenavigation.parse.params.Button;
14 13
 import com.reactnativenavigation.parse.params.Colour;
@@ -18,11 +17,10 @@ import com.reactnativenavigation.parse.params.Text;
18 17
 import com.reactnativenavigation.utils.ButtonPresenter;
19 18
 import com.reactnativenavigation.viewcontrollers.button.IconResolver;
20 19
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
20
+import com.reactnativenavigation.views.titlebar.TitleBar;
21 21
 
22 22
 import org.junit.Test;
23 23
 
24
-import androidx.appcompat.widget.Toolbar;
25
-
26 24
 import static org.assertj.core.api.Java6Assertions.assertThat;
27 25
 import static org.mockito.ArgumentMatchers.any;
28 26
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -44,13 +42,13 @@ public class TopBarButtonControllerTest extends BaseTest {
44 42
         button = new Button();
45 43
         final Activity activity = newActivity();
46 44
 
47
-        TopBarButtonCreatorMock buttonCreatorMock = new TopBarButtonCreatorMock();
45
+        TitleBarButtonCreatorMock buttonCreatorMock = new TitleBarButtonCreatorMock();
48 46
         stackController = spy(TestUtils.newStackController(activity).build());
49 47
         stackController.getView().layout(0, 0, 1080, 1920);
50 48
         stackController.getTopBar().layout(0, 0, 1080, 200);
51 49
         getTitleBar().layout(0, 0, 1080, 200);
52 50
 
53
-        optionsPresenter = spy(new ButtonPresenter(getTitleBar(), button));
51
+        optionsPresenter = spy(new ButtonPresenter(button));
54 52
         uut = new TitleBarButtonController(activity, new IconResolver(activity, ImageLoaderMock.mock()), optionsPresenter, button, buttonCreatorMock, (buttonId) -> {});
55 53
 
56 54
         stackController.ensureViewIsCreated();
@@ -98,71 +96,10 @@ public class TopBarButtonControllerTest extends BaseTest {
98 96
         verify(optionsPresenter, times(0)).tint(any(), anyInt());
99 97
     }
100 98
 
101
-    @Test
102
-    public void fontFamily() {
103
-        setTextButton();
104
-        uut.addToMenu(getTitleBar(), 0);
105
-        verify(optionsPresenter, times(1)).setTypeFace(Typeface.MONOSPACE);
106
-    }
107
-
108
-    @Test
109
-    public void fontSize() {
110
-        setTextButton();
111
-        uut.addToMenu(getTitleBar(), 0);
112
-        verify(optionsPresenter, times(0)).setFontSize(getTitleBar().getMenu().getItem(0));
113
-
114
-        clearMenu();
115
-        button.fontSize = new Number(10);
116
-        uut.addToMenu(getTitleBar(), 0);
117
-        verify(optionsPresenter, times(1)).setFontSize(getTitleBar().getMenu().getItem(0));
118
-    }
119
-
120
-    @Test
121
-    public void textColor_enabled() {
122
-        setTextButton();
123
-        button.enabled = new Bool(false);
124
-        uut.addToMenu(getTitleBar(), 0);
125
-        dispatchPreDraw(getTitleBar());
126
-        verify(optionsPresenter, times(0)).setEnabledColor(any());
127
-
128
-        clearMenu();
129
-        button.enabled = new Bool(true);
130
-        button.color = new Colour(android.graphics.Color.RED);
131
-        uut.addToMenu(getTitleBar(), 0);
132
-        dispatchPreDraw(getTitleBar());
133
-        verify(optionsPresenter, times(1)).setEnabledColor(any());
134
-    }
135
-
136
-    private void clearMenu() {
137
-        getTitleBar().getMenu().clear();
138
-    }
139
-
140
-    @Test
141
-    public void textColor_disabled() {
142
-        setTextButton();
143
-        button.enabled = new Bool(false);
144
-        uut.addToMenu(getTitleBar(), 0);
145
-        dispatchPreDraw(getTitleBar());
146
-        verify(optionsPresenter, times(1)).setDisabledColor(any(), eq(Color.LTGRAY));
147
-
148
-        clearMenu();
149
-        button.disabledColor = new Colour(android.graphics.Color.BLACK);
150
-        uut.addToMenu(getTitleBar(), 0);
151
-        dispatchPreDraw(getTitleBar());
152
-        verify(optionsPresenter, times(1)).setDisabledColor(any(), eq(Color.BLACK));
153
-    }
154
-
155
-    private Toolbar getTitleBar() {
99
+    private TitleBar getTitleBar() {
156 100
         return stackController.getTopBar().getTitleBar();
157 101
     }
158 102
 
159
-    private void setTextButton() {
160
-        button.id = "btn1";
161
-        button.text = new Text("Button");
162
-        button.fontFamily = Typeface.MONOSPACE;
163
-        button.showAsAction = new Number(MenuItem.SHOW_AS_ACTION_ALWAYS);
164
-    }
165
-
166 103
     private void setIconButton(boolean enabled) {
167 104
         button.id = "btn1";
168 105
         button.icon = new Text("someIcon");

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

@@ -0,0 +1,144 @@
1
+package com.reactnativenavigation.viewcontrollers;
2
+
3
+import android.app.Activity;
4
+
5
+import com.reactnativenavigation.BaseTest;
6
+import com.reactnativenavigation.parse.params.Button;
7
+import com.reactnativenavigation.parse.params.Text;
8
+import com.reactnativenavigation.react.Constants;
9
+import com.reactnativenavigation.react.ReactView;
10
+import com.reactnativenavigation.viewcontrollers.topbar.TopBarController;
11
+import com.reactnativenavigation.views.StackLayout;
12
+
13
+import org.junit.Test;
14
+import org.mockito.Mockito;
15
+
16
+import java.util.ArrayList;
17
+import java.util.Arrays;
18
+import java.util.Collections;
19
+import java.util.List;
20
+
21
+import static com.reactnativenavigation.utils.CollectionUtils.*;
22
+import static com.reactnativenavigation.utils.TitleBarHelper.createButtonController;
23
+import static org.assertj.core.api.Java6Assertions.assertThat;
24
+import static org.mockito.Mockito.spy;
25
+
26
+public class TopBarControllerTest extends BaseTest {
27
+    private TopBarController uut;
28
+    private Activity activity;
29
+    private Button leftButton;
30
+    private Button textButton1;
31
+    private Button textButton2;
32
+    private Button componentButton;
33
+
34
+    @Override
35
+    public void beforeEach() {
36
+        activity = newActivity();
37
+        uut = spy(new TopBarController());
38
+        StackLayout stack = Mockito.mock(StackLayout.class);
39
+        uut.createView(activity, stack);
40
+
41
+        createButtons();
42
+    }
43
+
44
+    @Test
45
+    public void setButton_setsTextButton() {
46
+        uut.applyRightButtons(rightButtons(textButton1));
47
+        uut.setLeftButtons(leftButton(leftButton));
48
+        assertThat(uut.getRightButton(0).getTitle().toString()).isEqualTo(textButton1.text.get());
49
+    }
50
+
51
+    @Test
52
+    public void setButton_setsCustomButton() {
53
+        uut.setLeftButtons(leftButton(leftButton));
54
+        uut.applyRightButtons(rightButtons(componentButton));
55
+        ReactView btnView = (ReactView) uut.getRightButton(0).getActionView();
56
+        assertThat(btnView.getComponentName()).isEqualTo(componentButton.component.name.get());
57
+    }
58
+
59
+    @Test
60
+    public void applyRightButtons_emptyButtonsListClearsRightButtons() {
61
+        uut.setLeftButtons(new ArrayList<>());
62
+        uut.applyRightButtons(rightButtons(componentButton, textButton1));
63
+        uut.setLeftButtons(new ArrayList<>());
64
+        uut.applyRightButtons(new ArrayList<>());
65
+        assertThat(uut.getRightButtonsCount()).isEqualTo(0);
66
+    }
67
+
68
+    @Test
69
+    public void applyRightButtons_previousButtonsAreCleared() {
70
+        uut.applyRightButtons(rightButtons(textButton1, componentButton));
71
+        assertThat(uut.getRightButtonsCount()).isEqualTo(2);
72
+
73
+        uut.applyRightButtons(rightButtons(textButton2));
74
+        assertThat(uut.getRightButtonsCount()).isEqualTo(1);
75
+    }
76
+
77
+    @Test
78
+    public void applyRightButtons_buttonsAreAddedInReverseOrderToMatchOrderOnIOs() {
79
+        uut.setLeftButtons(new ArrayList<>());
80
+        uut.applyRightButtons(rightButtons(textButton1, componentButton));
81
+        assertThat(uut.getRightButton(1).getTitle().toString()).isEqualTo(textButton1.text.get());
82
+    }
83
+
84
+    @Test
85
+    public void applyRightButtons_componentButtonIsReapplied() {
86
+        List<TitleBarButtonController> initialButtons = rightButtons(componentButton);
87
+        uut.applyRightButtons(initialButtons);
88
+        assertThat(uut.getRightButton(0).getItemId()).isEqualTo(componentButton.getIntId());
89
+
90
+        uut.applyRightButtons(rightButtons(textButton1));
91
+        assertThat(uut.getRightButton(0).getItemId()).isEqualTo(textButton1.getIntId());
92
+
93
+        uut.applyRightButtons(initialButtons);
94
+        assertThat(uut.getRightButton(0).getItemId()).isEqualTo(componentButton.getIntId());
95
+    }
96
+
97
+    @Test
98
+    public void mergeRightButtons_componentButtonIsNotAddedIfAlreadyAddedToMenu() {
99
+        List<TitleBarButtonController> initialButtons = rightButtons(componentButton);
100
+        uut.applyRightButtons(initialButtons);
101
+
102
+        uut.mergeRightButtons(initialButtons, Collections.EMPTY_LIST);
103
+
104
+    }
105
+
106
+    @Test
107
+    public void setLeftButtons_emptyButtonsListClearsLeftButton() {
108
+        uut.setLeftButtons(leftButton(leftButton));
109
+        uut.applyRightButtons(rightButtons(componentButton));
110
+        assertThat(uut.getLeftButton()).isNotNull();
111
+
112
+        uut.setLeftButtons(new ArrayList<>());
113
+        uut.applyRightButtons(rightButtons(textButton1));
114
+        assertThat(uut.getLeftButton()).isNull();
115
+    }
116
+
117
+    private void createButtons() {
118
+        leftButton = new Button();
119
+        leftButton.id = Constants.BACK_BUTTON_ID;
120
+
121
+        textButton1 = createTextButton("1");
122
+        textButton2 = createTextButton("2");
123
+
124
+        componentButton = new Button();
125
+        componentButton.id = "customBtn";
126
+        componentButton.component.name = new Text("com.rnn.customBtn");
127
+        componentButton.component.componentId = new Text("component4");
128
+    }
129
+
130
+    private Button createTextButton(String id) {
131
+        Button button = new Button();
132
+        button.id = id;
133
+        button.text = new Text("txt" + id);
134
+        return button;
135
+    }
136
+
137
+    private List<TitleBarButtonController> leftButton(Button leftButton) {
138
+        return Collections.singletonList(createButtonController(activity, leftButton));
139
+    }
140
+
141
+    private List<TitleBarButtonController> rightButtons(Button... buttons) {
142
+        return map(Arrays.asList(buttons), button -> createButtonController(activity, button));
143
+    }
144
+}

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

@@ -14,7 +14,7 @@ import com.reactnativenavigation.mocks.ImageLoaderMock;
14 14
 import com.reactnativenavigation.mocks.SimpleViewController;
15 15
 import com.reactnativenavigation.mocks.TitleBarReactViewCreatorMock;
16 16
 import com.reactnativenavigation.mocks.TopBarBackgroundViewCreatorMock;
17
-import com.reactnativenavigation.mocks.TopBarButtonCreatorMock;
17
+import com.reactnativenavigation.mocks.TitleBarButtonCreatorMock;
18 18
 import com.reactnativenavigation.parse.AnimationOptions;
19 19
 import com.reactnativenavigation.parse.NestedAnimationsOptions;
20 20
 import com.reactnativenavigation.parse.Options;
@@ -102,7 +102,7 @@ public class StackControllerTest extends BaseTest {
102 102
                     activity,
103 103
                     new TitleBarReactViewCreatorMock(),
104 104
                     new TopBarBackgroundViewCreatorMock(),
105
-                    new TopBarButtonCreatorMock(),
105
+                    new TitleBarButtonCreatorMock(),
106 106
                     new IconResolver(activity, ImageLoaderMock.mock()),
107 107
                     new RenderChecker(),
108 108
                     new Options()

+ 8
- 0
lib/src/interfaces/Options.ts View File

@@ -339,6 +339,14 @@ export interface OptionsTopBarButton {
339 339
      * Properties to pass down to the component
340 340
      */
341 341
     passProps?: object;
342
+    /**
343
+     * (Android only) component width
344
+     */
345
+    width?: number;
346
+    /**
347
+     * (Android only) component height
348
+     */
349
+    height?: number;
342 350
   };
343 351
   /**
344 352
    * (iOS only) Set the button as an iOS system icon

+ 8
- 2
playground/src/screens/OptionsScreen.js View File

@@ -40,7 +40,7 @@ class Options extends Component {
40 40
         <Button label='Set React Title View' testID={SET_REACT_TITLE_VIEW} onPress={this.setReactTitleView} />
41 41
         <Button label='Show Yellow Box' testID={SHOW_YELLOW_BOX_BTN} onPress={() => console.warn('Yellow Box')} />
42 42
         <Button label='StatusBar' onPress={this.statusBarScreen} />
43
-        <Button label='Buttons Screen' testID={GOTO_BUTTONS_SCREEN} onPress={this.goToButtonsScreen} />
43
+        <Button label='Buttons Screen' testID={GOTO_BUTTONS_SCREEN} onPress={this.pushButtonsScreen} />
44 44
       </Root>
45 45
     );
46 46
   }
@@ -101,7 +101,13 @@ class Options extends Component {
101 101
 
102 102
   statusBarScreen = () => Navigation.showModal(Screens.StatusBar);
103 103
 
104
-  goToButtonsScreen = () => Navigation.push(this, Screens.Buttons);
104
+  pushButtonsScreen = () => Navigation.push(this, Screens.Buttons, {
105
+    animations: {
106
+      push: {
107
+        waitForRender: true
108
+      }
109
+    }
110
+  });
105 111
 }
106 112
 
107 113
 module.exports = Options;

+ 2
- 1
playground/src/screens/RoundedButton.js View File

@@ -39,7 +39,8 @@ const styles = StyleSheet.create({
39 39
     backgroundColor: 'transparent',
40 40
     flexDirection: 'column',
41 41
     justifyContent: 'center',
42
-    alignItems: 'center'
42
+    alignItems: 'center',
43
+    padding: 4
43 44
   },
44 45
   button: {
45 46
     width: 40,

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

@@ -50,6 +50,7 @@ module.exports = {
50 50
   SHOW_TABS_BTN: 'SHOW_TABS_BTN',
51 51
   HIDE_TABS_PUSH_BTN: 'HIDE_TABS_PUSH_BTN',
52 52
   ROUND_BUTTON: 'ROUND_BUTTON',
53
+  ROUND_BUTTON_2: 'ROUND_BUTTON_2',
53 54
   BUTTON_ONE: 'BUTTON_ONE',
54 55
   LEFT_BUTTON: 'LEFT_BUTTON',
55 56
   HIDE_TOPBAR_DEFAULT_OPTIONS: 'HIDE_TOPBAR_DEFAULT_OPTIONS',
@@ -92,6 +93,7 @@ module.exports = {
92 93
   NAVIGATION_SCREEN: `NAVIGATION_SCREEN`,
93 94
 
94 95
   // Buttons
96
+  ADD_BUTTON: 'ADD_BUTTON',
95 97
   TAB_BASED_APP_BUTTON: `TAB_BASED_APP_BUTTON`,
96 98
   TAB_BASED_APP_SIDE_BUTTON: `TAB_BASED_APP_SIDE_BUTTON`,
97 99
   PUSH_STATIC_LIFECYCLE_BUTTON: `PUSH_STATIC_LIFECYCLE_BUTTON`,