fix: Font not updated to expressive variant (#6911)

* fix: Font not updated to expressive variant

Change-Id: 6a36a1d751f327b639ae825d
Change-Summary: Font has not been updated to expressive variant in Launcher3
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>

* fix: Add this file to version control

Change-Id: 6a36a1d751f327b639ae825d
Change-Summary: Font has not been updated to expressive variant in Launcher3
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>

* chore: Apply suggestions

Change-Id: 6a36a1d751f327b639ae825d
Change-Summary: Font has not been updated to expressive variant in Launcher3
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>

---------

Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
This commit is contained in:
Pun Butrach authored and GitHub committed 2026-06-22 21:17:31 +07:00
1 parent 0521b1e5b1
commit c75a0a116e
4 files changed
+117 -10

No files matched your search

+29 -4
View File
@@ -120,6 +120,18 @@ class FontCache @Inject constructor(
),
)
/**
* A Google Sans Flex [ResourceFont] with the given variation [axes]. Used to back the AOSP
* Material 3 Expressive `variable-*` font family names, mirroring
* [app.lawnchair.ui.theme.GoogleSansFlex].
*/
fun googleSansFlexVariable(axes: Map<String, Float>): ResourceFont = ResourceFont(
context,
R.font.googlesansflex_variable,
"Google Sans Flex",
axes,
)
suspend fun getTypeface(font: Font): Typeface? {
return loadFontAsync(font).await()?.typeface
}
@@ -432,16 +444,29 @@ class FontCache @Inject constructor(
}
companion object {
private val extractionLock = Any()
private fun createTypeface(context: Context, resId: Int, axes: Map<String, Float>): Typeface? {
if (axes.isEmpty()) {
return ResourcesCompat.getFont(context, resId)
}
return try {
context.resources.openRawResourceFd(resId).use { pfd ->
Typeface.Builder(pfd.fileDescriptor)
.setFontVariationSettings(FontAxes.mapToString(axes))
.build()
// Our locally stored Google Sans Flex font is stored compressed (DEFLATED)
// in the APK, so openRawResourceFd() throws and the variation settings would
// be silently dropped, leaving the font at its default (non-expressive) axes.
val cacheFile = File(context.cacheDir, "font_res_$resId.ttf")
synchronized(extractionLock) {
if (!cacheFile.exists() || cacheFile.length() == 0L) {
val tmpFile = File(context.cacheDir, "${cacheFile.name}.tmp")
context.resources.openRawResource(resId).use { input ->
tmpFile.outputStream().use { output -> input.copyTo(output) }
}
tmpFile.renameTo(cacheFile)
}
}
Typeface.Builder(cacheFile)
.setFontVariationSettings(FontAxes.mapToString(axes))
.build()
} catch (e: Exception) {
ResourcesCompat.getFont(context, resId)
}
@@ -29,6 +29,8 @@ class FontManager @Inject constructor(
private val specMap = createFontMap()
private val variableFonts = mutableMapOf<String, FontCache.Font>()
private fun createFontMap(): Map<Int, FontSpec> {
val sansSerif = Typeface.SANS_SERIF
val sansSerifMedium = Typeface.create("sans-serif-medium", Typeface.NORMAL)
@@ -50,6 +52,7 @@ class FontManager @Inject constructor(
var fontType = -1
var fontWeight = -1
var ap = -1
var fontFamily: String? = null
context.obtainStyledAttributes(
attrs,
R.styleable.CustomFont,
@@ -58,6 +61,9 @@ class FontManager @Inject constructor(
fontWeight = a.getInt(R.styleable.CustomFont_customFontWeight, -1)
ap = a.getResourceId(R.styleable.CustomFont_android_textAppearance, -1)
}
context.obtainStyledAttributes(attrs, FONT_FAMILY_ATTR).use { a ->
fontFamily = a.getString(0)
}
if (ap != -1) {
context.obtainStyledAttributes(ap, R.styleable.CustomFont).use { a ->
@@ -68,6 +74,20 @@ class FontManager @Inject constructor(
fontWeight = a.getInt(R.styleable.CustomFont_customFontWeight, -1)
}
}
if (fontFamily == null) {
context.obtainStyledAttributes(ap, FONT_FAMILY_ATTR).use { a ->
fontFamily = a.getString(0)
}
}
}
val gsfAxes = GoogleSansFlexVariableFont.axesFor(fontFamily)
if (gsfAxes != null) {
val font = variableFonts.getOrPut(fontFamily!!) {
fontCache.googleSansFlexVariable(gsfAxes)
}
applyFont(textView, font)
return
}
if (fontType != -1) {
@@ -80,9 +100,13 @@ class FontManager @Inject constructor(
@JvmOverloads
fun setCustomFont(textView: TextView, @IdRes type: Int, style: Int = -1) {
val spec = specMap[type] ?: return
val lifecycleOwner = textView.context.lookupLifecycleOwner()
lifecycleOwner?.lifecycleScope?.launch {
val typeface = fontCache.getTypeface(spec.font.createWithWeight(style)) ?: spec.fallback
applyFont(textView, spec.font.createWithWeight(style), spec.fallback)
}
private fun applyFont(textView: TextView, font: FontCache.Font, fallback: Typeface? = null) {
val lifecycleOwner = textView.context.lookupLifecycleOwner() ?: return
lifecycleOwner.lifecycleScope.launch {
val typeface = fontCache.getTypeface(font) ?: fallback ?: return@launch
runOnMainThread {
textView.typeface = typeface
}
@@ -102,5 +126,7 @@ class FontManager @Inject constructor(
companion object {
@JvmField
val INSTANCE = DaggerSingletonObject(LauncherAppComponent::getFontManager)
private val FONT_FAMILY_ATTR = intArrayOf(android.R.attr.fontFamily)
}
}
@@ -0,0 +1,48 @@
package app.lawnchair.font
/**
* Resolves the AOSP variable-* names used in the launcher to the matching GSF variation axes.
*
* @see [app.lawnchair.ui.theme.GoogleSansFlex]
*/
object GoogleSansFlexVariableFont {
private const val PREFIX = "variable-"
private const val EMPHASIZED_SUFFIX = "-emphasized"
private val opticalSizes = mapOf(
"display-large" to 57f, "display-medium" to 45f, "display-small" to 36f,
"headline-large" to 32f, "headline-medium" to 28f, "headline-small" to 24f,
"title-large" to 22f, "title-medium" to 16f, "title-small" to 14f,
"body-large" to 16f, "body-medium" to 14f, "body-small" to 12f,
"label-large" to 14f, "label-medium" to 12f, "label-small" to 11f,
)
private fun weightFor(category: String, emphasized: Boolean): Int = when (category) {
"label" -> if (emphasized) 600 else 500
"title" -> 500
else -> if (emphasized) 500 else 400 // display, headline, body
}
fun isVariableFamily(name: String?): Boolean = name != null && name.startsWith(PREFIX)
/**
* Returns the GSF variation axes for AOSP `variable-*` family [name], or null
* when [name] is not a recognised role.
*/
fun axesFor(name: String?): Map<String, Float>? {
if (!isVariableFamily(name)) return null
var role = name!!.removePrefix(PREFIX)
val emphasized = role.endsWith(EMPHASIZED_SUFFIX)
if (emphasized) role = role.removeSuffix(EMPHASIZED_SUFFIX)
val opticalSize = opticalSizes[role] ?: return null
val category = role.substringBefore('-')
return mapOf(
FontAxes.WEIGHT to weightFor(category, emphasized).toFloat(),
FontAxes.WIDTH to 100f,
FontAxes.GRADE to 0f,
FontAxes.ROUNDNESS to 100f,
FontAxes.OPTICAL_SIZE to opticalSize,
)
}
}
+11 -3
View File
@@ -378,7 +378,9 @@
<item name="android:importantForAccessibility">no</item>
</style>
<style name="BaseIconRoot" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
<style name="BaseIconRoot" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
<item name="customFontType">@id/font_base_icon</item>
</style>
<style name="BaseIconUnBounded" parent="BaseIconRoot">
<item name="android:layout_width">match_parent</item>
@@ -390,6 +392,7 @@
<item name="android:defaultFocusHighlightEnabled">false</item>
<!-- No shadows in the base theme -->
<item name="android:shadowRadius">0</item>
<item name="customFontType">@id/font_base_icon</item>
</style>
<!-- Base theme for BubbleTextView and sub classes -->
@@ -452,11 +455,15 @@
<item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">variable-title-medium</item>
</style>
<style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" />
<style name="TextHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
<item name="customFontType">@id/font_body_medium</item>
</style>
<style name="PrimaryHeadline" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"/>
<style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault" />
<style name="TextTitle" parent="@android:style/TextAppearance.DeviceDefault">
<item name="customFontType">@id/font_heading</item>
</style>
<style name="Button.TopRounded.Bordered" parent="@android:style/Widget.Material.Button">
<item name="android:background">@drawable/button_top_rounded_bordered_ripple</item>
@@ -517,6 +524,7 @@
<style name="PrivateSpaceHeaderTextStyle">
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/materialColorOnSurface</item>
<item name="customFontType">@id/font_heading</item>
<item name="android:fontFamily" android:featureFlag="com.android.launcher3.gsf_res">
variable-title-medium
</item>