Forem

Cover image for Making basicMarquee-Modifier More Accessible
Eevis
Eevis

Posted on • Originally published at eevis.codes

Making basicMarquee-Modifier More Accessible

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 CompositionLocals:

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!")
    }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can read the value of the LocalRemoveAnimations in the components inside the app:

@Composable
fun ComponentSomewhereInTheHierarchy() {
    val removeAnimations = LocalRemoveAnimations.current.enabled
    ....
}
Enter fullscreen mode Exit fullscreen mode

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 {  

}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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?

Links in the Blog Post

Top comments (0)