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("MdcTheme")
18 @file:Suppress("DEPRECATION")
19 
20 package com.google.accompanist.themeadapter.material
21 
22 import android.content.Context
23 import android.content.res.Resources
24 import android.view.View
25 import androidx.compose.material.Colors
26 import androidx.compose.material.LocalContentColor
27 import androidx.compose.material.MaterialTheme
28 import androidx.compose.material.Shapes
29 import androidx.compose.material.Typography
30 import androidx.compose.material.darkColors
31 import androidx.compose.material.lightColors
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.CompositionLocalProvider
34 import androidx.compose.runtime.remember
35 import androidx.compose.ui.platform.LocalContext
36 import androidx.compose.ui.platform.LocalLayoutDirection
37 import androidx.compose.ui.text.TextStyle
38 import androidx.compose.ui.text.font.FontFamily
39 import androidx.compose.ui.unit.Density
40 import androidx.compose.ui.unit.LayoutDirection
41 import androidx.core.content.res.getResourceIdOrThrow
42 import androidx.core.content.res.use
43 import com.google.accompanist.themeadapter.core.FontFamilyWithWeight
44 import com.google.accompanist.themeadapter.core.parseColor
45 import com.google.accompanist.themeadapter.core.parseFontFamily
46 import com.google.accompanist.themeadapter.core.parseShapeAppearance
47 import com.google.accompanist.themeadapter.core.parseTextAppearance
48 import java.lang.reflect.Method
49 
50 /**
51  * A [MaterialTheme] which reads the corresponding values from a Material Components for Android
52  * theme in the given [context].
53  *
54  * By default the text colors from any associated `TextAppearance`s from the theme are *not* read.
55  * This is because setting a fixed color in the resulting [TextStyle] breaks the usage of
56  * [androidx.compose.material.ContentAlpha] through [androidx.compose.material.LocalContentAlpha].
57  * You can customize this through the [setTextColors] parameter.
58  *
59  * For [Shapes], the configuration layout direction is taken into account when reading corner sizes
60  * of `ShapeAppearance`s from the theme. For example, [Shapes.medium.topStart] will be read from
61  * `cornerSizeTopLeft` for [View.LAYOUT_DIRECTION_LTR] and `cornerSizeTopRight` for
62  * [View.LAYOUT_DIRECTION_RTL].
63  *
64  * @param context The context to read the theme from.
65  * @param readColors whether the read the MDC color palette from the [context]'s theme.
66  * If `false`, the current value of [MaterialTheme.colors] is preserved.
67  * @param readTypography whether the read the MDC text appearances from [context]'s theme.
68  * If `false`, the current value of [MaterialTheme.typography] is preserved.
69  * @param readShapes whether the read the MDC shape appearances from the [context]'s theme.
70  * If `false`, the current value of [MaterialTheme.shapes] is preserved.
71  * @param setTextColors whether to read the colors from the `TextAppearance`s associated from the
72  * theme. Defaults to `false`.
73  * @param setDefaultFontFamily whether to read and prioritize the `fontFamily` attributes from
74  * [context]'s theme, over any specified in the MDC text appearances. Defaults to `false`.
75  */
76 @Deprecated(
77     """
78    Material ThemeAdapter is deprecated.
79 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
80 """
81 )
82 @Composable
83 public fun MdcTheme(
84     context: Context = LocalContext.current,
85     readColors: Boolean = true,
86     readTypography: Boolean = true,
87     readShapes: Boolean = true,
88     setTextColors: Boolean = false,
89     setDefaultFontFamily: Boolean = false,
90     content: @Composable () -> Unit
91 ) {
92     // We try and use the theme key value if available, which should be a perfect key for caching
93     // and avoid the expensive theme lookups in re-compositions.
94     //
95     // If the key is not available, we use the Theme itself as a rough approximation. Using the
96     // Theme instance as the key is not perfect, but it should work for 90% of cases.
97     // It falls down when the theme is manually mutated after a composition has happened
98     // (via `applyStyle()`, `rebase()`, `setTo()`), but the majority of apps do not use those.
99     val key = context.theme.key ?: context.theme
100 
101     val layoutDirection = LocalLayoutDirection.current
102 
103     val themeParams = remember(key) {
104         createMdcTheme(
105             context = context,
106             layoutDirection = layoutDirection,
107             readColors = readColors,
108             readTypography = readTypography,
109             readShapes = readShapes,
110             setTextColors = setTextColors,
111             setDefaultFontFamily = setDefaultFontFamily
112         )
113     }
114 
115     MaterialTheme(
116         colors = themeParams.colors ?: MaterialTheme.colors,
117         typography = themeParams.typography ?: MaterialTheme.typography,
118         shapes = themeParams.shapes ?: MaterialTheme.shapes,
119     ) {
120         // We update the LocalContentColor to match our onBackground. This allows the default
121         // content color to be more appropriate to the theme background
122         CompositionLocalProvider(
123             LocalContentColor provides MaterialTheme.colors.onBackground,
124             content = content
125         )
126     }
127 }
128 
129 /**
130  * This class contains the individual components of a [MaterialTheme]: [Colors], [Typography]
131  * and [Shapes].
132  */
133 @Deprecated(
134     """
135    Material ThemeAdapter is deprecated.
136 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
137 """
138 )
139 public data class ThemeParameters(
140     val colors: Colors?,
141     val typography: Typography?,
142     val shapes: Shapes?
143 )
144 
145 /**
146  * This function creates the components of a [androidx.compose.material.MaterialTheme], reading the
147  * values from an Material Components for Android theme.
148  *
149  * By default the text colors from any associated `TextAppearance`s from the theme are *not* read.
150  * This is because setting a fixed color in the resulting [TextStyle] breaks the usage of
151  * [androidx.compose.material.ContentAlpha] through [androidx.compose.material.LocalContentAlpha].
152  * You can customize this through the [setTextColors] parameter.
153  *
154  * For [Shapes], the [layoutDirection] is taken into account when reading corner sizes of
155  * `ShapeAppearance`s from the theme. For example, [Shapes.medium.topStart] will be read from
156  * `cornerSizeTopLeft` for [LayoutDirection.Ltr] and `cornerSizeTopRight` for [LayoutDirection.Rtl].
157  *
158  * The individual components of the returned [ThemeParameters] may be `null`, depending on the
159  * matching 'read' parameter. For example, if you set [readColors] to `false`,
160  * [ThemeParameters.colors] will be null.
161  *
162  * @param context The context to read the theme from.
163  * @param layoutDirection The layout direction to be used when reading shapes.
164  * @param density The current density.
165  * @param readColors whether the read the MDC color palette from the [context]'s theme.
166  * @param readTypography whether the read the MDC text appearances from [context]'s theme.
167  * @param readShapes whether the read the MDC shape appearances from the [context]'s theme.
168  * @param setTextColors whether to read the colors from the `TextAppearance`s associated from the
169  * theme. Defaults to `false`.
170  * @param setDefaultFontFamily whether to read and prioritize the `fontFamily` attributes from
171  * [context]'s theme, over any specified in the MDC text appearances. Defaults to `false`.
172  * @return [ThemeParameters] instance containing the resulting [Colors], [Typography]
173  * and [Shapes].
174  */
175 @Deprecated(
176     """
177    Material ThemeAdapter is deprecated.
178 For more migration information, please visit https://google.github.io/accompanist/themeadapter-material/
179 """
180 )
createMdcThemenull181 public fun createMdcTheme(
182     context: Context,
183     layoutDirection: LayoutDirection,
184     density: Density = Density(context),
185     readColors: Boolean = true,
186     readTypography: Boolean = true,
187     readShapes: Boolean = true,
188     setTextColors: Boolean = false,
189     setDefaultFontFamily: Boolean = false
190 ): ThemeParameters {
191     return context.obtainStyledAttributes(R.styleable.ThemeAdapterMaterialTheme).use { ta ->
192         require(ta.hasValue(R.styleable.ThemeAdapterMaterialTheme_isMaterialTheme)) {
193             "createMdcTheme requires the host context's theme" +
194                 " to extend Theme.MaterialComponents"
195         }
196 
197         val colors: Colors? = if (readColors) {
198             /* First we'll read the Material color palette */
199             val primary = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorPrimary)
200             val primaryVariant = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorPrimaryVariant)
201             val onPrimary = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorOnPrimary)
202             val secondary = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorSecondary)
203             val secondaryVariant = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorSecondaryVariant)
204             val onSecondary = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorOnSecondary)
205             val background = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_android_colorBackground)
206             val onBackground = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorOnBackground)
207             val surface = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorSurface)
208             val onSurface = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorOnSurface)
209             val error = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorError)
210             val onError = ta.parseColor(R.styleable.ThemeAdapterMaterialTheme_colorOnError)
211 
212             val isLightTheme = ta.getBoolean(R.styleable.ThemeAdapterMaterialTheme_isLightTheme, true)
213 
214             if (isLightTheme) {
215                 lightColors(
216                     primary = primary,
217                     primaryVariant = primaryVariant,
218                     onPrimary = onPrimary,
219                     secondary = secondary,
220                     secondaryVariant = secondaryVariant,
221                     onSecondary = onSecondary,
222                     background = background,
223                     onBackground = onBackground,
224                     surface = surface,
225                     onSurface = onSurface,
226                     error = error,
227                     onError = onError
228                 )
229             } else {
230                 darkColors(
231                     primary = primary,
232                     primaryVariant = primaryVariant,
233                     onPrimary = onPrimary,
234                     secondary = secondary,
235                     secondaryVariant = secondaryVariant,
236                     onSecondary = onSecondary,
237                     background = background,
238                     onBackground = onBackground,
239                     surface = surface,
240                     onSurface = onSurface,
241                     error = error,
242                     onError = onError
243                 )
244             }
245         } else null
246 
247         /**
248          * Next we'll create a typography instance, using the Material Theme text appearances
249          * for TextStyles.
250          *
251          * We create a normal 'empty' instance first to start from the defaults, then merge in our
252          * created text styles from the Android theme.
253          */
254 
255         val typography = if (readTypography) {
256             val defaultFontFamily = if (setDefaultFontFamily) {
257                 val defaultFontFamilyWithWeight: FontFamilyWithWeight? = ta.parseFontFamily(
258                     R.styleable.ThemeAdapterMaterialTheme_fontFamily
259                 ) ?: ta.parseFontFamily(R.styleable.ThemeAdapterMaterialTheme_android_fontFamily)
260                 defaultFontFamilyWithWeight?.fontFamily
261             } else {
262                 null
263             }
264             Typography(defaultFontFamily = defaultFontFamily ?: FontFamily.Default).merge(
265                 h1 = parseTextAppearance(
266                     context,
267                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline1),
268                     density,
269                     setTextColors,
270                     defaultFontFamily
271                 ),
272                 h2 = parseTextAppearance(
273                     context,
274                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline2),
275                     density,
276                     setTextColors,
277                     defaultFontFamily
278                 ),
279                 h3 = parseTextAppearance(
280                     context,
281                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline3),
282                     density,
283                     setTextColors,
284                     defaultFontFamily
285                 ),
286                 h4 = parseTextAppearance(
287                     context,
288                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline4),
289                     density,
290                     setTextColors,
291                     defaultFontFamily
292                 ),
293                 h5 = parseTextAppearance(
294                     context,
295                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline5),
296                     density,
297                     setTextColors,
298                     defaultFontFamily
299                 ),
300                 h6 = parseTextAppearance(
301                     context,
302                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceHeadline6),
303                     density,
304                     setTextColors,
305                     defaultFontFamily
306                 ),
307                 subtitle1 = parseTextAppearance(
308                     context,
309                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceSubtitle1),
310                     density,
311                     setTextColors,
312                     defaultFontFamily
313                 ),
314                 subtitle2 = parseTextAppearance(
315                     context,
316                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceSubtitle2),
317                     density,
318                     setTextColors,
319                     defaultFontFamily
320                 ),
321                 body1 = parseTextAppearance(
322                     context,
323                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceBody1),
324                     density,
325                     setTextColors,
326                     defaultFontFamily
327                 ),
328                 body2 = parseTextAppearance(
329                     context,
330                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceBody2),
331                     density,
332                     setTextColors,
333                     defaultFontFamily
334                 ),
335                 button = parseTextAppearance(
336                     context,
337                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceButton),
338                     density,
339                     setTextColors,
340                     defaultFontFamily
341                 ),
342                 caption = parseTextAppearance(
343                     context,
344                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceCaption),
345                     density,
346                     setTextColors,
347                     defaultFontFamily
348                 ),
349                 overline = parseTextAppearance(
350                     context,
351                     ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_textAppearanceOverline),
352                     density,
353                     setTextColors,
354                     defaultFontFamily
355                 )
356             )
357         } else null
358 
359         /**
360          * Now read the shape appearances, taking into account the layout direction.
361          */
362         val shapes = if (readShapes) {
363             Shapes(
364                 small = parseShapeAppearance(
365                     context = context,
366                     id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_shapeAppearanceSmallComponent),
367                     layoutDirection = layoutDirection,
368                     fallbackShape = emptyShapes.small
369                 ),
370                 medium = parseShapeAppearance(
371                     context = context,
372                     id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_shapeAppearanceMediumComponent),
373                     layoutDirection = layoutDirection,
374                     fallbackShape = emptyShapes.medium,
375                 ),
376                 large = parseShapeAppearance(
377                     context = context,
378                     id = ta.getResourceIdOrThrow(R.styleable.ThemeAdapterMaterialTheme_shapeAppearanceLargeComponent),
379                     layoutDirection = layoutDirection,
380                     fallbackShape = emptyShapes.large
381                 )
382             )
383         } else null
384 
385         ThemeParameters(colors, typography, shapes)
386     }
387 }
388 
389 private val emptyShapes = Shapes()
390 
391 /**
392  * This is gross, but we need a way to check for theme equality. Theme does not implement
393  * `equals()` or `hashCode()`, but it does have a hidden method called `getKey()`.
394  *
395  * The cost of this reflective invoke is a lot cheaper than the full theme read which can
396  * happen on each re-composition.
397  */
398 private inline val Resources.Theme.key: Any?
399     get() {
400         if (!sThemeGetKeyMethodFetched) {
401             try {
402                 @Suppress("SoonBlockedPrivateApi")
403                 sThemeGetKeyMethod = Resources.Theme::class.java.getDeclaredMethod("getKey")
<lambda>null404                     .apply { isAccessible = true }
405             } catch (e: ReflectiveOperationException) {
406                 // Failed to retrieve Theme.getKey method
407             }
408             sThemeGetKeyMethodFetched = true
409         }
410         if (sThemeGetKeyMethod != null) {
411             return try {
412                 sThemeGetKeyMethod?.invoke(this)
413             } catch (e: ReflectiveOperationException) {
414                 // Failed to invoke Theme.getKey()
415             }
416         }
417         return null
418     }
419 
420 private var sThemeGetKeyMethodFetched = false
421 private var sThemeGetKeyMethod: Method? = null
422