1 /*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.ui.text.font
18
19 import androidx.compose.runtime.Immutable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.text.ExperimentalTextApi
22
23 /**
24 * The interface of the font resource.
25 *
26 * @see ResourceFont
27 */
28 @Immutable
29 interface Font {
30 /**
31 * The weight of the font. The system uses this to match a font to a font request that is given
32 * in a [androidx.compose.ui.text.SpanStyle].
33 */
34 val weight: FontWeight
35
36 /**
37 * The style of the font, normal or italic. The system uses this to match a font to a font
38 * request that is given in a [androidx.compose.ui.text.SpanStyle].
39 */
40 val style: FontStyle
41
42 /** Interface used to load a font resource. */
43 @Deprecated(
44 "Replaced with FontFamily.Resolver during the introduction of async fonts, " +
45 "all usages should be replaced. Custom subclasses can be converted into a " +
46 "FontFamily.Resolver by calling createFontFamilyResolver(myFontFamilyResolver, context)"
47 )
48 interface ResourceLoader {
49 /**
50 * Loads resource represented by the [Font] object.
51 *
52 * @param font [Font] to be loaded
53 * @return platform specific typeface
54 * @throws Exception if font cannot be loaded
55 * @throws IllegalStateException if font cannot be loaded
56 */
57 @Deprecated(
58 "Replaced by FontFamily.Resolver, this method should not be called",
59 ReplaceWith("FontFamily.Resolver.resolve(font, )"),
60 )
loadnull61 fun load(font: Font): Any
62 }
63
64 /** Loading strategy for this font. */
65 val loadingStrategy: FontLoadingStrategy
66 get() = FontLoadingStrategy.Blocking
67
68 companion object {
69 /**
70 * This is the global timeout for fetching an [FontLoadingStrategy.Async] font.
71 *
72 * This defines the "loading" window for a font. After this timeout, a font load may no
73 * longer trigger text reflow and is considered "resolved."
74 *
75 * Each async font is given separate loading window and goes through these states:
76 * ```
77 * (initial) -> (loading with timeout) -> (resolved)
78 * ```
79 * - In the initial state, a fallback typeface is used to display text, which will reflow if
80 * the font successfully loads.
81 * - In the loading state, the font continues to use the fallback typeface and may cause one
82 * text reflow by finishing load. After a successful load it is considered resolved and
83 * will not cause another text reflow.
84 * - If the font fails to load by the timeout, the failure is permanent, and the font will
85 * never attempt to load again. Failure never causes text reflow.
86 *
87 * After a font is in resolved, it will never cause text reflow unless it is evicted from
88 * the font cache and re-enters initial.
89 *
90 * This timeout is not configurable, and timers are maintained globally.
91 */
92 const val MaximumAsyncTimeoutMillis = 15_000L
93 }
94 }
95
96 /** Interface used to load a font resource into a platform-specific typeface. */
97 internal interface PlatformFontLoader {
98 /**
99 * Loads the resource represented by the [Font] in a blocking manner for use in the current
100 * frame.
101 *
102 * This method may safely throw if a font fails to load, or return null.
103 *
104 * This method will be called on a UI-critical thread, however the font has been determined to
105 * be critical to the current frame display and blocking for file system reads is permitted.
106 *
107 * @param font [Font] to be loaded
108 * @return platform specific typeface, or null if not available
109 * @throws Exception subclass may optionally be thrown if font cannot be loaded
110 */
loadBlockingnull111 fun loadBlocking(font: Font): Any?
112
113 /**
114 * Loads resource represented by the [Font] object in a non-blocking manner which causes text
115 * reflow when the font resolves.
116 *
117 * This method may safely throw if the font cannot be loaded, or return null.
118 *
119 * This method will be called on a UI-critical thread and should not block the thread beyond
120 * loading local fonts from disk. Loading fonts from sources slower than the local file system
121 * such as a network access should not block the calling thread.
122 *
123 * @param font [Font] to be loaded
124 * @return platform specific typeface, or null if not available
125 * @throws Exception subclass may optionally be thrown if font cannot be loaded
126 */
127 suspend fun awaitLoad(font: Font): Any?
128
129 /**
130 * If this loader returns different results for the same [Font] than the platform default loader
131 * return a non-null object that uniquely identifies this loader for caching. This cache key
132 * will be retained in global maps, and should ensure that it does not create a memory leak.
133 *
134 * Loaders that return the same results for all fonts as the platform default may return null.
135 *
136 * This cache key ensures that [FontFamily.Resolver] can lookup cache results per-loader.
137 */
138 val cacheKey: Any?
139 }
140
141 /**
142 * Defines a font to be used while rendering text with resource ID.
143 *
144 * @sample androidx.compose.ui.text.samples.CustomFontFamilySample
145 * @param resId The resource ID of the font file in font resources. i.e. "R.font.myfont".
146 * @param weight The weight of the font. The system uses this to match a font to a font request that
147 * is given in a [androidx.compose.ui.text.TextStyle].
148 * @param style The style of the font, normal or italic. The system uses this to match a font to a
149 * font request that is given in a [androidx.compose.ui.text.TextStyle].
150 * @param loadingStrategy Load strategy for this font
151 * @see FontFamily
152 */
153 @OptIn(ExperimentalTextApi::class)
154 class ResourceFont
155 internal constructor(
156 val resId: Int,
157 override val weight: FontWeight = FontWeight.Normal,
158 override val style: FontStyle = FontStyle.Normal,
159 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
160 @ExperimentalTextApi
161 @get:ExperimentalTextApi
162 val variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
163 loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Async
164 ) : Font {
165
166 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "CanBePrimaryConstructorProperty")
167 @get:ExperimentalTextApi
168 @ExperimentalTextApi
169 override val loadingStrategy: FontLoadingStrategy = loadingStrategy
170
171 fun copy(
172 resId: Int = this.resId,
173 weight: FontWeight = this.weight,
174 style: FontStyle = this.style
175 ): ResourceFont = copy(resId, weight, style, loadingStrategy = loadingStrategy)
176
177 @ExperimentalTextApi
178 fun copy(
179 resId: Int = this.resId,
180 weight: FontWeight = this.weight,
181 style: FontStyle = this.style,
182 loadingStrategy: FontLoadingStrategy = this.loadingStrategy,
183 variationSettings: FontVariation.Settings = this.variationSettings
184 ): ResourceFont {
185 return ResourceFont(
186 resId = resId,
187 weight = weight,
188 style = style,
189 variationSettings = variationSettings,
190 loadingStrategy = loadingStrategy
191 )
192 }
193
194 override fun equals(other: Any?): Boolean {
195 if (this === other) return true
196 if (other !is ResourceFont) return false
197 if (resId != other.resId) return false
198 if (weight != other.weight) return false
199 if (style != other.style) return false
200 if (variationSettings != other.variationSettings) return false
201 if (loadingStrategy != other.loadingStrategy) return false
202 return true
203 }
204
205 override fun hashCode(): Int {
206 var result = resId
207 result = 31 * result + weight.hashCode()
208 result = 31 * result + style.hashCode()
209 result = 31 * result + loadingStrategy.hashCode()
210 result = 31 * result + variationSettings.hashCode()
211 return result
212 }
213
214 override fun toString(): String {
215 return "ResourceFont(resId=$resId, weight=$weight, style=$style, " +
216 "loadingStrategy=$loadingStrategy)"
217 }
218 }
219
220 /**
221 * Creates a Font with using resource ID.
222 *
223 * By default, this will load fonts using [FontLoadingStrategy.Blocking], which blocks the first
224 * frame they are used until the font is loaded. This is the correct behavior for small fonts
225 * available locally.
226 *
227 * @param resId The resource ID of the font file in font resources. i.e. "R.font.myfont".
228 * @param weight The weight of the font. The system uses this to match a font to a font request that
229 * is given in a [androidx.compose.ui.text.SpanStyle].
230 * @param style The style of the font, normal or italic. The system uses this to match a font to a
231 * font request that is given in a [androidx.compose.ui.text.SpanStyle].
232 *
233 * Fonts made with this factory are local fonts, and will block the first frame for loading. To
234 * allow async font loading use [Font(resId, weight, style, isLocal)][Font]
235 *
236 * @see FontFamily
237 */
238 // TODO(b/219783755): Remove this when safe after Compose 1.3
239 @Deprecated(
240 "Maintained for binary compatibility until Compose 1.3.",
241 replaceWith = ReplaceWith("Font(resId, weight, style)"),
242 DeprecationLevel.HIDDEN
243 )
244 @Stable
Fontnull245 fun Font(
246 resId: Int,
247 weight: FontWeight = FontWeight.Normal,
248 style: FontStyle = FontStyle.Normal
249 ): Font = ResourceFont(resId, weight, style, loadingStrategy = FontLoadingStrategy.Blocking)
250
251 /**
252 * Creates a Font with using resource ID.
253 *
254 * Allows control over [FontLoadingStrategy] strategy. You may supply
255 * [FontLoadingStrategy.Blocking], or [FontLoadingStrategy.OptionalLocal] for fonts that are
256 * expected on the first frame.
257 *
258 * [FontLoadingStrategy.Async], will load the font in the background and cause text reflow when
259 * loading completes. Fonts loaded from a remote source via resources should use
260 * [FontLoadingStrategy.Async].
261 *
262 * @param resId The resource ID of the font file in font resources. i.e. "R.font.myfont".
263 * @param weight The weight of the font. The system uses this to match a font to a font request that
264 * is given in a [androidx.compose.ui.text.SpanStyle].
265 * @param style The style of the font, normal or italic. The system uses this to match a font to a
266 * font request that is given in a [androidx.compose.ui.text.SpanStyle].
267 * @param loadingStrategy Load strategy for this font, may be async for async resource fonts
268 * @see FontFamily
269 */
270 @Stable
271 fun Font(
272 resId: Int,
273 weight: FontWeight = FontWeight.Normal,
274 style: FontStyle = FontStyle.Normal,
275 loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking
276 ): Font = ResourceFont(resId, weight, style, FontVariation.Settings(), loadingStrategy)
277
278 @ExperimentalTextApi
279 fun Font(
280 resId: Int,
281 weight: FontWeight = FontWeight.Normal,
282 style: FontStyle = FontStyle.Normal,
283 loadingStrategy: FontLoadingStrategy = FontLoadingStrategy.Blocking,
284 variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
285 ): Font = ResourceFont(resId, weight, style, variationSettings, loadingStrategy)
286
287 /** Create a [FontFamily] from this single [Font]. */
288 @Stable fun Font.toFontFamily() = FontFamily(this)
289