TextAppearanceSpan with custom font on min SDK 21 – part 2

In the previous post we created FontAwareTextAppearanceSpan. A TextAppearanceSpan descendant that can be used exactly as its parent

textView.text = buildSpannedString {
inSpans(FontAwareTextAppearanceSpan(context, R.style.AcmeText)) {
append(context.getString(R.string.le0nidas_gr))
}
}

but with the addition that it uses the font found in the provided style.

The thing is that this implementation works only in debug apks or if the project has AGP v4.1 and lower!

Optimizing resources

Android Gradle Plugin 4.2 introduced a number of resources optimizations in order to cut down the apk’s size. One of these optimization is the obfuscation/shortening of their filenames.
This means that when the span reads the family name from the style

public TextAppearanceSpan(Context context, int appearance, int colorList) {
// …
if (mTypeface != null) {
mFamilyName = null;
} else {
String family = a.getString(com.android.internal.R.styleable.TextAppearance_fontFamily);
if (family != null) {
mFamilyName = family;
}
// …
}

instead of getting something like res/font/acme_family.xml it gets res/Zx.xml which breaks completely FontAwareTextAppearanceSpan‘s getFont since it takes for granted that the resource’s name can be extracted from the aforementioned value

val cleanFamilyName = family.removePrefix("res/font/").removeSuffix(".xml")

Temporary fix

One way to fix this is by adding android.enableResourceOptimizations=false in gradle.properties. This will prevent the optimization from happening thus allowing the extraction of the resource’s name.

But, and this is a big but, this is just a temporary fix since google has announced that from AGP v8 and on the optimizations will be hard forced with no way to change that. You can see it as a message when building while using the flag:

The option setting 'android.enableResourceOptimizations=false' is deprecated.
The current default is 'true'.
It will be removed in version 8.0 of the Android Gradle plugin.

Permanent fix

Turns out that the best way to go is to provide the font’s name ourselfs. In other words there must be a duplication of information since the name already exists in the style.

We could change FontAwareTextAppearanceSpan and pass the name in its constructor but this means that the duplication takes place in many places: in the style and in every instantiation. Also the developer instead of just using the span by providing a style, she needs to open the style, figure out the font’s name and then pass it to the constructor. Manual work that is error prone.

A better approach is to have the duplicated information at the place that gets provided instead the one that gets consumed. This leaves us with the style itself:

<style name="AcmeText" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:fontFamily">@font/acme_family</item>
<item name="fontFamily">@font/acme_family</item>
<item name="android:textSize">20sp</item>
<item name="fontFamilyName">@string/acme_family</item>
</style>

where fontFamilyName is an attribute:

<attr name="fontFamilyName" format="string" />

This way the information gets duplicated once and the developer uses the span as before by just providing the style.

Ofcourse we need to change FontAwareTextAppearanceSpan so that it reads the resource’s name from the style:

class FontAwareTextAppearanceSpan(
private val context: Context,
private val appearance: Int
) : TextAppearanceSpan(context, appearance) {
private var font: Typeface? = null
override fun updateMeasureState(ds: TextPaint) {
super.updateMeasureState(ds)
val font = getFont() ?: Typeface.DEFAULT
val oldStyle = ds.typeface?.style ?: 0
ds.typeface = Typeface.create(font, oldStyle)
}
private fun getFont(): Typeface? {
if (font != null) {
return font
}
val cleanFamilyName = getFontFamilyName() ?: return null
val appPackageName = context.applicationContext.packageName
val id = context.resources.getIdentifier(cleanFamilyName, "font", appPackageName)
return getFont(context, id).also { font = it }
}
private fun getFontFamilyName(): String? {
val attrs = intArrayOf(R.attr.fontFamilyName)
val a = context.obtainStyledAttributes(appearance, attrs)
val fontFamilyName = a.getString(0)
a.recycle()
return fontFamilyName
}
}

And that is it 🙂 .

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s