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:
1 parent
0521b1e5b1
commit
c75a0a116e
4 files changed
+117
-10
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user