
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 thisbasicMarquee
-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 thebasicMarquee
-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. CustomCompositionLocal
is one tool for that. As Android documentation describesCompositionLocal
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.ktdata classRemoveAnimations(valcontext:Context){valenabled:Boolean}valLocalRemoveAnimations=staticCompositionLocalOf<RemoveAnimations>{error("No user found!")}
Here, we're defining theLocalRemoveAnimations
asstaticCompositionLocalOf
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:
valanimationDuration=try{Settings.Global.getFloat(context.contentResolver,Settings.Global.ANIMATOR_DURATION_SCALE,1f)}catch(e:Settings.SettingNotFoundException){1f}
So, we try to read the value ofSettings.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.ktdata classRemoveAnimations(valcontext:Context,){valenabled:Booleanget()=try{Settings.Global.getFloat(context.contentResolver,Settings.Global.ANIMATOR_DURATION_SCALE,1f,)}catch(e:Settings.SettingNotFoundException){1f}==0f}
Note that for theenabled
-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.ktsetContent{valcontext=LocalContext.currentCompositionLocalProvider(LocalRemoveAnimationsprovidesRemoveAnimations(context=context,),){// App content goes here}}
Finally, we can read the value of theLocalRemoveAnimations
in the components inside the app:
@ComposablefunComponentSomewhereInTheHierarchy(){valremoveAnimations=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 viaLocalRemoveAnimations
, we can use it to adjust the text using thebasicMarquee
-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 itsafeMarquee
:
// MarqueeScreen.kt@ComposablefunModifier.safeMarquee():Modifier{}
Next, we want to read the value fromLocalRemoveAnimations
and, using that information, either add thebasicMarquee
-modifier to the modifier chain or return the current modifier chain without any additional ones. Here's the code to do so:
// MarqueeScreen.kt@ComposablefunModifier.safeMarquee():Modifier{valanimationsRemoved=LocalRemoveAnimations.current.isEnabled()returnif(animationsRemoved)thiselsethisthenbasicMarquee()}
With these code changes, we get the desired result. Here's a video showing a preview of aText
-component with thesafeMarquee
-modifier:
Wrapping Up
In this blog post, we've looked into how to make thebasicMarquee
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 thebasicMarquee
to the element.
Do you use the "Remove animations"-setting? Or have you encountered problems with it as a user or developer?
Links in the Blog Post
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse