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