widget: rewrite in kotlin
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
2fe5b92035
commit
1054e54c89
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2017-2020 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2020 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
@ -15,7 +15,6 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
|
||||
*/
|
||||
|
||||
object EdgeToEdge {
|
||||
|
||||
@JvmStatic
|
||||
fun setUpRoot(root: ViewGroup) {
|
||||
root.systemUiVisibility =
|
||||
|
@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.crypto.Key;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard private/public keys encoded with base64.
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class KeyInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || c == '+' || c == '/';
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new KeyInputFilter();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to the base64 character set.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if (((dIndex + 1 < Key.Format.BASE64.getLength() && isAllowed(c)) ||
|
||||
(dIndex + 1 == Key.Format.BASE64.getLength() && c == '=')) &&
|
||||
dLength + (sIndex - sStart) < Key.Format.BASE64.getLength()) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import com.wireguard.crypto.Key
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard private/public keys encoded with base64.
|
||||
*/
|
||||
class KeyInputFilter : InputFilter {
|
||||
override fun filter(source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int): CharSequence? {
|
||||
var replacement: SpannableStringBuilder? = null
|
||||
var rIndex = 0
|
||||
val dLength = dest.length
|
||||
for (sIndex in sStart until sEnd) {
|
||||
val c = source[sIndex]
|
||||
val dIndex = dStart + (sIndex - sStart)
|
||||
// Restrict characters to the base64 character set.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex + 1 < Key.Format.BASE64.length && isAllowed(c) ||
|
||||
dIndex + 1 == Key.Format.BASE64.length && c == '=') &&
|
||||
dLength + (sIndex - sStart) < Key.Format.BASE64.length) {
|
||||
++rIndex
|
||||
} else {
|
||||
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
|
||||
replacement.delete(rIndex, rIndex + 1)
|
||||
}
|
||||
}
|
||||
return replacement
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || c == '+' || c == '/'
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() = KeyInputFilter()
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import com.wireguard.android.R;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
@NonNullForAll
|
||||
public class MultiselectableRelativeLayout extends RelativeLayout {
|
||||
private static final int[] STATE_MULTISELECTED = {R.attr.state_multiselected};
|
||||
private boolean multiselected;
|
||||
|
||||
public MultiselectableRelativeLayout(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public MultiselectableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] onCreateDrawableState(final int extraSpace) {
|
||||
if (multiselected) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
mergeDrawableStates(drawableState, STATE_MULTISELECTED);
|
||||
return drawableState;
|
||||
}
|
||||
return super.onCreateDrawableState(extraSpace);
|
||||
}
|
||||
|
||||
public void setMultiSelected(final boolean on) {
|
||||
if (!multiselected) {
|
||||
multiselected = true;
|
||||
refreshDrawableState();
|
||||
}
|
||||
setActivated(on);
|
||||
}
|
||||
|
||||
public void setSingleSelected(final boolean on) {
|
||||
if (multiselected) {
|
||||
multiselected = false;
|
||||
refreshDrawableState();
|
||||
}
|
||||
setActivated(on);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.RelativeLayout
|
||||
import com.wireguard.android.R
|
||||
|
||||
class MultiselectableRelativeLayout : RelativeLayout {
|
||||
private var multiselected = false
|
||||
|
||||
constructor(context: Context?) : super(context) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}
|
||||
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {}
|
||||
|
||||
override fun onCreateDrawableState(extraSpace: Int): IntArray {
|
||||
if (multiselected) {
|
||||
val drawableState = super.onCreateDrawableState(extraSpace + 1)
|
||||
View.mergeDrawableStates(drawableState, STATE_MULTISELECTED)
|
||||
return drawableState
|
||||
}
|
||||
return super.onCreateDrawableState(extraSpace)
|
||||
}
|
||||
|
||||
fun setMultiSelected(on: Boolean) {
|
||||
if (!multiselected) {
|
||||
multiselected = true
|
||||
refreshDrawableState()
|
||||
}
|
||||
isActivated = on
|
||||
}
|
||||
|
||||
fun setSingleSelected(on: Boolean) {
|
||||
if (multiselected) {
|
||||
multiselected = false
|
||||
refreshDrawableState()
|
||||
}
|
||||
isActivated = on
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val STATE_MULTISELECTED = intArrayOf(R.attr.state_multiselected)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.text.InputFilter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.wireguard.android.backend.Tunnel;
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
*/
|
||||
|
||||
@NonNullForAll
|
||||
public class NameInputFilter implements InputFilter {
|
||||
private static boolean isAllowed(final char c) {
|
||||
return Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0;
|
||||
}
|
||||
|
||||
public static InputFilter newInstance() {
|
||||
return new NameInputFilter();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence filter(final CharSequence source,
|
||||
final int sStart, final int sEnd,
|
||||
final Spanned dest,
|
||||
final int dStart, final int dEnd) {
|
||||
SpannableStringBuilder replacement = null;
|
||||
int rIndex = 0;
|
||||
final int dLength = dest.length();
|
||||
for (int sIndex = sStart; sIndex < sEnd; ++sIndex) {
|
||||
final char c = source.charAt(sIndex);
|
||||
final int dIndex = dStart + (sIndex - sStart);
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if ((dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c)) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
++rIndex;
|
||||
} else {
|
||||
if (replacement == null)
|
||||
replacement = new SpannableStringBuilder(source, sStart, sEnd);
|
||||
replacement.delete(rIndex, rIndex + 1);
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import com.wireguard.android.backend.Tunnel
|
||||
|
||||
/**
|
||||
* InputFilter for entering WireGuard configuration names (Linux interface names).
|
||||
*/
|
||||
class NameInputFilter : InputFilter {
|
||||
override fun filter(source: CharSequence,
|
||||
sStart: Int, sEnd: Int,
|
||||
dest: Spanned,
|
||||
dStart: Int, dEnd: Int): CharSequence? {
|
||||
var replacement: SpannableStringBuilder? = null
|
||||
var rIndex = 0
|
||||
val dLength = dest.length
|
||||
for (sIndex in sStart until sEnd) {
|
||||
val c = source[sIndex]
|
||||
val dIndex = dStart + (sIndex - sStart)
|
||||
// Restrict characters to those valid in interfaces.
|
||||
// Ensure adding this character does not push the length over the limit.
|
||||
if (dIndex < Tunnel.NAME_MAX_LENGTH && isAllowed(c) &&
|
||||
dLength + (sIndex - sStart) < Tunnel.NAME_MAX_LENGTH) {
|
||||
++rIndex
|
||||
} else {
|
||||
if (replacement == null) replacement = SpannableStringBuilder(source, sStart, sEnd)
|
||||
replacement.delete(rIndex, rIndex + 1)
|
||||
}
|
||||
}
|
||||
return replacement
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun isAllowed(c: Char) = Character.isLetterOrDigit(c) || "_=+.-".indexOf(c) >= 0
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() = NameInputFilter()
|
||||
}
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2018 The Android Open Source Project
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Path.Direction;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff.Mode;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.FloatProperty;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@NonNullForAll
|
||||
public class SlashDrawable extends Drawable {
|
||||
|
||||
private static final float CENTER_X = 10.65f;
|
||||
private static final float CENTER_Y = 11.869239f;
|
||||
private static final float CORNER_RADIUS = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 0f : 1f;
|
||||
// Draw the slash washington-monument style; rotate to no-u-turn style
|
||||
private static final float DEFAULT_ROTATION = -45f;
|
||||
private static final long QS_ANIM_LENGTH = 350;
|
||||
private static final float SCALE = 24f;
|
||||
private static final float SLASH_HEIGHT = 28f;
|
||||
// These values are derived in un-rotated (vertical) orientation
|
||||
private static final float SLASH_WIDTH = 1.8384776f;
|
||||
// Bottom is derived during animation
|
||||
private static final float LEFT = (CENTER_X - (SLASH_WIDTH / 2)) / SCALE;
|
||||
private static final float RIGHT = (CENTER_X + (SLASH_WIDTH / 2)) / SCALE;
|
||||
private static final float TOP = (CENTER_Y - (SLASH_HEIGHT / 2)) / SCALE;
|
||||
private static final FloatProperty mSlashLengthProp = new FloatProperty<SlashDrawable>("slashLength") {
|
||||
@Override
|
||||
public Float get(final SlashDrawable object) {
|
||||
return object.mCurrentSlashLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(final SlashDrawable object, final float value) {
|
||||
object.mCurrentSlashLength = value;
|
||||
}
|
||||
};
|
||||
private final Drawable mDrawable;
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path mPath = new Path();
|
||||
private final RectF mSlashRect = new RectF(0, 0, 0, 0);
|
||||
private boolean mAnimationEnabled = true;
|
||||
// Animate this value on change
|
||||
private float mCurrentSlashLength;
|
||||
private float mRotation;
|
||||
private boolean mSlashed;
|
||||
|
||||
public SlashDrawable(final Drawable d) {
|
||||
mDrawable = d;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void draw(final Canvas canvas) {
|
||||
canvas.save();
|
||||
final Matrix m = new Matrix();
|
||||
final int width = getBounds().width();
|
||||
final int height = getBounds().height();
|
||||
final float radiusX = scale(CORNER_RADIUS, width);
|
||||
final float radiusY = scale(CORNER_RADIUS, height);
|
||||
updateRect(
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
);
|
||||
|
||||
mPath.reset();
|
||||
// Draw the slash vertically
|
||||
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Direction.CW);
|
||||
// Rotate -45 + desired rotation
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
canvas.drawPath(mPath, mPaint);
|
||||
|
||||
// Rotate back to vertical
|
||||
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
// Draw another rect right next to the first, for clipping
|
||||
m.setTranslate(mSlashRect.width(), 0);
|
||||
mPath.transform(m);
|
||||
mPath.addRoundRect(mSlashRect, 1.0f * width, 1.0f * height, Direction.CW);
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2, height / 2);
|
||||
mPath.transform(m);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
canvas.clipPath(mPath, Region.Op.DIFFERENCE);
|
||||
else
|
||||
canvas.clipOutPath(mPath);
|
||||
|
||||
mDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return mDrawable.getIntrinsicHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return mDrawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(final Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
mDrawable.setBounds(bounds);
|
||||
}
|
||||
|
||||
private float scale(final float frac, final int width) {
|
||||
return frac * width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) final int alpha) {
|
||||
mDrawable.setAlpha(alpha);
|
||||
mPaint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
public void setAnimationEnabled(final boolean enabled) {
|
||||
mAnimationEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable final ColorFilter colorFilter) {
|
||||
mDrawable.setColorFilter(colorFilter);
|
||||
mPaint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
private void setDrawableTintList(@Nullable final ColorStateList tint) {
|
||||
mDrawable.setTintList(tint);
|
||||
}
|
||||
|
||||
public void setRotation(final float rotation) {
|
||||
if (mRotation == rotation)
|
||||
return;
|
||||
mRotation = rotation;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setSlashed(final boolean slashed) {
|
||||
if (mSlashed == slashed) return;
|
||||
|
||||
mSlashed = slashed;
|
||||
|
||||
final float end = mSlashed ? SLASH_HEIGHT / SCALE : 0f;
|
||||
final float start = mSlashed ? 0f : SLASH_HEIGHT / SCALE;
|
||||
|
||||
if (mAnimationEnabled) {
|
||||
final ObjectAnimator anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end);
|
||||
anim.addUpdateListener((ValueAnimator valueAnimator) -> invalidateSelf());
|
||||
anim.setDuration(QS_ANIM_LENGTH);
|
||||
anim.start();
|
||||
} else {
|
||||
mCurrentSlashLength = end;
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTint(@ColorInt final int tintColor) {
|
||||
super.setTint(tintColor);
|
||||
mDrawable.setTint(tintColor);
|
||||
mPaint.setColor(tintColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintList(@Nullable final ColorStateList tint) {
|
||||
super.setTintList(tint);
|
||||
setDrawableTintList(tint);
|
||||
mPaint.setColor(tint == null ? 0 : tint.getDefaultColor());
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTintMode(final Mode tintMode) {
|
||||
super.setTintMode(tintMode);
|
||||
mDrawable.setTintMode(tintMode);
|
||||
}
|
||||
|
||||
private void updateRect(final float left, final float top, final float right, final float bottom) {
|
||||
mSlashRect.left = left;
|
||||
mSlashRect.top = top;
|
||||
mSlashRect.right = right;
|
||||
mSlashRect.bottom = bottom;
|
||||
}
|
||||
}
|
174
ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
Normal file
174
ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright © 2018 The Android Open Source Project
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.util.FloatProperty
|
||||
import android.util.Property
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
class SlashDrawable(private val mDrawable: Drawable) : Drawable() {
|
||||
private val mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val mPath = Path()
|
||||
private val mSlashRect = RectF()
|
||||
private var mAnimationEnabled = true
|
||||
// Animate this value on change
|
||||
private var mCurrentSlashLength = 0f
|
||||
private var mRotation = 0f
|
||||
private var mSlashed = false
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.save()
|
||||
val m = Matrix()
|
||||
val width = bounds.width()
|
||||
val height = bounds.height()
|
||||
val radiusX = scale(CORNER_RADIUS, width)
|
||||
val radiusY = scale(CORNER_RADIUS, height)
|
||||
updateRect(
|
||||
scale(LEFT, width),
|
||||
scale(TOP, height),
|
||||
scale(RIGHT, width),
|
||||
scale(TOP + mCurrentSlashLength, height)
|
||||
)
|
||||
mPath.reset()
|
||||
// Draw the slash vertically
|
||||
mPath.addRoundRect(mSlashRect, radiusX, radiusY, Path.Direction.CW)
|
||||
// Rotate -45 + desired rotation
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2f, height / 2f)
|
||||
mPath.transform(m)
|
||||
canvas.drawPath(mPath, mPaint)
|
||||
|
||||
// Rotate back to vertical
|
||||
m.setRotate(-mRotation - DEFAULT_ROTATION, width / 2f, height / 2f)
|
||||
mPath.transform(m)
|
||||
|
||||
// Draw another rect right next to the first, for clipping
|
||||
m.setTranslate(mSlashRect.width(), 0f)
|
||||
mPath.transform(m)
|
||||
mPath.addRoundRect(mSlashRect, 1f * width, 1f * height, Path.Direction.CW)
|
||||
m.setRotate(mRotation + DEFAULT_ROTATION, width / 2f, height / 2f)
|
||||
mPath.transform(m)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) canvas.clipPath(mPath, Region.Op.DIFFERENCE) else canvas.clipOutPath(mPath)
|
||||
mDrawable.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight() = mDrawable.intrinsicHeight
|
||||
|
||||
override fun getIntrinsicWidth() = mDrawable.intrinsicWidth
|
||||
|
||||
override fun getOpacity() = PixelFormat.OPAQUE
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
mDrawable.bounds = bounds
|
||||
}
|
||||
|
||||
private fun scale(frac: Float, width: Int) = frac * width
|
||||
|
||||
override fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) {
|
||||
mDrawable.alpha = alpha
|
||||
mPaint.alpha = alpha
|
||||
}
|
||||
|
||||
fun setAnimationEnabled(enabled: Boolean) {
|
||||
mAnimationEnabled = enabled
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
mDrawable.colorFilter = colorFilter
|
||||
mPaint.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
private fun setDrawableTintList(tint: ColorStateList?) {
|
||||
mDrawable.setTintList(tint)
|
||||
}
|
||||
|
||||
fun setRotation(rotation: Float) {
|
||||
if (mRotation == rotation) return
|
||||
mRotation = rotation
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
fun setSlashed(slashed: Boolean) {
|
||||
if (mSlashed == slashed) return
|
||||
mSlashed = slashed
|
||||
val end = if (mSlashed) SLASH_HEIGHT / SCALE else 0f
|
||||
val start = if (mSlashed) 0f else SLASH_HEIGHT / SCALE
|
||||
if (mAnimationEnabled) {
|
||||
val anim = ObjectAnimator.ofFloat(this, mSlashLengthProp, start, end)
|
||||
anim.addUpdateListener { _ -> invalidateSelf() }
|
||||
anim.duration = QS_ANIM_LENGTH
|
||||
anim.start()
|
||||
} else {
|
||||
mCurrentSlashLength = end
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTint(@ColorInt tintColor: Int) {
|
||||
super.setTint(tintColor)
|
||||
mDrawable.setTint(tintColor)
|
||||
mPaint.color = tintColor
|
||||
}
|
||||
|
||||
override fun setTintList(tint: ColorStateList?) {
|
||||
super.setTintList(tint)
|
||||
setDrawableTintList(tint)
|
||||
mPaint.color = tint?.defaultColor ?: 0
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setTintMode(tintMode: PorterDuff.Mode?) {
|
||||
super.setTintMode(tintMode)
|
||||
mDrawable.setTintMode(tintMode)
|
||||
}
|
||||
|
||||
private fun updateRect(left: Float, top: Float, right: Float, bottom: Float) {
|
||||
mSlashRect.left = left
|
||||
mSlashRect.top = top
|
||||
mSlashRect.right = right
|
||||
mSlashRect.bottom = bottom
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CENTER_X = 10.65f
|
||||
private const val CENTER_Y = 11.869239f
|
||||
private val CORNER_RADIUS = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) 0f else 1f
|
||||
|
||||
// Draw the slash washington-monument style; rotate to no-u-turn style
|
||||
private const val DEFAULT_ROTATION = -45f
|
||||
private const val QS_ANIM_LENGTH: Long = 350
|
||||
private const val SCALE = 24f
|
||||
private const val SLASH_HEIGHT = 28f
|
||||
|
||||
// These values are derived in un-rotated (vertical) orientation
|
||||
private const val SLASH_WIDTH = 1.8384776f
|
||||
|
||||
// Bottom is derived during animation
|
||||
private const val LEFT = (CENTER_X - SLASH_WIDTH / 2) / SCALE
|
||||
private const val RIGHT = (CENTER_X + SLASH_WIDTH / 2) / SCALE
|
||||
private const val TOP = (CENTER_Y - SLASH_HEIGHT / 2) / SCALE
|
||||
private val mSlashLengthProp: FloatProperty<SlashDrawable> = object : FloatProperty<SlashDrawable>("slashLength") {
|
||||
override fun get(obj: SlashDrawable): Float {
|
||||
return obj.mCurrentSlashLength
|
||||
}
|
||||
|
||||
override fun setValue(obj: SlashDrawable, value: Float) {
|
||||
obj.mCurrentSlashLength = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2013 The Android Open Source Project
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.wireguard.android.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Switch;
|
||||
|
||||
import com.wireguard.util.NonNullForAll;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NonNullForAll
|
||||
public class ToggleSwitch extends Switch {
|
||||
private boolean isRestoringState;
|
||||
@Nullable private OnBeforeCheckedChangeListener listener;
|
||||
|
||||
public ToggleSwitch(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
|
||||
public ToggleSwitch(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestoreInstanceState(final Parcelable state) {
|
||||
isRestoringState = true;
|
||||
super.onRestoreInstanceState(state);
|
||||
isRestoringState = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
if (checked == isChecked())
|
||||
return;
|
||||
if (isRestoringState || listener == null) {
|
||||
super.setChecked(checked);
|
||||
return;
|
||||
}
|
||||
setEnabled(false);
|
||||
listener.onBeforeCheckedChanged(this, checked);
|
||||
}
|
||||
|
||||
public void setCheckedInternal(final boolean checked) {
|
||||
super.setChecked(checked);
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
public void setOnBeforeCheckedChangeListener(final OnBeforeCheckedChangeListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public interface OnBeforeCheckedChangeListener {
|
||||
void onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright © 2013 The Android Open Source Project
|
||||
* Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package com.wireguard.android.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Switch
|
||||
|
||||
class ToggleSwitch @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : Switch(context, attrs) {
|
||||
private var isRestoringState = false
|
||||
private var listener: OnBeforeCheckedChangeListener? = null
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
isRestoringState = true
|
||||
super.onRestoreInstanceState(state)
|
||||
isRestoringState = false
|
||||
}
|
||||
|
||||
override fun setChecked(checked: Boolean) {
|
||||
if (checked == isChecked) return
|
||||
if (isRestoringState || listener == null) {
|
||||
super.setChecked(checked)
|
||||
return
|
||||
}
|
||||
isEnabled = false
|
||||
listener!!.onBeforeCheckedChanged(this, checked)
|
||||
}
|
||||
|
||||
fun setCheckedInternal(checked: Boolean) {
|
||||
super.setChecked(checked)
|
||||
isEnabled = true
|
||||
}
|
||||
|
||||
fun setOnBeforeCheckedChangeListener(listener: OnBeforeCheckedChangeListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
interface OnBeforeCheckedChangeListener {
|
||||
fun onBeforeCheckedChanged(toggleSwitch: ToggleSwitch?, checked: Boolean)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user