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) {