androidspannablestringandroid-snackbarsnackbarspannablestringbuilder

Why does adding an ImageSpan to a Snackbar's action text work on Android devices SDK level 26 but not on SDK level 25?


I want to show a Snackbar and use an image instead of text for the action.

I use the following code:

    val imageSpan = ImageSpan(this, R.drawable.star)
    val builder = SpannableStringBuilder(" ")
    builder.setSpan(
        imageSpan,
        0,
        1,
        SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    Snackbar.make(findViewById(R.id.container), "Hello Snackbar", Snackbar.LENGTH_INDEFINITE)
        .setAction(builder) {}.show()

drawable_star being a vector graphic asset, but the same happens with a png.

On an Android device lvl 26 and above this yields:

pic1

as expected, whereas on device lvl 25 the image is not visible:

pic2

Does someone know the reason for this and if there a workaround?

PS: You can check out my test project here: https://github.com/fmweigl/SpannableTest


Solution

  • That is due to the textAllCaps bug on Android versions prior to Oreo (API level 26). That Button's default style sets that attribute to true, which causes its text to be converted to all uppercase.

    That conversion is done with the platform AllCapsTransformationMethod class which, on Nougat 7.1 and below, treats everything as flat Strings, essentially stripping any formatting spans you may have set. The fix is to turn that attribute off and handle any necessary uppercase conversions yourself in code.

    Snackbar offers the snackbarButtonStyle theme attribute as a means to style its action Button, and we can create a simple <style> to override textAllCaps with false. For example, here's the linked project's styles.xml updated:

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    
        <item name="snackbarButtonStyle">@style/NoCapsButton</item>>
    </style>
    
    <style name="NoCapsButton" parent="Widget.AppCompat.Button">
        <item name="textAllCaps">false</item>
    </style>
    

    If you're using a Material Components theme, the parent for NoCapsButton should instead be Widget.MaterialComponents.Button.TextButton.Snackbar.

    In this specific case, that's all you need to do since there's no text to convert.