diff options
Diffstat (limited to 'app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java')
-rw-r--r-- | app/src/main/java/com/wireguard/android/widget/fab/FloatingActionButton.java | 409 |
1 files changed, 409 insertions, 0 deletions
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(); + } + } +} |