FloatingActionButton: import cleaned up getbase code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
@ -33,7 +33,6 @@ ext {
|
|||||||
databindingVersion = '3.1.2'
|
databindingVersion = '3.1.2'
|
||||||
supportLibsVersion = '27.1.1'
|
supportLibsVersion = '27.1.1'
|
||||||
daggerVersion = '2.14.1'
|
daggerVersion = '2.14.1'
|
||||||
fabLibVersion = '1.10.1'
|
|
||||||
streamsupportVersion = '1.6.0'
|
streamsupportVersion = '1.6.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +44,12 @@ dependencies {
|
|||||||
implementation "com.android.support:design:$supportLibsVersion"
|
implementation "com.android.support:design:$supportLibsVersion"
|
||||||
implementation "com.android.support:preference-v14:$supportLibsVersion"
|
implementation "com.android.support:preference-v14:$supportLibsVersion"
|
||||||
implementation "com.android.support:support-annotations:$supportLibsVersion"
|
implementation "com.android.support:support-annotations:$supportLibsVersion"
|
||||||
implementation "com.getbase:floatingactionbutton:$fabLibVersion"
|
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrofuture:$streamsupportVersion"
|
||||||
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
implementation "net.sourceforge.streamsupport:android-retrostreams:$streamsupportVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
options.compilerArgs << '-Xlint:unchecked'
|
||||||
|
options.deprecation = true
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/fab_bg_mini.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/drawable-hdpi/fab_bg_normal.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/drawable-mdpi/fab_bg_mini.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-mdpi/fab_bg_normal.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
app/src/main/res/drawable-xhdpi/fab_bg_mini.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/drawable-xhdpi/fab_bg_normal.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/fab_bg_mini.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/drawable-xxhdpi/fab_bg_normal.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/fab_bg_mini.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/fab_bg_normal.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
@ -29,7 +29,7 @@
|
|||||||
app:items="@{tunnels}"
|
app:items="@{tunnels}"
|
||||||
app:layout="@{@layout/tunnel_list_item}" />
|
app:layout="@{@layout/tunnel_list_item}" />
|
||||||
|
|
||||||
<com.getbase.floatingactionbutton.FloatingActionsMenu
|
<com.wireguard.android.widget.fab.FloatingActionsMenu
|
||||||
android:id="@+id/create_menu"
|
android:id="@+id/create_menu"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -39,7 +39,7 @@
|
|||||||
app:fab_labelsPosition="left"
|
app:fab_labelsPosition="left"
|
||||||
app:layout_dodgeInsetEdges="bottom">
|
app:layout_dodgeInsetEdges="bottom">
|
||||||
|
|
||||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
<com.wireguard.android.widget.fab.FloatingActionButton
|
||||||
android:id="@+id/create_empty"
|
android:id="@+id/create_empty"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -48,7 +48,7 @@
|
|||||||
app:fab_size="mini"
|
app:fab_size="mini"
|
||||||
app:fab_title="@string/create_empty" />
|
app:fab_title="@string/create_empty" />
|
||||||
|
|
||||||
<com.getbase.floatingactionbutton.FloatingActionButton
|
<com.wireguard.android.widget.fab.FloatingActionButton
|
||||||
android:id="@+id/create_from_file"
|
android:id="@+id/create_from_file"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -56,6 +56,6 @@
|
|||||||
app:fab_icon="@drawable/ic_action_open"
|
app:fab_icon="@drawable/ic_action_open"
|
||||||
app:fab_size="mini"
|
app:fab_size="mini"
|
||||||
app:fab_title="@string/create_from_file" />
|
app:fab_title="@string/create_from_file" />
|
||||||
</com.getbase.floatingactionbutton.FloatingActionsMenu>
|
</com.wireguard.android.widget.fab.FloatingActionsMenu>
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
58
app/src/main/res/values/fab.xml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="fab_expand_menu_button" type="id"/>
|
||||||
|
<item name="fab_label" type="id"/>
|
||||||
|
|
||||||
|
<dimen name="fab_size_normal">56dp</dimen>
|
||||||
|
<dimen name="fab_size_mini">40dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="fab_icon_size">24dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="fab_plus_icon_size">14dp</dimen>
|
||||||
|
<dimen name="fab_plus_icon_stroke">2dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="fab_shadow_offset">3dp</dimen>
|
||||||
|
<dimen name="fab_shadow_radius">9dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="fab_stroke_width">1dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="fab_actions_spacing">16dp</dimen>
|
||||||
|
<dimen name="fab_labels_margin">8dp</dimen>
|
||||||
|
|
||||||
|
<declare-styleable name="FloatingActionButton">
|
||||||
|
<attr name="fab_colorPressed" format="color"/>
|
||||||
|
<attr name="fab_colorDisabled" format="color"/>
|
||||||
|
<attr name="fab_colorNormal" format="color"/>
|
||||||
|
<attr name="fab_icon" format="reference"/>
|
||||||
|
<attr name="fab_size" format="enum">
|
||||||
|
<enum name="normal" value="0"/>
|
||||||
|
<enum name="mini" value="1"/>
|
||||||
|
</attr>
|
||||||
|
<attr name="fab_title" format="string"/>
|
||||||
|
<attr name="fab_stroke_visible" format="boolean"/>
|
||||||
|
</declare-styleable>
|
||||||
|
<declare-styleable name="AddFloatingActionButton">
|
||||||
|
<attr name="fab_plusIconColor" format="color"/>
|
||||||
|
</declare-styleable>
|
||||||
|
<declare-styleable name="FloatingActionsMenu">
|
||||||
|
<attr name="fab_addButtonColorPressed" format="color"/>
|
||||||
|
<attr name="fab_addButtonColorNormal" format="color"/>
|
||||||
|
<attr name="fab_addButtonSize" format="enum">
|
||||||
|
<enum name="normal" value="0"/>
|
||||||
|
<enum name="mini" value="1"/>
|
||||||
|
</attr>
|
||||||
|
<attr name="fab_addButtonPlusIconColor" format="color"/>
|
||||||
|
<attr name="fab_addButtonStrokeVisible" format="boolean"/>
|
||||||
|
<attr name="fab_labelStyle" format="reference"/>
|
||||||
|
<attr name="fab_labelsPosition" format="enum">
|
||||||
|
<enum name="left" value="0"/>
|
||||||
|
<enum name="right" value="1"/>
|
||||||
|
</attr>
|
||||||
|
<attr name="fab_expandDirection" format="enum">
|
||||||
|
<enum name="up" value="0"/>
|
||||||
|
<enum name="down" value="1"/>
|
||||||
|
<enum name="left" value="2"/>
|
||||||
|
<enum name="right" value="3"/>
|
||||||
|
</attr>
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|