Преглед на файлове

Fix flickering FAB when changing bottom tabs (#6225)

* Fix flickering FAB when changing bottom tabs

Fab was instantiated with the default margin (16dp), without the containing screen's bottom inset. The caused the FAB to jump up when the screen appeared since it then received the correct bottom margins (16dp + bottom insets)

Fixes #5816
Guy Carmeli преди 4 години
родител
ревизия
9a8bc54dd4
No account linked to committer's email address

+ 61
- 31
lib/android/app/src/main/java/com/reactnativenavigation/presentation/FabPresenter.java Целия файл

@@ -1,12 +1,15 @@
1 1
 package com.reactnativenavigation.presentation;
2 2
 
3 3
 
4
+import android.animation.Animator;
5
+import android.animation.AnimatorListenerAdapter;
4 6
 import android.view.Gravity;
5 7
 import android.view.View;
6 8
 import android.view.ViewGroup;
7 9
 
8 10
 import com.reactnativenavigation.R;
9 11
 import com.reactnativenavigation.parse.FabOptions;
12
+import com.reactnativenavigation.utils.UiUtils;
10 13
 import com.reactnativenavigation.viewcontrollers.ViewController;
11 14
 import com.reactnativenavigation.views.Fab;
12 15
 import com.reactnativenavigation.views.FabMenu;
@@ -20,28 +23,28 @@ import static com.github.clans.fab.FloatingActionButton.SIZE_NORMAL;
20 23
 import static com.reactnativenavigation.utils.ObjectUtils.perform;
21 24
 
22 25
 public class FabPresenter {
26
+    private static final int DURATION = 200;
27
+
23 28
     private ViewGroup viewGroup;
24
-    private ViewController component;
25 29
 
26 30
     private Fab fab;
27 31
     private FabMenu fabMenu;
28 32
 
29 33
     public void applyOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
30 34
         this.viewGroup = viewGroup;
31
-        this.component = component;
32 35
 
33 36
         if (options.id.hasValue()) {
34 37
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
35 38
                 fabMenu.bringToFront();
36
-                applyFabMenuOptions(fabMenu, options);
37
-                setParams(fabMenu, options);
39
+                applyFabMenuOptions(component, fabMenu, options);
40
+                setParams(component, fabMenu, options);
38 41
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
39 42
                 fab.bringToFront();
40
-                applyFabOptions(fab, options);
41
-                setParams(fab, options);
43
+                setParams(component, fab, options);
44
+                applyFabOptions(component, fab, options);
42 45
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
43 46
             } else {
44
-                createFab(options);
47
+                createFab(component, options);
45 48
             }
46 49
         } else {
47 50
             removeFab();
@@ -51,35 +54,38 @@ public class FabPresenter {
51 54
 
52 55
     public void mergeOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
53 56
         this.viewGroup = viewGroup;
54
-        this.component = component;
55 57
         if (options.id.hasValue()) {
56 58
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
57 59
                 mergeParams(fabMenu, options);
58 60
                 fabMenu.bringToFront();
59
-                mergeFabMenuOptions(fabMenu, options);
61
+                mergeFabMenuOptions(component, fabMenu, options);
60 62
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
61 63
                 mergeParams(fab, options);
62 64
                 fab.bringToFront();
63
-                mergeFabOptions(fab, options);
65
+                mergeFabOptions(component, fab, options);
64 66
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
65 67
             } else {
66
-                createFab(options);
68
+                createFab(component, options);
67 69
             }
68 70
         }
69 71
     }
70 72
 
71
-    private void createFab(FabOptions options) {
73
+    private void createFab(ViewController component, FabOptions options) {
72 74
         if (options.actionsArray.size() > 0) {
73 75
             fabMenu = new FabMenu(viewGroup.getContext(), options.id.get());
74
-            setParams(fabMenu, options);
75
-            applyFabMenuOptions(fabMenu, options);
76
+            setParams(component, fabMenu, options);
77
+            applyFabMenuOptions(component, fabMenu, options);
76 78
             viewGroup.addView(fabMenu);
77 79
         } else {
78 80
             fab = new Fab(viewGroup.getContext(), options.id.get());
79
-            setParams(fab, options);
80
-            applyFabOptions(fab, options);
81
-            fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
81
+            setParams(component, fab, options);
82
+            applyFabOptions(component, fab, options);
82 83
             viewGroup.addView(fab);
84
+            fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
85
+            UiUtils.doOnLayout(fab, () -> {
86
+                fab.setPivotX(fab.getWidth() / 2f);
87
+                fab.setPivotY(fab.getHeight() / 2f);
88
+            });
83 89
         }
84 90
     }
85 91
 
@@ -93,18 +99,32 @@ public class FabPresenter {
93 99
 
94 100
     private void removeFab() {
95 101
         if (fab != null) {
96
-            fab.hide(true);
97
-            viewGroup.removeView(fab);
98
-            fab = null;
102
+            animateHide(() -> {
103
+                viewGroup.removeView(fab);
104
+                fab = null;
105
+            });
99 106
         }
100 107
     }
101 108
 
102
-    private void setParams(View fab, FabOptions options) {
109
+    public void animateHide(Runnable onAnimationEnd) {
110
+        fab.animate()
111
+                .scaleX(0f)
112
+                .scaleY(0f)
113
+                .setDuration(DURATION)
114
+                .setListener(new AnimatorListenerAdapter() {
115
+                    @Override
116
+                    public void onAnimationEnd(Animator animation) {
117
+                        onAnimationEnd.run();
118
+                    }
119
+                });
120
+    }
121
+
122
+    private void setParams(ViewController component, View fab, FabOptions options) {
103 123
         CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
104 124
         lp.rightMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
105 125
         lp.leftMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
106
-        lp.bottomMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
107
-        fab.setTag(R.id.fab_bottom_margin, lp.bottomMargin);
126
+        lp.bottomMargin = component.getBottomInset() + (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
127
+        fab.setTag(R.id.fab_bottom_margin, (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin));
108 128
         lp.gravity = Gravity.BOTTOM;
109 129
         if (options.alignHorizontally.hasValue()) {
110 130
             if ("right".equals(options.alignHorizontally.get())) {
@@ -140,12 +160,22 @@ public class FabPresenter {
140 160
         fab.setLayoutParams(lp);
141 161
     }
142 162
 
143
-    private void applyFabOptions(Fab fab, FabOptions options) {
163
+    private void applyFabOptions(ViewController component, Fab fab, FabOptions options) {
144 164
         if (options.visible.isTrueOrUndefined()) {
145
-            fab.show(true);
165
+            fab.setScaleX(0.6f);
166
+            fab.setScaleY(0.6f);
167
+            fab.animate()
168
+                    .scaleX(1f)
169
+                    .scaleY(1f)
170
+                    .setDuration(DURATION)
171
+                    .start();
146 172
         }
147 173
         if (options.visible.isFalse()) {
148
-            fab.hide(true);
174
+            fab.animate()
175
+                    .scaleX(0f)
176
+                    .scaleY(0f)
177
+                    .setDuration(DURATION)
178
+                    .start();
149 179
         }
150 180
         if (options.backgroundColor.hasValue()) {
151 181
             fab.setColorNormal(options.backgroundColor.get());
@@ -170,7 +200,7 @@ public class FabPresenter {
170 200
         }
171 201
     }
172 202
 
173
-    private void mergeFabOptions(Fab fab, FabOptions options) {
203
+    private void mergeFabOptions(ViewController component, Fab fab, FabOptions options) {
174 204
         if (options.visible.isTrue()) {
175 205
             fab.show(true);
176 206
         }
@@ -200,7 +230,7 @@ public class FabPresenter {
200 230
         }
201 231
     }
202 232
 
203
-    private void applyFabMenuOptions(FabMenu fabMenu, FabOptions options) {
233
+    private void applyFabMenuOptions(ViewController component, FabMenu fabMenu, FabOptions options) {
204 234
         if (options.visible.isTrueOrUndefined()) {
205 235
             fabMenu.showMenuButton(true);
206 236
         }
@@ -223,7 +253,7 @@ public class FabPresenter {
223 253
         fabMenu.getActions().clear();
224 254
         for (FabOptions fabOption : options.actionsArray) {
225 255
             Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
226
-            applyFabOptions(fab, fabOption);
256
+            applyFabOptions(component, fab, fabOption);
227 257
             fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
228 258
 
229 259
             fabMenu.getActions().add(fab);
@@ -237,7 +267,7 @@ public class FabPresenter {
237 267
         }
238 268
     }
239 269
 
240
-    private void mergeFabMenuOptions(FabMenu fabMenu, FabOptions options) {
270
+    private void mergeFabMenuOptions(ViewController component, FabMenu fabMenu, FabOptions options) {
241 271
         if (options.visible.isTrue()) {
242 272
             fabMenu.showMenuButton(true);
243 273
         }
@@ -261,7 +291,7 @@ public class FabPresenter {
261 291
             fabMenu.getActions().clear();
262 292
             for (FabOptions fabOption : options.actionsArray) {
263 293
                 Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
264
-                applyFabOptions(fab, fabOption);
294
+                applyFabOptions(component, fab, fabOption);
265 295
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
266 296
 
267 297
                 fabMenu.getActions().add(fab);

+ 0
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java Целия файл

@@ -10,7 +10,6 @@ import com.reactnativenavigation.interfaces.ScrollEventListener;
10 10
 import com.reactnativenavigation.parse.Options;
11 11
 import com.reactnativenavigation.parse.params.Bool;
12 12
 import com.reactnativenavigation.parse.params.NullBool;
13
-import com.reactnativenavigation.presentation.FabPresenter;
14 13
 import com.reactnativenavigation.utils.CommandListener;
15 14
 import com.reactnativenavigation.utils.Functions.Func1;
16 15
 import com.reactnativenavigation.utils.StringUtils;
@@ -67,7 +66,6 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
67 66
     private boolean isShown;
68 67
     private boolean isDestroyed;
69 68
     private ViewVisibilityListener viewVisibilityListener = new ViewVisibilityListenerAdapter();
70
-    protected FabPresenter fabOptionsPresenter;
71 69
     private ViewControllerOverlay overlay;
72 70
     @Nullable public abstract String getCurrentComponentName();
73 71
 
@@ -79,7 +77,6 @@ public abstract class ViewController<T extends ViewGroup> implements ViewTreeObs
79 77
         this.activity = activity;
80 78
         this.id = id;
81 79
         this.yellowBoxDelegate = yellowBoxDelegate;
82
-        fabOptionsPresenter = new FabPresenter();
83 80
         this.initialOptions = initialOptions;
84 81
         this.overlay = overlay;
85 82
         options = initialOptions.copy();

+ 6
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java Целия файл

@@ -7,6 +7,7 @@ import android.view.ViewGroup;
7 7
 import com.reactnativenavigation.anim.NavigationAnimator;
8 8
 import com.reactnativenavigation.parse.NestedAnimationsOptions;
9 9
 import com.reactnativenavigation.parse.Options;
10
+import com.reactnativenavigation.presentation.FabPresenter;
10 11
 import com.reactnativenavigation.presentation.Presenter;
11 12
 import com.reactnativenavigation.presentation.StackPresenter;
12 13
 import com.reactnativenavigation.react.Constants;
@@ -50,14 +51,16 @@ public class StackController extends ParentController<StackLayout> {
50 51
     private TopBarController topBarController;
51 52
     private BackButtonHelper backButtonHelper;
52 53
     private final StackPresenter presenter;
54
+    private final FabPresenter fabPresenter;
53 55
 
54
-    public StackController(Activity activity, List<ViewController> children, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, TopBarController topBarController, NavigationAnimator animator, String id, Options initialOptions, BackButtonHelper backButtonHelper, StackPresenter stackPresenter, Presenter presenter) {
56
+    public StackController(Activity activity, List<ViewController> children, ChildControllersRegistry childRegistry, EventEmitter eventEmitter, TopBarController topBarController, NavigationAnimator animator, String id, Options initialOptions, BackButtonHelper backButtonHelper, StackPresenter stackPresenter, Presenter presenter, FabPresenter fabPresenter) {
55 57
         super(activity, childRegistry, id, presenter, initialOptions);
56 58
         this.eventEmitter = eventEmitter;
57 59
         this.topBarController = topBarController;
58 60
         this.animator = animator;
59 61
         this.backButtonHelper = backButtonHelper;
60 62
         this.presenter = stackPresenter;
63
+        this.fabPresenter = fabPresenter;
61 64
         stackPresenter.setButtonOnClickListener(this::onNavigationButtonPressed);
62 65
         for (ViewController child : children) {
63 66
             child.setParentController(this);
@@ -105,7 +108,7 @@ public class StackController extends ParentController<StackLayout> {
105 108
     public void applyChildOptions(Options options, ViewController child) {
106 109
         super.applyChildOptions(options, child);
107 110
         presenter.applyChildOptions(resolveCurrentOptions(), this, child);
108
-        fabOptionsPresenter.applyOptions(this.options.fabOptions, child, getView());
111
+        fabPresenter.applyOptions(this.options.fabOptions, child, getView());
109 112
         performOnParentController(parent ->
110 113
                 parent.applyChildOptions(
111 114
                         this.options.copy()
@@ -125,7 +128,7 @@ public class StackController extends ParentController<StackLayout> {
125 128
         if (child.isViewShown() && peek() == child) {
126 129
             presenter.mergeChildOptions(options, resolveCurrentOptions(), this, child);
127 130
             if (options.fabOptions.hasValue()) {
128
-                fabOptionsPresenter.mergeOptions(options.fabOptions, child, getView());
131
+                fabPresenter.mergeOptions(options.fabOptions, child, getView());
129 132
             }
130 133
         }
131 134
         performOnParentController(parent ->

+ 9
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java Целия файл

@@ -4,6 +4,7 @@ import android.app.Activity;
4 4
 
5 5
 import com.reactnativenavigation.anim.NavigationAnimator;
6 6
 import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.presentation.FabPresenter;
7 8
 import com.reactnativenavigation.presentation.Presenter;
8 9
 import com.reactnativenavigation.presentation.StackPresenter;
9 10
 import com.reactnativenavigation.react.events.EventEmitter;
@@ -28,6 +29,7 @@ public class StackControllerBuilder {
28 29
     private StackPresenter stackPresenter;
29 30
     private List<ViewController> children = new ArrayList<>();
30 31
     private EventEmitter eventEmitter;
32
+    private FabPresenter fabPresenter = new FabPresenter();
31 33
 
32 34
     public StackControllerBuilder(Activity activity, EventEmitter eventEmitter) {
33 35
         this.activity = activity;
@@ -90,6 +92,11 @@ public class StackControllerBuilder {
90 92
         return this;
91 93
     }
92 94
 
95
+    public StackControllerBuilder setFabPresenter(FabPresenter fabPresenter) {
96
+        this.fabPresenter = fabPresenter;
97
+        return this;
98
+    }
99
+
93 100
     public StackController build() {
94 101
         return new StackController(activity,
95 102
                 children,
@@ -101,7 +108,8 @@ public class StackControllerBuilder {
101 108
                 initialOptions,
102 109
                 backButtonHelper,
103 110
                 stackPresenter,
104
-                presenter
111
+                presenter,
112
+                fabPresenter
105 113
         );
106 114
     }
107 115
 }

+ 13
- 1
lib/android/app/src/test/java/com/reactnativenavigation/viewcontrollers/FloatingActionButtonTest.java Целия файл

@@ -10,6 +10,7 @@ import com.reactnativenavigation.mocks.SimpleViewController;
10 10
 import com.reactnativenavigation.parse.FabOptions;
11 11
 import com.reactnativenavigation.parse.Options;
12 12
 import com.reactnativenavigation.parse.params.Text;
13
+import com.reactnativenavigation.presentation.FabPresenter;
13 14
 import com.reactnativenavigation.utils.CommandListenerAdapter;
14 15
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
15 16
 import com.reactnativenavigation.views.Fab;
@@ -37,7 +38,9 @@ public class FloatingActionButtonTest extends BaseTest {
37 38
         super.beforeEach();
38 39
         activity = newActivity();
39 40
         childRegistry = new ChildControllersRegistry();
40
-        stackController = TestUtils.newStackController(activity).build();
41
+        stackController = TestUtils.newStackController(activity)
42
+                .setFabPresenter(createFabPresenter())
43
+                .build();
41 44
         stackController.ensureViewIsCreated();
42 45
         Options options = getOptionsWithFab();
43 46
         childFab = new SimpleViewController(activity, childRegistry, "child1", options);
@@ -140,4 +143,13 @@ public class FloatingActionButtonTest extends BaseTest {
140 143
         }
141 144
         return false;
142 145
     }
146
+
147
+    private FabPresenter createFabPresenter() {
148
+        return new FabPresenter() {
149
+            @Override
150
+            public void animateHide(Runnable onAnimationEnd) {
151
+                onAnimationEnd.run();
152
+            }
153
+        };
154
+    }
143 155
 }