aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt')
-rw-r--r--ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt174
1 files changed, 174 insertions, 0 deletions
diff --git a/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
new file mode 100644
index 00000000..a93e35f5
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
@@ -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
+ }
+ }
+ }
+
+}