mirror of https://github.com/beemdevelopment/Aegis
Add support for the Material Design 3 FAB Menu component
Co-authored-by: Michael Schättgen <michael@schattgen.me>pull/1743/head
parent
491a81584d
commit
276cdb584a
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue