diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml index 2251ed796d..38d2180bf2 100644 --- a/lawnchair/res/values/strings.xml +++ b/lawnchair/res/values/strings.xml @@ -91,4 +91,9 @@ No feed installed. Fuzzy Search Approximate matching for app searches. + Theme + Light + Dark + System default + Follow wallpaper diff --git a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt index 2c96dc975f..a87bf0460f 100644 --- a/lawnchair/src/app/lawnchair/LawnchairLauncher.kt +++ b/lawnchair/src/app/lawnchair/LawnchairLauncher.kt @@ -30,6 +30,7 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner import app.lawnchair.gestures.GestureController import app.lawnchair.nexuslauncher.OverlayCallbackImpl +import app.lawnchair.preferences.PreferenceManager import app.lawnchair.util.restartLauncher import com.android.launcher3.BaseActivity import com.android.launcher3.LauncherAppState @@ -49,6 +50,11 @@ open class LawnchairLauncher : QuickstepLauncher(), LifecycleOwner, val gestureController by lazy { GestureController(this) } private val defaultOverlay by lazy { OverlayCallbackImpl(this) } + private fun subscribePreferences() { + val preferenceManager = PreferenceManager.getInstance(this) + preferenceManager.launcherTheme.subscribe(this) { updateTheme() } + } + override fun setupViews() { super.setupViews() val launcherRootView = findViewById(R.id.launcher) @@ -60,6 +66,7 @@ open class LawnchairLauncher : QuickstepLauncher(), LifecycleOwner, savedStateRegistryController.performRestore(savedInstanceState) super.onCreate(savedInstanceState) lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + subscribePreferences() } override fun onStart() { diff --git a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt index 7a5a0b93de..9edf681c10 100644 --- a/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt +++ b/lawnchair/src/app/lawnchair/preferences/PreferenceManager.kt @@ -74,6 +74,7 @@ class PreferenceManager private constructor(private val context: Context) : Base val feedProvider = StringPref("pref_feedProvider", "") val ignoreFeedWhitelist = BoolPref("pref_ignoreFeedWhitelist", false) val workspaceDt2s = BoolPref("pref_doubleTap2Sleep", true) + val launcherTheme = StringPref("pref_launcherTheme", "system") init { sp.registerOnSharedPreferenceChangeListener(this) diff --git a/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt b/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt index 541fa65a56..053d724947 100644 --- a/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt +++ b/lawnchair/src/app/lawnchair/ui/AlertBottomSheetContent.kt @@ -15,16 +15,21 @@ import com.google.accompanist.insets.navigationBarsPadding fun AlertBottomSheetContent( buttons: @Composable RowScope.() -> Unit, title: (@Composable () -> Unit)? = null, - text: @Composable (() -> Unit)? = null + text: @Composable (() -> Unit)? = null, + content: @Composable (() -> Unit)? = null ) { + val contentPadding = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) Column( modifier = Modifier .navigationBarsPadding() - .padding(16.dp) + .padding( + top = 16.dp, + bottom = 16.dp, + ) .fillMaxWidth() ) { if (title != null) { - Box(modifier = Modifier.padding(bottom = 16.dp)) { + Box(modifier = Modifier.then(contentPadding)) { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { val textStyle = MaterialTheme.typography.h6 ProvideTextStyle(textStyle, title) @@ -32,15 +37,21 @@ fun AlertBottomSheetContent( } } if (text != null) { - Box(modifier = Modifier.padding(bottom = 16.dp)) { + Box(modifier = Modifier.then(contentPadding)) { val textStyle = MaterialTheme.typography.body2 ProvideTextStyle(textStyle, text) } } + if (content != null) { + Box(modifier = Modifier.padding(bottom = 16.dp)) { + content() + } + } Row( horizontalArrangement = Arrangement.End, modifier = Modifier .padding(top = 4.dp) + .then(contentPadding) .fillMaxWidth() ) { buttons() diff --git a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt index 0095ec1174..08fb87f91a 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt @@ -56,11 +56,11 @@ fun GeneralPreferences() { NavigationActionPreference( label = stringResource(id = R.string.icon_pack), destination = subRoute(name = GeneralRoutes.ICON_PACK), - showDivider = false, subtitle = LocalPreferenceInteractor.current.getIconPacks() .find { it.packageName == preferenceManager().iconPackPackage.get() }?.name ) + ThemePreference(showDivider = false) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val wrapAdaptiveIcons = prefs.wrapAdaptiveIcons.observeAsState() diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt index 6e8887df9f..3060b45c4c 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/BottomSheet.kt @@ -21,9 +21,9 @@ import kotlinx.coroutines.launch @ExperimentalMaterialApi @Composable fun BottomSheet( - sheetContent: @Composable () -> Unit, sheetState: BottomSheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), scrimColor: Color = ModalBottomSheetDefaults.scrimColor, + sheetContent: @Composable () -> Unit, ) { val currentSheetContent by rememberUpdatedState(sheetContent) val modalBottomSheetState = sheetState.modalBottomSheetState diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt new file mode 100644 index 0000000000..5196f882bc --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/ListPreference.kt @@ -0,0 +1,116 @@ +package app.lawnchair.ui.preferences.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.lawnchair.preferences.PreferenceAdapter +import app.lawnchair.ui.AlertBottomSheetContent +import app.lawnchair.ui.util.addIf +import kotlinx.coroutines.launch + +@ExperimentalMaterialApi +@Composable +fun ListPreference( + adapter: PreferenceAdapter, + entries: List>, + label: String, + enabled: Boolean = true, + showDivider: Boolean = true +) { + val sheetState = rememberBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + + val currentValue = adapter.state.value + val currentLabel = entries + .firstOrNull { it.value == currentValue } + ?.label?.invoke() + + PreferenceTemplate(height = if (currentLabel != null) 72.dp else 52.dp, showDivider = showDivider) { + Row( + modifier = Modifier + .clickable(enabled) { scope.launch { sheetState.show() } } + .fillMaxHeight() + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier + .weight(1f) + .addIf(!enabled) { alpha(ContentAlpha.disabled) } + ) { + Text( + text = label, + style = MaterialTheme.typography.subtitle1, + color = MaterialTheme.colors.onBackground, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + currentLabel?.let { + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.medium, + LocalContentColor provides MaterialTheme.colors.onBackground + ) { + Text( + text = it, + style = MaterialTheme.typography.body2, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + } + } + } + } + + BottomSheet(sheetState = sheetState) { + AlertBottomSheetContent( + title = { Text(label) }, + buttons = { + OutlinedButton( + shape = MaterialTheme.shapes.small, + onClick = { scope.launch { sheetState.hide() } } + ) { + Text(text = stringResource(id = android.R.string.cancel)) + } + } + ) { + LazyColumn { + items(entries) { item -> + Row( + modifier = Modifier + .fillMaxSize() + .clickable { + adapter.onChange(item.value) + scope.launch { sheetState.hide() } + } + .padding(16.dp) + ) { + RadioButton( + modifier = Modifier.padding(end = 16.dp), + selected = item.value == currentValue, + onClick = null + ) + Text(text = item.label()) + } + } + } + } + } +} + +class ListPreferenceEntry( + val value: T, + val label: @Composable () -> String, +) diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/ThemePreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/ThemePreference.kt new file mode 100644 index 0000000000..8d0f63e391 --- /dev/null +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/ThemePreference.kt @@ -0,0 +1,39 @@ +package app.lawnchair.ui.preferences.components + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import app.lawnchair.preferences.getAdapter +import app.lawnchair.preferences.preferenceManager +import com.android.launcher3.R +import com.android.launcher3.Utilities + +object ThemeChoice { + val light = "light" + val dark = "dark" + val system = "system" +} + +val themeEntries = listOf( + ListPreferenceEntry(ThemeChoice.light) { stringResource(id = R.string.theme_light) }, + ListPreferenceEntry(ThemeChoice.dark) { stringResource(id = R.string.theme_dark) }, + ListPreferenceEntry(ThemeChoice.system) { + when { + Utilities.ATLEAST_Q -> stringResource(id = R.string.theme_system_default) + else -> stringResource(id = R.string.theme_follow_wallpaper) + } + }, +) + +@ExperimentalMaterialApi +@Composable +fun ThemePreference( + showDivider: Boolean = true +) { + ListPreference( + adapter = preferenceManager().launcherTheme.getAdapter(), + entries = themeEntries, + label = stringResource(id = R.string.theme_label), + showDivider = showDivider + ) +} diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/TopBar.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/TopBar.kt index de7760f09e..05af5fe426 100644 --- a/lawnchair/src/app/lawnchair/ui/preferences/components/TopBar.kt +++ b/lawnchair/src/app/lawnchair/ui/preferences/components/TopBar.kt @@ -16,6 +16,7 @@ package app.lawnchair.ui.preferences.components +import androidx.compose.animation.Animatable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState @@ -33,6 +34,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material.icons.rounded.ArrowForward import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -99,7 +101,10 @@ val shadowColors = listOf(Color(0, 0, 0, 31), Color.Transparent) @Composable fun TopBarSurface(floating: Boolean, content: @Composable () -> Unit) { val (normalColor, floatingColor) = topBarColors() - val color by animateColorAsState(if (floating) floatingColor else normalColor) + val color = remember(key1 = normalColor) { Animatable(normalColor) } + LaunchedEffect(floating) { + color.animateTo(if (floating) floatingColor else normalColor) + } val shadowAlpha by animateFloatAsState(if (floating) 1f else 0f) Column( modifier = Modifier @@ -107,7 +112,7 @@ fun TopBarSurface(floating: Boolean, content: @Composable () -> Unit) { ) { Box( modifier = Modifier - .background(color) + .background(color.value) .statusBarsPadding() .pointerInput(remember { MutableInteractionSource() }) { // consume touch diff --git a/lawnchair/src/app/lawnchair/ui/theme/Theme.kt b/lawnchair/src/app/lawnchair/ui/theme/Theme.kt index f2268d7c87..efab825d09 100644 --- a/lawnchair/src/app/lawnchair/ui/theme/Theme.kt +++ b/lawnchair/src/app/lawnchair/ui/theme/Theme.kt @@ -20,8 +20,14 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import app.lawnchair.preferences.observeAsState +import app.lawnchair.preferences.preferenceManager +import app.lawnchair.ui.preferences.components.ThemeChoice +import com.android.launcher3.Utilities +import com.android.launcher3.uioverrides.WallpaperColorInfo private val DarkColorPalette = darkColors( primary = Blue600, @@ -36,7 +42,7 @@ private val LightColorPalette = lightColors( @Composable fun LawnchairTheme( - darkTheme: Boolean = isSystemInDarkTheme(), + darkTheme: Boolean = isSelectedThemeDark(), content: @Composable () -> Unit ) { val colors = if (darkTheme) { @@ -51,4 +57,34 @@ fun LawnchairTheme( shapes = Shapes, content = content ) -} \ No newline at end of file +} + +@Composable +fun isSelectedThemeDark(): Boolean { + val themeChoice by preferenceManager().launcherTheme.observeAsState() + return when (themeChoice) { + ThemeChoice.light -> false + ThemeChoice.dark -> true + else -> isAutoThemeDark() + } +} + +@Composable +fun isAutoThemeDark(): Boolean { + return when { + Utilities.ATLEAST_Q -> isSystemInDarkTheme() + else -> isWallpaperDark() + } +} + +@Composable +fun isWallpaperDark(): Boolean { + val wallpaperColorInfo = WallpaperColorInfo.INSTANCE[LocalContext.current] + var isDark by remember { mutableStateOf(wallpaperColorInfo.isDark) } + DisposableEffect(wallpaperColorInfo) { + val listener = WallpaperColorInfo.OnChangeListener { isDark = it.isDark } + wallpaperColorInfo.addOnChangeListener(listener) + onDispose { wallpaperColorInfo.removeOnChangeListener(listener) } + } + return isDark +} diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java index f2a5c656ff..f3dee88537 100644 --- a/src/com/android/launcher3/BaseDraggingActivity.java +++ b/src/com/android/launcher3/BaseDraggingActivity.java @@ -106,7 +106,7 @@ public abstract class BaseDraggingActivity extends BaseActivity updateTheme(); } - private void updateTheme() { + protected void updateTheme() { if (mThemeRes != Themes.getActivityThemeRes(this)) { recreate(); } diff --git a/src/com/android/launcher3/util/Themes.java b/src/com/android/launcher3/util/Themes.java index b74686fbd3..3d245fe818 100644 --- a/src/com/android/launcher3/util/Themes.java +++ b/src/com/android/launcher3/util/Themes.java @@ -30,6 +30,8 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.uioverrides.WallpaperColorInfo; +import app.lawnchair.preferences.PreferenceManager; + /** * Various utility methods associated with theming. */ @@ -37,13 +39,27 @@ public class Themes { public static int getActivityThemeRes(Context context) { WallpaperColorInfo wallpaperColorInfo = WallpaperColorInfo.INSTANCE.get(context); + + PreferenceManager preferenceManager = PreferenceManager.getInstance(context); + String themeChoice = preferenceManager.getLauncherTheme().get(); + boolean darkTheme; - if (Utilities.ATLEAST_Q) { - Configuration configuration = context.getResources().getConfiguration(); - int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; - darkTheme = nightMode == Configuration.UI_MODE_NIGHT_YES; - } else { - darkTheme = wallpaperColorInfo.isDark(); + switch(themeChoice) { + case "light": + darkTheme = false; + break; + case "dark": + darkTheme = true; + break; + default: + if (Utilities.ATLEAST_Q) { + Configuration configuration = context.getResources().getConfiguration(); + int nightMode = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + darkTheme = nightMode == Configuration.UI_MODE_NIGHT_YES; + } else { + darkTheme = wallpaperColorInfo.isDark(); + } + break; } if (darkTheme) {