]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java
more pronounced day/month delimiters in the transaction list
[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.os.Looper;
27 import android.view.MotionEvent;
28 import android.view.ViewGroup;
29 import android.view.ViewPropertyAnimator;
30
31 import androidx.annotation.NonNull;
32 import androidx.recyclerview.widget.RecyclerView;
33
34 import com.google.android.material.floatingactionbutton.FloatingActionButton;
35
36 import net.ktnx.mobileledger.utils.DimensionUtils;
37 import net.ktnx.mobileledger.utils.Logger;
38
39 public class FabManager {
40     private static final boolean FAB_SHOWN = true;
41     private static final boolean FAB_HIDDEN = false;
42     private static final int AUTO_SHOW_DELAY_MILLS = 4000;
43     private final FloatingActionButton fab;
44     private boolean wantedFabState = FAB_SHOWN;
45     private ViewPropertyAnimator fabSlideAnimator;
46     private int fabVerticalOffset;
47     public FabManager(FloatingActionButton fab) {
48         this.fab = fab;
49     }
50     public static void handle(FabHandler fabHandler, RecyclerView recyclerView) {
51         new ScrollFabHandler(fabHandler, recyclerView);
52     }
53     private void slideFabTo(int target, long duration, TimeInterpolator interpolator) {
54         fabSlideAnimator = fab.animate()
55                               .translationY((float) target)
56                               .setInterpolator(interpolator)
57                               .setDuration(duration)
58                               .setListener(new AnimatorListenerAdapter() {
59                                   public void onAnimationEnd(Animator animation) {
60                                       fabSlideAnimator = null;
61                                   }
62                               });
63     }
64     public void showFab() {
65         if (wantedFabState == FAB_SHOWN) {
66 //            Logger.debug("fab", "Ignoring request to show already visible FAB");
67             return;
68         }
69
70 //        b.btnAddTransaction.show();
71         if (this.fabSlideAnimator != null) {
72             this.fabSlideAnimator.cancel();
73             fab.clearAnimation();
74         }
75
76         Logger.debug("fab", "Showing FAB");
77         wantedFabState = FAB_SHOWN;
78         slideFabTo(0, 200L,
79                 com.google.android.material.animation.AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
80     }
81     public void hideFab() {
82         if (wantedFabState == FAB_HIDDEN) {
83 //            Logger.debug("fab", "Ignoring request to hide FAB -- already hidden");
84             return;
85         }
86
87         calcVerticalFabOffset();
88
89 //        b.btnAddTransaction.hide();
90         if (this.fabSlideAnimator != null) {
91             this.fabSlideAnimator.cancel();
92             fab.clearAnimation();
93         }
94
95         Logger.debug("fab", "Hiding FAB");
96         wantedFabState = FAB_HIDDEN;
97         slideFabTo(fabVerticalOffset, 150L,
98                 com.google.android.material.animation.AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
99     }
100     private void calcVerticalFabOffset() {
101         if (fabVerticalOffset > 0)
102             return;// already calculated
103         fab.measure(0, 0);
104
105         int height = fab.getMeasuredHeight();
106
107         int bottomMargin;
108
109         ViewGroup.LayoutParams layoutParams = fab.getLayoutParams();
110         if (layoutParams instanceof ViewGroup.MarginLayoutParams)
111             bottomMargin = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
112         else
113             throw new RuntimeException("Unsupported layout params " + layoutParams.getClass()
114                                                                                   .getCanonicalName());
115
116         fabVerticalOffset = height + bottomMargin;
117     }
118     public interface FabHandler {
119         Context getContext();
120
121         void showManagedFab();
122
123         void hideManagedFab();
124     }
125
126     public static class ScrollFabHandler {
127         final private FabHandler fabHandler;
128         private int generation = 0;
129         @SuppressLint("ClickableViewAccessibility")
130         ScrollFabHandler(FabHandler fabHandler, RecyclerView recyclerView) {
131             this.fabHandler = fabHandler;
132             final float triggerAbsolutePixels = DimensionUtils.dp2px(fabHandler.getContext(), 20f);
133             final float triggerRelativePixels = triggerAbsolutePixels / 4f;
134             recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
135                 @Override
136                 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
137 //                    Logger.debug("touch", "Scrolled " + dy);
138                     if (dy <= 0) {
139                         showFab();
140                     }
141                     else
142                         hideFab();
143
144                     super.onScrolled(recyclerView, dx, dy);
145                 }
146             });
147             recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
148                 private float absoluteAnchor = -1;
149                 @Override
150                 public boolean onInterceptTouchEvent(@NonNull RecyclerView rv,
151                                                      @NonNull MotionEvent e) {
152                     switch (e.getActionMasked()) {
153                         case MotionEvent.ACTION_DOWN:
154                             absoluteAnchor = e.getRawY();
155 //                        Logger.debug("touch",
156 //                                String.format(Locale.US, "Touch down at %4.2f", absoluteAnchor));
157                             break;
158                         case MotionEvent.ACTION_MOVE:
159                             if (absoluteAnchor < 0)
160                                 break;
161
162                             final float absoluteY = e.getRawY();
163 //                        Logger.debug("touch", String.format(Locale.US, "Move to %4.2f",
164 //                        absoluteY));
165
166                             if (absoluteY > absoluteAnchor + triggerAbsolutePixels) {
167                                 // swipe down
168 //                            Logger.debug("touch", "SHOW");
169                                 showFab();
170                                 absoluteAnchor = absoluteY;
171                             }
172                             else if (absoluteY < absoluteAnchor - triggerAbsolutePixels) {
173                                 // swipe up
174 //                            Logger.debug("touch", "HIDE");
175                                 hideFab();
176                                 absoluteAnchor = absoluteY;
177                             }
178
179                             break;
180                     }
181                     return false;
182                 }
183             });
184         }
185         private void hideFab() {
186             generation++;
187             int thisGeneration = generation;
188             fabHandler.hideManagedFab();
189             new Handler(Looper.getMainLooper()).postDelayed(() -> {
190                 if (generation != thisGeneration)
191                     return;
192
193                 showFab();
194             }, AUTO_SHOW_DELAY_MILLS);
195         }
196         private void showFab() {
197             generation++;
198             fabHandler.showManagedFab();
199         }
200     }
201 }