-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
AndroidFont.kt
188 lines (164 loc) · 8.76 KB
/
AndroidFont.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.unciv.app
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.fonts.Font
import android.graphics.fonts.FontFamily
import android.graphics.fonts.FontStyle
import android.graphics.fonts.SystemFonts
import android.os.Build
import androidx.annotation.RequiresApi
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.unciv.ui.components.fonts.FontFamilyData
import com.unciv.ui.components.fonts.FontImplementation
import com.unciv.ui.components.fonts.FontMetricsCommon
import com.unciv.ui.components.fonts.Fonts
import com.unciv.utils.Log
import java.util.Locale
import kotlin.math.abs
import kotlin.math.ceil
class AndroidFont : FontImplementation {
private val fontList by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet()
else SystemFonts.getAvailableFonts()
}
private val paint: Paint = getPaintInstance()
private var currentFontFamily: String? = null
private fun getPaintInstance() = Paint().apply {
isAntiAlias = true
strokeWidth = 0f
setARGB(255, 255, 255, 255)
}
override fun setFontFamily(fontFamilyData: FontFamilyData, size: Int) {
paint.textSize = size.toFloat()
// Don't have to reload typeface if font-family didn't change
if (currentFontFamily == fontFamilyData.invariantName) return
currentFontFamily = fontFamilyData.invariantName
paint.typeface =
if (fontFamilyData.filePath != null) // Mod font
createTypefaceCustom(fontFamilyData.filePath!!)
else // System font
createTypefaceSystem(fontFamilyData.invariantName)
}
private fun createTypefaceSystem(name: String): Typeface {
if (name.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val font = fontList.mapNotNull {
val distanceToRegular = it.matchesFamily(name)
if (distanceToRegular == Int.MAX_VALUE) null else it to distanceToRegular
}.minByOrNull { it.second }?.first
if (font != null)
return Typeface.CustomFallbackBuilder(FontFamily.Builder(font).build())
.setSystemFallback(name).build()
}
return Typeface.create(name, Typeface.NORMAL)
}
private fun createTypefaceCustom(path: String): Typeface {
return try {
Typeface.createFromFile(Gdx.files.local(path).file())
} catch (e: Exception) {
Log.error("Failed to create typeface, falling back to default", e)
// Falling back to default
Typeface.create(Fonts.DEFAULT_FONT_FAMILY, Typeface.NORMAL)
}
}
/** Helper within the VERSION_CODES.Q gate: Evaluate a Font's desirability (lower = better) for a given family. */
@RequiresApi(Build.VERSION_CODES.Q)
private fun Font.matchesFamily(family: String): Int {
val name = file?.nameWithoutExtension ?: return Int.MAX_VALUE
if (name == family) return 0
if (!name.startsWith("$family-")) return Int.MAX_VALUE
if (style.weight == FontStyle.FONT_WEIGHT_NORMAL && style.slant == FontStyle.FONT_SLANT_UPRIGHT) return 1
return 2 +
abs(style.weight - FontStyle.FONT_WEIGHT_NORMAL) / 100 +
abs(style.slant - FontStyle.FONT_SLANT_UPRIGHT)
}
override fun getFontSize(): Int {
return paint.textSize.toInt()
}
override fun getCharPixmap(char: Char): Pixmap {
val metric = getMetrics() // Use our interpretation instead of paint.fontMetrics because it fixes some bad metrics
var width = paint.measureText(char.toString()).toInt()
var height = ceil(metric.height).toInt()
if (width == 0) {
height = getFontSize()
width = height
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawText(char.toString(), 0f, metric.leading + metric.ascent + 1f, paint)
val pixmap = Pixmap(width, height, Pixmap.Format.RGBA8888)
val data = IntArray(width * height)
bitmap.getPixels(data, 0, width, 0, 0, width, height) // faster than bitmap[x, y]
for (x in 0 until width) {
for (y in 0 until height) {
pixmap.drawPixel(x, y, Integer.rotateLeft(data[x + (y * width)], 8))
}
}
bitmap.recycle()
return pixmap
}
override fun getSystemFonts(): Sequence<FontFamilyData> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
return sequenceOf(FontFamilyData("sans-serif"), FontFamilyData("serif"), FontFamilyData("mono"))
fun String.stripFromFirstDash(): String {
val dashPos = indexOf('-')
if (dashPos < 0) return this
return this.substring(0, dashPos)
}
// To get _all_ Languages a user has in their Android settings, we would need more help
// from the launcher: (Activity).resources.configuration.locales
val languageTag = Locale.getDefault().toLanguageTag() // e.g. he-IL, corresponds to the _first_ Language in Android settings
val supportedLocales = arrayOf(languageTag, "en-US")
val supportedLanguages = supportedLocales.map { it.take(2) }
return fontList.asSequence()
.mapNotNull {
if (it.file == null) return@mapNotNull null
val fontLocale = it.localeList.getFirstMatch(supportedLocales)
val fontScriptToLanguage = fontLocale?.script?.take(2)?.lowercase()
// The font localeList contains locales that have nothing to do with the system locales
// their language and country fields are empty - so **guess** that the first two letters
// of their Script (coming in at 4 chars) corresponds to the first two of the default Locale toLanguageTag:
if (!it.localeList.isEmpty && fontScriptToLanguage !in supportedLanguages)
return@mapNotNull null
// The API talks about FontFamily, but I see no methods to ask for the family of a Font instance.
// No displayName either. So, again, infer from the file name:
it.file!!.nameWithoutExtension.stripFromFirstDash()
}.distinct()
.map { FontFamilyData(it, it) }
}
override fun getMetrics(): FontMetricsCommon {
val ascent = -paint.fontMetrics.ascent // invert to get distance
val descent = paint.fontMetrics.descent
val top = -paint.fontMetrics.top // invert to get distance
val bottom = paint.fontMetrics.bottom
val height = top + bottom
val leading = top - ascent
val ascentDescentHeight = ascent + descent
// Corrections: Some Android fonts report bullshit
// Examples: "ComingSoon" and "NotoSansSymbols" fonts on Android "S"
// See https://github.com/yairm210/Unciv/issues/10308
// Hardcode values for the worst of them - I've seen NotoSansSymbols returning (42.4, 11.1, 68.1, 11.1),
// or (53.4, 14.6, 117.7, 28.8): looks off, or (53.4, 14.6, 53.4, -11.1) - top below ascent
// By discarding and re-instantiating our Paint instance on every setFontFamily you can get Noto to almost,
// but not quite, report exclusively the "off" metrics. Curiously, soft start (Unciv exited through its own dialog,
// but not removed from android's "recents list") or hard start (dev-tools kill, reinstall or swipe out of recents)
// do seem to have an effect on the metrics reported by Noto (and only by Noto), though I haven't been able to
// prove a reliable pattern. These hardcoded values are empiric.
if (currentFontFamily == "NotoSansSymbols")
return FontMetricsCommon(53.4f, 14.6f, 100f, 33f)
if (height >= 1.02f * ascentDescentHeight)
// maximum dimensions at least 2% larger than recommended ascender top to descender bottom distance:
// looks sensible, keep those metrics
return FontMetricsCommon(ascent, descent, height, leading)
// When recommended size equals maximum size...
if (height >= 0.98f * ascentDescentHeight)
// "ComingSoon" reports top==ascent and bottom==descent: give it some room.
// Note: It still looks way off with all virtual leading at the top, but that's unavoidable -
// it **really has** those monster descenders (the 'y') and you gotta put them somewhere.
return FontMetricsCommon(ascent, descent, ascentDescentHeight * 1.25f, ascentDescentHeight * 0.25f)
// recommended size bigger than maximum size - O|O - swap inner and outer metrics
return FontMetricsCommon(top, bottom, ascentDescentHeight, -leading)
}
}