I have this "Remove animations"-setting turned on on my phone because different kinds of movement make me feel physically sick. When the setting is on, animations are usually removed from native Android apps.
And when there are some animations, I notice them. I've started seeing more and more of some horizontally scrolling texts, and I have been wondering why.
Then I came across this basicMarquee
-modifier. And it doesn't respect the "Remove animations" accessibility setting. And that's bad - many users (me included) rely on that setting to not see animations. Marquee-styled animations are one of the worst triggers of my motion sickness symptoms.
If you want to learn more about my symptoms and why animations can be super problematic for some, I've written two blog posts, one from Android and one from a web point of view:
But since bringing up solutions is often better received than bringing up problems, this blog post will demonstrate one idea on how to improve the situation for us who rely on that "Remove animations" setting.
The main idea is to read the value of this setting and then use it to decide if the basicMarquee
-modifier is used. In this blog post, I'm using composition locals to accomplish it.
Remove Animations Setting
Before we dive into the code, a couple of words about the "Remove animations" setting. It's a global setting, which you can find from the accessibility settings. In my Pixel phone, "Remove animations" is under the "Color and motion"-section.
From a technical perspective, the setting changes the animator duration scale to 0, so, in other words, the animations end right after they start. This accessibility setting is not exposed via accessibility services the same way as, for example, screen reader availability, so we'll need to be a bit creative. The following section explains how we can read the value of this setting.
Composition Local for Remove Animations
We'll want to store the value of the "Remove animations" setting and make it available for the components in the component hierarchy. Custom CompositionLocal
is one tool for that. As Android documentation describes CompositionLocal
s:
CompositionLocal
is a tool for passing data down through the Composition implicitly.
Let's start by creating the custom composition local and the data type it provides:
// LocalRemoveAnimations.kt
data class RemoveAnimations(
val context: Context
) {
val enabled: Boolean
}
val LocalRemoveAnimations =
staticCompositionLocalOf<RemoveAnimations> {
error("No user found!")
}
Here, we're defining the LocalRemoveAnimations
as staticCompositionLocalOf
as the value is not likely to change - in fact, the app needs to be restarted to test it. Or at least I haven't found a way to observe the value; just read it synchronously.
We can read the value of the animation duration scale with the following code:
val animationDuration = try {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f
)
} catch (e: Settings.SettingNotFoundException) {
1f
}
So, we try to read the value of Settings.Global.ANIMATOR_DURATION_SCALE
with a default value of 1f. If the setting is not found, Settings.SettingNotFoundException
is thrown; we want to catch it and set the default value. One example of when the setting is unavailable is when the phone's Android version is older than 12.
And putting them together, we get:
// LocalRemoveAnimations.kt
data class RemoveAnimations(
val context: Context,
) {
val enabled: Boolean
get() =
try {
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
} catch (e: Settings.SettingNotFoundException) {
1f
} == 0f
}
Note that for the enabled
-value, we add a check if the value from the try/catch-block is 0f to know if the "Remove animations" setting is enabled.
The next step is to provide it in code:
// MainActivity.kt
setContent {
val context = LocalContext.current
CompositionLocalProvider(
LocalRemoveAnimations provides
RemoveAnimations(
context = context,
),
) {
// App content goes here
}
}
Finally, we can read the value of the LocalRemoveAnimations
in the components inside the app:
@Composable
fun ComponentSomewhereInTheHierarchy() {
val removeAnimations = LocalRemoveAnimations.current.enabled
....
}
All right, now we have everything to build the safer marquee modifier. Let's do that in the next section.
safeMarquee
-Modifier
Now that we have the information about the user's "Remove animations"-setting available via LocalRemoveAnimations
, we can use it to adjust the text using the basicMarquee
-modifier.
If the user hasn't enabled the "Remove animations" setting, we want to show the marquee; otherwise, let the text flow on multiple lines. Let's define a custom modifier with the composable modifier factory and call it safeMarquee
:
// MarqueeScreen.kt
@Composable
fun Modifier.safeMarquee(): Modifier {
}
Next, we want to read the value from LocalRemoveAnimations
and, using that information, either add the basicMarquee
-modifier to the modifier chain or return the current modifier chain without any additional ones. Here's the code to do so:
// MarqueeScreen.kt
@Composable
fun Modifier.safeMarquee(): Modifier {
val animationsRemoved = LocalRemoveAnimations.current.isEnabled()
return if (animationsRemoved)
this
else
this then basicMarquee()
}
With these code changes, we get the desired result. Here's a video showing a preview of a Text
-component with the safeMarquee
-modifier:
Wrapping Up
In this blog post, we've looked into how to make the basicMarquee
modifier more accessible for users with the "Remove animations"-setting turned on. We checked the value of this setting, stored it as static composition local, and then used it to decide if we add the basicMarquee
to the element.
Do you use the "Remove animations"-setting? Or have you encountered problems with it as a user or developer?
Top comments (0)