1
+ package com.materialstudies.reply.ui.home
2
+
3
+ import android.animation.ValueAnimator
4
+ import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.ColorFilter
7
+ import android.graphics.Paint
8
+ import android.graphics.PixelFormat
9
+ import android.graphics.Rect
10
+ import android.graphics.RectF
11
+ import android.graphics.drawable.Drawable
12
+ import androidx.annotation.ColorInt
13
+ import androidx.core.content.ContextCompat
14
+ import androidx.interpolator.view.animation.FastOutSlowInInterpolator
15
+ import com.materialstudies.reply.R
16
+ import com.materialstudies.reply.util.getColorFromAttr
17
+ import com.materialstudies.reply.util.lerp
18
+ import com.materialstudies.reply.util.lerpArgb
19
+ import kotlin.math.hypot
20
+
21
+ /* *
22
+ * A [Drawable] which handles drawing the background behind an email item preview and responds to
23
+ * changes in the activated state. This includes:
24
+ * - Drawing the star icon always
25
+ * - On activation animating the star's color and scale
26
+ * - On activation animating a colored circle background
27
+ */
28
+ class EmailSwipeActionDrawable (context : Context ) : Drawable() {
29
+
30
+ private val circlePaint = Paint (Paint .ANTI_ALIAS_FLAG ).apply {
31
+ color = context.getColorFromAttr(R .attr.colorSecondary)
32
+ style = Paint .Style .FILL
33
+ }
34
+
35
+ // Rect to represent the circle used for the background/circular reveal animation.
36
+ private val circle = RectF ()
37
+ private var cx = 0F
38
+ private var cr = 0F
39
+
40
+ private val icon = ContextCompat .getDrawable(context, R .drawable.ic_twotone_star)!!
41
+ private val iconMargin = context.resources.getDimension(R .dimen.keyline_6)
42
+ private val iconIntrinsicWidth = icon?.intrinsicWidth ? : 0
43
+ private val iconIntrinsicHeight = icon?.intrinsicHeight ? : 0
44
+
45
+ @ColorInt private val iconTint = context.getColorFromAttr(R .attr.colorOnBackground)
46
+ @ColorInt private val iconTintActive = context.getColorFromAttr(R .attr.colorOnSecondary)
47
+
48
+ // Amount that we should 'overshoot' the icon's scale by when animating.
49
+ private val iconMaxScaleAddition = 0.5F
50
+
51
+ private var progress = 0F
52
+ set(value) {
53
+ val constrained = value.coerceIn(0F , 1F )
54
+ if (constrained != field) {
55
+ field = constrained
56
+ callback?.invalidateDrawable(this )
57
+ }
58
+ }
59
+ private var progressAnim: ValueAnimator ? = null
60
+ private val interp = FastOutSlowInInterpolator ()
61
+
62
+ override fun onBoundsChange (bounds : Rect ? ) {
63
+ if (bounds == null ) return
64
+ update()
65
+ }
66
+
67
+ fun update () {
68
+ circle.set(
69
+ bounds.left.toFloat(),
70
+ bounds.top.toFloat(),
71
+ bounds.right.toFloat(),
72
+ bounds.bottom.toFloat()
73
+ )
74
+
75
+ val sideToIconCenter = iconMargin + (iconIntrinsicWidth / 2F )
76
+ cx = circle.left + iconMargin + (iconIntrinsicWidth / 2F )
77
+ // Get the longest visible distance at which the circle will be displayed (the hypotenuse of
78
+ // the triangle from the center of the icon, to the furthest side of the rect, to the top
79
+ // corner of the rect.
80
+ cr = hypot(circle.right - sideToIconCenter, (circle.height() / 2F ))
81
+
82
+ callback?.invalidateDrawable(this )
83
+ }
84
+
85
+ override fun isStateful (): Boolean = true
86
+
87
+ override fun onStateChange (state : IntArray? ): Boolean {
88
+ val initialProgress = progress
89
+ val newProgress = if (state?.contains(android.R .attr.state_activated) == true ) {
90
+ 1F
91
+ } else {
92
+ 0F
93
+ }
94
+ progressAnim?.cancel()
95
+ progressAnim = ValueAnimator .ofFloat(initialProgress, newProgress).apply {
96
+ addUpdateListener {
97
+ progress = animatedValue as Float
98
+ }
99
+ interpolator = interp
100
+ duration = (Math .abs(newProgress - initialProgress) * 250F ).toLong()
101
+ }
102
+ progressAnim?.start()
103
+ return newProgress == initialProgress
104
+ }
105
+
106
+ override fun draw (canvas : Canvas ) {
107
+ // Draw the circular reveal background.
108
+ canvas.drawCircle(
109
+ cx,
110
+ circle.centerY(),
111
+ cr * progress,
112
+ circlePaint
113
+ )
114
+
115
+ // Map our progress range from 0-1 to 0-PI
116
+ val range = lerp(
117
+ 0F ,
118
+ Math .PI .toFloat(),
119
+ progress
120
+ )
121
+ // Take the sin of our ranged progress * our maxScaleAddition as what we should
122
+ // increase the icon's scale by.
123
+ val additive = (Math .sin(range.toDouble()) * iconMaxScaleAddition)
124
+ .coerceIn(0.0 , 1.0 )
125
+ val scaleFactor = 1 + additive
126
+ icon.setBounds(
127
+ (cx - (iconIntrinsicWidth / 2F ) * scaleFactor).toInt(),
128
+ (circle.centerY() - (iconIntrinsicHeight / 2F ) * scaleFactor).toInt(),
129
+ (cx + (iconIntrinsicWidth / 2F ) * scaleFactor).toInt(),
130
+ (circle.centerY() + (iconIntrinsicHeight / 2F ) * scaleFactor).toInt()
131
+ )
132
+
133
+ // Draw/animate the color of the icon
134
+ icon.setTint(
135
+ lerpArgb(iconTint, iconTintActive, 0F , 0.15F , progress)
136
+ )
137
+
138
+ // Draw the icon
139
+ icon.draw(canvas)
140
+ }
141
+
142
+ override fun setAlpha (alpha : Int ) {
143
+ circlePaint.alpha = alpha
144
+ }
145
+
146
+ override fun getOpacity (): Int = PixelFormat .TRANSLUCENT
147
+
148
+ override fun setColorFilter (filter : ColorFilter ? ) {
149
+ circlePaint.colorFilter = filter
150
+ }
151
+
152
+ }
0 commit comments