summaryrefslogtreecommitdiffhomepage
path: root/ui/src/main/java/com/wireguard/android/widget/SlashDrawable.kt
blob: 5074126475b42dcfe9f2a31c82057f7545d8671a (plain)
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
176
/*
 * 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)
        @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
            }
        }
    }

}