diff options
Diffstat (limited to 'app/src/main/java/com')
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; + } +} |