1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
/*
* Copyright © 2018 The Android Open Source Project
* Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package com.wireguard.android.widget
import android.animation.ObjectAnimator
import android.content.res.ColorStateList
import android.graphics.*
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.FloatProperty
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)
@Suppress("DEPRECATION")
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
}
}
}
}
|