diff options
author | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-05-29 18:37:14 +0200 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2018-05-29 19:03:47 +0200 |
commit | 09833a1ba51ca01680b9929410bc70218d985eb4 (patch) | |
tree | 1b204c71a221bfa9a3aa1b5ec2a0a4518d2e87e3 /app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java | |
parent | SharedLibraryLoader: introduce to work around Samsung PackageManager bugs (diff) | |
download | wireguard-android-09833a1ba51ca01680b9929410bc70218d985eb4.tar.xz wireguard-android-09833a1ba51ca01680b9929410bc70218d985eb4.zip |
FloatingActionButton: import cleaned up getbase code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
Diffstat (limited to 'app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java')
-rw-r--r-- | app/src/main/java/com/wireguard/android/widget/fab/FloatingActionsMenu.java | 643 |
1 files changed, 643 insertions, 0 deletions
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); + } + }); + } + } +} |