Port tunnel creation UI from Viscerion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
d25702d99d
commit
02ea696070
@ -1,4 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply from: 'nonnull.gradle'
|
||||
|
||||
// Create a variable called keystorePropertiesFile, and initialize it to your
|
||||
@ -70,9 +71,11 @@ ext {
|
||||
annotationsVersion = '1.1.0'
|
||||
appcompatVersion = '1.1.0'
|
||||
cardviewVersion = '1.0.0'
|
||||
coordinatorLayoutVersion = '1.1.3'
|
||||
databindingVersion = '3.5.3'
|
||||
materialComponentsVersion = '1.1.0'
|
||||
jsr305Version = '3.0.2'
|
||||
kotlinVersion = '1.3.61'
|
||||
preferenceVersion = '1.1.0'
|
||||
streamsupportVersion = '1.7.1'
|
||||
threetenabpVersion = '1.2.2'
|
||||
@ -96,6 +99,8 @@ dependencies {
|
||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||
implementation "net.i2p.crypto:eddsa:$eddsaVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$coordinatorLayoutVersion"
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
|
@ -14,13 +14,11 @@ import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.android.fragment.TunnelDetailFragment;
|
||||
import com.wireguard.android.fragment.TunnelEditorFragment;
|
||||
import com.wireguard.android.fragment.TunnelListFragment;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
|
||||
/**
|
||||
@ -33,16 +31,10 @@ public class MainActivity extends BaseActivity
|
||||
implements FragmentManager.OnBackStackChangedListener {
|
||||
@Nullable private ActionBar actionBar;
|
||||
private boolean isTwoPaneLayout;
|
||||
@Nullable private TunnelListFragment listFragment;
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
final int backStackEntries = getSupportFragmentManager().getBackStackEntryCount();
|
||||
// If the action menu is visible and expanded, collapse it instead of navigating back.
|
||||
if (isTwoPaneLayout || backStackEntries == 0) {
|
||||
if (listFragment != null && listFragment.collapseActionMenu())
|
||||
return;
|
||||
}
|
||||
// If the two-pane layout does not have an editor open, going back should exit the app.
|
||||
if (isTwoPaneLayout && backStackEntries <= 1) {
|
||||
finish();
|
||||
@ -74,12 +66,8 @@ public class MainActivity extends BaseActivity
|
||||
setContentView(R.layout.main_activity);
|
||||
actionBar = getSupportActionBar();
|
||||
isTwoPaneLayout = findViewById(R.id.master_detail_wrapper) instanceof LinearLayout;
|
||||
listFragment = (TunnelListFragment) getSupportFragmentManager().findFragmentByTag("LIST");
|
||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
||||
onBackStackChanged();
|
||||
final View actionBarView = findViewById(R.id.action_bar);
|
||||
if (actionBarView != null)
|
||||
actionBarView.setOnTouchListener((v, e) -> listFragment != null && listFragment.collapseActionMenu());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.wireguard.android.R
|
||||
import com.wireguard.android.activity.TunnelCreatorActivity
|
||||
import com.wireguard.android.util.resolveAttribute
|
||||
|
||||
class AddTunnelsSheet : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var behavior: BottomSheetBehavior<FrameLayout>
|
||||
private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTheme(): Int {
|
||||
return R.style.BottomSheetDialogTheme
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
if (savedInstanceState != null) dismiss()
|
||||
return inflater.inflate(R.layout.add_tunnels_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
val dialog = dialog as BottomSheetDialog? ?: return
|
||||
behavior = dialog.behavior
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
behavior.peekHeight = 0
|
||||
behavior.addBottomSheetCallback(bottomSheetCallback)
|
||||
dialog.findViewById<View>(R.id.create_empty)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestCreateConfig()
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_from_file)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestImportConfig()
|
||||
}
|
||||
dialog.findViewById<View>(R.id.create_from_qrcode)?.setOnClickListener {
|
||||
dismiss()
|
||||
onRequestScanQRCode()
|
||||
}
|
||||
}
|
||||
})
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(requireContext().resolveAttribute(R.attr.colorBackground))
|
||||
}
|
||||
view.background = gradientDrawable
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
super.dismiss()
|
||||
behavior.removeBottomSheetCallback(bottomSheetCallback)
|
||||
}
|
||||
|
||||
private fun requireTargetFragment(): Fragment {
|
||||
return requireNotNull(targetFragment) { "A target fragment should always be set" }
|
||||
}
|
||||
|
||||
private fun onRequestCreateConfig() {
|
||||
startActivity(Intent(activity, TunnelCreatorActivity::class.java))
|
||||
}
|
||||
|
||||
private fun onRequestImportConfig() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
}
|
||||
requireTargetFragment().startActivityForResult(intent, TunnelListFragment.REQUEST_IMPORT)
|
||||
}
|
||||
|
||||
private fun onRequestScanQRCode() {
|
||||
val integrator = IntentIntegrator.forSupportFragment(requireTargetFragment()).apply {
|
||||
setOrientationLocked(false)
|
||||
setBeepEnabled(false)
|
||||
setPrompt(getString(R.string.qr_code_hint))
|
||||
}
|
||||
integrator.initiateScan(listOf(IntentIntegrator.QR_CODE))
|
||||
}
|
||||
}
|
@ -39,7 +39,6 @@ import com.wireguard.android.databinding.TunnelListItemBinding;
|
||||
import com.wireguard.android.model.Tunnel;
|
||||
import com.wireguard.android.util.ErrorMessages;
|
||||
import com.wireguard.android.widget.MultiselectableRelativeLayout;
|
||||
import com.wireguard.android.widget.fab.FloatingActionsMenuRecyclerViewScrollListener;
|
||||
import com.wireguard.config.BadConfigException;
|
||||
import com.wireguard.config.Config;
|
||||
|
||||
@ -65,21 +64,14 @@ import java9.util.stream.StreamSupport;
|
||||
*/
|
||||
|
||||
public class TunnelListFragment extends BaseFragment {
|
||||
private static final int REQUEST_IMPORT = 1;
|
||||
public static final int REQUEST_IMPORT = 1;
|
||||
private static final int REQUEST_TARGET_FRAGMENT = 2;
|
||||
private static final String TAG = "WireGuard/" + TunnelListFragment.class.getSimpleName();
|
||||
|
||||
private final ActionModeListener actionModeListener = new ActionModeListener();
|
||||
@Nullable private ActionMode actionMode;
|
||||
@Nullable private TunnelListFragmentBinding binding;
|
||||
|
||||
public boolean collapseActionMenu() {
|
||||
if (binding != null && binding.createMenu.isExpanded()) {
|
||||
binding.createMenu.collapse();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void importTunnel(@NonNull final String configText) {
|
||||
try {
|
||||
// Ensure the config text is parseable before proceeding…
|
||||
@ -218,21 +210,17 @@ public class TunnelListFragment extends BaseFragment {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
binding = TunnelListFragmentBinding.inflate(inflater, container, false);
|
||||
|
||||
binding.tunnelList.setOnTouchListener((view, motionEvent) -> {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
return false;
|
||||
binding.createFab.setOnClickListener(v -> {
|
||||
final AddTunnelsSheet bottomSheet = new AddTunnelsSheet();
|
||||
bottomSheet.setTargetFragment(this, REQUEST_TARGET_FRAGMENT);
|
||||
bottomSheet.show(requireFragmentManager(), "BOTTOM_SHEET");
|
||||
});
|
||||
binding.tunnelList.setOnScrollListener(new FloatingActionsMenuRecyclerViewScrollListener(binding.createMenu));
|
||||
binding.executePendingBindings();
|
||||
return binding.getRoot();
|
||||
}
|
||||
@ -245,36 +233,11 @@ public class TunnelListFragment extends BaseFragment {
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (binding != null) {
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void onRequestCreateConfig(@SuppressWarnings("unused") final View view) {
|
||||
startActivity(new Intent(getActivity(), TunnelCreatorActivity.class));
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestImportConfig(@SuppressWarnings("unused") final View view) {
|
||||
final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, REQUEST_IMPORT);
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
public void onRequestScanQRCode(@SuppressWarnings("unused") final View view) {
|
||||
final IntentIntegrator intentIntegrator = IntentIntegrator.forSupportFragment(this);
|
||||
intentIntegrator.setOrientationLocked(false);
|
||||
intentIntegrator.setBeepEnabled(false);
|
||||
intentIntegrator.setPrompt(getString(R.string.qr_code_hint));
|
||||
intentIntegrator.initiateScan(Collections.singletonList(IntentIntegrator.QR_CODE));
|
||||
|
||||
if (binding != null)
|
||||
binding.createMenu.collapse();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -296,6 +259,14 @@ public class TunnelListFragment extends BaseFragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void showSnackbar(final CharSequence message) {
|
||||
if (binding != null) {
|
||||
final Snackbar snackbar = Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.setAnchorView(binding.createFab);
|
||||
snackbar.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void onTunnelDeletionFinished(final Integer count, @Nullable final Throwable throwable) {
|
||||
final String message;
|
||||
if (throwable == null) {
|
||||
@ -305,9 +276,7 @@ public class TunnelListFragment extends BaseFragment {
|
||||
message = getResources().getQuantityString(R.plurals.delete_error, count, count, error);
|
||||
Log.e(TAG, message, throwable);
|
||||
}
|
||||
if (binding != null) {
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
showSnackbar(message);
|
||||
}
|
||||
|
||||
private void onTunnelImportFinished(final List<Tunnel> tunnels, final Collection<Throwable> throwables) {
|
||||
@ -331,8 +300,7 @@ public class TunnelListFragment extends BaseFragment {
|
||||
tunnels.size() + throwables.size(),
|
||||
tunnels.size(), tunnels.size() + throwables.size());
|
||||
|
||||
if (binding != null)
|
||||
Snackbar.make(binding.mainContainer, message, Snackbar.LENGTH_LONG).show();
|
||||
showSnackbar(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
16
app/src/main/java/com/wireguard/android/util/Extensions.kt
Normal file
16
app/src/main/java/com/wireguard/android/util/Extensions.kt
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import androidx.annotation.AttrRes
|
||||
|
||||
fun Context.resolveAttribute(@AttrRes attrRes: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(attrRes, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior<FloatingActionsMenu> {
|
||||
|
||||
private static final long ANIMATION_DURATION = 250;
|
||||
private static final TimeInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
|
||||
|
||||
public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private static void animateChange(final FloatingActionsMenu child, final float destination, final float fullSpan) {
|
||||
final float origin = child.getBehaviorYTranslation();
|
||||
if (Math.abs(destination - origin) < fullSpan / 2) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
return;
|
||||
}
|
||||
final ValueAnimator animator = new ValueAnimator();
|
||||
animator.setFloatValues(origin, destination);
|
||||
animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
|
||||
animator.setDuration((long) (ANIMATION_DURATION * (Math.abs(destination - origin) / fullSpan)));
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator a) {
|
||||
child.setBehaviorYTranslation(destination);
|
||||
}
|
||||
});
|
||||
animator.addUpdateListener(a -> child.setBehaviorYTranslation((float) a.getAnimatedValue()));
|
||||
animator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean layoutDependsOn(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
return dependency instanceof Snackbar.SnackbarLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDependentViewChanged(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, Math.min(0, dependency.getTranslationY() - dependency.getMeasuredHeight()), dependency.getMeasuredHeight());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDependentViewRemoved(final CoordinatorLayout parent, final FloatingActionsMenu child,
|
||||
final View dependency) {
|
||||
animateChange(child, 0, dependency.getMeasuredHeight());
|
||||
}
|
||||
}
|
@ -1,629 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class FloatingActionsMenu extends ViewGroup {
|
||||
public static final int EXPAND_DOWN = 1;
|
||||
public static final int EXPAND_LEFT = 2;
|
||||
public static final int EXPAND_RIGHT = 3;
|
||||
public static final int EXPAND_UP = 0;
|
||||
public static final int LABELS_ON_LEFT_SIDE = 0;
|
||||
public static final int LABELS_ON_RIGHT_SIDE = 1;
|
||||
private static final TimeInterpolator ALPHA_EXPAND_INTERPOLATOR = new DecelerateInterpolator();
|
||||
private static final int ANIMATION_DURATION = 300;
|
||||
private static final boolean BROKEN_LABEL_STYLE = Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1 && Build.BRAND.equalsIgnoreCase("ASUS");
|
||||
private static final float COLLAPSED_PLUS_ROTATION = 0f;
|
||||
private static final TimeInterpolator COLLAPSE_INTERPOLATOR = new DecelerateInterpolator(3f);
|
||||
private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
|
||||
private static final TimeInterpolator EXPAND_INTERPOLATOR = new OvershootInterpolator();
|
||||
private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
|
||||
private final Rect touchArea = new Rect(0, 0, 0, 0);
|
||||
private float behaviorYTranslation;
|
||||
@Nullable private FloatingActionButton mAddButton;
|
||||
private int mButtonSpacing;
|
||||
private int mButtonsCount;
|
||||
private int mExpandDirection;
|
||||
private boolean mExpanded;
|
||||
private int mLabelsMargin;
|
||||
private int mLabelsPosition;
|
||||
private int mLabelsStyle;
|
||||
private int mLabelsVerticalOffset;
|
||||
@Nullable private OnFloatingActionsMenuUpdateListener mListener;
|
||||
private int mMaxButtonHeight;
|
||||
private int mMaxButtonWidth;
|
||||
@Nullable private RotatingDrawable mRotatingDrawable;
|
||||
@Nullable private TouchDelegateGroup mTouchDelegateGroup;
|
||||
private float scrollYTranslation;
|
||||
|
||||
public FloatingActionsMenu(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public FloatingActionsMenu(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private static int adjustForOvershoot(final int dimension) {
|
||||
return dimension * 12 / 10;
|
||||
}
|
||||
|
||||
public void addButton(final LabeledFloatingActionButton button) {
|
||||
addView(button, mButtonsCount - 1);
|
||||
mButtonsCount++;
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public void collapse() {
|
||||
collapse(false);
|
||||
}
|
||||
|
||||
private void collapse(final boolean immediately) {
|
||||
if (mExpanded) {
|
||||
mExpanded = false;
|
||||
mTouchDelegateGroup.setEnabled(false);
|
||||
mCollapseAnimation.setDuration(immediately ? 0 : ANIMATION_DURATION);
|
||||
mCollapseAnimation.start();
|
||||
mExpandAnimation.cancel();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuCollapsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void collapseImmediately() {
|
||||
collapse(true);
|
||||
}
|
||||
|
||||
private void createAddButton(final Context context) {
|
||||
final RotatingDrawable rotatingDrawable = new RotatingDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_action_add_white, context.getTheme()));
|
||||
mRotatingDrawable = rotatingDrawable;
|
||||
|
||||
final TimeInterpolator interpolator = new OvershootInterpolator();
|
||||
|
||||
final ObjectAnimator collapseAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", EXPANDED_PLUS_ROTATION, COLLAPSED_PLUS_ROTATION);
|
||||
final ObjectAnimator expandAnimator = ObjectAnimator.ofFloat(rotatingDrawable, "rotation", COLLAPSED_PLUS_ROTATION, EXPANDED_PLUS_ROTATION);
|
||||
|
||||
collapseAnimator.setInterpolator(interpolator);
|
||||
expandAnimator.setInterpolator(interpolator);
|
||||
|
||||
mExpandAnimation.play(expandAnimator);
|
||||
mCollapseAnimation.play(collapseAnimator);
|
||||
|
||||
mAddButton = new FloatingActionButton(context);
|
||||
mAddButton.setImageDrawable(rotatingDrawable);
|
||||
mAddButton.setId(R.id.fab_expand_menu_button);
|
||||
mAddButton.setOnClickListener(v -> toggle());
|
||||
|
||||
addView(mAddButton, super.generateDefaultLayoutParams());
|
||||
mButtonsCount++;
|
||||
}
|
||||
|
||||
private void createLabels() {
|
||||
final Context context = BROKEN_LABEL_STYLE ? getContext() : new ContextThemeWrapper(getContext(), mLabelsStyle);
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
|
||||
|
||||
if (button instanceof LabeledFloatingActionButton) {
|
||||
final String title = ((LabeledFloatingActionButton) button).getTitle();
|
||||
|
||||
final AppCompatTextView label = new AppCompatTextView(context);
|
||||
if (!BROKEN_LABEL_STYLE)
|
||||
label.setTextAppearance(context, mLabelsStyle);
|
||||
label.setText(title);
|
||||
addView(label);
|
||||
|
||||
button.setTag(R.id.fab_label, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void expand() {
|
||||
if (!mExpanded) {
|
||||
mExpanded = true;
|
||||
mTouchDelegateGroup.setEnabled(true);
|
||||
mCollapseAnimation.cancel();
|
||||
mExpandAnimation.start();
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onMenuExpanded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean expandsHorizontally() {
|
||||
return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
|
||||
return new LayoutParams(super.generateDefaultLayoutParams());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewGroup.LayoutParams generateLayoutParams(final AttributeSet attrs) {
|
||||
return new LayoutParams(super.generateLayoutParams(attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ViewGroup.LayoutParams generateLayoutParams(final ViewGroup.LayoutParams p) {
|
||||
return new LayoutParams(super.generateLayoutParams(p));
|
||||
}
|
||||
|
||||
public float getBehaviorYTranslation() {
|
||||
return behaviorYTranslation;
|
||||
}
|
||||
|
||||
public float getScrollYTranslation() {
|
||||
return scrollYTranslation;
|
||||
}
|
||||
|
||||
private void init(final Context context, @Nullable final AttributeSet attributeSet) {
|
||||
mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing));
|
||||
mLabelsMargin = getResources().getDimensionPixelSize(R.dimen.fab_labels_margin);
|
||||
mLabelsVerticalOffset = getResources().getDimensionPixelSize(R.dimen.fab_shadow_offset);
|
||||
|
||||
mTouchDelegateGroup = new TouchDelegateGroup(this);
|
||||
setTouchDelegate(mTouchDelegateGroup);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionsMenu, 0, 0);
|
||||
mExpandDirection = attr.getInt(R.styleable.FloatingActionsMenu_fab_expandDirection, EXPAND_UP);
|
||||
mLabelsStyle = attr.getResourceId(R.styleable.FloatingActionsMenu_fab_labelStyle, 0);
|
||||
mLabelsPosition = attr.getInt(R.styleable.FloatingActionsMenu_fab_labelsPosition, LABELS_ON_LEFT_SIDE);
|
||||
attr.recycle();
|
||||
|
||||
if (mLabelsStyle != 0 && expandsHorizontally()) {
|
||||
throw new IllegalStateException("Action labels in horizontal expand orientation are not supported");
|
||||
}
|
||||
|
||||
createAddButton(context);
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return mExpanded;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
bringChildToFront(mAddButton);
|
||||
mButtonsCount = getChildCount();
|
||||
|
||||
if (mLabelsStyle != 0) {
|
||||
createLabels();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
final boolean expandUp = mExpandDirection == EXPAND_UP;
|
||||
|
||||
if (changed) {
|
||||
mTouchDelegateGroup.clearTouchDelegates();
|
||||
}
|
||||
|
||||
final int addButtonY = expandUp ? b - t - mAddButton.getMeasuredHeight() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int buttonsHorizontalCenter = (mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? r - l - mMaxButtonWidth / 2
|
||||
: mMaxButtonWidth / 2);
|
||||
final int addButtonLeft = buttonsHorizontalCenter - mAddButton.getMeasuredWidth() / 2;
|
||||
mAddButton.layout(addButtonLeft, addButtonY, addButtonLeft + mAddButton.getMeasuredWidth(), addButtonY + mAddButton.getMeasuredHeight());
|
||||
|
||||
final int labelsOffset = mMaxButtonWidth / 2 + mLabelsMargin;
|
||||
final int labelsXNearButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? buttonsHorizontalCenter - labelsOffset
|
||||
: buttonsHorizontalCenter + labelsOffset;
|
||||
|
||||
int nextY = expandUp ?
|
||||
addButtonY - mButtonSpacing :
|
||||
addButtonY + mAddButton.getMeasuredHeight() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = buttonsHorizontalCenter - child.getMeasuredWidth() / 2;
|
||||
final int childY = expandUp ? nextY - child.getMeasuredHeight() : nextY;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonY - childY;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
final View label = (View) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
final int labelXAwayFromButton = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton - label.getMeasuredWidth()
|
||||
: labelsXNearButton + label.getMeasuredWidth();
|
||||
|
||||
final int labelLeft = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelXAwayFromButton
|
||||
: labelsXNearButton;
|
||||
|
||||
final int labelRight = mLabelsPosition == LABELS_ON_LEFT_SIDE
|
||||
? labelsXNearButton
|
||||
: labelXAwayFromButton;
|
||||
|
||||
final int labelTop = childY - mLabelsVerticalOffset + (child.getMeasuredHeight() - label.getMeasuredHeight()) / 2;
|
||||
|
||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight());
|
||||
|
||||
touchArea.set(Math.min(childX, labelLeft),
|
||||
childY - mButtonSpacing / 2,
|
||||
Math.max(childX + child.getMeasuredWidth(), labelRight),
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing / 2);
|
||||
mTouchDelegateGroup.addTouchDelegate(new TouchDelegate(new Rect(touchArea), child));
|
||||
|
||||
label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
label.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams labelParams = (LayoutParams) label.getLayoutParams();
|
||||
labelParams.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
labelParams.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
labelParams.setAnimationsTarget(label);
|
||||
}
|
||||
|
||||
nextY = expandUp ?
|
||||
childY - mButtonSpacing :
|
||||
childY + child.getMeasuredHeight() + mButtonSpacing;
|
||||
}
|
||||
break;
|
||||
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
final boolean expandLeft = mExpandDirection == EXPAND_LEFT;
|
||||
|
||||
final int addButtonX = expandLeft ? r - l - mAddButton.getMeasuredWidth() : 0;
|
||||
// Ensure mAddButton is centered on the line where the buttons should be
|
||||
final int addButtonTop = b - t - mMaxButtonHeight + (mMaxButtonHeight - mAddButton.getMeasuredHeight()) / 2;
|
||||
mAddButton.layout(addButtonX, addButtonTop, addButtonX + mAddButton.getMeasuredWidth(), addButtonTop + mAddButton.getMeasuredHeight());
|
||||
|
||||
int nextX = expandLeft ?
|
||||
addButtonX - mButtonSpacing :
|
||||
addButtonX + mAddButton.getMeasuredWidth() + mButtonSpacing;
|
||||
|
||||
for (int i = mButtonsCount - 1; i >= 0; i--) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child == mAddButton || child.getVisibility() == GONE) continue;
|
||||
|
||||
final int childX = expandLeft ? nextX - child.getMeasuredWidth() : nextX;
|
||||
final int childY = addButtonTop + (mAddButton.getMeasuredHeight() - child.getMeasuredHeight()) / 2;
|
||||
child.layout(childX, childY, childX + child.getMeasuredWidth(), childY + child.getMeasuredHeight());
|
||||
|
||||
final float collapsedTranslation = addButtonX - childX;
|
||||
final float expandedTranslation = 0f;
|
||||
|
||||
child.setTranslationX(mExpanded ? expandedTranslation : collapsedTranslation);
|
||||
child.setAlpha(mExpanded ? 1f : 0f);
|
||||
|
||||
final LayoutParams params = (LayoutParams) child.getLayoutParams();
|
||||
params.mCollapseDir.setFloatValues(expandedTranslation, collapsedTranslation);
|
||||
params.mExpandDir.setFloatValues(collapsedTranslation, expandedTranslation);
|
||||
params.setAnimationsTarget(child);
|
||||
|
||||
nextX = expandLeft ?
|
||||
childX - mButtonSpacing :
|
||||
childX + child.getMeasuredWidth() + mButtonSpacing;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
mMaxButtonWidth = 0;
|
||||
mMaxButtonHeight = 0;
|
||||
int maxLabelWidth = 0;
|
||||
|
||||
for (int i = 0; i < mButtonsCount; i++) {
|
||||
final View child = getChildAt(i);
|
||||
|
||||
if (child.getVisibility() == GONE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mMaxButtonWidth = Math.max(mMaxButtonWidth, child.getMeasuredWidth());
|
||||
height += child.getMeasuredHeight();
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += child.getMeasuredWidth();
|
||||
mMaxButtonHeight = Math.max(mMaxButtonHeight, child.getMeasuredHeight());
|
||||
break;
|
||||
}
|
||||
|
||||
if (!expandsHorizontally()) {
|
||||
final TextView label = (TextView) child.getTag(R.id.fab_label);
|
||||
if (label != null) {
|
||||
maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expandsHorizontally()) {
|
||||
height = mMaxButtonHeight;
|
||||
} else {
|
||||
width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
|
||||
}
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
height += mButtonSpacing * (mButtonsCount - 1);
|
||||
height = adjustForOvershoot(height);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
width += mButtonSpacing * (mButtonsCount - 1);
|
||||
width = adjustForOvershoot(width);
|
||||
break;
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
if (state instanceof SavedState) {
|
||||
final SavedState savedState = (SavedState) state;
|
||||
mExpanded = savedState.mExpanded;
|
||||
mTouchDelegateGroup.setEnabled(mExpanded);
|
||||
|
||||
if (mRotatingDrawable != null) {
|
||||
mRotatingDrawable.setRotation(mExpanded ? EXPANDED_PLUS_ROTATION : COLLAPSED_PLUS_ROTATION);
|
||||
}
|
||||
|
||||
super.onRestoreInstanceState(savedState.getSuperState());
|
||||
} else {
|
||||
super.onRestoreInstanceState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parcelable onSaveInstanceState() {
|
||||
final Parcelable superState = super.onSaveInstanceState();
|
||||
final SavedState savedState = new SavedState(superState);
|
||||
savedState.mExpanded = mExpanded;
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
public void removeButton(final LabeledFloatingActionButton button) {
|
||||
removeView(button.getLabelView());
|
||||
removeView(button);
|
||||
button.setTag(R.id.fab_label, null);
|
||||
mButtonsCount--;
|
||||
}
|
||||
|
||||
public void setBehaviorYTranslation(final float behaviorYTranslation) {
|
||||
this.behaviorYTranslation = behaviorYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(final boolean enabled) {
|
||||
super.setEnabled(enabled);
|
||||
|
||||
mAddButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void setScrollYTranslation(final float scrollYTranslation) {
|
||||
this.scrollYTranslation = scrollYTranslation;
|
||||
setTranslationY(behaviorYTranslation + scrollYTranslation);
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
if (mExpanded) {
|
||||
collapse();
|
||||
} else {
|
||||
expand();
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnFloatingActionsMenuUpdateListener {
|
||||
void onMenuCollapsed();
|
||||
|
||||
void onMenuExpanded();
|
||||
}
|
||||
|
||||
private static class RotatingDrawable extends LayerDrawable {
|
||||
private float mRotation;
|
||||
|
||||
RotatingDrawable(final Drawable drawable) {
|
||||
super(new Drawable[]{drawable});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
|
||||
super.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public float getRotation() {
|
||||
return mRotation;
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
public void setRotation(final float rotation) {
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SavedState extends BaseSavedState {
|
||||
public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
|
||||
|
||||
@Override
|
||||
public SavedState createFromParcel(final Parcel in) {
|
||||
return new SavedState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavedState[] newArray(final int size) {
|
||||
return new SavedState[size];
|
||||
}
|
||||
};
|
||||
private boolean mExpanded;
|
||||
|
||||
public SavedState(final Parcelable parcel) {
|
||||
super(parcel);
|
||||
}
|
||||
|
||||
private SavedState(final Parcel in) {
|
||||
super(in);
|
||||
mExpanded = in.readInt() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel out, final int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeInt(mExpanded ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class LayoutParams extends ViewGroup.LayoutParams {
|
||||
|
||||
private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mCollapseDir = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
|
||||
private final ObjectAnimator mExpandDir = new ObjectAnimator();
|
||||
private boolean animationsSetToPlay;
|
||||
|
||||
LayoutParams(final ViewGroup.LayoutParams source) {
|
||||
super(source);
|
||||
|
||||
mExpandDir.setInterpolator(EXPAND_INTERPOLATOR);
|
||||
mExpandAlpha.setInterpolator(ALPHA_EXPAND_INTERPOLATOR);
|
||||
mCollapseDir.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
mCollapseAlpha.setInterpolator(COLLAPSE_INTERPOLATOR);
|
||||
|
||||
mCollapseAlpha.setProperty(View.ALPHA);
|
||||
mCollapseAlpha.setFloatValues(1f, 0f);
|
||||
|
||||
mExpandAlpha.setProperty(View.ALPHA);
|
||||
mExpandAlpha.setFloatValues(0f, 1f);
|
||||
|
||||
switch (mExpandDirection) {
|
||||
case EXPAND_UP:
|
||||
case EXPAND_DOWN:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_Y);
|
||||
mExpandDir.setProperty(View.TRANSLATION_Y);
|
||||
break;
|
||||
case EXPAND_LEFT:
|
||||
case EXPAND_RIGHT:
|
||||
mCollapseDir.setProperty(View.TRANSLATION_X);
|
||||
mExpandDir.setProperty(View.TRANSLATION_X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void addLayerTypeListener(final Animator animator, final View view) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_NONE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(final Animator animation) {
|
||||
view.setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setAnimationsTarget(final View view) {
|
||||
mCollapseAlpha.setTarget(view);
|
||||
mCollapseDir.setTarget(view);
|
||||
mExpandAlpha.setTarget(view);
|
||||
mExpandDir.setTarget(view);
|
||||
|
||||
// Now that the animations have targets, set them to be played
|
||||
if (!animationsSetToPlay) {
|
||||
addLayerTypeListener(mExpandDir, view);
|
||||
addLayerTypeListener(mCollapseDir, view);
|
||||
|
||||
mCollapseAnimation.play(mCollapseAlpha);
|
||||
mCollapseAnimation.play(mCollapseDir);
|
||||
mExpandAnimation.play(mExpandAlpha);
|
||||
mExpandAnimation.play(mExpandDir);
|
||||
animationsSetToPlay = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class FloatingActionsMenuRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
|
||||
private static final float SCALE_FACTOR = 1.5f;
|
||||
private final FloatingActionsMenu menu;
|
||||
|
||||
public FloatingActionsMenuRecyclerViewScrollListener(final FloatingActionsMenu menu) {
|
||||
this.menu = menu;
|
||||
}
|
||||
|
||||
private static float bound(final float min, final float proposal, final float max) {
|
||||
return Math.min(max, Math.max(min, proposal));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
menu.setScrollYTranslation(bound(0, menu.getScrollYTranslation() + dy * SCALE_FACTOR, menu.getMeasuredHeight() - menu.getTranslationY()));
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
|
||||
public class LabeledFloatingActionButton extends FloatingActionButton {
|
||||
|
||||
@Nullable private final String title;
|
||||
|
||||
public LabeledFloatingActionButton(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LabeledFloatingActionButton(final Context context, @Nullable final AttributeSet attrs, final int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.LabeledFloatingActionButton, 0, 0);
|
||||
title = attr.getString(R.styleable.LabeledFloatingActionButton_fab_title);
|
||||
attr.recycle();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TextView getLabelView() {
|
||||
return (TextView) getTag(R.id.fab_label);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibility(final int visibility) {
|
||||
final TextView label = getLabelView();
|
||||
if (label != null) {
|
||||
label.setVisibility(visibility);
|
||||
}
|
||||
|
||||
super.setVisibility(visibility);
|
||||
}
|
||||
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2014 Jerzy Chalupski
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget.fab;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class TouchDelegateGroup extends TouchDelegate {
|
||||
private static final Rect USELESS_HACKY_RECT = new Rect();
|
||||
private final Collection<TouchDelegate> mTouchDelegates = new ArrayList<>();
|
||||
@Nullable private TouchDelegate mCurrentTouchDelegate;
|
||||
private boolean mEnabled;
|
||||
|
||||
public TouchDelegateGroup(final View uselessHackyView) {
|
||||
super(USELESS_HACKY_RECT, uselessHackyView);
|
||||
}
|
||||
|
||||
public void addTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.add(touchDelegate);
|
||||
}
|
||||
|
||||
public void clearTouchDelegates() {
|
||||
mTouchDelegates.clear();
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
if (!mEnabled)
|
||||
return false;
|
||||
|
||||
TouchDelegate delegate = null;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
for (final TouchDelegate touchDelegate : mTouchDelegates) {
|
||||
if (touchDelegate.onTouchEvent(event)) {
|
||||
mCurrentTouchDelegate = touchDelegate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
delegate = mCurrentTouchDelegate;
|
||||
mCurrentTouchDelegate = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return delegate != null && delegate.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void removeTouchDelegate(final TouchDelegate touchDelegate) {
|
||||
mTouchDelegates.remove(touchDelegate);
|
||||
if (mCurrentTouchDelegate == touchDelegate) {
|
||||
mCurrentTouchDelegate = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setEnabled(final boolean enabled) {
|
||||
mEnabled = enabled;
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="4dp" />
|
||||
<padding
|
||||
android:bottom="4dp"
|
||||
android:left="8dp"
|
||||
android:right="8dp"
|
||||
android:top="4dp" />
|
||||
<solid android:color="@color/fab_label_background_color" />
|
||||
</shape>
|
73
app/src/main/res/layout/add_tunnels_bottom_sheet.xml
Normal file
73
app/src/main/res/layout/add_tunnels_bottom_sheet.xml
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/bottom_sheet_top_padding">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/create_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:layout_marginStart="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:text="@string/create_empty"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:icon="@drawable/ic_action_edit"
|
||||
app:iconPadding="@dimen/bottom_sheet_icon_padding"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/create_from_file"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="?attr/colorSecondary"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/create_from_file"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:layout_marginStart="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:text="@string/create_from_file"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:icon="@drawable/ic_action_open_white"
|
||||
app:iconPadding="@dimen/bottom_sheet_icon_padding"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@+id/create_empty"
|
||||
app:layout_constraintBottom_toTopOf="@+id/create_from_qrcode"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="?attr/colorSecondary"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/create_from_qrcode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:layout_marginLeft="@dimen/normal_margin"
|
||||
android:layout_marginRight="@dimen/normal_margin"
|
||||
android:layout_marginStart="@dimen/normal_margin"
|
||||
android:layout_marginEnd="@dimen/normal_margin"
|
||||
android:text="@string/create_from_qr_code"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:icon="@drawable/ic_action_scan_qr_code_white"
|
||||
app:iconPadding="@dimen/bottom_sheet_icon_padding"
|
||||
app:iconTint="?attr/colorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@+id/create_from_file"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:rippleColor="?attr/colorSecondary"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -65,44 +65,14 @@
|
||||
android:text="@string/tunnel_list_placeholder"
|
||||
android:textSize="20sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
||||
android:id="@+id/create_menu"
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
style="@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon"
|
||||
android:id="@+id/create_fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
android:clipChildren="false"
|
||||
app:fab_labelStyle="@style/fab_label"
|
||||
app:fab_labelsPosition="@integer/label_position"
|
||||
app:layout_behavior="com.wireguard.android.widget.fab.FloatingActionButtonBehavior">
|
||||
app:icon="@drawable/ic_action_add_white" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_from_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestImportConfig}"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_file"
|
||||
app:srcCompat="@drawable/ic_action_open_white" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_from_qrcode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestScanQRCode}"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_from_qr_code"
|
||||
app:srcCompat="@drawable/ic_action_scan_qr_code_white" />
|
||||
|
||||
<com.wireguard.android.widget.fab.LabeledFloatingActionButton
|
||||
android:id="@+id/create_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="@{fragment::onRequestCreateConfig}"
|
||||
app:fabSize="mini"
|
||||
app:fab_title="@string/create_empty"
|
||||
app:srcCompat="@drawable/ic_action_edit_white" />
|
||||
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="label_position">1</integer>
|
||||
</resources>
|
@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
|
||||
<color name="fab_label_text_color">#000000</color>
|
||||
<color name="fab_label_background_color">#bbbbbb</color>
|
||||
|
||||
<!-- Base palette -->
|
||||
<color name="primary_color">#ff212121</color>
|
||||
<color name="primary_light_color">#ff484848</color>
|
||||
|
@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- TODO(msf): remove these 2 hard-coded colors and replace with theme colors -->
|
||||
<color name="fab_label_text_color">#ffffff</color>
|
||||
<color name="fab_label_background_color">#444444</color>
|
||||
|
||||
<!-- Base palette -->
|
||||
<color name="primary_color">#ffffffff</color>
|
||||
<color name="primary_light_color">#ffffffff</color>
|
||||
|
@ -1,4 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="fab_margin">16dp</dimen>
|
||||
<dimen name="extra_margin">12dp</dimen>
|
||||
<dimen name="bottom_sheet_item_height">56dp</dimen>
|
||||
<dimen name="normal_margin">8dp</dimen>
|
||||
<dimen name="bottom_sheet_top_padding">8dp</dimen>
|
||||
<dimen name="bottom_sheet_icon_padding">16dp</dimen>
|
||||
</resources>
|
@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="fab_expand_menu_button" type="id" />
|
||||
<item name="fab_label" type="id" />
|
||||
|
||||
<dimen name="fab_shadow_offset">3dp</dimen>
|
||||
<dimen name="fab_shadow_radius">9dp</dimen>
|
||||
|
||||
<dimen name="fab_stroke_width">1dp</dimen>
|
||||
|
||||
<dimen name="fab_actions_spacing">24dp</dimen>
|
||||
<dimen name="fab_labels_margin">8dp</dimen>
|
||||
|
||||
<declare-styleable name="LabeledFloatingActionButton">
|
||||
<attr name="fab_title" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="FloatingActionsMenu">
|
||||
<attr name="fab_labelStyle" format="reference" />
|
||||
<attr name="fab_labelsPosition" format="enum">
|
||||
<enum name="left" value="0" />
|
||||
<enum name="right" value="1" />
|
||||
</attr>
|
||||
<attr name="fab_expandDirection" format="enum">
|
||||
<enum name="up" value="0" />
|
||||
<enum name="down" value="1" />
|
||||
<enum name="left" value="2" />
|
||||
<enum name="right" value="3" />
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
<integer name="label_position">0</integer>
|
||||
</resources>
|
@ -28,6 +28,18 @@
|
||||
<item name="android:windowBackground">?attr/colorBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="BottomSheetDialogTheme" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:navigationBarColor">?attr/colorBackground</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
<item name="android:backgroundDimEnabled">true</item>
|
||||
<item name="android:backgroundDimAmount">0.5</item>
|
||||
<item name="android:windowTranslucentStatus">false</item>
|
||||
<item name="android:colorBackground">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="NoBackgroundTheme" parent="AppTheme">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
@ -41,9 +53,4 @@
|
||||
<item name="android:windowExitAnimation">@android:anim/fade_out</item>
|
||||
</style>
|
||||
|
||||
<style name="fab_label" parent="TextAppearance.AppCompat.Inverse">
|
||||
<item name="android:background">@drawable/fab_label_background</item>
|
||||
<item name="android:textColor">@color/fab_label_text_color</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -8,6 +8,7 @@ allprojects {
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61'
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
Loading…
Reference in New Issue
Block a user