TextView Marquee speed extension
As discussed on Slack, here's a proposal for an extension which uses reflection to change the marquee ellipsis animation speed.
import android.os.Build.VERSION.SDK_INT
import android.text.Editable
import android.text.TextWatcher
import android.widget.TextView
import androidx.core.view.doOnNextLayout
import splitties.dimensions.dp
import java.lang.reflect.Field
import java.util.*
/**
* The marquee speed in pixels per second.
*
* The default value is 30dp.
*/
var TextView.marqueeSpeed: Float
get() = marquee?.speed ?: marqueeSpeeds[this]?.first ?: dp(defaultMarqueeSpeed)
set(speed) {
removeTextChangedListener(marqueeSpeeds.remove(this)?.second)
marquee?.speed = speed
// TODO: This should probably be defined cleaner in a class or at least outside of this setter
val listener = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable?) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
doOnNextLayout {
marquee?.speed = speed
}
}
}
addTextChangedListener(listener)
marqueeSpeeds[this] = speed to listener
}
// Internals of Marquee speed edition below
private val marqueeSpeeds = WeakHashMap<TextView, Pair<Float, TextWatcher>>()
private inline val defaultMarqueeSpeed: Int
get() = TextView::class.java.declaredClasses.single { it.simpleName == "Marquee" }.run {
getDeclaredField("MARQUEE_DP_PER_SECOND").accessible.getInt(this)
}
private typealias Marquee = Any
private inline val TextView.marquee: Marquee?
get() = TextView::class.java
.getDeclaredField("mMarquee")
.accessible
.get(this)
private inline val Marquee.speedField: Field
get() = javaClass.getDeclaredField(
when {
SDK_INT > 21 -> "mPixelsPerSecond"
else -> "mScrollUnit"
}
).accessible
private inline var Marquee.speed: Float
get() = speedField.getFloat(this)
set(speed) {
speedField.setFloat(this, speed)
}
// TODO: Maybe this shouldn't be here, or shouldn't be at all
private inline val Field.accessible: Field
get() = apply { isAccessible = true }
Todo list:
- [ ] Test it on all supported API levels (and maybe check that the default value has always been 30dp in the process?)
- [ ] Cleanup code, add some try-catch to future-proof it
- [x] Open an issue to ask for a public API for this use case
The code I provided doesn't work on Android 9, I'm trying to fix it. https://github.com/aosp-mirror/platform_frameworks_base/commit/fa83834a44052fb9bbdaa81e0faea6870e71268d
That's the first Android version I'd have tried to use it on 😅 Could you start by adding the try catch blocks so it becomes no-op instead of crashing? That'd allow to ensure it doesn't fail, with a mean to validate it.
I just submitted a new public API request on Android's issue tracker: https://issuetracker.google.com/issues/129999621
FYI I tried to make a version working from API 19 to 28 and didn't find a good way to do it. But I didn't have enough time to invest in this for now