1 /*
<lambda>null2 * Copyright 2022 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 * https://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 @file:Suppress("DEPRECATION")
18
19 package com.google.accompanist.themeadapter.core
20
21 import android.annotation.SuppressLint
22 import android.content.Context
23 import android.content.res.Resources
24 import android.content.res.TypedArray
25 import android.graphics.Typeface
26 import android.os.Build
27 import android.util.TypedValue
28 import androidx.annotation.RequiresApi
29 import androidx.annotation.StyleRes
30 import androidx.compose.foundation.shape.CornerBasedShape
31 import androidx.compose.foundation.shape.CornerSize
32 import androidx.compose.foundation.shape.CutCornerShape
33 import androidx.compose.foundation.shape.RoundedCornerShape
34 import androidx.compose.ui.geometry.Offset
35 import androidx.compose.ui.graphics.Color
36 import androidx.compose.ui.graphics.Shadow
37 import androidx.compose.ui.text.TextStyle
38 import androidx.compose.ui.text.font.Font
39 import androidx.compose.ui.text.font.FontFamily
40 import androidx.compose.ui.text.font.FontStyle
41 import androidx.compose.ui.text.font.FontWeight
42 import androidx.compose.ui.text.font.toFontFamily
43 import androidx.compose.ui.unit.Density
44 import androidx.compose.ui.unit.LayoutDirection
45 import androidx.compose.ui.unit.TextUnit
46 import androidx.compose.ui.unit.dp
47 import androidx.compose.ui.unit.em
48 import androidx.compose.ui.unit.sp
49 import androidx.core.content.res.FontResourcesParserCompat
50 import androidx.core.content.res.getColorOrThrow
51 import androidx.core.content.res.use
52 import kotlin.concurrent.getOrSet
53
54 /**
55 * Returns the given index as a [Color], or [fallbackColor] if the value can't be coerced to a
56 * [Color].
57 *
58 * @param index Index of attribute to retrieve.
59 * @param fallbackColor Value to return if the attribute is not defined or can't be coerced to a
60 * [Color].
61 */
62 @Deprecated(
63 """
64 ThemeAdapter is deprecated.
65 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
66 """
67 )
68 public fun TypedArray.parseColor(
69 index: Int,
70 fallbackColor: Color = Color.Unspecified
71 ): Color = if (hasValue(index)) Color(getColorOrThrow(index)) else fallbackColor
72
73 /**
74 * Returns the given style resource ID as a [TextStyle].
75 *
76 * @param context The current context.
77 * @param id ID of style resource to retrieve.
78 * @param density The current display density.
79 * @param setTextColors Whether to read and set text colors from the style. Defaults to `false`.
80 * @param defaultFontFamily Optional default font family to use in [TextStyle]s.
81 */
82 @Deprecated(
83 """
84 ThemeAdapter is deprecated.
85 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
86 """
87 )
88 public fun parseTextAppearance(
89 context: Context,
90 @StyleRes id: Int,
91 density: Density,
92 setTextColors: Boolean,
93 defaultFontFamily: FontFamily?
94 ): TextStyle {
95 return context.obtainStyledAttributes(id, R.styleable.ThemeAdapterTextAppearance).use { a ->
96 val textStyle = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_textStyle, -1)
97 val textFontWeight = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_textFontWeight, -1)
98 val typeface = a.getInt(R.styleable.ThemeAdapterTextAppearance_android_typeface, -1)
99
100 // TODO read and expand android:fontVariationSettings.
101 // Variable fonts are not supported in Compose yet
102
103 // FYI, this only works with static font files in assets
104 val fontFamily: FontFamilyWithWeight? = a.parseFontFamily(
105 R.styleable.ThemeAdapterTextAppearance_fontFamily
106 ) ?: a.parseFontFamily(R.styleable.ThemeAdapterTextAppearance_android_fontFamily)
107
108 TextStyle(
109 color = when {
110 setTextColors -> {
111 a.parseColor(R.styleable.ThemeAdapterTextAppearance_android_textColor)
112 }
113 else -> Color.Unspecified
114 },
115 fontSize = a.parseTextUnit(R.styleable.ThemeAdapterTextAppearance_android_textSize, density),
116 lineHeight = run {
117 a.parseTextUnit(
118 R.styleable.ThemeAdapterTextAppearance_lineHeight, density,
119 fallbackTextUnit = a.parseTextUnit(
120 R.styleable.ThemeAdapterTextAppearance_android_lineHeight, density
121 )
122 )
123 },
124 fontFamily = when {
125 defaultFontFamily != null -> defaultFontFamily
126 fontFamily != null -> fontFamily.fontFamily
127 // Values below are from frameworks/base attrs.xml
128 typeface == 1 -> FontFamily.SansSerif
129 typeface == 2 -> FontFamily.Serif
130 typeface == 3 -> FontFamily.Monospace
131 else -> null
132 },
133 fontStyle = when {
134 (textStyle and Typeface.ITALIC) != 0 -> FontStyle.Italic
135 else -> FontStyle.Normal
136 },
137 fontWeight = when {
138 textFontWeight in 0..149 -> FontWeight.W100
139 textFontWeight in 150..249 -> FontWeight.W200
140 textFontWeight in 250..349 -> FontWeight.W300
141 textFontWeight in 350..449 -> FontWeight.W400
142 textFontWeight in 450..549 -> FontWeight.W500
143 textFontWeight in 550..649 -> FontWeight.W600
144 textFontWeight in 650..749 -> FontWeight.W700
145 textFontWeight in 750..849 -> FontWeight.W800
146 textFontWeight in 850..999 -> FontWeight.W900
147 // Else, check the text style for bold
148 (textStyle and Typeface.BOLD) != 0 -> FontWeight.Bold
149 // Else, the font family might have an implicit weight (san-serif-light, etc)
150 fontFamily != null -> fontFamily.weight
151 else -> null
152 },
153 fontFeatureSettings = a.getString(R.styleable.ThemeAdapterTextAppearance_android_fontFeatureSettings),
154 shadow = run {
155 val shadowColor = a.parseColor(R.styleable.ThemeAdapterTextAppearance_android_shadowColor)
156 if (shadowColor != Color.Unspecified) {
157 val dx = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowDx, 0f)
158 val dy = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowDy, 0f)
159 val rad = a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_shadowRadius, 0f)
160 Shadow(color = shadowColor, offset = Offset(dx, dy), blurRadius = rad)
161 } else null
162 },
163 letterSpacing = when {
164 a.hasValue(R.styleable.ThemeAdapterTextAppearance_android_letterSpacing) -> {
165 a.getFloat(R.styleable.ThemeAdapterTextAppearance_android_letterSpacing, 0f).em
166 }
167 // FIXME: Normally we'd use TextUnit.Unspecified,
168 // but this can cause a crash due to mismatched Sp and Em TextUnits
169 // https://issuetracker.google.com/issues/182881244
170 else -> 0.em
171 }
172 )
173 }
174 }
175
176 /**
177 * Returns the given index as a [FontFamilyWithWeight], or `null` if the value can't be coerced to
178 * a [FontFamilyWithWeight].
179 *
180 * @param index Index of attribute to retrieve.
181 */
182 @Deprecated(
183 """
184 ThemeAdapter is deprecated.
185 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
186 """
187 )
parseFontFamilynull188 public fun TypedArray.parseFontFamily(index: Int): FontFamilyWithWeight? {
189 val tv = tempTypedValue.getOrSet(::TypedValue)
190 if (getValue(index, tv) && tv.type == TypedValue.TYPE_STRING) {
191 return when (tv.string) {
192 "sans-serif" -> FontFamilyWithWeight(FontFamily.SansSerif)
193 "sans-serif-thin" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Thin)
194 "sans-serif-light" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Light)
195 "sans-serif-medium" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Medium)
196 "sans-serif-black" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Black)
197 "serif" -> FontFamilyWithWeight(FontFamily.Serif)
198 "cursive" -> FontFamilyWithWeight(FontFamily.Cursive)
199 "monospace" -> FontFamilyWithWeight(FontFamily.Monospace)
200 // TODO: Compose does not expose a FontFamily for all strings yet
201 else -> {
202 // If there's a resource ID and the string starts with res/,
203 // it's probably a @font resource
204 if (tv.resourceId != 0 && tv.string.startsWith("res/")) {
205 // If we're running on API 23+ and the resource is an XML, we can parse
206 // the fonts into a full FontFamily.
207 if (Build.VERSION.SDK_INT >= 23 && tv.string.endsWith(".xml")) {
208 resources.parseXmlFontFamily(tv.resourceId)?.let(::FontFamilyWithWeight)
209 } else {
210 // Otherwise we just load it as a single font
211 FontFamilyWithWeight(Font(tv.resourceId).toFontFamily())
212 }
213 } else null
214 }
215 }
216 }
217 return null
218 }
219
220 /**
221 * A lightweight class for storing a [FontFamily] and [FontWeight].
222 */
223 @Deprecated(
224 """
225 ThemeAdapter is deprecated.
226 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
227 """
228 )
229 public data class FontFamilyWithWeight(
230 val fontFamily: FontFamily,
231 val weight: FontWeight = FontWeight.Normal
232 )
233
234 /**
235 * Returns the given XML resource ID as a [FontFamily], or `null` if the value can't be coerced to
236 * a [FontFamily].
237 *
238 * @param id ID of XML resource to retrieve.
239 */
240 @Deprecated(
241 """
242 ThemeAdapter is deprecated.
243 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
244 """
245 )
246 @SuppressLint("RestrictedApi") // FontResourcesParserCompat.*
247 @RequiresApi(23)
248 // XML font families with > 1 fonts are only supported on API 23+
parseXmlFontFamilynull249 public fun Resources.parseXmlFontFamily(id: Int): FontFamily? {
250 val parser = getXml(id)
251
252 // Can't use {} since XmlResourceParser is AutoCloseable, not Closeable
253 @Suppress("ConvertTryFinallyToUseCall")
254 try {
255 val result = FontResourcesParserCompat.parse(parser, this)
256 if (result is FontResourcesParserCompat.FontFamilyFilesResourceEntry) {
257 val fonts = result.entries.map { font ->
258 Font(
259 resId = font.resourceId,
260 weight = fontWeightOf(font.weight),
261 style = if (font.isItalic) FontStyle.Italic else FontStyle.Normal
262 )
263 }
264 return FontFamily(fonts)
265 }
266 } finally {
267 parser.close()
268 }
269 return null
270 }
271
fontWeightOfnull272 private fun fontWeightOf(weight: Int): FontWeight = when (weight) {
273 in 0..149 -> FontWeight.W100
274 in 150..249 -> FontWeight.W200
275 in 250..349 -> FontWeight.W300
276 in 350..449 -> FontWeight.W400
277 in 450..549 -> FontWeight.W500
278 in 550..649 -> FontWeight.W600
279 in 650..749 -> FontWeight.W700
280 in 750..849 -> FontWeight.W800
281 in 850..999 -> FontWeight.W900
282 // Else, we use the 'normal' weight
283 else -> FontWeight.W400
284 }
285
286 /**
287 * Returns the given index as a [TextUnit], or [fallbackTextUnit] if the value can't be coerced to
288 * a [TextUnit].
289 *
290 * @param index Index of attribute to retrieve.
291 * @param density The current display density.
292 * @param fallbackTextUnit Value to return if the attribute is not defined or can't be coerced to a
293 * [TextUnit].
294 */
295 @Deprecated(
296 """
297 ThemeAdapter is deprecated.
298 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
299 """
300 )
parseTextUnitnull301 public fun TypedArray.parseTextUnit(
302 index: Int,
303 density: Density,
304 fallbackTextUnit: TextUnit = TextUnit.Unspecified
305 ): TextUnit {
306 val tv = tempTypedValue.getOrSet { TypedValue() }
307 if (getValue(index, tv) && tv.type == TypedValue.TYPE_DIMENSION) {
308 return when (tv.complexUnitCompat) {
309 // For SP values, we convert the value directly to an TextUnit.Sp
310 TypedValue.COMPLEX_UNIT_SP -> TypedValue.complexToFloat(tv.data).sp
311 // For DIP values, we convert the value to an TextUnit.Em (roughly equivalent)
312 TypedValue.COMPLEX_UNIT_DIP -> TypedValue.complexToFloat(tv.data).em
313 // For another other types, we let the TypedArray flatten to a px value, and
314 // we convert it to an Sp based on the current density
315 else -> with(density) { getDimension(index, 0f).toSp() }
316 }
317 }
318 return fallbackTextUnit
319 }
320
321 /**
322 * Returns the given style resource ID as a [CornerBasedShape], or [fallbackShape] if the value
323 * can't be coerced to a [CornerBasedShape].
324 *
325 * @param context The current context.
326 * @param id ID of style resource to retrieve.
327 * @param layoutDirection The current display layout direction.
328 * @param fallbackShape Value to return if the style resource ID is not defined or can't be coerced
329 * to a [CornerBasedShape].
330 */
331 @Deprecated(
332 """
333 ThemeAdapter is deprecated.
334 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
335 """
336 )
parseShapeAppearancenull337 public fun parseShapeAppearance(
338 context: Context,
339 @StyleRes id: Int,
340 layoutDirection: LayoutDirection,
341 fallbackShape: CornerBasedShape
342 ): CornerBasedShape {
343 return context.obtainStyledAttributes(id, R.styleable.ThemeAdapterShapeAppearance).use { a ->
344 val defaultCornerSize = a.parseCornerSize(
345 R.styleable.ThemeAdapterShapeAppearance_cornerSize
346 )
347 val cornerSizeTL = a.parseCornerSize(
348 R.styleable.ThemeAdapterShapeAppearance_cornerSizeTopLeft
349 )
350 val cornerSizeTR = a.parseCornerSize(
351 R.styleable.ThemeAdapterShapeAppearance_cornerSizeTopRight
352 )
353 val cornerSizeBL = a.parseCornerSize(
354 R.styleable.ThemeAdapterShapeAppearance_cornerSizeBottomLeft
355 )
356 val cornerSizeBR = a.parseCornerSize(
357 R.styleable.ThemeAdapterShapeAppearance_cornerSizeBottomRight
358 )
359 val isRtl = layoutDirection == LayoutDirection.Rtl
360 val cornerSizeTS = if (isRtl) cornerSizeTR else cornerSizeTL
361 val cornerSizeTE = if (isRtl) cornerSizeTL else cornerSizeTR
362 val cornerSizeBS = if (isRtl) cornerSizeBR else cornerSizeBL
363 val cornerSizeBE = if (isRtl) cornerSizeBL else cornerSizeBR
364
365 /**
366 * We do not support the individual `cornerFamilyTopLeft`, etc, since Compose only supports
367 * one corner type per shape. Therefore we only read the `cornerFamily` attribute.
368 */
369 when (a.getInt(R.styleable.ThemeAdapterShapeAppearance_cornerFamily, 0)) {
370 0 -> {
371 RoundedCornerShape(
372 topStart = cornerSizeTS ?: defaultCornerSize ?: fallbackShape.topStart,
373 topEnd = cornerSizeTE ?: defaultCornerSize ?: fallbackShape.topEnd,
374 bottomEnd = cornerSizeBE ?: defaultCornerSize ?: fallbackShape.bottomEnd,
375 bottomStart = cornerSizeBS ?: defaultCornerSize ?: fallbackShape.bottomStart
376 )
377 }
378 1 -> {
379 CutCornerShape(
380 topStart = cornerSizeTS ?: defaultCornerSize ?: fallbackShape.topStart,
381 topEnd = cornerSizeTE ?: defaultCornerSize ?: fallbackShape.topEnd,
382 bottomEnd = cornerSizeBE ?: defaultCornerSize ?: fallbackShape.bottomEnd,
383 bottomStart = cornerSizeBS ?: defaultCornerSize ?: fallbackShape.bottomStart
384 )
385 }
386 else -> throw IllegalArgumentException("Unknown cornerFamily set in ShapeAppearance")
387 }
388 }
389 }
390
391 /**
392 * Returns the given index as a [CornerSize], or `null` if the value can't be coerced to a
393 * [CornerSize].
394 *
395 * @param index Index of attribute to retrieve.
396 */
397 @Deprecated(
398 """
399 ThemeAdapter is deprecated.
400 For more migration information, please visit https://google.github.io/accompanist/themeadapter-appcompat/
401 """
402 )
parseCornerSizenull403 public fun TypedArray.parseCornerSize(index: Int): CornerSize? {
404 val tv = tempTypedValue.getOrSet { TypedValue() }
405 if (getValue(index, tv)) {
406 return when (tv.type) {
407 TypedValue.TYPE_DIMENSION -> {
408 when (tv.complexUnitCompat) {
409 // For DIP and PX values, we convert the value to the equivalent
410 TypedValue.COMPLEX_UNIT_DIP -> CornerSize(TypedValue.complexToFloat(tv.data).dp)
411 TypedValue.COMPLEX_UNIT_PX -> CornerSize(TypedValue.complexToFloat(tv.data))
412 // For another other dim types, we let the TypedArray flatten to a px value
413 else -> CornerSize(getDimensionPixelSize(index, 0))
414 }
415 }
416 TypedValue.TYPE_FRACTION -> CornerSize(tv.getFraction(1f, 1f))
417 else -> null
418 }
419 }
420 return null
421 }
422
423 /**
424 * A workaround since [TypedValue.getComplexUnit] is API 22+
425 */
426 private inline val TypedValue.complexUnitCompat
427 get() = when {
428 Build.VERSION.SDK_INT > 22 -> complexUnit
429 else -> TypedValue.COMPLEX_UNIT_MASK and (data shr TypedValue.COMPLEX_UNIT_SHIFT)
430 }
431
432 private val tempTypedValue = ThreadLocal<TypedValue>()
433