How Do We Get SpannedString Objects From String Resources Using Data Binding?

Viewed 974

Florina Muntenescu wrote up a cool post about using <annotation> in string resources for being able to have flexible markup that you can process in your app using custom spans. I am trying to leverage it in data binding, but I cannot quite figure out how to get a SpannedString edition of the string resource from data binding.

In my layout, I have app:thingy="@{@string/my_annotated_string}" as an attribute on a TextView. I have a binding adapter set up to handle thingy attributes. However, the data binding system seems to insist that my value is a String.

I have tried:

@BindingAdapter("thingy")
@JvmStatic
fun handleThingy(textView: TextView, thingy: SpannedString) { /* stuff goes here */ }

and:

@BindingAdapter("thingy")
@JvmStatic
fun handleThingy(textView: TextView, thingy: Spanned) { /* stuff goes here */ }

and:

@BindingAdapter("thingy")
@JvmStatic
fun handleThingy(textView: TextView, @StringRes thingy: Int) { /* stuff goes here */ }

In all cases, I get Cannot find the setter for attribute 'app:thingy' with parameter type java.lang.String on android.widget.TextView build errors.

If I use String or CharSequence for the thingy parameter type, it builds, but then I get passed a String and I do not have my annotation spans from the string resource.

So, how can I either:

  • Get the SpannedString corresponding to my string resource (i.e., what you get from getText() instead of getString()), or
  • Get the string resource ID of my string resource, so I can call getText() myself to get my SpannedString
4 Answers

As an expression, @string/my_annotated_string evaluates to a string. Eventhough it resembles a string resource reference in XML, it's actually only a String value.

It would be nice to have a @text/my_annotated_string version as well, but as of the documentation this is not available.

Instead you'd have to use the actual resource within your binding expression:

app:thingy="@{string.my_annotated_string}"
app:thingy="@{context.getText(string.my_annotated_string)}"

This is assuming the import of the string class:

<import type="path.to.R.string"/>

Here is a maybe slightly less icky way:

Define the annotated string.

<string name="my_annotated_string">A <annotation font="title_emphasis">cool</annotation> annotation <annotation font="title_emphasis">thingy</annotation>.</string>

Place a reference to that string resource into a TypedArray:

<resources>
    <array name="annotated_text">
        <item>@string/my_annotated_string</item>
    </array>
</resources>

Reference the TypedArray in the layout:

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:thingy="@{@typedArray/annotated_text}" />

Finally, set a BindingAdapter to capture the SpannedString with the annotations:

@BindingAdapter("thingy")
public static void setThingy(TextView textView, TypedArray strings) {
    SpannedString ss = (SpannedString) strings.getText(0);
    Object spans[] = ss.getSpans(0, ss.length(), Object.class);
}

Although a little involved, this works. If there are multiple strings, the array can be expanded.

Use this binding adapter:

@BindingAdapter("thingy")
fun handleThingy(textView: TextView, @StringRes thingy: Int) { /* stuff goes here */ }

and instead of using app:thingy="@{@string/my_annotated_string}" pass resource into binding: thingy="@string/my_annotated_string" (without app:).

one can define just any data-type with <import>:

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List<String>"/>
    <variable name="sparse" type="SparseArray<String>"/>
    <variable name="map" type="Map<String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>

and then obtain the text from it:

android:text="@{sparse[index]}"

to define a thingy with data-type SpannedString (or one that extends SpannedString):

<data>
    <import type="com.acme.model.BindableSpannedString"/>
    <variable name="thingy" type="BindableSpannedString"/>
    <variable name="index" type="int"/>
</data>

the getter annotated with @Bindable should be index access:

android:text="@{thingy.getSpanAt(index)}"

SpannedString lacks the getter required to bind it, nor can one add annotations to framework classes.

Related