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