diff --git a/.github/ISSUE_TEMPLATE/appcompat-theme-adapter-bug-report.md b/.github/ISSUE_TEMPLATE/appcompat-theme-adapter-bug-report.md new file mode 100644 index 000000000..ec0f51bd3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/appcompat-theme-adapter-bug-report.md @@ -0,0 +1,16 @@ +--- +name: AppCompat Theme Adapter bug report +about: Create a report to help us improve +title: "[AppCompat Theme Adapter]" +labels: '' +assignees: ricknout + +--- + +**Description** + +**Steps to reproduce** + +**Expected behavior** + +**Additional context** diff --git a/README.md b/README.md index a84b50b36..cfbe6b124 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ See our [Migration Guide](https://google.github.io/accompanist/insets/) for migr ### ⬇️ [Swipe to Refresh](./swiperefresh/) (Deprecated) See our [Migration Guide](https://google.github.io/accompanist/swiperefresh/) for migrating to PullRefresh in Compose Material. +### 🎨 [AppCompat Theme Adapter](./appcompat-theme/) (Deprecated) +See our [Migration Guide](https://google.github.io/accompanist/appcompat-theme/) for migrating to the new artifact in Accompanist. + --- ## Future? diff --git a/appcompat-theme/README.md b/appcompat-theme/README.md new file mode 100644 index 000000000..b629013ef --- /dev/null +++ b/appcompat-theme/README.md @@ -0,0 +1,21 @@ +# AppCompat Compose Theme Adapter + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-appcompat-theme)](https://search.maven.org/search?q=g:com.google.accompanist) + +> :warning: This library has been deprecated in favor of the new `themeadapter-appcompat` artifact. Please see our [Migration Guide](https://google.github.io/accompanist/appcompat-theme/) for how to migrate. + +## Download + +```groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "com.google.accompanist:accompanist-appcompat-theme:" +} +``` + +Snapshots of the development version are available in Sonatype's `snapshots` [repository][snap]. These are updated on every commit. + + [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/google/accompanist/accompanist-appcompat-theme/ \ No newline at end of file diff --git a/appcompat-theme/api/current.api b/appcompat-theme/api/current.api new file mode 100644 index 000000000..190a52338 --- /dev/null +++ b/appcompat-theme/api/current.api @@ -0,0 +1,27 @@ +// Signature format: 4.0 +package com.google.accompanist.appcompattheme { + + public final class AppCompatTheme { + method @Deprecated @androidx.compose.runtime.Composable public static void AppCompatTheme(optional android.content.Context context, optional boolean readColors, optional boolean readTypography, optional androidx.compose.material.Shapes shapes, kotlin.jvm.functions.Function0 content); + method @Deprecated public static com.google.accompanist.appcompattheme.ThemeParameters createAppCompatTheme(android.content.Context, optional boolean readColors, optional boolean readTypography); + } + + public final class ColorKt { + } + + @Deprecated public final class ThemeParameters { + ctor @Deprecated public ThemeParameters(androidx.compose.material.Colors? colors, androidx.compose.material.Typography? typography); + method @Deprecated public androidx.compose.material.Colors? component1(); + method @Deprecated public androidx.compose.material.Typography? component2(); + method @Deprecated public com.google.accompanist.appcompattheme.ThemeParameters copy(androidx.compose.material.Colors? colors, androidx.compose.material.Typography? typography); + method @Deprecated public androidx.compose.material.Colors? getColors(); + method @Deprecated public androidx.compose.material.Typography? getTypography(); + property public final androidx.compose.material.Colors? colors; + property public final androidx.compose.material.Typography? typography; + } + + public final class TypedArrayUtilsKt { + } + +} + diff --git a/appcompat-theme/build.gradle b/appcompat-theme/build.gradle new file mode 100644 index 000000000..1c60d7a97 --- /dev/null +++ b/appcompat-theme/build.gradle @@ -0,0 +1,113 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'org.jetbrains.dokka' +} + +kotlin { + explicitApi() +} + +android { + compileSdkVersion 33 + + defaultConfig { + minSdkVersion 21 + // targetSdkVersion has no effect for libraries. This is only used for the test APK + targetSdkVersion 33 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + compose true + buildConfig false + } + + composeOptions { + kotlinCompilerExtensionVersion libs.versions.composeCompiler.get() + } + + lintOptions { + textReport true + textOutput 'stdout' + // We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks + checkReleaseBuilds false + } + + packagingOptions { + // Multiple dependencies bring these files in. Exclude them to enable + // our test APK to build (has no effect on our AARs) + excludes += "/META-INF/AL2.0" + excludes += "/META-INF/LGPL2.1" + } + + testOptions { + unitTests { + includeAndroidResources = true + } + animationsDisabled true + } + + sourceSets { + test { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + androidTest { + java.srcDirs += 'src/sharedTest/kotlin' + res.srcDirs += 'src/sharedTest/res' + } + } +} + +dependencies { + implementation libs.androidx.core + implementation libs.compose.material.material + implementation libs.kotlin.coroutines.android + + api libs.androidx.appcompat + + // ====================== + // Test dependencies + // ====================== + + androidTestImplementation project(':internal-testutils') + testImplementation project(':internal-testutils') + + androidTestImplementation libs.junit + testImplementation libs.junit + + androidTestImplementation libs.compose.ui.test.junit4 + testImplementation libs.compose.ui.test.junit4 + + androidTestImplementation libs.androidx.test.runner + testImplementation libs.androidx.test.runner + + androidTestImplementation libs.androidx.test.espressoCore + testImplementation libs.androidx.test.espressoCore + + testImplementation libs.robolectric +} + +apply plugin: 'com.vanniktech.maven.publish' diff --git a/appcompat-theme/gradle.properties b/appcompat-theme/gradle.properties new file mode 100644 index 000000000..397fdbbf8 --- /dev/null +++ b/appcompat-theme/gradle.properties @@ -0,0 +1,19 @@ +# +# Copyright 2020 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +POM_ARTIFACT_ID=accompanist-appcompat-theme +POM_NAME=AppCompat Theme Adapter for Compose +POM_PACKAGING=aar diff --git a/appcompat-theme/src/androidTest/AndroidManifest.xml b/appcompat-theme/src/androidTest/AndroidManifest.xml new file mode 100644 index 000000000..1ce193f97 --- /dev/null +++ b/appcompat-theme/src/androidTest/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/appcompat-theme/src/androidTest/kotlin/com/google/accompanist/appcompattheme/InstrumentedAppCompatThemeTest.kt b/appcompat-theme/src/androidTest/kotlin/com/google/accompanist/appcompattheme/InstrumentedAppCompatThemeTest.kt new file mode 100644 index 000000000..ce7677ab7 --- /dev/null +++ b/appcompat-theme/src/androidTest/kotlin/com/google/accompanist/appcompattheme/InstrumentedAppCompatThemeTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("DEPRECATION") + +package com.google.accompanist.appcompattheme + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.toFontFamily +import androidx.test.filters.SdkSuppress +import com.google.accompanist.appcompattheme.test.R +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Version of [BaseAppCompatThemeTest] which is designed to be run on device/emulators. + */ +@RunWith(Parameterized::class) +class InstrumentedAppCompatThemeTest( + activityClass: Class +) : BaseAppCompatThemeTest(activityClass) { + companion object { + @JvmStatic + @Parameterized.Parameters + fun activities() = listOf( + DarkAppCompatActivity::class.java, + LightAppCompatActivity::class.java + ) + } + + /** + * On API 21-22, the family is loaded with only the 400 font. + * + * This only works on device as Robolectric seems to always use the behavior from API 23+, + * which is not what we want to test. + */ + @Test + @SdkSuppress(maxSdkVersion = 22) + fun type_rubik_family_api21() = composeTestRule.setContent { + val rubik = Font(R.font.rubik, FontWeight.W400).toFontFamily() + + WithThemeOverlay(R.style.ThemeOverlay_RubikFontFamily) { + AppCompatTheme { + MaterialTheme.typography.assertFontFamily(expected = rubik) + } + } + } +} diff --git a/appcompat-theme/src/main/AndroidManifest.xml b/appcompat-theme/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2fa78822a --- /dev/null +++ b/appcompat-theme/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/AppCompatTheme.kt b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/AppCompatTheme.kt new file mode 100644 index 000000000..8cbe1da87 --- /dev/null +++ b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/AppCompatTheme.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("AppCompatTheme") +@file:Suppress("DEPRECATION") + +package com.google.accompanist.appcompattheme + +import android.content.Context +import androidx.compose.material.Colors +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.Typography +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.res.use + +/** + * This function creates the components of a [MaterialTheme], synthesizing a material theme + * from values in the [context]'s `Theme.AppCompat` theme. + * + * If you are using [Material Design Components](https://material.io/develop/android/) + * in your app, you should use the + * [MDC Compose Theme Adapter](https://github.com/material-components/material-components-android-compose-theme-adapter) + * instead, as it allows much finer-grained reading of your theme. + * + * Synthesizing a material theme from an `AppCompat` theme is not perfect, since `Theme.AppCompat` + * does not expose the same level of customization as `Theme.MaterialComponents`. + * Going through the pillars of material theming: + * + * ### Colors + * + * AppCompat has a limited set of top-level color attributes, which means that [AppCompatTheme] + * has to generate/select alternative colors in certain situations. The mapping is currently: + * + * | MaterialTheme color | AppCompat | + * |---------------------|-------------------------------------------------------| + * | primary | colorPrimary | + * | primaryVariant | colorPrimaryDark | + * | onPrimary | Calculated black/white | + * | secondary | colorAccent | + * | secondaryVariant | colorAccent | + * | onSecondary | Calculated black/white | + * | surface | Default | + * | onSurface | android:textColorPrimary, else calculated black/white | + * | background | android:colorBackground | + * | onBackground | android:textColorPrimary, else calculated black/white | + * | error | colorError | + * | onError | Calculated black/white | + * + * Where the table says "calculated black/white", this means either black/white, depending on + * which provides the greatest contrast against the corresponding background color. + * + * ### Typography + * + * AppCompat does not provide any semantic text appearances (such as headline6, body1, etc), and + * instead relies on text appearances for specific widgets or use cases. As such, the only thing + * we read from an AppCompat theme is the default `app:fontFamily` or `android:fontFamily`. + * For example: + * + * ``` + * + * ``` + * + * Compose does not currently support downloadable fonts, so any font referenced from the theme + * should from your resources. See [here](https://developer.android.com/guide/topics/resources/font-resource) + * for more information. + * + * ### Shape + * + * AppCompat has no concept of shape theming, therefore we use the default value from + * [MaterialTheme.shapes]. If you wish to provide custom values, use the [shapes] parameter. + * + * @param context The context to read the theme from. + * @param readColors whether the read the color palette from the [context]'s theme. + * @param readTypography whether to read the font family from [context]'s theme. + * @param shapes A set of shapes to be used by the components in this hierarchy. + */ +@Deprecated( + """ + accompanist/appcompat-theme is deprecated. + The API has moved to accompanist/themeadapter/appcompat. + For more migration information, please visit https://google.github.io/accompanist/appcompat-theme/#migration + """ +) +@Composable +fun AppCompatTheme( + context: Context = LocalContext.current, + readColors: Boolean = true, + readTypography: Boolean = true, + shapes: Shapes = MaterialTheme.shapes, + content: @Composable () -> Unit +) { + val themeParams = remember(context.theme) { + context.createAppCompatTheme( + readColors = readColors, + readTypography = readTypography + ) + } + + MaterialTheme( + colors = themeParams.colors ?: MaterialTheme.colors, + typography = themeParams.typography ?: MaterialTheme.typography, + shapes = shapes, + ) { + // We update the LocalContentColor to match our onBackground. This allows the default + // content color to be more appropriate to the theme background + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + content = content + ) + } +} + +/** + * This class contains some of the individual components of a [MaterialTheme]: + * [Colors] & [Typography]. + */ +@Deprecated( + """ + accompanist/appcompat-theme is deprecated. + The API has moved to accompanist/themeadapter/appcompat. + For more migration information, please visit https://google.github.io/accompanist/appcompat-theme/#migration + """ +) +data class ThemeParameters( + val colors: Colors?, + val typography: Typography? +) + +/** + * This function creates the components of a [androidx.compose.material.MaterialTheme], reading the + * values from the `Theme.AppCompat` Android theme. Please see the documentation + * of [AppCompatTheme] for more information on how the theme is read. + * + * The individual components of the returned [ThemeParameters] may be `null`, depending on the + * matching 'read' parameter. For example, if you set [readColors] to `false`, + * [ThemeParameters.colors] will be null. + * + * @param readColors whether the read the color palette from this context's theme. + * @param readTypography whether to read the font family from this context's theme. + * + * @return [ThemeParameters] instance containing the resulting [Colors] and [Typography] + */ +@Deprecated( + """ + accompanist/appcompat-theme is deprecated. + The API has moved to accompanist/themeadapter/appcompat. + For more migration information, please visit https://google.github.io/accompanist/appcompat-theme/#migration + """ +) +fun Context.createAppCompatTheme( + readColors: Boolean = true, + readTypography: Boolean = true +): ThemeParameters = obtainStyledAttributes(R.styleable.AppCompatThemeAdapterTheme).use { ta -> + require(ta.hasValue(R.styleable.AppCompatThemeAdapterTheme_windowActionBar)) { + "createAppCompatTheme requires the host context's theme to extend Theme.AppCompat" + } + + val colors = if (readColors) { + val isLightTheme = ta.getBoolean(R.styleable.AppCompatThemeAdapterTheme_isLightTheme, true) + + val defaultColors = if (isLightTheme) lightColors() else darkColors() + + /* First we'll read the Material color palette */ + val primary = ta.getComposeColor(R.styleable.AppCompatThemeAdapterTheme_colorPrimary) + // colorPrimaryDark is roughly equivalent to primaryVariant + val primaryVariant = + ta.getComposeColor(R.styleable.AppCompatThemeAdapterTheme_colorPrimaryDark) + val onPrimary = primary.calculateOnColor() + + // colorAccent is roughly equivalent to secondary + val secondary = ta.getComposeColor(R.styleable.AppCompatThemeAdapterTheme_colorAccent) + // We don't have a secondaryVariant, so just use the secondary + val secondaryVariant = secondary + val onSecondary = secondary.calculateOnColor() + + // We try and use the android:textColorPrimary value (with forced 100% alpha) for the + // onSurface and onBackground colors + val textColorPrimary = ta.getComposeColor( + R.styleable.AppCompatThemeAdapterTheme_android_textColorPrimary + ).let { color -> + // We only force the alpha value if it's not Unspecified + if (color != Color.Unspecified) color.copy(alpha = 1f) else color + } + + val surface = defaultColors.surface + val onSurface = surface.calculateOnColorWithTextColorPrimary(textColorPrimary) + + val background = + ta.getComposeColor(R.styleable.AppCompatThemeAdapterTheme_android_colorBackground) + val onBackground = background.calculateOnColorWithTextColorPrimary(textColorPrimary) + + val error = ta.getComposeColor(R.styleable.AppCompatThemeAdapterTheme_colorError) + val onError = error.calculateOnColor() + + defaultColors.copy( + primary = primary, + primaryVariant = primaryVariant, + onPrimary = onPrimary, + secondary = secondary, + secondaryVariant = secondaryVariant, + onSecondary = onSecondary, + surface = surface, + onSurface = onSurface, + background = background, + onBackground = onBackground, + error = error, + onError = onError + ) + } else null + + /** + * Next we'll create a typography instance. We only use the default app:fontFamily or + * android:fontFamily set in the theme. If neither of these are set, we return null. + */ + val typography = if (readTypography) { + val fontFamily = ta.getFontFamilyOrNull(R.styleable.AppCompatThemeAdapterTheme_fontFamily) + ?: ta.getFontFamilyOrNull(R.styleable.AppCompatThemeAdapterTheme_android_fontFamily) + fontFamily?.let { + Typography(defaultFontFamily = it.fontFamily) + } + } else null + + ThemeParameters(colors, typography) +} diff --git a/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/Color.kt b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/Color.kt new file mode 100644 index 000000000..585d893fc --- /dev/null +++ b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/Color.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.graphics.ColorUtils + +internal fun Color.calculateContrastForForeground(foreground: Color): Double { + return ColorUtils.calculateContrast(foreground.toArgb(), toArgb()) +} + +/** + * The WCAG AA minimum contrast for body text is 4.5:1. We may wish to increase this to + * the AAA level of 7:1 ratio. + */ +private const val MINIMUM_CONTRAST = 4.5 + +/** + * Calculates the 'on' color for this background color. + * + * This version of the function tries to use the given [textColorPrimary], as long as it + * meets the minimum contrast against this color. + */ +internal fun Color.calculateOnColorWithTextColorPrimary(textColorPrimary: Color): Color { + if (textColorPrimary != Color.Unspecified && + calculateContrastForForeground(textColorPrimary) >= MINIMUM_CONTRAST + ) { + return textColorPrimary + } + return calculateOnColor() +} + +/** + * Calculates the 'on' color for this background color. + * + * In practice this returns either black or white, depending on which has the highest + * contrast against this color. + */ +internal fun Color.calculateOnColor(): Color { + val contrastForBlack = calculateContrastForForeground(Color.Black) + val contrastForWhite = calculateContrastForForeground(Color.White) + return if (contrastForBlack > contrastForWhite) Color.Black else Color.White +} diff --git a/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/TypedArrayUtils.kt b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/TypedArrayUtils.kt new file mode 100644 index 000000000..999fc4200 --- /dev/null +++ b/appcompat-theme/src/main/java/com/google/accompanist/appcompattheme/TypedArrayUtils.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import android.annotation.SuppressLint +import android.content.res.Resources +import android.content.res.TypedArray +import android.os.Build +import android.util.TypedValue +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.toFontFamily +import androidx.core.content.res.FontResourcesParserCompat +import androidx.core.content.res.getColorOrThrow +import kotlin.concurrent.getOrSet + +private val tempTypedValue = ThreadLocal() + +internal fun TypedArray.getComposeColor( + index: Int, + fallbackColor: Color = Color.Unspecified +): Color = if (hasValue(index)) Color(getColorOrThrow(index)) else fallbackColor + +/** + * Returns the given index as a [FontFamily] and [FontWeight], + * or `null` if the value can not be coerced to a [FontFamily]. + * + * @param index index of attribute to retrieve. + */ +internal fun TypedArray.getFontFamilyOrNull(index: Int): FontFamilyWithWeight? { + val tv = tempTypedValue.getOrSet(::TypedValue) + if (getValue(index, tv) && tv.type == TypedValue.TYPE_STRING) { + return when (tv.string) { + "sans-serif" -> FontFamilyWithWeight(FontFamily.SansSerif) + "sans-serif-thin" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Thin) + "sans-serif-light" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Light) + "sans-serif-medium" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Medium) + "sans-serif-black" -> FontFamilyWithWeight(FontFamily.SansSerif, FontWeight.Black) + "serif" -> FontFamilyWithWeight(FontFamily.Serif) + "cursive" -> FontFamilyWithWeight(FontFamily.Cursive) + "monospace" -> FontFamilyWithWeight(FontFamily.Monospace) + // TODO: Compose does not expose a FontFamily for all strings yet + else -> { + // If there's a resource ID and the string starts with res/font, + // it's probably a @font resource + if (tv.resourceId != 0 && tv.string.startsWith("res/font")) { + // If we're running on API 23+ and the resource is an XML, we can parse + // the fonts into a full FontFamily. + if (Build.VERSION.SDK_INT >= 23 && tv.string.endsWith(".xml")) { + resources.parseXmlFontFamily(tv.resourceId)?.let(::FontFamilyWithWeight) + } else { + // Otherwise we just load it as a single font + FontFamilyWithWeight(Font(tv.resourceId).toFontFamily()) + } + } else null + } + } + } + return null +} + +@SuppressLint("RestrictedApi") // FontResourcesParserCompat.* +@RequiresApi(23) // XML font families with >1 fonts are only supported on API 23+ +private fun Resources.parseXmlFontFamily(resourceId: Int): FontFamily? { + val parser = getXml(resourceId) + + // Can't use {} since XmlResourceParser is AutoCloseable, not Closeable + @Suppress("ConvertTryFinallyToUseCall") + try { + val result = FontResourcesParserCompat.parse(parser, this) + if (result is FontResourcesParserCompat.FontFamilyFilesResourceEntry) { + val fonts = result.entries.map { font -> + Font( + resId = font.resourceId, + weight = fontWeightOf(font.weight), + style = if (font.isItalic) FontStyle.Italic else FontStyle.Normal + ) + } + return FontFamily(fonts) + } + } finally { + parser.close() + } + return null +} + +private fun fontWeightOf(weight: Int): FontWeight = when (weight) { + in 0..149 -> FontWeight.W100 + in 150..249 -> FontWeight.W200 + in 250..349 -> FontWeight.W300 + in 350..449 -> FontWeight.W400 + in 450..549 -> FontWeight.W500 + in 550..649 -> FontWeight.W600 + in 650..749 -> FontWeight.W700 + in 750..849 -> FontWeight.W800 + in 850..999 -> FontWeight.W900 + // Else, we use the 'normal' weight + else -> FontWeight.W400 +} + +internal data class FontFamilyWithWeight( + val fontFamily: FontFamily, + val weight: FontWeight = FontWeight.Normal +) diff --git a/appcompat-theme/src/main/res/values/theme_attrs.xml b/appcompat-theme/src/main/res/values/theme_attrs.xml new file mode 100644 index 000000000..e33b41332 --- /dev/null +++ b/appcompat-theme/src/main/res/values/theme_attrs.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/BaseAppCompatThemeTest.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/BaseAppCompatThemeTest.kt new file mode 100644 index 000000000..ba403d567 --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/BaseAppCompatThemeTest.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("DEPRECATION") + +package com.google.accompanist.appcompattheme + +import android.view.ContextThemeWrapper +import androidx.annotation.StyleRes +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.toFontFamily +import androidx.test.filters.SdkSuppress +import com.google.accompanist.appcompattheme.test.R +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test + +/** + * Class which contains the majority of the tests. This class is extended + * in both the `androidTest` and `test` source sets for setup of the relevant + * test runner. + */ +abstract class BaseAppCompatThemeTest( + activityClass: Class +) { + @get:Rule + val composeTestRule = createAndroidComposeRule(activityClass) + + @Test + fun colors() = composeTestRule.setContent { + AppCompatTheme { + val color = MaterialTheme.colors + + assertEquals(colorResource(R.color.aquamarine), color.primary) + // By default, onSecondary is calculated to the highest contrast of black/white + // against primary + assertEquals(Color.Black, color.onPrimary) + // primaryVariant == colorPrimaryDark + assertEquals(colorResource(R.color.royal_blue), color.primaryVariant) + + assertEquals(colorResource(R.color.dark_golden_rod), color.secondary) + // By default, onSecondary is calculated to the highest contrast of black/white + // against secondary + assertEquals(Color.Black, color.onSecondary) + // Assert that secondaryVariant == secondary + assertEquals(colorResource(R.color.dark_golden_rod), color.secondaryVariant) + + assertEquals(colorResource(R.color.dark_salmon), color.error) + // onError is calculated to the highest contrast of black/white against error + assertEquals(Color.Black, color.onError) + + assertEquals(colorResource(R.color.light_coral), color.background) + // By default, onBackground is calculated to the highest contrast of black/white + // against background + assertEquals(Color.Black, color.onBackground) + // AppCompatTheme updates the LocalContentColor to match the calculated onBackground + assertEquals(Color.Black, LocalContentColor.current) + } + } + + @Test + fun colors_textColorPrimary() = composeTestRule.setContent { + WithThemeOverlay(R.style.ThemeOverlay_TextColorPrimary) { + AppCompatTheme { + val color = MaterialTheme.colors + + assertEquals(colorResource(R.color.aquamarine), color.primary) + assertEquals(Color.Black, color.onPrimary) + assertEquals(colorResource(R.color.royal_blue), color.primaryVariant) + assertEquals(colorResource(R.color.dark_golden_rod), color.secondary) + assertEquals(Color.Black, color.onSecondary) + assertEquals(colorResource(R.color.dark_golden_rod), color.secondaryVariant) + assertEquals(colorResource(R.color.dark_salmon), color.error) + assertEquals(Color.Black, color.onError) + + assertEquals(colorResource(R.color.light_coral), color.background) + // Our textColorPrimary (midnight_blue) contains provides enough contrast vs + // the background color, so it should be used. + assertEquals(colorResource(R.color.midnight_blue), color.onBackground) + // AppCompatTheme updates the LocalContentColor to match the calculated onBackground + assertEquals(colorResource(R.color.midnight_blue), LocalContentColor.current) + + if (!isSystemInDarkTheme()) { + // Our textColorPrimary (midnight_blue) provides enough contrast vs + // the light surface color, so it should be used. + assertEquals(colorResource(R.color.midnight_blue), color.onSurface) + } else { + // In dark theme, textColorPrimary (midnight_blue) does not provide + // enough contrast vs the light surface color, + // so we use a computed value of white + assertEquals(Color.White, color.onSurface) + } + } + } + } + + @Test + @SdkSuppress(minSdkVersion = 23) // XML font families with >1 fonts are only supported on API 23+ + open fun type_rubik_family_api23() = composeTestRule.setContent { + val rubik = FontFamily( + Font(R.font.rubik_300, FontWeight.W300), + Font(R.font.rubik_400, FontWeight.W400), + Font(R.font.rubik_500, FontWeight.W500), + Font(R.font.rubik_700, FontWeight.W700), + ) + + WithThemeOverlay(R.style.ThemeOverlay_RubikFontFamily) { + AppCompatTheme { + MaterialTheme.typography.assertFontFamily(expected = rubik) + } + } + } + + @Test + fun type_rubik_fixed400() = composeTestRule.setContent { + val rubik400 = Font(R.font.rubik_400, FontWeight.W400).toFontFamily() + WithThemeOverlay(R.style.ThemeOverlay_Rubik400) { + AppCompatTheme { + MaterialTheme.typography.assertFontFamily(expected = rubik400) + } + } + } +} + +internal fun Typography.assertFontFamily(expected: FontFamily) { + assertEquals(expected, h1.fontFamily) + assertEquals(expected, h2.fontFamily) + assertEquals(expected, h3.fontFamily) + assertEquals(expected, h4.fontFamily) + assertEquals(expected, h5.fontFamily) + assertEquals(expected, h5.fontFamily) + assertEquals(expected, h6.fontFamily) + assertEquals(expected, body1.fontFamily) + assertEquals(expected, body2.fontFamily) + assertEquals(expected, button.fontFamily) + assertEquals(expected, caption.fontFamily) + assertEquals(expected, overline.fontFamily) +} + +/** + * Function which applies an Android theme overlay to the current context. + */ +@Composable +fun WithThemeOverlay( + @StyleRes themeOverlayId: Int, + content: @Composable () -> Unit, +) { + val themedContext = ContextThemeWrapper(LocalContext.current, themeOverlayId) + CompositionLocalProvider(LocalContext provides themedContext, content = content) +} diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/DarkAppCompatActivity.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/DarkAppCompatActivity.kt new file mode 100644 index 000000000..0f82da82c --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/DarkAppCompatActivity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate + +/** + * An [AppCompatActivity] which forces the night mode to 'dark theme'. + */ +class DarkAppCompatActivity : AppCompatActivity() { + override fun attachBaseContext(newBase: Context) { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + super.attachBaseContext(newBase) + } +} diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/FakeTests.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/FakeTests.kt new file mode 100644 index 000000000..6680b51de --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/FakeTests.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * Fake tests to help with sharding: https://github.com/android/android-test/issues/973 + */ +@RunWith(JUnit4::class) +class FakeTests { + @Test + fun fake1() = Unit + + @Test + fun fake2() = Unit + + @Test + fun fake3() = Unit + + @Test + fun fake4() = Unit + + @Test + fun fake5() = Unit + + @Test + fun fake6() = Unit + + @Test + fun fake7() = Unit + + @Test + fun fake8() = Unit + + @Test + fun fake9() = Unit + + @Test + fun fake10() = Unit +} diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/LightAppCompatActivity.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/LightAppCompatActivity.kt new file mode 100644 index 000000000..7bc3f3864 --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/LightAppCompatActivity.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate + +/** + * An [AppCompatActivity] which forces the night mode to 'light theme'. + */ +class LightAppCompatActivity : AppCompatActivity() { + override fun attachBaseContext(newBase: Context) { + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_NO + super.attachBaseContext(newBase) + } +} diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatActivity.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatActivity.kt new file mode 100644 index 000000000..30065406a --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatActivity.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import androidx.activity.ComponentActivity + +class NotAppCompatActivity : ComponentActivity() diff --git a/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatThemeTest.kt b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatThemeTest.kt new file mode 100644 index 000000000..09dfe90ad --- /dev/null +++ b/appcompat-theme/src/sharedTest/kotlin/com/google/accompanist/appcompattheme/NotAppCompatThemeTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("DEPRECATION") + +package com.google.accompanist.appcompattheme + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class NotAppCompatThemeTest { + @get:Rule + val composeTestRule = createAndroidComposeRule() + + @Test(expected = IllegalArgumentException::class) + fun throwForNonAppCompatTheme() = composeTestRule.setContent { + AppCompatTheme { + // Nothing to do here, exception should be thrown + } + } +} diff --git a/appcompat-theme/src/sharedTest/res/font/rubik.xml b/appcompat-theme/src/sharedTest/res/font/rubik.xml new file mode 100644 index 000000000..3cff72a27 --- /dev/null +++ b/appcompat-theme/src/sharedTest/res/font/rubik.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/appcompat-theme/src/sharedTest/res/font/rubik_300.ttf b/appcompat-theme/src/sharedTest/res/font/rubik_300.ttf new file mode 100644 index 000000000..8189d848f Binary files /dev/null and b/appcompat-theme/src/sharedTest/res/font/rubik_300.ttf differ diff --git a/appcompat-theme/src/sharedTest/res/font/rubik_400.ttf b/appcompat-theme/src/sharedTest/res/font/rubik_400.ttf new file mode 100644 index 000000000..52b59ca4f Binary files /dev/null and b/appcompat-theme/src/sharedTest/res/font/rubik_400.ttf differ diff --git a/appcompat-theme/src/sharedTest/res/font/rubik_500.ttf b/appcompat-theme/src/sharedTest/res/font/rubik_500.ttf new file mode 100644 index 000000000..9e358b2f4 Binary files /dev/null and b/appcompat-theme/src/sharedTest/res/font/rubik_500.ttf differ diff --git a/appcompat-theme/src/sharedTest/res/font/rubik_700.ttf b/appcompat-theme/src/sharedTest/res/font/rubik_700.ttf new file mode 100644 index 000000000..4e77930f4 Binary files /dev/null and b/appcompat-theme/src/sharedTest/res/font/rubik_700.ttf differ diff --git a/appcompat-theme/src/sharedTest/res/values/test_colors.xml b/appcompat-theme/src/sharedTest/res/values/test_colors.xml new file mode 100644 index 000000000..438beee3f --- /dev/null +++ b/appcompat-theme/src/sharedTest/res/values/test_colors.xml @@ -0,0 +1,33 @@ + + + + + + #7FFFD4 + #4169E1 + #191970 + #B8860B + #8A2BE2 + #708090 + #00FA9A + #000080 + #F08080 + #DA70D6 + #E9967A + #F5F5DC + #6B8E23 + \ No newline at end of file diff --git a/appcompat-theme/src/sharedTest/res/values/themes.xml b/appcompat-theme/src/sharedTest/res/values/themes.xml new file mode 100644 index 000000000..dce479706 --- /dev/null +++ b/appcompat-theme/src/sharedTest/res/values/themes.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/appcompat-theme/src/test/AndroidManifest.xml b/appcompat-theme/src/test/AndroidManifest.xml new file mode 100644 index 000000000..1ce193f97 --- /dev/null +++ b/appcompat-theme/src/test/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/appcompat-theme/src/test/kotlin/com/google/accompanist/appcompattheme/RobolectricAppCompatThemeTest.kt b/appcompat-theme/src/test/kotlin/com/google/accompanist/appcompattheme/RobolectricAppCompatThemeTest.kt new file mode 100644 index 000000000..3c7221bed --- /dev/null +++ b/appcompat-theme/src/test/kotlin/com/google/accompanist/appcompattheme/RobolectricAppCompatThemeTest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.appcompattheme + +import androidx.appcompat.app.AppCompatActivity +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +/** + * Version of [BaseAppCompatThemeTest] which is designed to be run using Robolectric. + * + * All of the tests are provided by [BaseAppCompatThemeTest]. + */ +@RunWith(ParameterizedRobolectricTestRunner::class) +class RobolectricAppCompatThemeTest( + activityClass: Class +) : BaseAppCompatThemeTest(activityClass) { + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + fun activities() = listOf( + DarkAppCompatActivity::class.java, + LightAppCompatActivity::class.java + ) + } +} diff --git a/appcompat-theme/src/test/resources/robolectric.properties b/appcompat-theme/src/test/resources/robolectric.properties new file mode 100644 index 000000000..2806eaffa --- /dev/null +++ b/appcompat-theme/src/test/resources/robolectric.properties @@ -0,0 +1,3 @@ +# Pin SDK to 30 since Robolectric does not currently support API 31: +# https://github.com/robolectric/robolectric/issues/6635 +sdk=30 diff --git a/docs/appcompat-theme.md b/docs/appcompat-theme.md new file mode 100644 index 000000000..f0dacb8ae --- /dev/null +++ b/docs/appcompat-theme.md @@ -0,0 +1,194 @@ +# AppCompat Compose Theme Adapter + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-appcompat-theme)](https://search.maven.org/search?q=g:com.google.accompanist) + +!!! warning +**This library is deprecated in favor of the new [`themeadapter-appcompat`][themeadapterappcompatlib] artifact. The migration guide and original documentation is below. + +## Migration + +Accompanist AppCompat Theme Adapter has moved from the [`appcompat-theme`][appcompatthemelib] artifact to the [`themeadapter-appcompat`][themeadapterappcompatlib] artifact. +The implementation is identical but the dependency and import package have changed. + +### Migration steps + +1. Change the dependency from `com.google.accompanist:accompanist-appcompat-theme:` to `com.google.accompanist:accompanist-themeadapter-appcompat:` +2. Change any `com.google.accompanist.appcompattheme.*` imports to `com.google.accompanist.themeadapter.appcompat.*` + +## Original Docs + +A library that enables reuse of [AppCompat][appcompat] XML themes for theming in [Jetpack Compose][compose]. + +The basis of theming in [Jetpack Compose][compose] is the [`MaterialTheme`][materialtheme] composable, where you provide [`Colors`][colors], [`Shapes`][shapes] and [`Typography`][typography] instances containing your styling parameters: + +``` kotlin +MaterialTheme( + typography = type, + colors = colors, + shapes = shapes +) { + // Surface, Scaffold, etc +} +``` + +[AppCompat][appcompat] XML themes allow for similar but coarser theming via XML theme attributes, like so: + +``` xml + +``` + +This library attempts to bridge the gap between [AppCompat][appcompat] XML themes, and themes in [Jetpack Compose][compose], allowing your composable [`MaterialTheme`][materialtheme] to be based on the `Activity`'s XML theme: + +``` kotlin +AppCompatTheme { + // MaterialTheme.colors, MaterialTheme.shapes, MaterialTheme.typography + // will now contain copies of the context's theme +} +``` + +This is especially handy when you're migrating an existing app, a fragment (or other UI container) at a time. + +!!! caution + If you are using [Material Design Components](https://material.io/develop/android/) in your app, you should use the + [MDC Compose Theme Adapter](https://github.com/material-components/material-components-android-compose-theme-adapter) + instead, as it allows much finer-grained reading of your theme. + + +### Customizing the theme + +The [`AppCompatTheme()`][appcompattheme] function will automatically read the host context's AppCompat theme and pass them to [`MaterialTheme`][materialtheme] on your behalf, but if you want to customize the generated values, you can do so via the [`createAppCompatTheme()`][createappcompattheme] function: + +``` kotlin +val context = LocalContext.current +var (colors, type) = context.createAppCompatTheme() + +// Modify colors or type as required. Then pass them +// through to MaterialTheme... + +MaterialTheme( + colors = colors, + typography = type +) { + // rest of layout +} +``` + + + +## Generated theme + +Synthesizing a material theme from a `Theme.AppCompat` theme is not perfect, since `Theme.AppCompat` +does not expose the same level of customization as is available in material theming. +Going through the pillars of material theming: + +### Colors + +AppCompat has a limited set of top-level color attributes, which means that [`AppCompatTheme()`][appcompattheme] +has to generate/select alternative colors in certain situations. The mapping is currently: + +| MaterialTheme color | AppCompat attribute | +|---------------------|-------------------------------------------------------| +| primary | `colorPrimary` | +| primaryVariant | `colorPrimaryDark` | +| onPrimary | Calculated black/white | +| secondary | `colorAccent` | +| secondaryVariant | `colorAccent` | +| onSecondary | Calculated black/white | +| surface | Default | +| onSurface | `android:textColorPrimary`, else calculated black/white | +| background | `android:colorBackground` | +| onBackground | `android:textColorPrimary`, else calculated black/white | +| error | `colorError` | +| onError | Calculated black/white | + +Where the table says "calculated black/white", this means either black/white, depending on +which provides the greatest contrast against the corresponding background color. + +### Typography + +AppCompat does not provide any semantic text appearances (such as headline6, body1, etc), and +instead relies on text appearances for specific widgets or use cases. As such, the only thing +we read from an AppCompat theme is the default `app:fontFamily` or `android:fontFamily`. +For example: + +``` xml + +``` + +Compose does not currently support downloadable fonts, so any font referenced from the theme +should from your resources. See [here](https://developer.android.com/guide/topics/resources/font-resource) +for more information. + +### Shape + +AppCompat has no concept of shape theming, therefore we use the default value from +[`MaterialTheme.shapes`][shapes]. If you wish to provide custom values, use the `shapes` parameter on `AppCompatTheme`. + +## Limitations + +There are some known limitations with the implementation at the moment: + +* This relies on your `Activity`/`Context` theme extending one of the `Theme.AppCompat` themes. +* Variable fonts are not supported in Compose yet, meaning that the value of `android:fontVariationSettings` are currently ignored. +* You can modify the resulting `MaterialTheme` in Compose as required, but this _only_ works in Compose. Any changes you make will not be reflected in the Activity theme. + +--- + +## Usage + +[![Maven Central](https://img.shields.io/maven-central/v/com.google.accompanist/accompanist-appcompat-theme)](https://search.maven.org/search?q=g:com.google.accompanist) + +``` groovy +repositories { + mavenCentral() +} + +dependencies { + implementation "com.google.accompanist:accompanist-appcompat-theme:" +} +``` + +### Library Snapshots + +Snapshots of the current development version of this library are available, which track the latest commit. See [here](../using-snapshot-version) for more information on how to use them. + +--- + +## Contributions + +Please contribute! We will gladly review any pull requests. +Make sure to read the [Contributing](../contributing) page first though. + +## License + +``` +Copyright 2020 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + + [appcompatthemelib]: ../appcompat-theme + [themeadapterappcompatlib]: ../themeadapter-appcompat + [compose]: https://developer.android.com/jetpack/compose + [appcompat]: https://developer.android.com/jetpack/androidx/releases/appcompat + [appcompattheme]: ../api/appcompat-theme/appcompat-theme/com.google.accompanist.appcompattheme/-app-compat-theme.html + [createappcompattheme]: ../api/appcompat-theme/appcompat-theme/com.google.accompanist.appcompattheme/create-app-compat-theme.html + [materialtheme]: https://developer.android.com/reference/kotlin/androidx/compose/material/MaterialTheme + [shapes]: https://developer.android.com/reference/kotlin/androidx/compose/material/Shapes + [colors]: https://developer.android.com/reference/kotlin/androidx/compose/material/Colors + [typography]: https://developer.android.com/reference/kotlin/androidx/compose/material/Typography \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index eec027268..215be2b12 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,6 +21,9 @@ nav: - 'System UI Controller': - 'Guide': systemuicontroller.md - 'API': api/systemuicontroller/ + - 'AppCompat Theme': + - 'Guide': appcompat-theme.md + - 'API': api/appcompat-theme/ - 'AppCompat Theme Adapter': - 'Guide': themeadapter-appcompat.md - 'API': api/themeadapter-appcompat/ diff --git a/settings.gradle b/settings.gradle index 92914279a..28f5d9cec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ include ':adaptive' include ':internal-testutils' include ':insets' include ':insets-ui' +include ':appcompat-theme' include ':drawablepainter' include ':navigation-animation' include ':navigation-material'