diff --git a/lawnchair/src/app/lawnchair/font/FontCache.kt b/lawnchair/src/app/lawnchair/font/FontCache.kt index d561378775..7a639d24de 100644 --- a/lawnchair/src/app/lawnchair/font/FontCache.kt +++ b/lawnchair/src/app/lawnchair/font/FontCache.kt @@ -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): 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): 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) } diff --git a/lawnchair/src/app/lawnchair/font/FontManager.kt b/lawnchair/src/app/lawnchair/font/FontManager.kt index abf316911a..18651d0133 100644 --- a/lawnchair/src/app/lawnchair/font/FontManager.kt +++ b/lawnchair/src/app/lawnchair/font/FontManager.kt @@ -29,6 +29,8 @@ class FontManager @Inject constructor( private val specMap = createFontMap() + private val variableFonts = mutableMapOf() + private fun createFontMap(): Map { 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) } } diff --git a/lawnchair/src/app/lawnchair/font/GoogleSansFlexVariableFont.kt b/lawnchair/src/app/lawnchair/font/GoogleSansFlexVariableFont.kt new file mode 100644 index 0000000000..372b89c75d --- /dev/null +++ b/lawnchair/src/app/lawnchair/font/GoogleSansFlexVariableFont.kt @@ -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? { + 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, + ) + } +} diff --git a/res/values/styles.xml b/res/values/styles.xml index 64698b2210..1246547244 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -378,7 +378,9 @@ no - @@ -452,11 +455,15 @@ variable-title-medium -