Improve theme uniformity across OEM devices
- Make semantic colors and the page indicator theme-aware so they keep adequate contrast in dark mode instead of using fixed values. - Seed a full brand color scheme (#6C63FF) for devices without dynamic color (Android 11 and below) instead of falling back to Material's purple baseline; align the XML AppTheme accent accordingly. - Stop manually forcing transparent system bar colors in the Compose theme; those setters are no-ops on Android 15+ and stripped the system contrast scrim, causing the half-gray navigation bar on One UI. Keep syncing the bar icon appearance with the active theme. - Add a dynamic-color opt-out toggle (Android 12+), defaulting off on One UI where Samsung's palette extraction tends to look off.
This commit is contained in:
@@ -68,6 +68,10 @@ val isHuawei: Boolean
|
||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("hi3")
|
||||
|
||||
val isOneUI: Boolean
|
||||
get() = !getSystemProperty("ro.build.version.oneui").isNullOrBlank() ||
|
||||
Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
|
||||
@get:SuppressLint("PrivateApi")
|
||||
val isMiuiOptimizationDisabled: Boolean
|
||||
get() {
|
||||
|
||||
@@ -85,8 +85,13 @@ class AuroraApp : Application(), Configuration.Provider, SingletonImageLoader.Fa
|
||||
val themeStyle = Preferences.getInteger(this, Preferences.PREFERENCE_THEME_STYLE)
|
||||
setAppTheme(themeStyle)
|
||||
|
||||
// Apply dynamic colors to activities
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
// Apply dynamic colors to activities, unless disabled (opt-out, off by default on One UI)
|
||||
val dynamicColors = Preferences.getBoolean(
|
||||
this,
|
||||
Preferences.PREFERENCE_DYNAMIC_COLORS,
|
||||
Preferences.dynamicColorsDefault
|
||||
)
|
||||
if (dynamicColors) DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
|
||||
// Required for Shizuku installer
|
||||
if (isPAndAbove) HiddenApiBypass.addHiddenApiExemptions("I", "L")
|
||||
|
||||
@@ -15,12 +15,12 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -47,15 +47,13 @@ fun PageIndicator(modifier: Modifier = Modifier, totalPages: Int, currentPage: I
|
||||
Alignment.CenterHorizontally
|
||||
)
|
||||
) {
|
||||
val selectedColor = MaterialTheme.colorScheme.primary
|
||||
val unselectedColor = MaterialTheme.colorScheme.outlineVariant
|
||||
repeat(totalPages) { iteration ->
|
||||
val page = stringResource(R.string.page, iteration)
|
||||
val isSelected = currentPage == iteration
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
Color.DarkGray
|
||||
} else {
|
||||
Color.LightGray
|
||||
},
|
||||
targetValue = if (isSelected) selectedColor else unselectedColor,
|
||||
animationSpec = tween()
|
||||
)
|
||||
val size by animateDpAsState(
|
||||
|
||||
@@ -5,10 +5,101 @@
|
||||
|
||||
package com.aurora.store.compose.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
|
||||
val warningColor = Color(0xFFFF7600) // Amber
|
||||
val successColor = Color(0xFF1B8738) // Green
|
||||
/**
|
||||
* Whether the active [MaterialTheme] is dark.
|
||||
*
|
||||
* Derived from the resolved color scheme rather than [androidx.compose.foundation.isSystemInDarkTheme]
|
||||
* so it stays correct when the user forces a light/dark theme that differs from the system setting.
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun isAppInDarkTheme(): Boolean = MaterialTheme.colorScheme.surface.luminance() < 0.5f
|
||||
|
||||
val colorGreen = Color(0xFF4CAF50)
|
||||
val colorRed = Color(0xFFF44336)
|
||||
/**
|
||||
* Amber used to flag warnings/caveats. Lightened in dark theme for adequate contrast.
|
||||
*/
|
||||
val warningColor: Color
|
||||
@Composable @ReadOnlyComposable
|
||||
get() = if (isAppInDarkTheme()) Color(0xFFFFB74D) else Color(0xFFFF7600)
|
||||
|
||||
/**
|
||||
* Green used to flag positive/success states. Lightened in dark theme for adequate contrast.
|
||||
*/
|
||||
val successColor: Color
|
||||
@Composable @ReadOnlyComposable
|
||||
get() = if (isAppInDarkTheme()) Color(0xFF5BD27A) else Color(0xFF1B8738)
|
||||
|
||||
val colorGreen: Color
|
||||
@Composable @ReadOnlyComposable
|
||||
get() = if (isAppInDarkTheme()) Color(0xFF81C784) else Color(0xFF388E3C)
|
||||
|
||||
val colorRed: Color
|
||||
@Composable @ReadOnlyComposable
|
||||
get() = if (isAppInDarkTheme()) Color(0xFFE57373) else Color(0xFFD32F2F)
|
||||
|
||||
/**
|
||||
* Brand color schemes seeded from Aurora's accent (#6C63FF), used on devices that don't support
|
||||
* dynamic color (Android 11 and below) so the full palette stays on-brand instead of falling back
|
||||
* to Material's default purple baseline.
|
||||
*/
|
||||
val BrandLightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF6C63FF),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
primaryContainer = Color(0xFFE4DFFF),
|
||||
onPrimaryContainer = Color(0xFF1A0066),
|
||||
secondary = Color(0xFF5C5D72),
|
||||
onSecondary = Color(0xFFFFFFFF),
|
||||
secondaryContainer = Color(0xFFE1E0F9),
|
||||
onSecondaryContainer = Color(0xFF191A2C),
|
||||
tertiary = Color(0xFF78536B),
|
||||
onTertiary = Color(0xFFFFFFFF),
|
||||
tertiaryContainer = Color(0xFFFFD8EE),
|
||||
onTertiaryContainer = Color(0xFF2E1126),
|
||||
background = Color(0xFFFCF8FF),
|
||||
onBackground = Color(0xFF1B1B21),
|
||||
surface = Color(0xFFFCF8FF),
|
||||
onSurface = Color(0xFF1B1B21),
|
||||
surfaceVariant = Color(0xFFE4E1EC),
|
||||
onSurfaceVariant = Color(0xFF47464F),
|
||||
outline = Color(0xFF777680),
|
||||
outlineVariant = Color(0xFFC8C5D0),
|
||||
error = Color(0xFFBA1A1A),
|
||||
onError = Color(0xFFFFFFFF),
|
||||
errorContainer = Color(0xFFFFDAD6),
|
||||
onErrorContainer = Color(0xFF410002)
|
||||
)
|
||||
|
||||
val BrandDarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFFC8BFFF),
|
||||
onPrimary = Color(0xFF31149C),
|
||||
primaryContainer = Color(0xFF534BD6),
|
||||
onPrimaryContainer = Color(0xFFE4DFFF),
|
||||
secondary = Color(0xFFC5C4DD),
|
||||
onSecondary = Color(0xFF2E2F42),
|
||||
secondaryContainer = Color(0xFF444559),
|
||||
onSecondaryContainer = Color(0xFFE1E0F9),
|
||||
tertiary = Color(0xFFE8B9D5),
|
||||
onTertiary = Color(0xFF46263B),
|
||||
tertiaryContainer = Color(0xFF5E3C52),
|
||||
onTertiaryContainer = Color(0xFFFFD8EE),
|
||||
background = Color(0xFF131318),
|
||||
onBackground = Color(0xFFE4E1E9),
|
||||
surface = Color(0xFF131318),
|
||||
onSurface = Color(0xFFE4E1E9),
|
||||
surfaceVariant = Color(0xFF47464F),
|
||||
onSurfaceVariant = Color(0xFFC8C5D0),
|
||||
outline = Color(0xFF918F9A),
|
||||
outlineVariant = Color(0xFF47464F),
|
||||
error = Color(0xFFFFB4AB),
|
||||
onError = Color(0xFF690005),
|
||||
errorContainer = Color(0xFF93000A),
|
||||
onErrorContainer = Color(0xFFFFDAD6)
|
||||
)
|
||||
|
||||
@@ -11,27 +11,23 @@ import android.os.Build
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.util.Preferences
|
||||
|
||||
/**
|
||||
* App theme for Aurora Store based on [MaterialTheme]
|
||||
* App theme for Aurora Store based on [MaterialExpressiveTheme]
|
||||
*/
|
||||
@Composable
|
||||
fun AuroraTheme(content: @Composable () -> Unit) {
|
||||
@@ -40,28 +36,42 @@ fun AuroraTheme(content: @Composable () -> Unit) {
|
||||
var themeStyle by remember {
|
||||
mutableIntStateOf(Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE))
|
||||
}
|
||||
var dynamicColor by remember {
|
||||
mutableStateOf(
|
||||
Preferences.getBoolean(
|
||||
context,
|
||||
Preferences.PREFERENCE_DYNAMIC_COLORS,
|
||||
Preferences.dynamicColorsDefault
|
||||
)
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val prefs = Preferences.getPrefs(context)
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == Preferences.PREFERENCE_THEME_STYLE) {
|
||||
themeStyle = Preferences.getInteger(context, Preferences.PREFERENCE_THEME_STYLE)
|
||||
when (key) {
|
||||
Preferences.PREFERENCE_THEME_STYLE ->
|
||||
themeStyle = Preferences.getInteger(context, key)
|
||||
|
||||
Preferences.PREFERENCE_DYNAMIC_COLORS ->
|
||||
dynamicColor =
|
||||
Preferences.getBoolean(context, key, Preferences.dynamicColorsDefault)
|
||||
}
|
||||
}
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }
|
||||
}
|
||||
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
val useDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
|
||||
val lightScheme = if (isDynamicColorSupported) {
|
||||
val lightScheme = if (useDynamicColor) {
|
||||
dynamicLightColorScheme(context)
|
||||
} else {
|
||||
lightColorScheme(primary = colorResource(id = R.color.colorAccent))
|
||||
BrandLightColorScheme
|
||||
}
|
||||
|
||||
val darkScheme = if (isDynamicColorSupported) {
|
||||
val darkScheme = if (useDynamicColor) {
|
||||
dynamicDarkColorScheme(context)
|
||||
} else {
|
||||
darkColorScheme(primary = colorResource(id = R.color.colorAccent))
|
||||
BrandDarkColorScheme
|
||||
}
|
||||
|
||||
val colorScheme = when (themeStyle) {
|
||||
@@ -77,21 +87,22 @@ fun AuroraTheme(content: @Composable () -> Unit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Side effect to update the system bars to be transparent and match the theme's light/dark mode.
|
||||
* This is necessary on OEM devices that don't properly support dynamic theming and may have issues with light/dark status bar icons.
|
||||
* Keep the status/navigation bar icon appearance in sync with the theme's light/dark mode.
|
||||
*
|
||||
* The bars themselves are kept transparent and edge-to-edge by [androidx.activity.enableEdgeToEdge]
|
||||
* (called once in the host activity); we deliberately don't touch `window.statusBarColor` /
|
||||
* `navigationBarColor` here since those setters are deprecated no-ops on Android 15+ and would
|
||||
* otherwise strip the system contrast scrim. Re-applying the appearance is still required because
|
||||
* the user can force a light/dark theme that differs from the system, and that isn't known when
|
||||
* edge-to-edge is first configured.
|
||||
*/
|
||||
val view = LocalView.current
|
||||
val activity = LocalActivity.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val currentActivity = activity ?: return@SideEffect
|
||||
val window = currentActivity.window
|
||||
|
||||
window.statusBarColor = android.graphics.Color.TRANSPARENT
|
||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
|
||||
WindowCompat
|
||||
.getInsetsController(window, view)
|
||||
.getInsetsController(currentActivity.window, view)
|
||||
.apply {
|
||||
isAppearanceLightStatusBars = !darkTheme
|
||||
isAppearanceLightNavigationBars = !darkTheme
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.core.net.toUri
|
||||
import com.aurora.extensions.isSAndAbove
|
||||
import com.aurora.extensions.isTAndAbove
|
||||
import com.aurora.extensions.setAppTheme
|
||||
import com.aurora.store.R
|
||||
@@ -38,6 +39,7 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.ui.preferences.network.SingleChoiceDialog
|
||||
import com.aurora.store.util.Preferences
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_DEFAULT_SELECTED_TAB
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_DYNAMIC_COLORS
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_FOR_YOU
|
||||
import com.aurora.store.util.Preferences.PREFERENCE_THEME_STYLE
|
||||
import com.aurora.store.util.save
|
||||
@@ -62,6 +64,15 @@ private fun ScreenContent() {
|
||||
var forYou by remember {
|
||||
mutableStateOf(Preferences.getBoolean(context, PREFERENCE_FOR_YOU, true))
|
||||
}
|
||||
var dynamicColors by remember {
|
||||
mutableStateOf(
|
||||
Preferences.getBoolean(
|
||||
context,
|
||||
PREFERENCE_DYNAMIC_COLORS,
|
||||
Preferences.dynamicColorsDefault
|
||||
)
|
||||
)
|
||||
}
|
||||
var showThemeDialog by remember { mutableStateOf(false) }
|
||||
var showTabDialog by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -132,6 +143,29 @@ private fun ScreenContent() {
|
||||
supportingContent = { Text(themeEntries.getOrElse(themeStyle) { "" }) }
|
||||
)
|
||||
}
|
||||
if (isSAndAbove) {
|
||||
item {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
dynamicColors = !dynamicColors
|
||||
context.save(PREFERENCE_DYNAMIC_COLORS, dynamicColors)
|
||||
},
|
||||
headlineContent = { Text(stringResource(R.string.pref_ui_dynamic_color)) },
|
||||
supportingContent = {
|
||||
Text(stringResource(R.string.pref_ui_dynamic_color_desc))
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = dynamicColors,
|
||||
onCheckedChange = { checked ->
|
||||
dynamicColors = checked
|
||||
context.save(PREFERENCE_DYNAMIC_COLORS, checked)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
ListItem(headlineContent = { Text(stringResource(R.string.pref_ui_layout)) })
|
||||
|
||||
@@ -24,15 +24,24 @@ import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.aurora.extensions.isOneUI
|
||||
import com.aurora.store.BuildConfig
|
||||
|
||||
object Preferences {
|
||||
|
||||
const val PREFERENCE_DEFAULT = "PREFERENCE_DEFAULT"
|
||||
|
||||
/**
|
||||
* Default for [PREFERENCE_DYNAMIC_COLORS]. Dynamic color is opt-out everywhere except One UI,
|
||||
* where Samsung's palette extraction tends to look off, so it defaults off and users can opt in.
|
||||
*/
|
||||
val dynamicColorsDefault: Boolean
|
||||
get() = !isOneUI
|
||||
|
||||
const val PREFERENCE_AUTH_DATA = "PREFERENCE_AUTH_DATA"
|
||||
const val PREFERENCE_INSTALLER_ID = "PREFERENCE_INSTALLER_ID"
|
||||
const val PREFERENCE_THEME_STYLE = "PREFERENCE_THEME_STYLE"
|
||||
const val PREFERENCE_DYNAMIC_COLORS = "PREFERENCE_DYNAMIC_COLORS"
|
||||
const val PREFERENCE_FOR_YOU = "PREFERENCE_FOR_YOU"
|
||||
const val PREFERENCE_DEFAULT_SELECTED_TAB = "PREFERENCE_DEFAULT_SELECTED_TAB"
|
||||
const val PREFERENCE_INTRO = "PREFERENCE_INTRO"
|
||||
|
||||
@@ -4,5 +4,7 @@
|
||||
<style name="AppTheme" parent="Theme.Material3.DynamicColors.Dark.NoActionBar">
|
||||
<item name="chipStyle">@style/Chip.Filter</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<!-- Brand accent for devices without dynamic color (Android 11 and below) -->
|
||||
<item name="colorPrimary">@color/colorAccent</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -414,6 +414,8 @@
|
||||
<string name="pref_theme_style_system">Follow system</string>
|
||||
<string name="pref_theme_style_light">Light</string>
|
||||
<string name="pref_theme_style_dark">Dark</string>
|
||||
<string name="pref_ui_dynamic_color">Dynamic colors</string>
|
||||
<string name="pref_ui_dynamic_color_desc">Use colors from your wallpaper. Turn off for Aurora\'s brand palette.</string>
|
||||
<string name="pref_ui_layout">Layout</string>
|
||||
<string name="pref_ui_layout_tab">Select default tab</string>
|
||||
<string name="app_links_title">App links</string>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<item name="chipStyle">@style/Chip.Filter</item>
|
||||
<item name="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item>
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<!-- Brand accent for devices without dynamic color (Android 11 and below) -->
|
||||
<item name="colorPrimary">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.Translucent" parent="Theme.Material3.DynamicColors.DayNight.NoActionBar">
|
||||
|
||||
Reference in New Issue
Block a user