Skip to content

Commit 7897a09

Browse files
Merge pull request #1743 from voleum/master
Add support for the Material Design 3 FAB Menu component
2 parents 491a815 + 276cdb5 commit 7897a09

7 files changed

Lines changed: 399 additions & 31 deletions

File tree

app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ private void changeGroupFilter(String text) {
186186

187187
private void addEntry(VaultEntry entry) {
188188
onView(withId(R.id.fab)).perform(click());
189-
onView(withId(R.id.fab_enter)).perform(click());
189+
onView(withId(R.id.fab_menu_item_enter)).perform(click());
190190

191191
onView(withId(R.id.accordian_header)).perform(scrollTo(), click());
192192
onView(withId(R.id.text_name)).perform(typeText(entry.getName()), closeSoftKeyboard());
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.beemdevelopment.aegis.helpers;
2+
3+
import android.animation.ValueAnimator;
4+
import android.graphics.Matrix;
5+
import android.graphics.drawable.Drawable;
6+
import android.view.View;
7+
import android.view.ViewGroup;
8+
import android.view.animation.OvershootInterpolator;
9+
import android.widget.ImageView;
10+
11+
12+
import com.google.android.material.floatingactionbutton.FloatingActionButton;
13+
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.function.Consumer;
18+
19+
public class FabMenuHelper {
20+
private final static long ANIMATION_DURATION = 300L;
21+
private final static long ANIMATION_ACTION_DELAY = 50L;
22+
private final View _scrim;
23+
private final View _menuItemsContainer;
24+
private final FloatingActionButton _mainFab;
25+
private final List<View> _actions;
26+
private Consumer<Boolean> _stateListener;
27+
private boolean _isOpen = false;
28+
29+
public FabMenuHelper(
30+
View scrim,
31+
ViewGroup menuItemsContainer,
32+
FloatingActionButton fab,
33+
Map<View, Runnable> actions
34+
) {
35+
_scrim = scrim;
36+
_menuItemsContainer = menuItemsContainer;
37+
_mainFab = fab;
38+
_actions = new ArrayList<>(actions.keySet());
39+
40+
for (View action : _actions) {
41+
action.setVisibility(View.GONE);
42+
action.setAlpha(0f);
43+
action.setScaleX(0f);
44+
action.setScaleY(0f);
45+
}
46+
47+
setupClickListeners(actions);
48+
}
49+
50+
public void setOnFabMenuStateChangeListener(Consumer<Boolean> listener) {
51+
_stateListener = listener;
52+
}
53+
54+
private void setupClickListeners(Map<View, Runnable> actions) {
55+
_mainFab.setOnClickListener(v -> toggle());
56+
_scrim.setOnClickListener(v -> close());
57+
58+
actions.forEach((action, onClick) -> {
59+
action.setOnClickListener(v -> {
60+
if (onClick != null) {
61+
onClick.run();
62+
}
63+
close();
64+
});
65+
});
66+
}
67+
68+
public void toggle() {
69+
if (_isOpen) {
70+
close();
71+
} else {
72+
open();
73+
}
74+
}
75+
76+
public void open() {
77+
if (_isOpen) {
78+
return;
79+
}
80+
81+
_isOpen = true;
82+
83+
_scrim.animate()
84+
.alpha(0.5f)
85+
.setDuration(ANIMATION_DURATION)
86+
.withStartAction(() -> _scrim.setVisibility(View.VISIBLE))
87+
.start();
88+
89+
_menuItemsContainer.setVisibility(View.VISIBLE);
90+
91+
long delay = 0L;
92+
for (int i = _actions.size() - 1; i >= 0; i--) {
93+
animateActionIn(_actions.get(i), delay);
94+
delay += ANIMATION_ACTION_DELAY;
95+
}
96+
97+
animateFabIconForward(_mainFab);
98+
99+
if (_stateListener != null) {
100+
_stateListener.accept(true);
101+
}
102+
}
103+
104+
public void close() {
105+
if (!_isOpen) {
106+
return;
107+
}
108+
109+
_isOpen = false;
110+
111+
_scrim.animate()
112+
.alpha(0f)
113+
.setDuration(ANIMATION_DURATION)
114+
.withEndAction(() -> _scrim.setVisibility(View.GONE))
115+
.start();
116+
117+
long delay = 0L;
118+
for (View action : _actions) {
119+
animateActionOut(action, delay);
120+
delay += ANIMATION_ACTION_DELAY;
121+
}
122+
123+
animateFabIconBackward(_mainFab);
124+
125+
_mainFab.postDelayed(() -> {
126+
if (!_isOpen) {
127+
_menuItemsContainer.setVisibility(View.GONE);
128+
}
129+
}, ANIMATION_DURATION);
130+
131+
if (_stateListener != null) {
132+
_stateListener.accept(false);
133+
}
134+
}
135+
136+
private void animateFabIconForward(FloatingActionButton fab) {
137+
animateFabIcon(fab, 0f, 45f);
138+
}
139+
140+
private void animateFabIconBackward(FloatingActionButton fab) {
141+
animateFabIcon(fab, 45f, 0f);
142+
}
143+
144+
private void animateFabIcon(FloatingActionButton fab, float from, float to) {
145+
Drawable drawable = _mainFab.getDrawable();
146+
int width = drawable.getIntrinsicWidth();
147+
int height = drawable.getIntrinsicHeight();
148+
fab.setScaleType(ImageView.ScaleType.MATRIX);
149+
Matrix matrix = new Matrix();
150+
ValueAnimator anim = ValueAnimator.ofFloat(from, to);
151+
anim.setDuration(100L);
152+
153+
anim.addUpdateListener(valueAnimator -> {
154+
Float angle = (Float) valueAnimator.getAnimatedValue();
155+
matrix.reset();
156+
matrix.postRotate(angle, width / 2f, height / 2f);
157+
fab.setImageMatrix(matrix);
158+
});
159+
160+
anim.start();
161+
}
162+
163+
private void animateActionIn(View action, long delay) {
164+
action.setVisibility(View.VISIBLE);
165+
action.setAlpha(0f);
166+
action.setScaleX(0.4f);
167+
action.setScaleY(0.4f);
168+
169+
action.animate()
170+
.alpha(1f)
171+
.scaleX(1f)
172+
.scaleY(1f)
173+
.setDuration(ANIMATION_DURATION)
174+
.setStartDelay(delay)
175+
.setInterpolator(new OvershootInterpolator(1.2f))
176+
.start();
177+
}
178+
179+
private void animateActionOut(View action, long delay) {
180+
action.animate()
181+
.alpha(0f)
182+
.scaleX(0f)
183+
.scaleY(0f)
184+
.setDuration(ANIMATION_DURATION)
185+
.setStartDelay(delay)
186+
.withEndAction(() -> action.setVisibility(View.GONE))
187+
.start();
188+
}
189+
190+
public boolean isOpen() {
191+
return _isOpen;
192+
}
193+
}

app/src/main/java/com/beemdevelopment/aegis/ui/MainActivity.java

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import android.view.MenuInflater;
2424
import android.view.MenuItem;
2525
import android.view.View;
26+
import android.view.ViewGroup;
2627
import android.view.inputmethod.InputMethodManager;
2728
import android.widget.AutoCompleteTextView;
2829
import android.widget.Button;
@@ -46,6 +47,7 @@
4647
import com.beemdevelopment.aegis.SortCategory;
4748
import com.beemdevelopment.aegis.helpers.BitmapHelper;
4849
import com.beemdevelopment.aegis.helpers.DropdownHelper;
50+
import com.beemdevelopment.aegis.helpers.FabMenuHelper;
4951
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
5052
import com.beemdevelopment.aegis.helpers.PermissionHelper;
5153
import com.beemdevelopment.aegis.helpers.ViewHelper;
@@ -69,7 +71,6 @@
6971
import com.beemdevelopment.aegis.vault.VaultGroup;
7072
import com.beemdevelopment.aegis.vault.VaultRepository;
7173
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
72-
import com.google.android.material.bottomsheet.BottomSheetDialog;
7374
import com.google.android.material.chip.Chip;
7475
import com.google.android.material.chip.ChipGroup;
7576
import com.google.android.material.color.MaterialColors;
@@ -84,9 +85,11 @@
8485
import java.util.Collections;
8586
import java.util.Date;
8687
import java.util.HashSet;
88+
import java.util.LinkedHashMap;
8789
import java.util.List;
8890
import java.util.Map;
8991
import java.util.Objects;
92+
import java.util.SequencedMap;
9093
import java.util.Set;
9194
import java.util.UUID;
9295
import java.util.concurrent.atomic.AtomicReference;
@@ -117,13 +120,15 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
117120
private Set<UUID> _prefGroupFilter;
118121

119122
private FabScrollHelper _fabScrollHelper;
123+
private FabMenuHelper _fabMenuHelper;
120124

121125
private ActionMode _actionMode;
122126
private ActionMode.Callback _actionModeCallbacks = new ActionModeCallbacks();
123127

124128
private LockBackPressHandler _lockBackPressHandler;
125129
private SearchViewBackPressHandler _searchViewBackPressHandler;
126130
private ActionModeBackPressHandler _actionModeBackPressHandler;
131+
private FabMenuBackPressHandler _fabMenuBackPressHandler;
127132

128133
private final ActivityResultLauncher<Intent> authResultLauncher =
129134
registerForActivityResult(new StartActivityForResult(), activityResult -> {
@@ -207,6 +212,8 @@ protected void onCreate(Bundle savedInstanceState) {
207212
getOnBackPressedDispatcher().addCallback(this, _searchViewBackPressHandler);
208213
_actionModeBackPressHandler = new ActionModeBackPressHandler();
209214
getOnBackPressedDispatcher().addCallback(this, _actionModeBackPressHandler);
215+
_fabMenuBackPressHandler = new FabMenuBackPressHandler();
216+
getOnBackPressedDispatcher().addCallback(this, _fabMenuBackPressHandler);
210217

211218
_entryListView = (EntryListView) getSupportFragmentManager().findFragmentById(R.id.key_profiles);
212219
_entryListView.setListener(this);
@@ -226,27 +233,29 @@ protected void onCreate(Bundle savedInstanceState) {
226233
_entryListView.setSearchBehaviorMask(_prefs.getSearchBehaviorMask());
227234
_prefGroupFilter = _prefs.getGroupFilter();
228235

229-
FloatingActionButton fab = findViewById(R.id.fab);
230-
fab.setOnClickListener(v -> {
231-
View view = getLayoutInflater().inflate(R.layout.dialog_add_entry, null);
232-
BottomSheetDialog dialog = new BottomSheetDialog(this);
233-
dialog.setContentView(view);
234-
235-
view.findViewById(R.id.fab_enter).setOnClickListener(v1 -> {
236-
dialog.dismiss();
237-
startEditEntryActivityForManual();
238-
});
239-
view.findViewById(R.id.fab_scan_image).setOnClickListener(v2 -> {
240-
dialog.dismiss();
241-
startScanImageActivity();
242-
});
243-
view.findViewById(R.id.fab_scan).setOnClickListener(v3 -> {
244-
dialog.dismiss();
245-
startScanActivity();
246-
});
247-
248-
Dialogs.showSecureDialog(dialog);
249-
});
236+
View scrimOverlayLayout = LayoutInflater.from(this).inflate(R.layout.scrim_layout, null);
237+
View scrimOverlay = scrimOverlayLayout.findViewById(R.id.scrim);
238+
addContentView(scrimOverlayLayout, new ViewGroup.LayoutParams(
239+
ViewGroup.LayoutParams.MATCH_PARENT,
240+
ViewGroup.LayoutParams.MATCH_PARENT
241+
));
242+
243+
View fabMenuLayout = LayoutInflater.from(this).inflate(R.layout.fab_menu, null);
244+
addContentView(fabMenuLayout, new ViewGroup.LayoutParams(
245+
ViewGroup.LayoutParams.MATCH_PARENT,
246+
ViewGroup.LayoutParams.MATCH_PARENT
247+
));
248+
249+
ViewGroup menuItemsContainer = fabMenuLayout.findViewById(R.id.fab_menu_items_container);
250+
FloatingActionButton fab = fabMenuLayout.findViewById(R.id.fab);
251+
252+
LinkedHashMap<View, Runnable> actions = new LinkedHashMap<>();
253+
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan), this::startScanActivity);
254+
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_scan_image), this::startScanImageActivity);
255+
actions.put(fabMenuLayout.findViewById(R.id.fab_menu_item_enter), this::startEditEntryActivityForManual);
256+
257+
_fabMenuHelper = new FabMenuHelper(scrimOverlay, menuItemsContainer, fab, actions);
258+
_fabMenuHelper.setOnFabMenuStateChangeListener(_fabMenuBackPressHandler::setEnabled);
250259

251260
_groupChip = findViewById(R.id.groupChipGroup);
252261
_fabScrollHelper = new FabScrollHelper(fab);
@@ -1314,6 +1323,9 @@ public void onLocked(boolean userInitiated) {
13141323
if (_searchView != null && !_searchView.isIconified()) {
13151324
collapseSearchView();
13161325
}
1326+
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
1327+
_fabMenuHelper.close();
1328+
}
13171329

13181330
_entryListView.clearEntries();
13191331
_loaded = false;
@@ -1399,6 +1411,19 @@ public void handleOnBackPressed() {
13991411
}
14001412
}
14011413

1414+
private class FabMenuBackPressHandler extends OnBackPressedCallback {
1415+
public FabMenuBackPressHandler() {
1416+
super(false);
1417+
}
1418+
1419+
@Override
1420+
public void handleOnBackPressed() {
1421+
if (_fabMenuHelper != null && _fabMenuHelper.isOpen()) {
1422+
_fabMenuHelper.close();
1423+
}
1424+
}
1425+
}
1426+
14021427
private class ActionModeCallbacks implements ActionMode.Callback {
14031428
@Override
14041429
public boolean onCreateActionMode(ActionMode mode, Menu menu) {

app/src/main/res/layout/activity_main.xml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,4 @@
5959
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
6060
</LinearLayout>
6161

62-
<com.google.android.material.floatingactionbutton.FloatingActionButton
63-
android:id="@+id/fab"
64-
android:layout_width="wrap_content"
65-
android:layout_height="wrap_content"
66-
android:layout_gravity="bottom|end"
67-
android:layout_margin="@dimen/fab_margin"
68-
android:src="@drawable/ic_outline_add_24" />
69-
7062
</androidx.coordinatorlayout.widget.CoordinatorLayout>

0 commit comments

Comments
 (0)