From d63a611dce420b30fa0c0711eea1d02603945a43 Mon Sep 17 00:00:00 2001 From: Damyan Ivanov Date: Wed, 3 Mar 2021 12:31:51 +0000 Subject: [PATCH] separate FAB management in a helper class CoordinatorLayout + FAB with "hide bottom view on scroll" behaviour might seem to work, but there is a corner case when the view is just large enough so that the FAB overlaps it, but not large enough to cause scrolling and in this case it is possible to either be stuck with the FAB over the bottom of the business area, or to hide the FAB without an intuitive to make it pop up again also, use single FAB for the Templates activity --- .../net/ktnx/mobileledger/ui/FabManager.java | 162 ++++++++++++++++++ .../ui/MobileLedgerListFragment.java | 43 ----- .../AccountSummaryFragment.java | 4 +- .../ui/activity/MainActivity.java | 78 ++------- .../ui/templates/TemplateDetailsFragment.java | 6 + .../ui/templates/TemplateListFragment.java | 7 + .../ui/templates/TemplatesActivity.java | 37 +++- .../TransactionListFragment.java | 4 +- .../main/res/layout/activity_templates.xml | 16 +- 9 files changed, 231 insertions(+), 126 deletions(-) create mode 100644 app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java b/app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java new file mode 100644 index 00000000..b9165344 --- /dev/null +++ b/app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java @@ -0,0 +1,162 @@ +/* + * Copyright © 2021 Damyan Ivanov. + * This file is part of MoLe. + * MoLe is free software: you can distribute it and/or modify it + * under the term of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your opinion), any later version. + * + * MoLe is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License terms for details. + * + * You should have received a copy of the GNU General Public License + * along with MoLe. If not, see . + */ + +package net.ktnx.mobileledger.ui; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.ViewPropertyAnimator; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import net.ktnx.mobileledger.utils.DimensionUtils; +import net.ktnx.mobileledger.utils.Logger; + +public class FabManager { + private static final boolean FAB_SHOWN = true; + private static final boolean FAB_HIDDEN = false; + private final FloatingActionButton fab; + private boolean wantedFabState = FAB_SHOWN; + private ViewPropertyAnimator fabSlideAnimator; + private int fabVerticalOffset; + public FabManager(FloatingActionButton fab) { + this.fab = fab; + } + @SuppressLint("ClickableViewAccessibility") + public static void handle(FabHandler activity, RecyclerView recyclerView) { + final float triggerAbsolutePixels = DimensionUtils.dp2px(activity.getContext(), 20f); + final float triggerRelativePixels = triggerAbsolutePixels / 4f; + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + Logger.debug("touch", "Scrolled " + dy); + if (dy <= 0) + activity.showManagedFab(); + else + activity.hideManagedFab(); + + super.onScrolled(recyclerView, dx, dy); + } + }); + recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() { + private float absoluteAnchor = -1; + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + switch (e.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + absoluteAnchor = e.getRawY(); +// Logger.debug("touch", +// String.format(Locale.US, "Touch down at %4.2f", absoluteAnchor)); + break; + case MotionEvent.ACTION_MOVE: + if (absoluteAnchor < 0) + break; + + final float absoluteY = e.getRawY(); +// Logger.debug("touch", String.format(Locale.US, "Move to %4.2f", absoluteY)); + + if (absoluteY > absoluteAnchor + triggerAbsolutePixels) { + // swipe down +// Logger.debug("touch", "SHOW"); + activity.showManagedFab(); + absoluteAnchor = absoluteY; + } + else if (absoluteY < absoluteAnchor - triggerAbsolutePixels) { + // swipe up +// Logger.debug("touch", "HIDE"); + activity.hideManagedFab(); + absoluteAnchor = absoluteY; + } + + break; + } + return false; + } + }); + } + private void slideFabTo(int target, long duration, TimeInterpolator interpolator) { + fabSlideAnimator = fab.animate() + .translationY((float) target) + .setInterpolator(interpolator) + .setDuration(duration) + .setListener(new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animation) { + fabSlideAnimator = null; + } + }); + } + public void showFab() { + if (wantedFabState == FAB_SHOWN) + return; + +// b.btnAddTransaction.show(); + if (this.fabSlideAnimator != null) { + this.fabSlideAnimator.cancel(); + fab.clearAnimation(); + } + + wantedFabState = FAB_SHOWN; + slideFabTo(0, 200L, + com.google.android.material.animation.AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); + } + public void hideFab() { + if (wantedFabState == FAB_HIDDEN) + return; + + calcVerticalFabOffset(); + +// b.btnAddTransaction.hide(); + if (this.fabSlideAnimator != null) { + this.fabSlideAnimator.cancel(); + fab.clearAnimation(); + } + + wantedFabState = FAB_HIDDEN; + slideFabTo(fabVerticalOffset, 150L, + com.google.android.material.animation.AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR); + } + private void calcVerticalFabOffset() { + if (fabVerticalOffset > 0) + return;// already calculated + int top = fab.getTop(); + ViewParent parent = fab.getParent(); + while (parent != null && !(parent instanceof View)) + parent = parent.getParent(); + + if (parent instanceof View) { + View parentView = (View) parent; + int parentHeight = parentView.getHeight(); + fabVerticalOffset = parentHeight - top; + } + } + public interface FabHandler { + Context getContext(); + + void showManagedFab(); + + void hideManagedFab(); + } +} diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java index a61f44f6..c5bbe0c4 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java @@ -17,8 +17,6 @@ package net.ktnx.mobileledger.ui; -import android.view.MotionEvent; - import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; @@ -27,7 +25,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import net.ktnx.mobileledger.ui.activity.MainActivity; import net.ktnx.mobileledger.ui.transaction_list.TransactionListAdapter; import net.ktnx.mobileledger.utils.Colors; -import net.ktnx.mobileledger.utils.DimensionUtils; public class MobileLedgerListFragment extends Fragment { public SwipeRefreshLayout refreshLayout; @@ -47,44 +44,4 @@ public class MobileLedgerListFragment extends Fragment { return; refreshLayout.setRefreshing(isRunning); } - protected void manageFabOnScroll() { - final MainActivity mainActivity = getMainActivity(); - int triggerPixels = DimensionUtils.dp2px(mainActivity, 10f); - root.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() { - private float upAnchor = -1; - private float lastY; - @Override - public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - switch (e.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - lastY = upAnchor = e.getAxisValue(MotionEvent.AXIS_Y); - break; - case MotionEvent.ACTION_MOVE: - final float currentY = e.getAxisValue(MotionEvent.AXIS_Y); - if (currentY > lastY) { - // swipe down - upAnchor = lastY; - - mainActivity.fabShouldShow(); - } - else { - // swipe up - if (currentY < upAnchor - triggerPixels) - mainActivity.fabHide(); - } - - lastY = currentY; - - break; - } - return false; - } - @Override - public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { - } - @Override - public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { - } - }); - } } diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java index cd4d4239..d1aca96d 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.RecyclerView; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.model.AccountListItem; import net.ktnx.mobileledger.model.Data; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.ui.MobileLedgerListFragment; import net.ktnx.mobileledger.ui.activity.MainActivity; @@ -89,7 +90,8 @@ public class AccountSummaryFragment extends MobileLedgerListFragment { mainActivity.fabShouldShow(); - manageFabOnScroll(); + if (mainActivity instanceof FabManager.FabHandler) + FabManager.handle((FabManager.FabHandler) mainActivity, root); refreshLayout = view.findViewById(R.id.account_swipe_refresh_layout); Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged); diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java index da86d3ae..c4d5c24a 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java @@ -17,9 +17,7 @@ package net.ktnx.mobileledger.ui.activity; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.TimeInterpolator; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.ShortcutInfo; @@ -32,7 +30,6 @@ import android.os.Bundle; import android.text.format.DateUtils; import android.util.Log; import android.view.View; -import android.view.ViewPropertyAnimator; import android.view.animation.AnimationUtils; import android.widget.TextView; @@ -56,6 +53,7 @@ import net.ktnx.mobileledger.async.RetrieveTransactionsTask; import net.ktnx.mobileledger.databinding.ActivityMainBinding; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.MobileLedgerProfile; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment; import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity; @@ -78,7 +76,7 @@ import java.util.Objects; * TODO: reports * */ -public class MainActivity extends ProfileThemedActivity { +public class MainActivity extends ProfileThemedActivity implements FabManager.FabHandler { public static final String STATE_CURRENT_PAGE = "current_page"; public static final String BUNDLE_SAVED_STATE = "bundle_savedState"; public static final String STATE_ACC_FILTER = "account_filter"; @@ -95,8 +93,7 @@ public class MainActivity extends ProfileThemedActivity { private MainModel mainModel; private ActivityMainBinding b; private int fabVerticalOffset; - private ViewPropertyAnimator fabSlideAnimator; - private boolean wantedFabState = FAB_SHOWN; + private FabManager fabManager; @Override protected void onStart() { super.onStart(); @@ -256,6 +253,8 @@ public class MainActivity extends ProfileThemedActivity { mProfileListAdapter.notifyDataSetChanged(); }); + fabManager = new FabManager(b.btnAddTransaction); + LinearLayoutManager llm = new LinearLayoutManager(this); llm.setOrientation(RecyclerView.VERTICAL); @@ -271,7 +270,7 @@ public class MainActivity extends ProfileThemedActivity { @Override public void onDrawerSlide(@NonNull View drawerView, float slideOffset) { if (slideOffset > 0.2) - fabHide(); + fabManager.hideFab(); } @Override public void onDrawerClosed(View drawerView) { @@ -286,7 +285,7 @@ public class MainActivity extends ProfileThemedActivity { super.onDrawerOpened(drawerView); mProfileListAdapter.setAnimationsEnabled(true); Data.drawerOpen.setValue(true); - fabHide(); + fabManager.hideFab(); } }; b.drawerLayout.addDrawerListener(drawerListener); @@ -639,60 +638,21 @@ public class MainActivity extends ProfileThemedActivity { } } public void fabShouldShow() { - if (fabVerticalOffset <= 0) { - int top = b.btnAddTransaction.getTop(); - int parentHeight = b.mainAppLayout.getHeight(); - fabVerticalOffset = parentHeight - top; - } - if ((profile != null) && profile.isPostingPermitted() && !b.drawerLayout.isOpen()) { - fabShow(); - } - else - fabHide(); + if ((profile != null) && profile.isPostingPermitted() && !b.drawerLayout.isOpen()) + fabManager.showFab(); } - private void fabShow() { - if (wantedFabState == FAB_SHOWN) - return; - -// b.btnAddTransaction.show(); - if (this.fabSlideAnimator != null) { - this.fabSlideAnimator.cancel(); - b.btnAddTransaction.clearAnimation(); - } - - wantedFabState = FAB_SHOWN; - slideFabTo(b.btnAddTransaction, 0, 200L, - com.google.android.material.animation.AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); + @Override + public Context getContext() { + return this; } - public void fabHide() { - if (wantedFabState == FAB_HIDDEN) - return; - - if (fabVerticalOffset <= 0) - return; - -// b.btnAddTransaction.hide(); - if (this.fabSlideAnimator != null) { - this.fabSlideAnimator.cancel(); - b.btnAddTransaction.clearAnimation(); - } - - wantedFabState = FAB_HIDDEN; - slideFabTo(b.btnAddTransaction, fabVerticalOffset, 150L, - com.google.android.material.animation.AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR); + @Override + public void showManagedFab() { + fabShouldShow(); } - private void slideFabTo(View view, int target, long duration, TimeInterpolator interpolator) { - fabSlideAnimator = view.animate() - .translationY((float) target) - .setInterpolator(interpolator) - .setDuration(duration) - .setListener(new AnimatorListenerAdapter() { - public void onAnimationEnd(Animator animation) { - fabSlideAnimator = null; - } - }); + @Override + public void hideManagedFab() { + fabManager.hideFab(); } - public static class SectionsPagerAdapter extends FragmentStateAdapter { public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java index c023c62c..ac72d890 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelStoreOwner; import androidx.navigation.NavController; @@ -37,6 +38,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import net.ktnx.mobileledger.R; import net.ktnx.mobileledger.databinding.TemplateDetailsFragmentBinding; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.utils.Logger; public class TemplateDetailsFragment extends Fragment { @@ -122,6 +124,10 @@ public class TemplateDetailsFragment extends Fragment { mViewModel.getItems(mPatternId) .observe(getViewLifecycleOwner(), adapter::setItems); + FragmentActivity activity = requireActivity(); + if (activity instanceof FabManager.FabHandler) + FabManager.handle((FabManager.FabHandler) activity, b.patternDetailsRecyclerView); + return b.getRoot(); } interface InteractionListener { diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java index 52e41f2f..7bdad8d1 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java @@ -25,6 +25,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.LifecycleOwner; @@ -36,6 +37,7 @@ import net.ktnx.mobileledger.dao.TemplateHeaderDAO; import net.ktnx.mobileledger.databinding.FragmentTemplateListBinding; import net.ktnx.mobileledger.db.DB; import net.ktnx.mobileledger.db.TemplateHeader; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.utils.Logger; import org.jetbrains.annotations.NotNull; @@ -92,6 +94,11 @@ public class TemplateListFragment extends Fragment { LinearLayoutManager llm = new LinearLayoutManager(getContext()); llm.setOrientation(RecyclerView.VERTICAL); b.templateList.setLayoutManager(llm); + + FragmentActivity activity = requireActivity(); + if (activity instanceof FabManager.FabHandler) + FabManager.handle((FabManager.FabHandler) activity, b.templateList); + return b.getRoot(); } @Override diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java index 410b7d62..7bc72220 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java @@ -17,6 +17,7 @@ package net.ktnx.mobileledger.ui.templates; +import android.content.Context; import android.os.Bundle; import android.view.MenuItem; @@ -37,6 +38,7 @@ import net.ktnx.mobileledger.dao.TemplateHeaderDAO; import net.ktnx.mobileledger.databinding.ActivityTemplatesBinding; import net.ktnx.mobileledger.db.DB; import net.ktnx.mobileledger.db.TemplateWithAccounts; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.QR; import net.ktnx.mobileledger.ui.activity.CrashReportingActivity; import net.ktnx.mobileledger.utils.Logger; @@ -45,11 +47,13 @@ import java.util.Objects; public class TemplatesActivity extends CrashReportingActivity implements TemplateListFragment.OnTemplateListFragmentInteractionListener, - TemplateDetailsFragment.InteractionListener, QR.QRScanResultReceiver, QR.QRScanTrigger { + TemplateDetailsFragment.InteractionListener, QR.QRScanResultReceiver, QR.QRScanTrigger, + FabManager.FabHandler { public static final String ARG_ADD_TEMPLATE = "add-template"; private ActivityTemplatesBinding b; private NavController navController; private ActivityResultLauncher qrScanLauncher; + private FabManager fabManager; // @Override // public boolean onCreateOptionsMenu(Menu menu) { // super.onCreateOptionsMenu(menu); @@ -75,22 +79,27 @@ public class TemplatesActivity extends CrashReportingActivity navController.addOnDestinationChangedListener((controller, destination, arguments) -> { if (destination.getId() == R.id.templateListFragment) { - b.fabAdd.show(); - b.fabSave.hide(); b.toolbarLayout.setTitle(getString(R.string.title_activity_templates)); + b.fab.setImageResource(R.drawable.ic_add_white_24dp); } - if (destination.getId() == R.id.templateDetailsFragment) { - b.fabAdd.hide(); - b.fabSave.show(); + else { + b.fab.setImageResource(R.drawable.ic_save_white_24dp); } }); b.toolbarLayout.setTitle(getString(R.string.title_activity_templates)); - b.fabAdd.setOnClickListener(v -> onEditTemplate(null)); - b.fabSave.setOnClickListener(v -> onSaveTemplate()); + b.fab.setOnClickListener(v -> { + if (navController.getCurrentDestination() + .getId() == R.id.templateListFragment) + onEditTemplate(null); + else + onSaveTemplate(); + }); qrScanLauncher = QR.registerLauncher(this, this); + + fabManager = new FabManager(b.fab); } @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -170,4 +179,16 @@ public class TemplatesActivity extends CrashReportingActivity public void triggerQRScan() { qrScanLauncher.launch(null); } + @Override + public Context getContext() { + return this; + } + @Override + public void showManagedFab() { + fabManager.showFab(); + } + @Override + public void hideManagedFab() { + fabManager.hideFab(); + } } \ No newline at end of file diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java index 4495863c..e6820cf8 100644 --- a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java +++ b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java @@ -40,6 +40,7 @@ import net.ktnx.mobileledger.async.TransactionDateFinder; import net.ktnx.mobileledger.model.Data; import net.ktnx.mobileledger.model.MobileLedgerProfile; import net.ktnx.mobileledger.ui.DatePickerFragment; +import net.ktnx.mobileledger.ui.FabManager; import net.ktnx.mobileledger.ui.MainModel; import net.ktnx.mobileledger.ui.MobileLedgerListFragment; import net.ktnx.mobileledger.ui.activity.MainActivity; @@ -111,7 +112,8 @@ public class TransactionListFragment extends MobileLedgerListFragment mainActivity.fabShouldShow(); - manageFabOnScroll(); + if (mainActivity instanceof FabManager.FabHandler) + FabManager.handle((FabManager.FabHandler) mainActivity, root); LinearLayoutManager llm = new LinearLayoutManager(mainActivity); diff --git a/app/src/main/res/layout/activity_templates.xml b/app/src/main/res/layout/activity_templates.xml index e53fb95e..30c998df 100644 --- a/app/src/main/res/layout/activity_templates.xml +++ b/app/src/main/res/layout/activity_templates.xml @@ -66,25 +66,13 @@ - - -- 2.39.2