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:JvmName("Mdc3Theme")
18 @file:Suppress("DEPRECATION")
19
20 package com.google.accompanist.themeadapter.material3
21
22 import android.content.Context
23 import android.content.res.Resources
24 import androidx.compose.material3.ColorScheme
25 import androidx.compose.material3.LocalContentColor
26 import androidx.compose.material3.MaterialTheme
27 import androidx.compose.material3.Shapes
28 import androidx.compose.material3.Typography
29 import androidx.compose.material3.darkColorScheme
30 import androidx.compose.material3.lightColorScheme
31 import androidx.compose.runtime.Composable
32 import androidx.compose.runtime.CompositionLocalProvider
33 import androidx.compose.runtime.remember
34 import androidx.compose.ui.platform.LocalContext
35 import androidx.compose.ui.platform.LocalLayoutDirection
36 import androidx.compose.ui.text.TextStyle
37 import androidx.compose.ui.unit.Density
38 import androidx.compose.ui.unit.LayoutDirection
39 import androidx.core.content.res.getResourceIdOrThrow
40 import androidx.core.content.res.use
41 import com.google.accompanist.themeadapter.core.FontFamilyWithWeight
42 import com.google.accompanist.themeadapter.core.parseColor
43 import com.google.accompanist.themeadapter.core.parseFontFamily
44 import com.google.accompanist.themeadapter.core.parseShapeAppearance
45 import com.google.accompanist.themeadapter.core.parseTextAppearance
46 import java.lang.reflect.Method
47
48 /**
49 * A [MaterialTheme] which reads the corresponding values from a Material Components for Android
50 * theme in the given [context].
51 *
52 * By default the text colors from any associated `TextAppearance`s from the theme are *not* read.
53 * This is because setting a fixed color in the resulting [TextStyle] breaks the usage of
54 * [androidx.compose.material.ContentAlpha] through [androidx.compose.material.LocalContentAlpha].
55 * You can customize this through the [setTextColors] parameter.
56 *
57 * @param context The context to read the theme from.
58 * @param readColorScheme whether the read the MDC color palette from the [context]'s theme.
59 * If `false`, the current value of [MaterialTheme.colorScheme] is preserved.
60 * @param readTypography whether the read the MDC text appearances from [context]'s theme.
61 * If `false`, the current value of [MaterialTheme.typography] is preserved.
62 * @param readShapes whether the read the MDC shape appearances from the [context]'s theme.
63 * If `false`, the current value of [MaterialTheme.shapes] is preserved.
64 * @param setTextColors whether to read the colors from the `TextAppearance`s associated from the
65 * theme. Defaults to `false`.
66 * @param setDefaultFontFamily whether to read and prioritize the `fontFamily` attributes from
67 * [context]'s theme, over any specified in the MDC text appearances. Defaults to `false`.
68 */
69 @Deprecated(
70 """
71 Material ThemeAdapter is deprecated.
72 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
73 """
74 )
75 @Composable
76 public fun Mdc3Theme(
77 context: Context = LocalContext.current,
78 readColorScheme: Boolean = true,
79 readTypography: Boolean = true,
80 readShapes: Boolean = true,
81 setTextColors: Boolean = false,
82 setDefaultFontFamily: Boolean = false,
83 content: @Composable () -> Unit
84 ) {
85 // We try and use the theme key value if available, which should be a perfect key for caching
86 // and avoid the expensive theme lookups in re-compositions.
87 //
88 // If the key is not available, we use the Theme itself as a rough approximation. Using the
89 // Theme instance as the key is not perfect, but it should work for 90% of cases.
90 // It falls down when the theme is manually mutated after a composition has happened
91 // (via `applyStyle()`, `rebase()`, `setTo()`), but the majority of apps do not use those.
92 val key = context.theme.key ?: context.theme
93
94 val layoutDirection = LocalLayoutDirection.current
95
96 val themeParams = remember(key) {
97 createMdc3Theme(
98 context = context,
99 layoutDirection = layoutDirection,
100 readColorScheme = readColorScheme,
101 readTypography = readTypography,
102 readShapes = readShapes,
103 setTextColors = setTextColors,
104 setDefaultFontFamily = setDefaultFontFamily
105 )
106 }
107
108 MaterialTheme(
109 colorScheme = themeParams.colorScheme ?: MaterialTheme.colorScheme,
110 typography = themeParams.typography ?: MaterialTheme.typography,
111 shapes = themeParams.shapes ?: MaterialTheme.shapes
112 ) {
113 // We update the LocalContentColor to match our onBackground. This allows the default
114 // content color to be more appropriate to the theme background
115 CompositionLocalProvider(
116 LocalContentColor provides MaterialTheme.colorScheme.onBackground,
117 content = content
118 )
119 }
120 }
121
122 /**
123 * This class contains the individual components of a [MaterialTheme]: [ColorScheme] and
124 * [Typography].
125 */
126 @Deprecated(
127 """
128 Material ThemeAdapter is deprecated.
129 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
130 """
131 )
132 public data class Theme3Parameters(
133 val colorScheme: ColorScheme?,
134 val typography: Typography?,
135 val shapes: Shapes?
136 )
137
138 /**
139 * This function creates the components of a [androidx.compose.material.MaterialTheme], reading the
140 * values from an Material Components for Android theme.
141 *
142 * By default the text colors from any associated `TextAppearance`s from the theme are *not* read.
143 * This is because setting a fixed color in the resulting [TextStyle] breaks the usage of
144 * [androidx.compose.material.ContentAlpha] through [androidx.compose.material.LocalContentAlpha].
145 * You can customize this through the [setTextColors] parameter.
146 *
147 * For [Shapes], the [layoutDirection] is taken into account when reading corner sizes of
148 * `ShapeAppearance`s from the theme. For example, [Shapes.medium.topStart] will be read from
149 * `cornerSizeTopLeft` for [LayoutDirection.Ltr] and `cornerSizeTopRight` for [LayoutDirection.Rtl].
150 *
151 * The individual components of the returned [Theme3Parameters] may be `null`, depending on the
152 * matching 'read' parameter. For example, if you set [readColorScheme] to `false`,
153 * [Theme3Parameters.colors] will be null.
154 *
155 * @param context The context to read the theme from.
156 * @param layoutDirection The layout direction to be used when reading shapes.
157 * @param density The current density.
158 * @param readColorScheme whether the read the MDC color palette from the [context]'s theme.
159 * @param readTypography whether the read the MDC text appearances from [context]'s theme.
160 * @param readShapes whether the read the MDC shape appearances from the [context]'s theme.
161 * @param setTextColors whether to read the colors from the `TextAppearance`s associated from the
162 * theme. Defaults to `false`.
163 * @param setDefaultFontFamily whether to read and prioritize the `fontFamily` attributes from
164 * [context]'s theme, over any specified in the MDC text appearances. Defaults to `false`.
165 * @return [Theme3Parameters] instance containing the resulting [ColorScheme] and [Typography].
166 */
167 @Deprecated(
168 """
169 Material ThemeAdapter is deprecated.
170 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
171 """
172 )
createMdc3Themenull173 public fun createMdc3Theme(
174 context: Context,
175 layoutDirection: LayoutDirection,
176 density: Density = Density(context),
177 readColorScheme: Boolean = true,
178 readTypography: Boolean = true,
179 readShapes: Boolean = true,
180 setTextColors: Boolean = false,
181 setDefaultFontFamily: Boolean = false
182 ): Theme3Parameters {
183 return context.obtainStyledAttributes(R.styleable.ThemeAdapterMaterial3Theme).use { ta ->
184 require(ta.hasValue(R.styleable.ThemeAdapterMaterial3Theme_isMaterial3Theme)) {
185 "createMdc3Theme requires the host context's theme to extend Theme.Material3"
186 }
187
188 val colorScheme: ColorScheme? = if (readColorScheme) {
189 /* First we'll read the Material 3 color palette */
190 val primary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorPrimary)
191 val onPrimary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnPrimary)
192 val primaryInverse = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorPrimaryInverse)
193 val primaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorPrimaryContainer)
194 val onPrimaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnPrimaryContainer)
195 val secondary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorSecondary)
196 val onSecondary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnSecondary)
197 val secondaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorSecondaryContainer)
198 val onSecondaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnSecondaryContainer)
199 val tertiary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorTertiary)
200 val onTertiary = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnTertiary)
201 val tertiaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorTertiaryContainer)
202 val onTertiaryContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnTertiaryContainer)
203 val background = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_android_colorBackground)
204 val onBackground = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnBackground)
205 val surface = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorSurface)
206 val onSurface = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnSurface)
207 val surfaceVariant = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorSurfaceVariant)
208 val onSurfaceVariant = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnSurfaceVariant)
209 val elevationOverlay = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_elevationOverlayColor)
210 val surfaceInverse = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorSurfaceInverse)
211 val onSurfaceInverse = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnSurfaceInverse)
212 val outline = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOutline)
213 val outlineVariant = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOutlineVariant)
214 val error = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorError)
215 val onError = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnError)
216 val errorContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorErrorContainer)
217 val onErrorContainer = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_colorOnErrorContainer)
218 val scrimBackground = ta.parseColor(R.styleable.ThemeAdapterMaterial3Theme_scrimBackground)
219
220 val isLightTheme = ta.getBoolean(R.styleable.ThemeAdapterMaterial3Theme_isLightTheme, true)
221
222 if (isLightTheme) {
223 lightColorScheme(
224 primary = primary,
225 onPrimary = onPrimary,
226 inversePrimary = primaryInverse,
227 primaryContainer = primaryContainer,
228 onPrimaryContainer = onPrimaryContainer,
229 secondary = secondary,
230 onSecondary = onSecondary,
231 secondaryContainer = secondaryContainer,
232 onSecondaryContainer = onSecondaryContainer,
233 tertiary = tertiary,
234 onTertiary = onTertiary,
235 tertiaryContainer = tertiaryContainer,
236 onTertiaryContainer = onTertiaryContainer,
237 background = background,
238 onBackground = onBackground,
239 surface = surface,
240 onSurface = onSurface,
241 surfaceVariant = surfaceVariant,
242 onSurfaceVariant = onSurfaceVariant,
243 surfaceTint = elevationOverlay,
244 inverseSurface = surfaceInverse,
245 inverseOnSurface = onSurfaceInverse,
246 outline = outline,
247 outlineVariant = outlineVariant,
248 error = error,
249 onError = onError,
250 errorContainer = errorContainer,
251 onErrorContainer = onErrorContainer,
252 scrim = scrimBackground
253 )
254 } else {
255 darkColorScheme(
256 primary = primary,
257 onPrimary = onPrimary,
258 inversePrimary = primaryInverse,
259 primaryContainer = primaryContainer,
260 onPrimaryContainer = onPrimaryContainer,
261 secondary = secondary,
262 onSecondary = onSecondary,
263 secondaryContainer = secondaryContainer,
264 onSecondaryContainer = onSecondaryContainer,
265 tertiary = tertiary,
266 onTertiary = onTertiary,
267 tertiaryContainer = tertiaryContainer,
268 onTertiaryContainer = onTertiaryContainer,
269 background = background,
270 onBackground = onBackground,
271 surface = surface,
272 onSurface = onSurface,
273 surfaceVariant = surfaceVariant,
274 onSurfaceVariant = onSurfaceVariant,
275 surfaceTint = elevationOverlay,
276 inverseSurface = surfaceInverse,
277 inverseOnSurface = onSurfaceInverse,
278 outline = outline,
279 outlineVariant = outlineVariant,
280 error = error,
281 onError = onError,
282 errorContainer = errorContainer,
283 onErrorContainer = onErrorContainer,
284 scrim = scrimBackground
285 )
286 }
287 } else null
288
289 /**
290 * Next we'll create a typography instance, using the Material Theme text appearances
291 * for TextStyles.
292 *
293 * We create a normal 'empty' instance first to start from the defaults, then merge in our
294 * created text styles from the Android theme.
295 */
296
297 val typography = if (readTypography) {
298 val defaultFontFamily = if (setDefaultFontFamily) {
299 val defaultFontFamilyWithWeight: FontFamilyWithWeight? = ta.parseFontFamily(
300 R.styleable.ThemeAdapterMaterial3Theme_fontFamily
301 ) ?: ta.parseFontFamily(R.styleable.ThemeAdapterMaterial3Theme_android_fontFamily)
302 defaultFontFamilyWithWeight?.fontFamily
303 } else {
304 null
305 }
306 Typography(
307 displayLarge = parseTextAppearance(
308 context,
309 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceDisplayLarge),
310 density,
311 setTextColors,
312 defaultFontFamily
313 ),
314 displayMedium = parseTextAppearance(
315 context,
316 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceDisplayMedium),
317 density,
318 setTextColors,
319 defaultFontFamily
320 ),
321 displaySmall = parseTextAppearance(
322 context,
323 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceDisplaySmall),
324 density,
325 setTextColors,
326 defaultFontFamily
327 ),
328 headlineLarge = parseTextAppearance(
329 context,
330 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceHeadlineLarge),
331 density,
332 setTextColors,
333 defaultFontFamily
334 ),
335 headlineMedium = parseTextAppearance(
336 context,
337 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceHeadlineMedium),
338 density,
339 setTextColors,
340 defaultFontFamily
341 ),
342 headlineSmall = parseTextAppearance(
343 context,
344 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceHeadlineSmall),
345 density,
346 setTextColors,
347 defaultFontFamily
348 ),
349 titleLarge = parseTextAppearance(
350 context,
351 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceTitleLarge),
352 density,
353 setTextColors,
354 defaultFontFamily
355 ),
356 titleMedium = parseTextAppearance(
357 context,
358 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceTitleMedium),
359 density,
360 setTextColors,
361 defaultFontFamily
362 ),
363 titleSmall = parseTextAppearance(
364 context,
365 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceTitleSmall),
366 density,
367 setTextColors,
368 defaultFontFamily
369 ),
370 bodyLarge = parseTextAppearance(
371 context,
372 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceBodyLarge),
373 density,
374 setTextColors,
375 defaultFontFamily
376 ),
377 bodyMedium = parseTextAppearance(
378 context,
379 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceBodyMedium),
380 density,
381 setTextColors,
382 defaultFontFamily
383 ),
384 bodySmall = parseTextAppearance(
385 context,
386 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceBodySmall),
387 density,
388 setTextColors,
389 defaultFontFamily
390 ),
391 labelLarge = parseTextAppearance(
392 context,
393 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceLabelLarge),
394 density,
395 setTextColors,
396 defaultFontFamily
397 ),
398 labelMedium = parseTextAppearance(
399 context,
400 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceLabelMedium),
401 density,
402 setTextColors,
403 defaultFontFamily
404 ),
405 labelSmall = parseTextAppearance(
406 context,
407 ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_textAppearanceLabelSmall),
408 density,
409 setTextColors,
410 defaultFontFamily
411 ),
412 )
413 } else null
414
415 /**
416 * Now read the shape appearances, taking into account the layout direction.
417 */
418 val shapes = if (readShapes) {
419 Shapes(
420 extraSmall = parseShapeAppearance(
421 context = context,
422 id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_shapeAppearanceCornerExtraSmall),
423 layoutDirection = layoutDirection,
424 fallbackShape = emptyShapes.extraSmall
425 ),
426 small = parseShapeAppearance(
427 context = context,
428 id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_shapeAppearanceCornerSmall),
429 layoutDirection = layoutDirection,
430 fallbackShape = emptyShapes.small
431 ),
432 medium = parseShapeAppearance(
433 context = context,
434 id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_shapeAppearanceCornerMedium),
435 layoutDirection = layoutDirection,
436 fallbackShape = emptyShapes.medium
437 ),
438 large = parseShapeAppearance(
439 context = context,
440 id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_shapeAppearanceCornerLarge),
441 layoutDirection = layoutDirection,
442 fallbackShape = emptyShapes.large
443 ),
444 extraLarge = parseShapeAppearance(
445 context = context,
446 id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterial3Theme_shapeAppearanceCornerExtraLarge),
447 layoutDirection = layoutDirection,
448 fallbackShape = emptyShapes.extraLarge
449 )
450 )
451 } else null
452
453 Theme3Parameters(colorScheme, typography, shapes)
454 }
455 }
456
457 private val emptyShapes = Shapes()
458
459 /**
460 * This is gross, but we need a way to check for theme equality. Theme does not implement
461 * `equals()` or `hashCode()`, but it does have a hidden method called `getKey()`.
462 *
463 * The cost of this reflective invoke is a lot cheaper than the full theme read which can
464 * happen on each re-composition.
465 */
466 private inline val Resources.Theme.key: Any?
467 get() {
468 if (!sThemeGetKeyMethodFetched) {
469 try {
470 @Suppress("SoonBlockedPrivateApi")
471 sThemeGetKeyMethod = Resources.Theme::class.java.getDeclaredMethod("getKey")
<lambda>null472 .apply { isAccessible = true }
473 } catch (e: ReflectiveOperationException) {
474 // Failed to retrieve Theme.getKey method
475 }
476 sThemeGetKeyMethodFetched = true
477 }
478 if (sThemeGetKeyMethod != null) {
479 return try {
480 sThemeGetKeyMethod?.invoke(this)
481 } catch (e: ReflectiveOperationException) {
482 // Failed to invoke Theme.getKey()
483 }
484 }
485 return null
486 }
487
488 private var sThemeGetKeyMethodFetched = false
489 private var sThemeGetKeyMethod: Method? = null
490