Add support for the Material Design 3 FAB Menu component
Co-authored-by: Michael Schättgen <michael@schattgen.me>
This commit is contained in:
committed by
Michael Schättgen
co-authored by
Michael Schättgen
parent
491a81584d
commit
276cdb584a
@@ -186,7 +186,7 @@ public class OverallTest extends AegisTest {
|
||||
|
||||
private void addEntry(VaultEntry entry) {
|
||||
onView(withId(R.id.fab)).perform(click());
|
||||
onView(withId(R.id.fab_enter)).perform(click());
|
||||
onView(withId(R.id.fab_menu_item_enter)).perform(click());
|
||||
|
||||
onView(withId(R.id.accordian_header)).perform(scrollTo(), click());
|
||||
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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.view.animation.OvershootInterpolator;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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 List<View> _actions;
|
||||
private Consumer<Boolean> _stateListener;
|
||||
private boolean _isOpen = false;
|
||||
|
||||
public FabMenuHelper(
|
||||
View scrim,
|
||||
ViewGroup menuItemsContainer,
|
||||
FloatingActionButton fab,
|
||||
Map<View, Runnable> actions
|
||||
) {
|
||||
_scrim = scrim;
|
||||
_menuItemsContainer = menuItemsContainer;
|
||||
_mainFab = fab;
|
||||
_actions = new ArrayList<>(actions.keySet());
|
||||
|
||||
for (View action : _actions) {
|
||||
action.setVisibility(View.GONE);
|
||||
action.setAlpha(0f);
|
||||
action.setScaleX(0f);
|
||||
action.setScaleY(0f);
|
||||
}
|
||||
|
||||
setupClickListeners(actions);
|
||||
}
|
||||
|
||||
public void setOnFabMenuStateChangeListener(Consumer<Boolean> listener) {
|
||||
_stateListener = listener;
|
||||
}
|
||||
|
||||
private void setupClickListeners(Map<View, Runnable> 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 (int i = _actions.size() - 1; i >= 0; i--) {
|
||||
animateActionIn(_actions.get(i), 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.setVisibility(View.VISIBLE);
|
||||
action.setAlpha(0f);
|
||||
action.setScaleX(0.4f);
|
||||
action.setScaleY(0.4f);
|
||||
|
||||
action.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setStartDelay(delay)
|
||||
.setInterpolator(new OvershootInterpolator(1.2f))
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.view.Menu;
|
||||
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.R;
|
||||
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.VaultFile;
|
||||
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.Collection;
|
||||
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<UUID> _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<Intent> authResultLauncher =
|
||||
registerForActivityResult(new StartActivityForResult(), activityResult -> {
|
||||
@@ -207,6 +212,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
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,29 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
|
||||
_prefGroupFilter = _prefs.getGroupFilter();
|
||||
|
||||
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 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.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();
|
||||
});
|
||||
View fabMenuLayout = LayoutInflater.from(this).inflate(R.layout.fab_menu, null);
|
||||
addContentView(fabMenuLayout, new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
));
|
||||
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
});
|
||||
ViewGroup menuItemsContainer = fabMenuLayout.findViewById(R.id.fab_menu_items_container);
|
||||
FloatingActionButton fab = fabMenuLayout.findViewById(R.id.fab);
|
||||
|
||||
LinkedHashMap<View, Runnable> actions = new LinkedHashMap<>();
|
||||
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan), this::startScanActivity);
|
||||
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan_image), this::startScanImageActivity);
|
||||
actions.put(fabMenuLayout.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 +1323,9 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
if (_searchView != null && !_searchView.isIconified()) {
|
||||
collapseSearchView();
|
||||
}
|
||||
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
|
||||
_fabMenuHelper.close();
|
||||
}
|
||||
|
||||
_entryListView.clearEntries();
|
||||
_loaded = false;
|
||||
@@ -1399,6 +1411,19 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -59,12 +59,4 @@
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:src="@drawable/ic_outline_add_24" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/fab_menu_items_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginHorizontal="@dimen/fab_margin"
|
||||
android:layout_marginBottom="75dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/fab_menu_item_scan"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="6dp"
|
||||
app:cardCornerRadius="100dp"
|
||||
app:cardElevation="6dp"
|
||||
app:contentPadding="6dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/fab_menu_item_image_size"
|
||||
android:layout_height="@dimen/fab_menu_item_image_size"
|
||||
android:src="@drawable/ic_qrcode_scan" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan"
|
||||
android:textAppearance="?attr/textAppearanceLargePopupMenu" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/fab_menu_item_scan_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="6dp"
|
||||
app:cardCornerRadius="100dp"
|
||||
app:cardElevation="6dp"
|
||||
app:contentPadding="6dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/fab_menu_item_image_size"
|
||||
android:layout_height="@dimen/fab_menu_item_image_size"
|
||||
android:src="@drawable/ic_outline_add_photo_alternate_24" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/scan_image"
|
||||
android:textAppearance="?attr/textAppearanceLargePopupMenu" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/fab_menu_item_enter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginBottom="6dp"
|
||||
app:cardCornerRadius="100dp"
|
||||
app:cardElevation="6dp"
|
||||
app:contentPadding="6dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/fab_menu_item_image_size"
|
||||
android:layout_height="@dimen/fab_menu_item_image_size"
|
||||
android:src="@drawable/ic_outline_edit_24" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_manually"
|
||||
android:textAppearance="?attr/textAppearanceLargePopupMenu" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:contentDescription="@string/add_new_entry"
|
||||
android:src="@drawable/ic_outline_add_24" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:id="@+id/scrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000000"
|
||||
android:alpha="0"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,4 +1,5 @@
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
<dimen name="fab_menu_item_image_size">24dp</dimen>
|
||||
<dimen name="list_item_height">48dp</dimen>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user