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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
package com.wireguard.android.widget
|
package com.wireguard.android.widget
|
||||||
@ -15,7 +15,6 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
object EdgeToEdge {
|
object EdgeToEdge {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setUpRoot(root: ViewGroup) {
|
fun setUpRoot(root: ViewGroup) {
|
||||||
root.systemUiVisibility =
|
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