Ver código fonte

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 anos atrás
pai
commit
9a8bc54dd4
Nenhuma conta vinculada ao e-mail do autor do commit

+ 61
- 31
lib/android/app/src/main/java/com/reactnativenavigation/presentation/FabPresenter.java Ver arquivo

1
 package com.reactnativenavigation.presentation;
1
 package com.reactnativenavigation.presentation;
2
 
2
 
3
 
3
 
4
+import android.animation.Animator;
5
+import android.animation.AnimatorListenerAdapter;
4
 import android.view.Gravity;
6
 import android.view.Gravity;
5
 import android.view.View;
7
 import android.view.View;
6
 import android.view.ViewGroup;
8
 import android.view.ViewGroup;
7
 
9
 
8
 import com.reactnativenavigation.R;
10
 import com.reactnativenavigation.R;
9
 import com.reactnativenavigation.parse.FabOptions;
11
 import com.reactnativenavigation.parse.FabOptions;
12
+import com.reactnativenavigation.utils.UiUtils;
10
 import com.reactnativenavigation.viewcontrollers.ViewController;
13
 import com.reactnativenavigation.viewcontrollers.ViewController;
11
 import com.reactnativenavigation.views.Fab;
14
 import com.reactnativenavigation.views.Fab;
12
 import com.reactnativenavigation.views.FabMenu;
15
 import com.reactnativenavigation.views.FabMenu;
20
 import static com.reactnativenavigation.utils.ObjectUtils.perform;
23
 import static com.reactnativenavigation.utils.ObjectUtils.perform;
21
 
24
 
22
 public class FabPresenter {
25
 public class FabPresenter {
26
+    private static final int DURATION = 200;
27
+
23
     private ViewGroup viewGroup;
28
     private ViewGroup viewGroup;
24
-    private ViewController component;
25
 
29
 
26
     private Fab fab;
30
     private Fab fab;
27
     private FabMenu fabMenu;
31
     private FabMenu fabMenu;
28
 
32
 
29
     public void applyOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
33
     public void applyOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
30
         this.viewGroup = viewGroup;
34
         this.viewGroup = viewGroup;
31
-        this.component = component;
32
 
35
 
33
         if (options.id.hasValue()) {
36
         if (options.id.hasValue()) {
34
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
37
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
35
                 fabMenu.bringToFront();
38
                 fabMenu.bringToFront();
36
-                applyFabMenuOptions(fabMenu, options);
37
-                setParams(fabMenu, options);
39
+                applyFabMenuOptions(component, fabMenu, options);
40
+                setParams(component, fabMenu, options);
38
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
41
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
39
                 fab.bringToFront();
42
                 fab.bringToFront();
40
-                applyFabOptions(fab, options);
41
-                setParams(fab, options);
43
+                setParams(component, fab, options);
44
+                applyFabOptions(component, fab, options);
42
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
45
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
43
             } else {
46
             } else {
44
-                createFab(options);
47
+                createFab(component, options);
45
             }
48
             }
46
         } else {
49
         } else {
47
             removeFab();
50
             removeFab();
51
 
54
 
52
     public void mergeOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
55
     public void mergeOptions(FabOptions options, @NonNull ViewController component, @NonNull ViewGroup viewGroup) {
53
         this.viewGroup = viewGroup;
56
         this.viewGroup = viewGroup;
54
-        this.component = component;
55
         if (options.id.hasValue()) {
57
         if (options.id.hasValue()) {
56
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
58
             if (fabMenu != null && fabMenu.getFabId().equals(options.id.get())) {
57
                 mergeParams(fabMenu, options);
59
                 mergeParams(fabMenu, options);
58
                 fabMenu.bringToFront();
60
                 fabMenu.bringToFront();
59
-                mergeFabMenuOptions(fabMenu, options);
61
+                mergeFabMenuOptions(component, fabMenu, options);
60
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
62
             } else if (fab != null && fab.getFabId().equals(options.id.get())) {
61
                 mergeParams(fab, options);
63
                 mergeParams(fab, options);
62
                 fab.bringToFront();
64
                 fab.bringToFront();
63
-                mergeFabOptions(fab, options);
65
+                mergeFabOptions(component, fab, options);
64
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
66
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
65
             } else {
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
         if (options.actionsArray.size() > 0) {
74
         if (options.actionsArray.size() > 0) {
73
             fabMenu = new FabMenu(viewGroup.getContext(), options.id.get());
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
             viewGroup.addView(fabMenu);
78
             viewGroup.addView(fabMenu);
77
         } else {
79
         } else {
78
             fab = new Fab(viewGroup.getContext(), options.id.get());
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
             viewGroup.addView(fab);
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
 
99
 
94
     private void removeFab() {
100
     private void removeFab() {
95
         if (fab != null) {
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
         CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
123
         CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
104
         lp.rightMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
124
         lp.rightMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
105
         lp.leftMargin = (int) viewGroup.getContext().getResources().getDimension(R.dimen.margin);
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
         lp.gravity = Gravity.BOTTOM;
128
         lp.gravity = Gravity.BOTTOM;
109
         if (options.alignHorizontally.hasValue()) {
129
         if (options.alignHorizontally.hasValue()) {
110
             if ("right".equals(options.alignHorizontally.get())) {
130
             if ("right".equals(options.alignHorizontally.get())) {
140
         fab.setLayoutParams(lp);
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
         if (options.visible.isTrueOrUndefined()) {
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
         if (options.visible.isFalse()) {
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
         if (options.backgroundColor.hasValue()) {
180
         if (options.backgroundColor.hasValue()) {
151
             fab.setColorNormal(options.backgroundColor.get());
181
             fab.setColorNormal(options.backgroundColor.get());
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
         if (options.visible.isTrue()) {
204
         if (options.visible.isTrue()) {
175
             fab.show(true);
205
             fab.show(true);
176
         }
206
         }
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
         if (options.visible.isTrueOrUndefined()) {
234
         if (options.visible.isTrueOrUndefined()) {
205
             fabMenu.showMenuButton(true);
235
             fabMenu.showMenuButton(true);
206
         }
236
         }
223
         fabMenu.getActions().clear();
253
         fabMenu.getActions().clear();
224
         for (FabOptions fabOption : options.actionsArray) {
254
         for (FabOptions fabOption : options.actionsArray) {
225
             Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
255
             Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
226
-            applyFabOptions(fab, fabOption);
256
+            applyFabOptions(component, fab, fabOption);
227
             fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
257
             fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
228
 
258
 
229
             fabMenu.getActions().add(fab);
259
             fabMenu.getActions().add(fab);
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
         if (options.visible.isTrue()) {
271
         if (options.visible.isTrue()) {
242
             fabMenu.showMenuButton(true);
272
             fabMenu.showMenuButton(true);
243
         }
273
         }
261
             fabMenu.getActions().clear();
291
             fabMenu.getActions().clear();
262
             for (FabOptions fabOption : options.actionsArray) {
292
             for (FabOptions fabOption : options.actionsArray) {
263
                 Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
293
                 Fab fab = new Fab(viewGroup.getContext(), fabOption.id.get());
264
-                applyFabOptions(fab, fabOption);
294
+                applyFabOptions(component, fab, fabOption);
265
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
295
                 fab.setOnClickListener(v -> component.sendOnNavigationButtonPressed(options.id.get()));
266
 
296
 
267
                 fabMenu.getActions().add(fab);
297
                 fabMenu.getActions().add(fab);

+ 0
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/ViewController.java Ver arquivo

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

+ 6
- 3
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackController.java Ver arquivo

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

+ 9
- 1
lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/stack/StackControllerBuilder.java Ver arquivo

4
 
4
 
5
 import com.reactnativenavigation.anim.NavigationAnimator;
5
 import com.reactnativenavigation.anim.NavigationAnimator;
6
 import com.reactnativenavigation.parse.Options;
6
 import com.reactnativenavigation.parse.Options;
7
+import com.reactnativenavigation.presentation.FabPresenter;
7
 import com.reactnativenavigation.presentation.Presenter;
8
 import com.reactnativenavigation.presentation.Presenter;
8
 import com.reactnativenavigation.presentation.StackPresenter;
9
 import com.reactnativenavigation.presentation.StackPresenter;
9
 import com.reactnativenavigation.react.events.EventEmitter;
10
 import com.reactnativenavigation.react.events.EventEmitter;
28
     private StackPresenter stackPresenter;
29
     private StackPresenter stackPresenter;
29
     private List<ViewController> children = new ArrayList<>();
30
     private List<ViewController> children = new ArrayList<>();
30
     private EventEmitter eventEmitter;
31
     private EventEmitter eventEmitter;
32
+    private FabPresenter fabPresenter = new FabPresenter();
31
 
33
 
32
     public StackControllerBuilder(Activity activity, EventEmitter eventEmitter) {
34
     public StackControllerBuilder(Activity activity, EventEmitter eventEmitter) {
33
         this.activity = activity;
35
         this.activity = activity;
90
         return this;
92
         return this;
91
     }
93
     }
92
 
94
 
95
+    public StackControllerBuilder setFabPresenter(FabPresenter fabPresenter) {
96
+        this.fabPresenter = fabPresenter;
97
+        return this;
98
+    }
99
+
93
     public StackController build() {
100
     public StackController build() {
94
         return new StackController(activity,
101
         return new StackController(activity,
95
                 children,
102
                 children,
101
                 initialOptions,
108
                 initialOptions,
102
                 backButtonHelper,
109
                 backButtonHelper,
103
                 stackPresenter,
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 Ver arquivo

10
 import com.reactnativenavigation.parse.FabOptions;
10
 import com.reactnativenavigation.parse.FabOptions;
11
 import com.reactnativenavigation.parse.Options;
11
 import com.reactnativenavigation.parse.Options;
12
 import com.reactnativenavigation.parse.params.Text;
12
 import com.reactnativenavigation.parse.params.Text;
13
+import com.reactnativenavigation.presentation.FabPresenter;
13
 import com.reactnativenavigation.utils.CommandListenerAdapter;
14
 import com.reactnativenavigation.utils.CommandListenerAdapter;
14
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
15
 import com.reactnativenavigation.viewcontrollers.stack.StackController;
15
 import com.reactnativenavigation.views.Fab;
16
 import com.reactnativenavigation.views.Fab;
37
         super.beforeEach();
38
         super.beforeEach();
38
         activity = newActivity();
39
         activity = newActivity();
39
         childRegistry = new ChildControllersRegistry();
40
         childRegistry = new ChildControllersRegistry();
40
-        stackController = TestUtils.newStackController(activity).build();
41
+        stackController = TestUtils.newStackController(activity)
42
+                .setFabPresenter(createFabPresenter())
43
+                .build();
41
         stackController.ensureViewIsCreated();
44
         stackController.ensureViewIsCreated();
42
         Options options = getOptionsWithFab();
45
         Options options = getOptionsWithFab();
43
         childFab = new SimpleViewController(activity, childRegistry, "child1", options);
46
         childFab = new SimpleViewController(activity, childRegistry, "child1", options);
140
         }
143
         }
141
         return false;
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
 }