diff --git a/app/src/main/java/com/beemdevelopment/aegis/helpers/FabMenuHelper.java b/app/src/main/java/com/beemdevelopment/aegis/helpers/FabMenuHelper.java new file mode 100644 index 000000000..dac8a92f6 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/helpers/FabMenuHelper.java @@ -0,0 +1,186 @@ +package com.beemdevelopment.aegis.helpers; + +import android.animation.ValueAnimator; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.Map; +import java.util.SequencedMap; +import java.util.SequencedSet; +import java.util.function.Consumer; + +public class FabMenuHelper { + private final static long ANIMATION_DURATION = 300L; + private final static long ANIMATION_ACTION_DELAY = 50L; + private final View _scrim; + private final View _menuItemsContainer; + private final FloatingActionButton _mainFab; + private final SequencedSet _actions; + private Consumer _stateListener; + private boolean _isOpen = false; + + public FabMenuHelper( + View scrim, + ViewGroup menuItemsContainer, + FloatingActionButton fab, + SequencedMap actions + ) { + _scrim = scrim; + _menuItemsContainer = menuItemsContainer; + _mainFab = fab; + _actions = actions.sequencedKeySet(); + + for (View action : _actions) { + action.setVisibility(View.GONE); + action.setAlpha(0f); + action.setScaleX(0f); + action.setScaleY(0f); + } + + setupClickListeners(actions); + } + + public void setOnFabMenuStateChangeListener(Consumer listener) { + _stateListener = listener; + } + + private void setupClickListeners(Map actions) { + _mainFab.setOnClickListener(v -> toggle()); + _scrim.setOnClickListener(v -> close()); + + actions.forEach((action, onClick) -> { + action.setOnClickListener(v -> { + if (onClick != null) { + onClick.run(); + } + close(); + }); + }); + } + + public void toggle() { + if (_isOpen) { + close(); + } else { + open(); + } + } + + public void open() { + if (_isOpen) { + return; + } + + _isOpen = true; + + _scrim.animate() + .alpha(0.5f) + .setDuration(ANIMATION_DURATION) + .withStartAction(() -> _scrim.setVisibility(View.VISIBLE)) + .start(); + + _menuItemsContainer.setVisibility(View.VISIBLE); + + long delay = 0L; + for (View action : _actions.reversed()) { + animateActionIn(action, delay); + delay += ANIMATION_ACTION_DELAY; + } + + animateFabIconForward(_mainFab); + + if (_stateListener != null) { + _stateListener.accept(true); + } + } + + public void close() { + if (!_isOpen) { + return; + } + + _isOpen = false; + + _scrim.animate() + .alpha(0f) + .setDuration(ANIMATION_DURATION) + .withEndAction(() -> _scrim.setVisibility(View.GONE)) + .start(); + + long delay = 0L; + for (View action : _actions) { + animateActionOut(action, delay); + delay += ANIMATION_ACTION_DELAY; + } + + animateFabIconBackward(_mainFab); + + _mainFab.postDelayed(() -> { + if (!_isOpen) { + _menuItemsContainer.setVisibility(View.GONE); + } + }, ANIMATION_DURATION); + + if (_stateListener != null) { + _stateListener.accept(false); + } + } + + private void animateFabIconForward(FloatingActionButton fab) { + animateFabIcon(fab, 0f, 45f); + } + + private void animateFabIconBackward(FloatingActionButton fab) { + animateFabIcon(fab, 45f, 0f); + } + + private void animateFabIcon(FloatingActionButton fab, float from, float to) { + Drawable drawable = _mainFab.getDrawable(); + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + fab.setScaleType(ImageView.ScaleType.MATRIX); + Matrix matrix = new Matrix(); + ValueAnimator anim = ValueAnimator.ofFloat(from, to); + anim.setDuration(100L); + + anim.addUpdateListener(valueAnimator -> { + Float angle = (Float) valueAnimator.getAnimatedValue(); + matrix.reset(); + matrix.postRotate(angle, width / 2f, height / 2f); + fab.setImageMatrix(matrix); + }); + + anim.start(); + } + + private void animateActionIn(View action, long delay) { + action.animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(ANIMATION_DURATION) + .setStartDelay(delay) + .withStartAction(() -> action.setVisibility(View.VISIBLE)) + .start(); + } + + private void animateActionOut(View action, long delay) { + action.animate() + .alpha(0f) + .scaleX(0f) + .scaleY(0f) + .setDuration(ANIMATION_DURATION) + .setStartDelay(delay) + .withEndAction(() -> action.setVisibility(View.GONE)) + .start(); + } + + public boolean isOpen() { + return _isOpen; + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java index 592d15d7d..1f835012a 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java @@ -23,6 +23,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AutoCompleteTextView; import android.widget.Button; @@ -46,6 +47,7 @@ import com.beemdevelopment.aegis.SortCategory; import com.beemdevelopment.aegis.helpers.BitmapHelper; import com.beemdevelopment.aegis.helpers.DropdownHelper; +import com.beemdevelopment.aegis.helpers.FabMenuHelper; import com.beemdevelopment.aegis.helpers.FabScrollHelper; import com.beemdevelopment.aegis.helpers.PermissionHelper; import com.beemdevelopment.aegis.helpers.ViewHelper; @@ -69,7 +71,6 @@ import com.beemdevelopment.aegis.vault.VaultGroup; import com.beemdevelopment.aegis.vault.VaultRepository; import com.beemdevelopment.aegis.vault.VaultRepositoryException; -import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; @@ -84,9 +85,11 @@ import java.util.Collections; import java.util.Date; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.SequencedMap; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -117,6 +120,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private Set _prefGroupFilter; private FabScrollHelper _fabScrollHelper; + private FabMenuHelper _fabMenuHelper; private ActionMode _actionMode; private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks(); @@ -124,6 +128,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene private LockBackPressHandler _lockBackPressHandler; private SearchViewBackPressHandler _searchViewBackPressHandler; private ActionModeBackPressHandler _actionModeBackPressHandler; + private FabMenuBackPressHandler _fabMenuBackPressHandler; private final ActivityResultLauncher authResultLauncher = registerForActivityResult(new StartActivityForResult(), activityResult -> { @@ -207,6 +212,8 @@ protected void onCreate(Bundle savedInstanceState) { getOnBackPressedDispatcher().addCallback(this, _searchViewBackPressHandler); _actionModeBackPressHandler = new ActionModeBackPressHandler(); getOnBackPressedDispatcher().addCallback(this, _actionModeBackPressHandler); + _fabMenuBackPressHandler = new FabMenuBackPressHandler(); + getOnBackPressedDispatcher().addCallback(this, _fabMenuBackPressHandler); _entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles); _entryListView.setListener(this); @@ -226,27 +233,30 @@ protected void onCreate(Bundle savedInstanceState) { _entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask()); _prefGroupFilter = _prefs.getGroupFilter(); + View scrimOverlayLayout = LayoutInflater.from(this).inflate(R.layout.scrim_layout, null); + View scrimOverlay = scrimOverlayLayout.findViewById(R.id.scrim); + addContentView(scrimOverlayLayout, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + View fabMenuLayout = LayoutInflater.from(this).inflate(R.layout.fab_menu, null); + addContentView(fabMenuLayout, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + ViewGroup menuItemsContainer = findViewById(R.id.fab_menu_items_container); + FloatingActionButton fab = findViewById(R.id.fab); - fab.setOnClickListener(v -> { - View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null); - BottomSheetDialog dialog = new BottomSheetDialog(this); - dialog.setContentView(view); - - view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> { - dialog.dismiss(); - startEditEntryActivityForManual(); - }); - view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> { - dialog.dismiss(); - startScanImageActivity(); - }); - view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> { - dialog.dismiss(); - startScanActivity(); - }); - - Dialogs.showSecureDialog(dialog); - }); + + SequencedMap actions = new LinkedHashMap<>(); + actions.put(findViewById(R.id.fab_menu_item_scan), this::startScanActivity); + actions.put(findViewById(R.id.fab_menu_item_scan_image), this::startScanImageActivity); + actions.put(findViewById(R.id.fab_menu_item_enter), this::startEditEntryActivityForManual); + + _fabMenuHelper = new FabMenuHelper(scrimOverlay, menuItemsContainer, fab, actions); + _fabMenuHelper.setOnFabMenuStateChangeListener(_fabMenuBackPressHandler::setEnabled); _groupChip = findViewById(R.id.groupChipGroup); _fabScrollHelper = new FabScrollHelper(fab); @@ -1314,6 +1324,9 @@ public void onLocked(boolean userInitiated) { if (_searchView != null && !_searchView.isIconified()) { collapseSearchView(); } + if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) { + _fabMenuHelper.close(); + } _entryListView.clearEntries(); _loaded = false; @@ -1399,6 +1412,19 @@ public void handleOnBackPressed() { } } + private class FabMenuBackPressHandler extends OnBackPressedCallback { + public FabMenuBackPressHandler() { + super(false); + } + + @Override + public void handleOnBackPressed() { + if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) { + _fabMenuHelper.close(); + } + } + } + private class ActionModeCallbacks implements ActionMode.Callback { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b2ca2d006..f7cb30e3e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -59,12 +59,4 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - - diff --git a/app/src/main/res/layout/fab_menu.xml b/app/src/main/res/layout/fab_menu.xml new file mode 100644 index 000000000..2ed1a6ebf --- /dev/null +++ b/app/src/main/res/layout/fab_menu.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/scrim_layout.xml b/app/src/main/res/layout/scrim_layout.xml new file mode 100644 index 000000000..114c54e78 --- /dev/null +++ b/app/src/main/res/layout/scrim_layout.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 104bdeb93..b3a2db487 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,4 +1,5 @@ 16dp + 24dp 48dp