@@ -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; | |||
} | |||
} |
@@ -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) | |||
} | |||
} |