feat: 自定义主题

This commit is contained in:
HuanCheng65 2023-10-01 01:26:04 +08:00
parent b91e23f995
commit 5d48048117
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
3 changed files with 342 additions and 148 deletions

View File

@ -121,6 +121,7 @@ wire {
dependencies { dependencies {
implementation 'net.swiftzer.semver:semver:1.1.2' implementation 'net.swiftzer.semver:semver:1.1.2'
implementation 'com.godaddy.android.colorpicker:compose-color-picker:0.7.0'
implementation "com.airbnb.android:lottie:$lottie_version" implementation "com.airbnb.android:lottie:$lottie_version"
implementation "com.airbnb.android:lottie-compose:$lottie_version" implementation "com.airbnb.android:lottie-compose:$lottie_version"

View File

@ -1,9 +1,9 @@
package com.huanchengfly.tieba.post.ui.page.settings.theme package com.huanchengfly.tieba.post.ui.page.settings.theme
import android.os.Build import android.os.Build
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -15,48 +15,63 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Surface import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.ColorLens
import androidx.compose.material.icons.rounded.Colorize import androidx.compose.material.icons.rounded.Colorize
import androidx.compose.material.icons.rounded.NightsStay import androidx.compose.material.icons.rounded.NightsStay
import androidx.compose.material.icons.rounded.PhotoSizeSelectActual import androidx.compose.material.icons.rounded.PhotoSizeSelectActual
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import com.github.panpf.sketch.compose.AsyncImage import com.github.panpf.sketch.compose.AsyncImage
import com.github.panpf.sketch.fetch.newFileUri import com.github.panpf.sketch.fetch.newFileUri
import com.github.panpf.sketch.fetch.newResourceUri import com.github.panpf.sketch.fetch.newResourceUri
import com.godaddy.android.colorpicker.HsvColor
import com.godaddy.android.colorpicker.harmony.ColorHarmonyMode
import com.godaddy.android.colorpicker.harmony.HarmonyColorPicker
import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.App
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.TranslucentThemeActivity import com.huanchengfly.tieba.post.activities.TranslucentThemeActivity
import com.huanchengfly.tieba.post.components.dialogs.CustomThemeDialog
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.rememberPreferenceAsMutableState
import com.huanchengfly.tieba.post.rememberPreferenceAsState import com.huanchengfly.tieba.post.rememberPreferenceAsState
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.dynamicTonalPalette import com.huanchengfly.tieba.post.ui.common.theme.compose.dynamicTonalPalette
import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon
import com.huanchengfly.tieba.post.ui.widgets.compose.Dialog
import com.huanchengfly.tieba.post.ui.widgets.compose.DialogNegativeButton
import com.huanchengfly.tieba.post.ui.widgets.compose.DialogPositiveButton
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
import com.huanchengfly.tieba.post.utils.ThemeUtil import com.huanchengfly.tieba.post.utils.ThemeUtil
import com.huanchengfly.tieba.post.utils.appPreferences import com.huanchengfly.tieba.post.utils.appPreferences
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@ -75,12 +90,138 @@ fun AppThemePage(
key = booleanPreferencesKey("useDynamicColorTheme"), key = booleanPreferencesKey("useDynamicColorTheme"),
defaultValue = false defaultValue = false
) )
val customPrimaryColorDialogState = rememberDialogState()
var customPrimaryColor by remember {
mutableStateOf(
Color(
App.ThemeDelegate.getColorByAttr(
context,
R.attr.colorPrimary,
ThemeUtil.THEME_CUSTOM
)
)
)
}
var customToolbarPrimaryColor by rememberPreferenceAsMutableState(
key = booleanPreferencesKey(
ThemeUtil.KEY_CUSTOM_TOOLBAR_PRIMARY_COLOR
),
defaultValue = false
)
var customStatusBarFontDark by rememberPreferenceAsMutableState(
key = booleanPreferencesKey(
ThemeUtil.KEY_CUSTOM_STATUS_BAR_FONT_DARK
),
defaultValue = false
)
Dialog(
dialogState = customPrimaryColorDialogState,
title = { Text(text = stringResource(id = R.string.title_custom_theme)) },
content = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
HarmonyColorPicker(
harmonyMode = ColorHarmonyMode.ANALOGOUS,
color = HsvColor.from(customPrimaryColor),
onColorChanged = {
customPrimaryColor = it.toColor()
},
modifier = Modifier.size(350.dp)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
customToolbarPrimaryColor =
!customToolbarPrimaryColor
}
)
) {
Checkbox(
checked = customToolbarPrimaryColor,
onCheckedChange = {
customToolbarPrimaryColor = it
},
)
Text(text = stringResource(id = R.string.tip_toolbar_primary_color))
}
if (customToolbarPrimaryColor) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
customStatusBarFontDark =
!customStatusBarFontDark
}
)
) {
Checkbox(
checked = customStatusBarFontDark,
onCheckedChange = {
customStatusBarFontDark = it
},
)
Text(text = stringResource(id = R.string.tip_status_bar_font))
}
}
}
},
buttons = {
DialogPositiveButton(
text = stringResource(id = R.string.button_finish),
onClick = {
customStatusBarFontDark = customStatusBarFontDark || !customToolbarPrimaryColor
context.appPreferences.customPrimaryColor =
CustomThemeDialog.toString(customPrimaryColor.toArgb())
context.appPreferences.toolbarPrimaryColor = customToolbarPrimaryColor
context.appPreferences.customStatusBarFontDark = customStatusBarFontDark
ThemeUtil.setUseDynamicTheme(false)
ThemeUtil.switchTheme(ThemeUtil.THEME_CUSTOM)
}
)
DialogNegativeButton(
text = stringResource(id = R.string.button_cancel),
onClick = {
customPrimaryColor = Color(
App.ThemeDelegate.getColorByAttr(
context,
R.attr.colorPrimary,
ThemeUtil.THEME_CUSTOM
)
)
}
)
}
)
MyScaffold( MyScaffold(
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
topBar = { topBar = {
TitleCentredToolbar( TitleCentredToolbar(
title = stringResource(id = R.string.title_theme), title = {
Text(
text = stringResource(id = R.string.title_theme),
fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h6
)
},
navigationIcon = { navigationIcon = {
BackNavigationIcon(onBackPressed = { navigator.navigateUp() }) BackNavigationIcon(onBackPressed = { navigator.navigateUp() })
} }
@ -92,44 +233,77 @@ fun AppThemePage(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
Column( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
.weight(1f)
.padding(all = 16.dp)
) { ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
} item {
Column { val tonalPalette = remember { dynamicTonalPalette(context) }
ProvideContentColor(color = ExtendedTheme.colors.background) { Row(
Row( verticalAlignment = Alignment.CenterVertically,
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(IntrinsicSize.Min) .padding(16.dp)
.padding(horizontal = 16.dp), .clip(RoundedCornerShape(6.dp))
horizontalArrangement = Arrangement.spacedBy(8.dp) .background(
) { brush = Brush.sweepGradient(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { colors = listOf(
val tonalPalette = remember { dynamicTonalPalette(context) } tonalPalette.primary50,
tonalPalette.secondary50,
tonalPalette.tertiary50,
tonalPalette.primary50,
)
)
)
.clickable {
ThemeUtil.setUseDynamicTheme(true)
}
.padding(all = 16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = if (isDynamicTheme) {
Icons.Rounded.Check
} else {
Icons.Rounded.Colorize
},
contentDescription = null,
tint = ExtendedTheme.colors.windowBackground
)
Text(
text = stringResource(id = R.string.title_dynamic_theme),
fontWeight = FontWeight.Bold,
color = ExtendedTheme.colors.windowBackground
)
}
}
}
}
item {
ProvideContentColor(color = ExtendedTheme.colors.windowBackground) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.clip(RoundedCornerShape(100)) .clip(RoundedCornerShape(6.dp))
.background( .background(
brush = Brush.sweepGradient( color = customPrimaryColor,
colors = listOf(
tonalPalette.primary50,
tonalPalette.secondary50,
tonalPalette.tertiary50,
tonalPalette.primary50,
)
)
) )
.clickable { .clickable {
ThemeUtil.setUseDynamicTheme(true) customPrimaryColorDialogState.show()
} }
.padding(all = 16.dp) .padding(all = 16.dp)
) { ) {
@ -138,144 +312,151 @@ fun AppThemePage(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
Icon( Icon(
imageVector = if (isDynamicTheme) { imageVector = if (currentTheme == ThemeUtil.THEME_CUSTOM) {
Icons.Rounded.Check Icons.Rounded.Check
} else { } else {
Icons.Rounded.Colorize Icons.Rounded.ColorLens
}, },
contentDescription = null contentDescription = null
) )
Text( Text(
text = stringResource(id = R.string.title_dynamic_theme), text = stringResource(id = R.string.title_custom_color),
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} }
} }
} Box(
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(100))
.clickable {
context.goToActivity<TranslucentThemeActivity>()
},
contentAlignment = Alignment.Center
) {
val previewImageUri =
if (context.appPreferences.translucentThemeBackgroundPath != null) {
newFileUri(context.appPreferences.translucentThemeBackgroundPath!!)
} else {
newResourceUri(R.drawable.user_header)
}
AsyncImage(
imageUri = previewImageUri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxSize() .weight(1f)
.padding(all = 16.dp) .clip(RoundedCornerShape(6.dp))
.clickable {
context.goToActivity<TranslucentThemeActivity>()
},
contentAlignment = Alignment.Center
) { ) {
val previewImageUri =
if (context.appPreferences.translucentThemeBackgroundPath != null) {
newFileUri(context.appPreferences.translucentThemeBackgroundPath!!)
} else {
newResourceUri(R.drawable.user_header)
}
AsyncImage(
imageUri = previewImageUri,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp)
) { ) {
Icon( Row(
imageVector = if (ThemeUtil.isTranslucentTheme(currentTheme)) { verticalAlignment = Alignment.CenterVertically,
Icons.Rounded.Check horizontalArrangement = Arrangement.spacedBy(16.dp),
} else { ) {
Icons.Rounded.PhotoSizeSelectActual Icon(
}, imageVector = if (ThemeUtil.isTranslucentTheme(
contentDescription = null currentTheme
) )
Text( ) {
text = stringResource(id = R.string.title_theme_translucent), Icons.Rounded.Check
fontWeight = FontWeight.Bold } else {
) Icons.Rounded.PhotoSizeSelectActual
},
contentDescription = null
)
Text(
text = stringResource(id = R.string.title_theme_translucent),
fontWeight = FontWeight.Bold
)
}
} }
} }
} }
} }
} }
LazyRow( itemsIndexed(
verticalAlignment = Alignment.CenterVertically, items = themeValues.toList(),
horizontalArrangement = Arrangement.spacedBy(16.dp), key = { _, item -> item }
modifier = Modifier.padding(vertical = 16.dp) ) { index, item ->
) { val name = themeNames[index]
itemsIndexed( val backgroundColor = remember {
items = themeValues.toList(), Color(
key = { _, item -> item }
) { index, item ->
val name = themeNames[index]
val backgroundColor = Color(
App.ThemeDelegate.getColorByAttr( App.ThemeDelegate.getColorByAttr(
LocalContext.current, context,
R.attr.colorBackground, R.attr.colorBackground,
item item
) )
) )
val primaryColor = Color( }
val primaryColor = remember {
Color(
App.ThemeDelegate.getColorByAttr( App.ThemeDelegate.getColorByAttr(
LocalContext.current, context,
R.attr.colorNewPrimary, R.attr.colorNewPrimary,
item item
) )
) )
val accentColor = Color( }
val accentColor = remember {
Color(
App.ThemeDelegate.getColorByAttr( App.ThemeDelegate.getColorByAttr(
LocalContext.current, context,
R.attr.colorAccent, R.attr.colorAccent,
item item
) )
) )
val onAccentColor = Color( }
val onAccentColor = remember {
Color(
App.ThemeDelegate.getColorByAttr( App.ThemeDelegate.getColorByAttr(
LocalContext.current, context,
R.attr.colorOnAccent, R.attr.colorOnAccent,
item item
) )
) )
val onBackgroundColor = Color( }
val onBackgroundColor = remember {
Color(
App.ThemeDelegate.getColorByAttr( App.ThemeDelegate.getColorByAttr(
LocalContext.current, context,
R.attr.colorOnBackground, R.attr.colorOnBackground,
item item
) )
) )
if (index == 0) { }
Spacer(modifier = Modifier.size(16.dp)) if (index == 0) {
} Spacer(modifier = Modifier.size(16.dp))
if (ThemeUtil.isNightMode(item)) { }
ThemeItem( if (ThemeUtil.isNightMode(item)) {
themeName = name, ThemeItem(
themeValue = item, themeName = name,
primaryColor = backgroundColor, themeValue = item,
accentColor = backgroundColor, primaryColor = backgroundColor,
contentColor = onBackgroundColor, accentColor = backgroundColor,
selected = !isDynamicTheme && currentTheme == item, contentColor = onBackgroundColor,
onClick = { selected = !isDynamicTheme && currentTheme == item,
ThemeUtil.switchTheme(item) onClick = {
ThemeUtil.setUseDynamicTheme(false) ThemeUtil.switchTheme(item)
} ThemeUtil.setUseDynamicTheme(false)
) }
} else { )
ThemeItem( } else {
themeName = name, ThemeItem(
themeValue = item, themeName = name,
primaryColor = primaryColor, themeValue = item,
accentColor = accentColor, primaryColor = primaryColor,
contentColor = onAccentColor, accentColor = accentColor,
selected = !isDynamicTheme && currentTheme == item, contentColor = onAccentColor,
onClick = { selected = !isDynamicTheme && currentTheme == item,
ThemeUtil.switchTheme(item) onClick = {
ThemeUtil.setUseDynamicTheme(false) ThemeUtil.switchTheme(item)
} ThemeUtil.setUseDynamicTheme(false)
) }
} )
} }
} }
} }
@ -293,37 +474,50 @@ private fun ThemeItem(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Surface( Row(
border = BorderStroke(12.dp, accentColor), verticalAlignment = Alignment.CenterVertically,
color = primaryColor,
contentColor = contentColor,
shape = CircleShape,
modifier = Modifier modifier = Modifier
.size(48.dp) .fillMaxWidth()
.clip(CircleShape) .height(IntrinsicSize.Min)
.clickable( .clickable(
onClickLabel = themeName, onClickLabel = themeName,
role = Role.Button,
onClick = onClick onClick = onClick
), )
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .size(36.dp)
.padding(8.dp), .clip(CircleShape)
contentAlignment = Alignment.Center .background(
) { brush = Brush.radialGradient(
if (selected) { listOf(
Icon( primaryColor,
imageVector = Icons.Rounded.Check, accentColor,
contentDescription = stringResource(id = R.string.desc_checked), )
)
) )
} else if (ThemeUtil.isNightMode(themeValue)) { .padding(9.dp),
) {
if (ThemeUtil.isNightMode(themeValue)) {
Icon( Icon(
imageVector = Icons.Rounded.NightsStay, imageVector = Icons.Rounded.NightsStay,
contentDescription = stringResource(id = R.string.desc_night_theme), contentDescription = stringResource(id = R.string.desc_night_theme),
tint = contentColor
) )
} }
} }
Text(
text = themeName,
modifier = Modifier.weight(1f)
)
if (selected) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(id = R.string.desc_checked),
tint = ExtendedTheme.colors.primary
)
}
} }
} }

View File

@ -242,10 +242,9 @@ object ThemeUtil {
fun isStatusBarFontDark(): Boolean { fun isStatusBarFontDark(): Boolean {
val theme = getTheme() val theme = getTheme()
val isToolbarPrimaryColor: Boolean = val isToolbarPrimaryColor: Boolean = INSTANCE.appPreferences.toolbarPrimaryColor
dataStore.getBoolean(KEY_CUSTOM_TOOLBAR_PRIMARY_COLOR, false)
return if (theme == THEME_CUSTOM) { return if (theme == THEME_CUSTOM) {
dataStore.getBoolean(KEY_CUSTOM_STATUS_BAR_FONT_DARK, false) INSTANCE.appPreferences.customStatusBarFontDark
} else if (isTranslucentTheme(theme)) { } else if (isTranslucentTheme(theme)) {
theme.contains("dark", ignoreCase = true) theme.contains("dark", ignoreCase = true)
} else if (!isToolbarPrimaryColor) { } else if (!isToolbarPrimaryColor) {