]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java
12ee969ad66571e9141808dbc705b47ae05bfca8
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / FabManager.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.ui;
19
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.TimeInterpolator;
23 import android.annotation.SuppressLint;
24 import android.content.Context;
25 import android.os.Handler;
26 import android.view.MotionEvent;
27 import android.view.ViewGroup;
28 import android.view.ViewPropertyAnimator;
29
30 import androidx.annotation.NonNull;
31 import androidx.recyclerview.widget.RecyclerView;
32
33 import com.google.android.material.floatingactionbutton.FloatingActionButton;
34
35 import net.ktnx.mobileledger.utils.DimensionUtils;
36 import net.ktnx.mobileledger.utils.Logger;
37
38 public class FabManager {
39     private static final boolean FAB_SHOWN = true;
40     private static final boolean FAB_HIDDEN = false;
41     private static final int AUTO_SHOW_DELAY_MILLS = 4000;
42     private final FloatingActionButton fab;
43     private boolean wantedFabState = FAB_SHOWN;
44     private ViewPropertyAnimator fabSlideAnimator;
45     private int fabVerticalOffset;
46     public FabManager(FloatingActionButton fab) {
47         this.fab = fab;
48     }
49     public static void handle(FabHandler fabHandler, RecyclerView recyclerView) {
50         new ScrollFabHandler(fabHandler, recyclerView);
51     }
52     private void slideFabTo(int target, long duration, TimeInterpolator interpolator) {
53         fabSlideAnimator = fab.animate()
54                               .translationY((float) target)
55                               .setInterpolator(interpolator)
56                               .setDuration(duration)
57                               .setListener(new AnimatorListenerAdapter() {
58                                   public void onAnimationEnd(Animator animation) {
59                                       fabSlideAnimator = null;
60                                   }
61                               });
62     }
63     public void showFab() {
64         if (wantedFabState == FAB_SHOWN) {
65 //            Logger.debug("fab", "Ignoring request to show already visible FAB");
66             return;
67         }
68
69 //        b.btnAddTransaction.show();
70         if (this.fabSlideAnimator != null) {
71             this.fabSlideAnimator.cancel();
72             fab.clearAnimation();
73         }
74
75         Logger.debug("fab", "Showing FAB");
76         wantedFabState = FAB_SHOWN;
77         slideFabTo(0, 200L,
78                 com.google.android.material.animation.AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
79     }
80     public void hideFab() {
81         if (wantedFabState == FAB_HIDDEN) {
82 //            Logger.debug("fab", "Ignoring request to hide FAB -- already hidden");
83             return;
84         }
85
86         calcVerticalFabOffset();
87
88 //        b.btnAddTransaction.hide();
89         if (this.fabSlideAnimator != null) {
90             this.fabSlideAnimator.cancel();
91             fab.clearAnimation();
92         }
93
94         Logger.debug("fab", "Hiding FAB");
95         wantedFabState = FAB_HIDDEN;
96         slideFabTo(fabVerticalOffset, 150L,
97                 com.google.android.material.animation.AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
98     }
99     private void calcVerticalFabOffset() {
100         if (fabVerticalOffset > 0)
101             return;// already calculated
102         fab.measure(0, 0);
103
104         int height = fab.getMeasuredHeight();
105
106         int bottomMargin;
107
108         ViewGroup.LayoutParams layoutParams = fab.getLayoutParams();
109         if (layoutParams instanceof ViewGroup.MarginLayoutParams)
110             bottomMargin = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
111         else
112             throw new RuntimeException("Unsupported layout params " + layoutParams.getClass()
113                                                                                   .getCanonicalName());
114
115         fabVerticalOffset = height + bottomMargin;
116     }
117     public interface FabHandler {
118         Context getContext();
119
120         void showManagedFab();
121
122         void hideManagedFab();
123     }
124
125     public static class ScrollFabHandler {
126         final private FabHandler fabHandler;
127         private int generation = 0;
128         @SuppressLint("ClickableViewAccessibility")
129         ScrollFabHandler(FabHandler fabHandler, RecyclerView recyclerView) {
130             this.fabHandler = fabHandler;
131             final float triggerAbsolutePixels = DimensionUtils.dp2px(fabHandler.getContext(), 20f);
132             final float triggerRelativePixels = triggerAbsolutePixels / 4f;
133             recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
134                 @Override
135                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
136 //                    Logger.debug("touch", "Scrolled " + dy);
137                     if (dy <= 0) {
138                         showFab();
139                     }
140                     else
141                         hideFab();
142
143                     super.onScrolled(recyclerView, dx, dy);
144                 }
145             });
146             recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
147                 private float absoluteAnchor = -1;
148                 @Override
149                 public boolean onInterceptTouchEvent(@NonNull RecyclerView rv,
150                                                      @NonNull MotionEvent e) {
151                     switch (e.getActionMasked()) {
152                         case MotionEvent.ACTION_DOWN:
153                             absoluteAnchor = e.getRawY();
154 //                        Logger.debug("touch",
155 //                                String.format(Locale.US, "Touch down at %4.2f", absoluteAnchor));
156                             break;
157                         case MotionEvent.ACTION_MOVE:
158                             if (absoluteAnchor < 0)
159                                 break;
160
161                             final float absoluteY = e.getRawY();
162 //                        Logger.debug("touch", String.format(Locale.US, "Move to %4.2f",
163 //                        absoluteY));
164
165                             if (absoluteY > absoluteAnchor + triggerAbsolutePixels) {
166                                 // swipe down
167 //                            Logger.debug("touch", "SHOW");
168                                 showFab();
169                                 absoluteAnchor = absoluteY;
170                             }
171                             else if (absoluteY < absoluteAnchor - triggerAbsolutePixels) {
172                                 // swipe up
173 //                            Logger.debug("touch", "HIDE");
174                                 hideFab();
175                                 absoluteAnchor = absoluteY;
176                             }
177
178                             break;
179                     }
180                     return false;
181                 }
182             });
183         }
184         private void hideFab() {
185             generation++;
186             int thisGeneration = generation;
187             fabHandler.hideManagedFab();
188             new Handler().postDelayed(() -> {
189                 if (generation != thisGeneration)
190                     return;
191
192                 showFab();
193             }, AUTO_SHOW_DELAY_MILLS);
194         }
195         private void showFab() {
196             generation++;
197             fabHandler.showManagedFab();
198         }
199     }
200 }