summaryrefslogtreecommitdiffhomepage
path: root/app/src/main/java/com
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2018-05-29 18:37:14 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2018-05-29 19:03:47 +0200
commit09833a1ba51ca01680b9929410bc70218d985eb4 (patch)
tree1b204c71a221bfa9a3aa1b5ec2a0a4518d2e87e3 /app/src/main/java/com
parentded0191aae5bfe14409f343c0e3ce3b5db47261a (diff)
FloatingActionButton: import cleaned up getbase code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src/main/java/com')
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/AddFloatingActionButton.java96
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java409
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java643
-rw-r--r--app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java78
4 files changed, 1226 insertions, 0 deletions
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/AddFloatingActionButton.java b/app/src/main/java/com/wireguard/android/widget/fab/AddFloatingActionButton.java
new file mode 100644
index 00000000..4a4dd3b5
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/widget/fab/AddFloatingActionButton.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2014 Jerzy Chalupski
+ * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+package com.wireguard.android.widget.fab;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.Shape;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
+import android.util.AttributeSet;
+
+import com.wireguard.android.R;
+
+public class AddFloatingActionButton extends FloatingActionButton {
+ int mPlusColor;
+
+ public AddFloatingActionButton(final Context context) {
+ this(context, null);
+ }
+
+ public AddFloatingActionButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AddFloatingActionButton(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ void init(final Context context, final AttributeSet attributeSet) {
+ final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.AddFloatingActionButton, 0, 0);
+ mPlusColor = attr.getColor(R.styleable.AddFloatingActionButton_fab_plusIconColor, getColor(android.R.color.white));
+ attr.recycle();
+
+ super.init(context, attributeSet);
+ }
+
+ /**
+ * @return the current Color of plus icon.
+ */
+ public int getPlusColor() {
+ return mPlusColor;
+ }
+
+ public void setPlusColor(final int color) {
+ if (mPlusColor != color) {
+ mPlusColor = color;
+ updateBackground();
+ }
+ }
+
+ public void setPlusColorResId(@ColorRes int plusColor) {
+ setPlusColor(getColor(plusColor));
+ }
+
+ @Override
+ public void setIcon(@DrawableRes final int icon) {
+ throw new UnsupportedOperationException("Use FloatingActionButton if you want to use custom icon");
+ }
+
+ @Override
+ Drawable getIconDrawable() {
+ final float iconSize = getDimension(R.dimen.fab_icon_size);
+ final float iconHalfSize = iconSize / 2f;
+
+ final float plusSize = getDimension(R.dimen.fab_plus_icon_size);
+ final float plusHalfStroke = getDimension(R.dimen.fab_plus_icon_stroke) / 2f;
+ final float plusOffset = (iconSize - plusSize) / 2f;
+
+ final Shape shape = new Shape() {
+ @Override
+ public void draw(final Canvas canvas, final Paint paint) {
+ canvas.drawRect(plusOffset, iconHalfSize - plusHalfStroke, iconSize - plusOffset, iconHalfSize + plusHalfStroke, paint);
+ canvas.drawRect(iconHalfSize - plusHalfStroke, plusOffset, iconHalfSize + plusHalfStroke, iconSize - plusOffset, paint);
+ }
+ };
+
+ final ShapeDrawable drawable = new ShapeDrawable(shape);
+
+ final Paint paint = drawable.getPaint();
+ paint.setColor(mPlusColor);
+ paint.setStyle(Style.FILL);
+ paint.setAntiAlias(true);
+
+ return drawable;
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java
new file mode 100644
index 00000000..f636aff0
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright © 2014 Jerzy Chalupski
+ * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+package com.wireguard.android.widget.fab;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.*;
+import android.graphics.Paint.Style;
+import android.graphics.Shader.TileMode;
+import android.graphics.drawable.*;
+import android.graphics.drawable.ShapeDrawable.ShaderFactory;
+import android.graphics.drawable.shapes.OvalShape;
+import android.support.annotation.*;
+import android.support.v7.widget.AppCompatImageButton;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.wireguard.android.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class FloatingActionButton extends AppCompatImageButton {
+
+ public static final int SIZE_NORMAL = 0;
+ public static final int SIZE_MINI = 1;
+ int mColorNormal;
+ int mColorPressed;
+ int mColorDisabled;
+ String mTitle;
+ boolean mStrokeVisible;
+ @DrawableRes
+ private int mIcon;
+ private Drawable mIconDrawable;
+ private int mSize;
+
+ private float mCircleSize;
+ private float mShadowRadius;
+ private float mShadowOffset;
+ private int mDrawableSize;
+ public FloatingActionButton(final Context context) {
+ this(context, null);
+ }
+
+ public FloatingActionButton(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public FloatingActionButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ void init(final Context context, final AttributeSet attributeSet) {
+ final TypedArray attr = context.obtainStyledAttributes(attributeSet, R.styleable.FloatingActionButton, 0, 0);
+ mColorNormal = attr.getColor(R.styleable.FloatingActionButton_fab_colorNormal, getColor(android.R.color.holo_blue_dark));
+ mColorPressed = attr.getColor(R.styleable.FloatingActionButton_fab_colorPressed, getColor(android.R.color.holo_blue_light));
+ mColorDisabled = attr.getColor(R.styleable.FloatingActionButton_fab_colorDisabled, getColor(android.R.color.darker_gray));
+ mSize = attr.getInt(R.styleable.FloatingActionButton_fab_size, SIZE_NORMAL);
+ mIcon = attr.getResourceId(R.styleable.FloatingActionButton_fab_icon, 0);
+ mTitle = attr.getString(R.styleable.FloatingActionButton_fab_title);
+ mStrokeVisible = attr.getBoolean(R.styleable.FloatingActionButton_fab_stroke_visible, true);
+ attr.recycle();
+
+ updateCircleSize();
+ mShadowRadius = getDimension(R.dimen.fab_shadow_radius);
+ mShadowOffset = getDimension(R.dimen.fab_shadow_offset);
+ updateDrawableSize();
+
+ updateBackground();
+ }
+
+ private void updateDrawableSize() {
+ mDrawableSize = (int) (mCircleSize + 2 * mShadowRadius);
+ }
+
+ private void updateCircleSize() {
+ mCircleSize = getDimension(mSize == SIZE_NORMAL ? R.dimen.fab_size_normal : R.dimen.fab_size_mini);
+ }
+
+ @FAB_SIZE
+ public int getSize() {
+ return mSize;
+ }
+
+ public void setSize(@FAB_SIZE final int size) {
+ if (size != SIZE_MINI && size != SIZE_NORMAL) {
+ throw new IllegalArgumentException("Use @FAB_SIZE constants only!");
+ }
+
+ if (mSize != size) {
+ mSize = size;
+ updateCircleSize();
+ updateDrawableSize();
+ updateBackground();
+ }
+ }
+
+ public void setIcon(@DrawableRes final int icon) {
+ if (mIcon != icon) {
+ mIcon = icon;
+ mIconDrawable = null;
+ updateBackground();
+ }
+ }
+
+ /**
+ * @return the current Color for normal state.
+ */
+ public int getColorNormal() {
+ return mColorNormal;
+ }
+
+ public void setColorNormal(final int color) {
+ if (mColorNormal != color) {
+ mColorNormal = color;
+ updateBackground();
+ }
+ }
+
+ public void setColorNormalResId(@ColorRes final int colorNormal) {
+ setColorNormal(getColor(colorNormal));
+ }
+
+ /**
+ * @return the current color for pressed state.
+ */
+ public int getColorPressed() {
+ return mColorPressed;
+ }
+
+ public void setColorPressed(final int color) {
+ if (mColorPressed != color) {
+ mColorPressed = color;
+ updateBackground();
+ }
+ }
+
+ public void setColorPressedResId(@ColorRes final int colorPressed) {
+ setColorPressed(getColor(colorPressed));
+ }
+
+ /**
+ * @return the current color for disabled state.
+ */
+ public int getColorDisabled() {
+ return mColorDisabled;
+ }
+
+ public void setColorDisabled(final int color) {
+ if (mColorDisabled != color) {
+ mColorDisabled = color;
+ updateBackground();
+ }
+ }
+
+ public void setColorDisabledResId(@ColorRes final int colorDisabled) {
+ setColorDisabled(getColor(colorDisabled));
+ }
+
+ public boolean isStrokeVisible() {
+ return mStrokeVisible;
+ }
+
+ public void setStrokeVisible(final boolean visible) {
+ if (mStrokeVisible != visible) {
+ mStrokeVisible = visible;
+ updateBackground();
+ }
+ }
+
+ int getColor(@ColorRes final int id) {
+ return getResources().getColor(id);
+ }
+
+ float getDimension(@DimenRes final int id) {
+ return getResources().getDimension(id);
+ }
+
+ TextView getLabelView() {
+ return (TextView) getTag(R.id.fab_label);
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public void setTitle(final String title) {
+ mTitle = title;
+ final TextView label = getLabelView();
+ if (label != null) {
+ label.setText(title);
+ }
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(mDrawableSize, mDrawableSize);
+ }
+
+ void updateBackground() {
+ final float strokeWidth = getDimension(R.dimen.fab_stroke_width);
+ final float halfStrokeWidth = strokeWidth / 2f;
+
+ final LayerDrawable layerDrawable = new LayerDrawable(
+ new Drawable[]{
+ getResources().getDrawable(mSize == SIZE_NORMAL ? R.drawable.fab_bg_normal : R.drawable.fab_bg_mini, null),
+ createFillDrawable(strokeWidth),
+ createOuterStrokeDrawable(strokeWidth),
+ getIconDrawable()
+ });
+
+ final int iconOffset = (int) (mCircleSize - getDimension(R.dimen.fab_icon_size)) / 2;
+
+ final int circleInsetHorizontal = (int) (mShadowRadius);
+ final int circleInsetTop = (int) (mShadowRadius - mShadowOffset);
+ final int circleInsetBottom = (int) (mShadowRadius + mShadowOffset);
+
+ layerDrawable.setLayerInset(1,
+ circleInsetHorizontal,
+ circleInsetTop,
+ circleInsetHorizontal,
+ circleInsetBottom);
+
+ layerDrawable.setLayerInset(2,
+ (int) (circleInsetHorizontal - halfStrokeWidth),
+ (int) (circleInsetTop - halfStrokeWidth),
+ (int) (circleInsetHorizontal - halfStrokeWidth),
+ (int) (circleInsetBottom - halfStrokeWidth));
+
+ layerDrawable.setLayerInset(3,
+ circleInsetHorizontal + iconOffset,
+ circleInsetTop + iconOffset,
+ circleInsetHorizontal + iconOffset,
+ circleInsetBottom + iconOffset);
+
+ setBackground(layerDrawable);
+ }
+
+ Drawable getIconDrawable() {
+ if (mIconDrawable != null) {
+ return mIconDrawable;
+ } else if (mIcon != 0) {
+ return getResources().getDrawable(mIcon, null);
+ } else {
+ return new ColorDrawable(Color.TRANSPARENT);
+ }
+ }
+
+ public void setIconDrawable(@NonNull final Drawable iconDrawable) {
+ if (mIconDrawable != iconDrawable) {
+ mIcon = 0;
+ mIconDrawable = iconDrawable;
+ updateBackground();
+ }
+ }
+
+ private StateListDrawable createFillDrawable(final float strokeWidth) {
+ final StateListDrawable drawable = new StateListDrawable();
+ drawable.addState(new int[]{-android.R.attr.state_enabled}, createCircleDrawable(mColorDisabled, strokeWidth));
+ drawable.addState(new int[]{android.R.attr.state_pressed}, createCircleDrawable(mColorPressed, strokeWidth));
+ drawable.addState(new int[]{}, createCircleDrawable(mColorNormal, strokeWidth));
+ return drawable;
+ }
+
+ private Drawable createCircleDrawable(final int color, final float strokeWidth) {
+ final int alpha = Color.alpha(color);
+ final int opaqueColor = opaque(color);
+
+ final ShapeDrawable fillDrawable = new ShapeDrawable(new OvalShape());
+
+ final Paint paint = fillDrawable.getPaint();
+ paint.setAntiAlias(true);
+ paint.setColor(opaqueColor);
+
+ final Drawable[] layers = {
+ fillDrawable,
+ createInnerStrokesDrawable(opaqueColor, strokeWidth)
+ };
+
+ final LayerDrawable drawable = alpha == 255 || !mStrokeVisible
+ ? new LayerDrawable(layers)
+ : new TranslucentLayerDrawable(alpha, layers);
+
+ final int halfStrokeWidth = (int) (strokeWidth / 2f);
+ drawable.setLayerInset(1, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth);
+
+ return drawable;
+ }
+
+ private Drawable createOuterStrokeDrawable(final float strokeWidth) {
+ final ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
+
+ final Paint paint = shapeDrawable.getPaint();
+ paint.setAntiAlias(true);
+ paint.setStrokeWidth(strokeWidth);
+ paint.setStyle(Style.STROKE);
+ paint.setColor(Color.BLACK);
+ paint.setAlpha(opacityToAlpha(0.02f));
+
+ return shapeDrawable;
+ }
+
+ private int opacityToAlpha(final float opacity) {
+ return (int) (255f * opacity);
+ }
+
+ private int darkenColor(final int argb) {
+ return adjustColorBrightness(argb, 0.9f);
+ }
+
+ private int lightenColor(final int argb) {
+ return adjustColorBrightness(argb, 1.1f);
+ }
+
+ private int adjustColorBrightness(final int argb, final float factor) {
+ final float[] hsv = new float[3];
+ Color.colorToHSV(argb, hsv);
+
+ hsv[2] = Math.min(hsv[2] * factor, 1f);
+
+ return Color.HSVToColor(Color.alpha(argb), hsv);
+ }
+
+ private int halfTransparent(final int argb) {
+ return Color.argb(
+ Color.alpha(argb) / 2,
+ Color.red(argb),
+ Color.green(argb),
+ Color.blue(argb)
+ );
+ }
+
+ private int opaque(final int argb) {
+ return Color.rgb(
+ Color.red(argb),
+ Color.green(argb),
+ Color.blue(argb)
+ );
+ }
+
+ private Drawable createInnerStrokesDrawable(final int color, final float strokeWidth) {
+ if (!mStrokeVisible) {
+ return new ColorDrawable(Color.TRANSPARENT);
+ }
+
+ final ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
+
+ final int bottomStrokeColor = darkenColor(color);
+ final int bottomStrokeColorHalfTransparent = halfTransparent(bottomStrokeColor);
+ final int topStrokeColor = lightenColor(color);
+ final int topStrokeColorHalfTransparent = halfTransparent(topStrokeColor);
+
+ final Paint paint = shapeDrawable.getPaint();
+ paint.setAntiAlias(true);
+ paint.setStrokeWidth(strokeWidth);
+ paint.setStyle(Style.STROKE);
+ shapeDrawable.setShaderFactory(new ShaderFactory() {
+ @Override
+ public Shader resize(int width, int height) {
+ return new LinearGradient(width / 2, 0, width / 2, height,
+ new int[]{topStrokeColor, topStrokeColorHalfTransparent, color, bottomStrokeColorHalfTransparent, bottomStrokeColor},
+ new float[]{0f, 0.2f, 0.5f, 0.8f, 1f},
+ TileMode.CLAMP
+ );
+ }
+ });
+
+ return shapeDrawable;
+ }
+
+ @Override
+ public void setVisibility(final int visibility) {
+ final TextView label = getLabelView();
+ if (label != null) {
+ label.setVisibility(visibility);
+ }
+
+ super.setVisibility(visibility);
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SIZE_NORMAL, SIZE_MINI})
+ public @interface FAB_SIZE {
+ }
+
+ private static class TranslucentLayerDrawable extends LayerDrawable {
+ private final int mAlpha;
+
+ public TranslucentLayerDrawable(final int alpha, final Drawable... layers) {
+ super(layers);
+ mAlpha = alpha;
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ canvas.saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, mAlpha);
+ super.draw(canvas);
+ canvas.restore();
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java
new file mode 100644
index 00000000..8c270266
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java
@@ -0,0 +1,643 @@
+/*
+ * Copyright © 2014 Jerzy Chalupski
+ * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+package com.wireguard.android.widget.fab;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+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.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+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.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.TextView;
+
+import com.wireguard.android.R;
+
+public class FloatingActionsMenu extends ViewGroup {
+ public static final int EXPAND_UP = 0;
+ 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 LABELS_ON_LEFT_SIDE = 0;
+ public static final int LABELS_ON_RIGHT_SIDE = 1;
+
+ private static final int ANIMATION_DURATION = 300;
+ private static final float COLLAPSED_PLUS_ROTATION = 0f;
+ private static final float EXPANDED_PLUS_ROTATION = 90f + 45f;
+ private static final Interpolator sExpandInterpolator = new OvershootInterpolator();
+ private static final Interpolator sCollapseInterpolator = new DecelerateInterpolator(3f);
+ private static final Interpolator sAlphaExpandInterpolator = new DecelerateInterpolator();
+ private int mAddButtonPlusColor;
+ private int mAddButtonColorNormal;
+ private int mAddButtonColorPressed;
+ private int mAddButtonSize;
+ private boolean mAddButtonStrokeVisible;
+ private int mExpandDirection;
+ private int mButtonSpacing;
+ private int mLabelsMargin;
+ private int mLabelsVerticalOffset;
+ private boolean mExpanded;
+ private final AnimatorSet mExpandAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
+ private final AnimatorSet mCollapseAnimation = new AnimatorSet().setDuration(ANIMATION_DURATION);
+ private AddFloatingActionButton mAddButton;
+ private RotatingDrawable mRotatingDrawable;
+ private int mMaxButtonWidth;
+ private int mMaxButtonHeight;
+ private int mLabelsStyle;
+ private int mLabelsPosition;
+ private int mButtonsCount;
+ private TouchDelegateGroup mTouchDelegateGroup;
+ private OnFloatingActionsMenuUpdateListener mListener;
+ private final Rect touchArea = new Rect(0, 0, 0, 0);
+
+ public FloatingActionsMenu(final Context context) {
+ this(context, null);
+ }
+
+ public FloatingActionsMenu(final Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public FloatingActionsMenu(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ init(context, attrs);
+ }
+
+ private void init(final Context context, final AttributeSet attributeSet) {
+ mButtonSpacing = (int) (getResources().getDimension(R.dimen.fab_actions_spacing) - getResources().getDimension(R.dimen.fab_shadow_radius) - getResources().getDimension(R.dimen.fab_shadow_offset));
+ 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);
+ mAddButtonPlusColor = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonPlusIconColor, getColor(android.R.color.white));
+ mAddButtonColorNormal = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonColorNormal, getColor(android.R.color.holo_blue_dark));
+ mAddButtonColorPressed = attr.getColor(R.styleable.FloatingActionsMenu_fab_addButtonColorPressed, getColor(android.R.color.holo_blue_light));
+ mAddButtonSize = attr.getInt(R.styleable.FloatingActionsMenu_fab_addButtonSize, FloatingActionButton.SIZE_NORMAL);
+ mAddButtonStrokeVisible = attr.getBoolean(R.styleable.FloatingActionsMenu_fab_addButtonStrokeVisible, true);
+ 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 is not supported.");
+ }
+
+ createAddButton(context);
+ }
+
+ public void setOnFloatingActionsMenuUpdateListener(final OnFloatingActionsMenuUpdateListener listener) {
+ mListener = listener;
+ }
+
+ private boolean expandsHorizontally() {
+ return mExpandDirection == EXPAND_LEFT || mExpandDirection == EXPAND_RIGHT;
+ }
+
+ private void createAddButton(final Context context) {
+ mAddButton = new AddFloatingActionButton(context) {
+ @Override
+ void updateBackground() {
+ mPlusColor = mAddButtonPlusColor;
+ mColorNormal = mAddButtonColorNormal;
+ mColorPressed = mAddButtonColorPressed;
+ mStrokeVisible = mAddButtonStrokeVisible;
+ super.updateBackground();
+ }
+
+ @Override
+ Drawable getIconDrawable() {
+ final RotatingDrawable rotatingDrawable = new RotatingDrawable(super.getIconDrawable());
+ mRotatingDrawable = rotatingDrawable;
+
+ final OvershootInterpolator 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);
+
+ return rotatingDrawable;
+ }
+ };
+
+ mAddButton.setId(R.id.fab_expand_menu_button);
+ mAddButton.setSize(mAddButtonSize);
+ mAddButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ toggle();
+ }
+ });
+
+ addView(mAddButton, super.generateDefaultLayoutParams());
+ mButtonsCount++;
+ }
+
+ public void addButton(final FloatingActionButton button) {
+ addView(button, mButtonsCount - 1);
+ mButtonsCount++;
+
+ if (mLabelsStyle != 0) {
+ createLabels();
+ }
+ }
+
+ public void removeButton(final FloatingActionButton button) {
+ removeView(button.getLabelView());
+ removeView(button);
+ button.setTag(R.id.fab_label, null);
+ mButtonsCount--;
+ }
+
+ private int getColor(@ColorRes final int id) {
+ return getResources().getColor(id);
+ }
+
+ @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++) {
+ 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()) {
+ TextView label = (TextView) child.getTag(R.id.fab_label);
+ if (label != null) {
+ maxLabelWidth = Math.max(maxLabelWidth, label.getMeasuredWidth());
+ }
+ }
+ }
+
+ if (!expandsHorizontally()) {
+ width = mMaxButtonWidth + (maxLabelWidth > 0 ? maxLabelWidth + mLabelsMargin : 0);
+ } else {
+ height = mMaxButtonHeight;
+ }
+
+ 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);
+ }
+
+ private int adjustForOvershoot(final int dimension) {
+ return dimension * 12 / 10;
+ }
+
+ @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);
+
+ 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(touchArea, child));
+
+ label.setTranslationY(mExpanded ? expandedTranslation : collapsedTranslation);
+ label.setAlpha(mExpanded ? 1f : 0f);
+
+ 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 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));
+ }
+
+ @Override
+ protected boolean checkLayoutParams(final ViewGroup.LayoutParams p) {
+ return super.checkLayoutParams(p);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ bringChildToFront(mAddButton);
+ mButtonsCount = getChildCount();
+
+ if (mLabelsStyle != 0) {
+ createLabels();
+ }
+ }
+
+ private void createLabels() {
+ final Context context = new ContextThemeWrapper(getContext(), mLabelsStyle);
+
+ for (int i = 0; i < mButtonsCount; i++) {
+ final FloatingActionButton button = (FloatingActionButton) getChildAt(i);
+ final String title = button.getTitle();
+
+ if (button == mAddButton || title == null ||
+ button.getTag(R.id.fab_label) != null) continue;
+
+ final TextView label = new TextView(context);
+ label.setTextAppearance(context, mLabelsStyle);
+ label.setText(button.getTitle());
+ addView(label);
+
+ button.setTag(R.id.fab_label, label);
+ }
+ }
+
+ public void collapse() {
+ collapse(false);
+ }
+
+ public void collapseImmediately() {
+ collapse(true);
+ }
+
+ 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 toggle() {
+ if (mExpanded) {
+ collapse();
+ } else {
+ expand();
+ }
+ }
+
+ public void expand() {
+ if (!mExpanded) {
+ mExpanded = true;
+ mTouchDelegateGroup.setEnabled(true);
+ mCollapseAnimation.cancel();
+ mExpandAnimation.start();
+
+ if (mListener != null) {
+ mListener.onMenuExpanded();
+ }
+ }
+ }
+
+ public boolean isExpanded() {
+ return mExpanded;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+
+ mAddButton.setEnabled(enabled);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final SavedState savedState = new SavedState(superState);
+ savedState.mExpanded = mExpanded;
+
+ return savedState;
+ }
+
+ @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);
+ }
+ }
+
+ public interface OnFloatingActionsMenuUpdateListener {
+ void onMenuExpanded();
+
+ void onMenuCollapsed();
+ }
+
+ private static class RotatingDrawable extends LayerDrawable {
+ private float mRotation;
+
+ RotatingDrawable(final Drawable drawable) {
+ super(new Drawable[]{drawable});
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ public float getRotation() {
+ return mRotation;
+ }
+
+ @SuppressWarnings("UnusedDeclaration")
+ public void setRotation(final float rotation) {
+ mRotation = rotation;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ canvas.save();
+ canvas.rotate(mRotation, getBounds().centerX(), getBounds().centerY());
+ super.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ public static class SavedState extends BaseSavedState {
+ public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ public boolean mExpanded;
+
+ public SavedState(final Parcelable parcel) {
+ super(parcel);
+ }
+
+ private SavedState(final Parcel in) {
+ super(in);
+ mExpanded = in.readInt() == 1;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel out, final int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(mExpanded ? 1 : 0);
+ }
+ }
+
+ private class LayoutParams extends ViewGroup.LayoutParams {
+
+ private final ObjectAnimator mExpandDir = new ObjectAnimator();
+ private final ObjectAnimator mExpandAlpha = new ObjectAnimator();
+ private final ObjectAnimator mCollapseDir = new ObjectAnimator();
+ private final ObjectAnimator mCollapseAlpha = new ObjectAnimator();
+ private boolean animationsSetToPlay;
+
+ LayoutParams(final ViewGroup.LayoutParams source) {
+ super(source);
+
+ mExpandDir.setInterpolator(sExpandInterpolator);
+ mExpandAlpha.setInterpolator(sAlphaExpandInterpolator);
+ mCollapseDir.setInterpolator(sCollapseInterpolator);
+ mCollapseAlpha.setInterpolator(sCollapseInterpolator);
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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);
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java b/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java
new file mode 100644
index 00000000..9aa221ed
--- /dev/null
+++ b/app/src/main/java/com/wireguard/android/widget/fab/TouchDelegateGroup.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright © 2014 Jerzy Chalupski
+ * Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+package com.wireguard.android.widget.fab;
+
+import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TouchDelegateGroup extends TouchDelegate {
+ private static final Rect USELESS_HACKY_RECT = new Rect();
+ private final List<TouchDelegate> mTouchDelegates = new ArrayList<>();
+ private TouchDelegate mCurrentTouchDelegate;
+ private boolean mEnabled;
+
+ public TouchDelegateGroup(final View uselessHackyView) {
+ super(USELESS_HACKY_RECT, uselessHackyView);
+ }
+
+ public void addTouchDelegate(@NonNull final TouchDelegate touchDelegate) {
+ mTouchDelegates.add(touchDelegate);
+ }
+
+ public void removeTouchDelegate(final TouchDelegate touchDelegate) {
+ mTouchDelegates.remove(touchDelegate);
+ if (mCurrentTouchDelegate == touchDelegate) {
+ mCurrentTouchDelegate = null;
+ }
+ }
+
+ public void clearTouchDelegates() {
+ mTouchDelegates.clear();
+ mCurrentTouchDelegate = null;
+ }
+
+ @Override
+ public boolean onTouchEvent(@NonNull final MotionEvent event) {
+ if (!mEnabled) return false;
+
+ TouchDelegate delegate = null;
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ for (int i = 0; i < mTouchDelegates.size(); i++) {
+ final TouchDelegate touchDelegate = mTouchDelegates.get(i);
+ 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 setEnabled(final boolean enabled) {
+ mEnabled = enabled;
+ }
+}