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