-
-
Notifications
You must be signed in to change notification settings - Fork 224
/
FadingTextView.kt
248 lines (220 loc) · 7.32 KB
/
FadingTextView.kt
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package com.tomer.fadingtextview
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.annotation.ArrayRes
import androidx.appcompat.widget.AppCompatTextView
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
/**
* @author Tomer Rosenfeld AKA rosenpin
* Created by rosenpin on 12/8/16.
*/
class FadingTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
private val fadeInAnimation: Animation by lazy {
AnimationUtils.loadAnimation(
context,
R.anim.fadein
)
}
private val fadeOutAnimation: Animation by lazy {
AnimationUtils.loadAnimation(
context,
R.anim.fadeout
)
}
private val handler: Handler = Handler(Looper.getMainLooper())
var texts: Array<CharSequence> = emptyArray()
private set
private var isShown = true
private var position = 0
private var timeout = DEFAULT_TIME_OUT
private var stopped = false
init {
handleAttrs(attrs)
}
/**
* Resumes the animation
* Should only be used if you notice @see [onAttachedToWindow]} is not being executed as expected
*/
fun resume() {
isShown = true
startAnimation()
}
/**
* Pauses the animation
* Should only be used if you notice @see [onDetachedFromWindow] is not being executed as expected
*/
fun pause() {
isShown = false
stopAnimation()
}
/**
* Stops the animation
* Unlike the pause function, the stop method will permanently stop the animation until the view is restarted
*/
fun stop() {
isShown = false
stopped = true
stopAnimation()
}
/**
* Restarts the animation
* Only use this to restart the animation after stopping it using {@see [stop]}
*/
fun restart() {
isShown = true
stopped = false
startAnimation()
invalidate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
pause()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
resume()
}
/**
* Handle the xml attributes
* set the texts
* set the timeout
*
* @param attrs provided attributes
*/
private fun handleAttrs(attrs: AttributeSet?) {
attrs?.let { attributeSet ->
val typedArray =
context.obtainStyledAttributes(attributeSet, R.styleable.FadingTextView)
typedArray.getTextArray(R.styleable.FadingTextView_fadingTextViewTexts)?.let { textArray ->
texts = textArray
}
val baseTimeout = typedArray.getInteger(
R.styleable.FadingTextView_fadingTextViewTimeout,
DEFAULT_TIME_OUT.toInt(DurationUnit.MILLISECONDS)
).milliseconds
val animationDuration =
resources.getInteger(android.R.integer.config_longAnimTime).milliseconds
timeout = baseTimeout + animationDuration
typedArray.getBoolean(R.styleable.FadingTextView_fadingTextViewShuffle, false).also { shouldShuffle ->
if (shouldShuffle) {
shuffle()
}
}
typedArray.recycle()
}
}
/**
* Sets the texts to be shuffled using a string array
*
* @param texts The string array to use for the texts
*/
fun setTexts(texts: Array<String>) {
require(texts.isNotEmpty()) { "There must be at least one text" }
this.texts = texts.map { it }.toTypedArray()
stopAnimation()
position = 0
startAnimation()
}
/**
* Sets the texts to be shuffled using a string array resource
*
* @param texts The string array resource to use for the texts
*/
fun setTexts(@ArrayRes texts: Int) {
val mTexts = resources.getStringArray(texts)
setTexts(mTexts)
}
/**
* This method should only be used to forcefully apply timeout changes
* It will dismiss the currently queued animation change and start a new animation
*/
fun forceRefresh() {
stopAnimation()
startAnimation()
}
/**
* Fades text to position in provided array and pauses
* Consider calling pause() method before calling this function to avoid overriding currently active animation
*/
fun fadeTo(position: Int) {
this.position = position
isShown = true
startAnimation()
pause()
}
/**
* Shuffle the strings
* Each time this method is ran, the order of the strings will be randomized
* After you set texts dynamically you will have to call shuffle again
*
* @throws IllegalArgumentException if you don't supply texts to the FadingTextView in your XML file. You can leave it empty by using FTV.placeholder and set it manually later using the setTexts method
*/
fun shuffle() {
require(texts.isNotEmpty()) { "You must provide a string array to the FadingTextView using the texts parameter" }
val textsList = texts.toMutableList()
textsList.shuffle()
this.texts = textsList.toTypedArray()
}
/**
* Sets the length of time to wait between text changes in specific time units
*
* @param timeout The duration to wait between text changes
* @throws IllegalArgumentException if the duration is not positive.
*/
fun setTimeout(timeout: Duration) {
require(timeout.isPositive()) { "Timeout must be longer than 0" }
this.timeout = timeout
}
/**
* This method is overridden to prevent animations from starting when the view is not shown
* If you want to start the animation manually, use the @see [resume] method
* @param animation the animation to start now
*/
override fun startAnimation(animation: Animation) {
if (isShown && !stopped) {
super.startAnimation(animation)
}
}
/**
* Start the animation
*/
private fun startAnimation() {
if (isInEditMode || texts.isEmpty()) return
text = texts[position]
startAnimation(fadeInAnimation)
handler.postDelayed({
startAnimation(fadeOutAnimation)
animation?.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
if (isShown) {
position = if (position == texts.size - 1) 0 else position + 1
startAnimation()
}
}
override fun onAnimationRepeat(animation: Animation) {}
})
}, timeout.inWholeMilliseconds)
}
/**
* Stop the currently active animation
*/
private fun stopAnimation() {
handler.removeCallbacksAndMessages(null)
animation?.cancel()
}
companion object {
private val DEFAULT_TIME_OUT = 15.seconds
}
}