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 {
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-compose:$lottie_version"

View File

@ -1,9 +1,9 @@
package com.huanchengfly.tieba.post.ui.page.settings.theme
import android.os.Build
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.padding
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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
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.Surface
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
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.NightsStay
import androidx.compose.material.icons.rounded.PhotoSizeSelectActual
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.github.panpf.sketch.compose.AsyncImage
import com.github.panpf.sketch.fetch.newFileUri
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.R
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.rememberPreferenceAsMutableState
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.dynamicTonalPalette
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.ProvideContentColor
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.appPreferences
import com.ramcosta.composedestinations.annotation.Destination
@ -75,12 +90,138 @@ fun AppThemePage(
key = booleanPreferencesKey("useDynamicColorTheme"),
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(
backgroundColor = Color.Transparent,
topBar = {
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 = {
BackNavigationIcon(onBackPressed = { navigator.navigateUp() })
}
@ -92,32 +233,19 @@ fun AppThemePage(
.fillMaxSize()
.padding(paddingValues)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(all = 16.dp)
) {
}
Column {
ProvideContentColor(color = ExtendedTheme.colors.background) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
item {
val tonalPalette = remember { dynamicTonalPalette(context) }
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(100))
.fillMaxWidth()
.padding(16.dp)
.clip(RoundedCornerShape(6.dp))
.background(
brush = Brush.sweepGradient(
colors = listOf(
@ -143,19 +271,64 @@ fun AppThemePage(
} else {
Icons.Rounded.Colorize
},
contentDescription = null
contentDescription = null,
tint = ExtendedTheme.colors.windowBackground
)
Text(
text = stringResource(id = R.string.title_dynamic_theme),
fontWeight = FontWeight.Bold
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(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(6.dp))
.background(
color = customPrimaryColor,
)
.clickable {
customPrimaryColorDialogState.show()
}
.padding(all = 16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = if (currentTheme == ThemeUtil.THEME_CUSTOM) {
Icons.Rounded.Check
} else {
Icons.Rounded.ColorLens
},
contentDescription = null
)
Text(
text = stringResource(id = R.string.title_custom_color),
fontWeight = FontWeight.Bold
)
}
}
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(100))
.clip(RoundedCornerShape(6.dp))
.clickable {
context.goToActivity<TranslucentThemeActivity>()
},
@ -185,7 +358,10 @@ fun AppThemePage(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Icon(
imageVector = if (ThemeUtil.isTranslucentTheme(currentTheme)) {
imageVector = if (ThemeUtil.isTranslucentTheme(
currentTheme
)
) {
Icons.Rounded.Check
} else {
Icons.Rounded.PhotoSizeSelectActual
@ -201,51 +377,57 @@ fun AppThemePage(
}
}
}
LazyRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(vertical = 16.dp)
) {
}
itemsIndexed(
items = themeValues.toList(),
key = { _, item -> item }
) { index, item ->
val name = themeNames[index]
val backgroundColor = Color(
val backgroundColor = remember {
Color(
App.ThemeDelegate.getColorByAttr(
LocalContext.current,
context,
R.attr.colorBackground,
item
)
)
val primaryColor = Color(
}
val primaryColor = remember {
Color(
App.ThemeDelegate.getColorByAttr(
LocalContext.current,
context,
R.attr.colorNewPrimary,
item
)
)
val accentColor = Color(
}
val accentColor = remember {
Color(
App.ThemeDelegate.getColorByAttr(
LocalContext.current,
context,
R.attr.colorAccent,
item
)
)
val onAccentColor = Color(
}
val onAccentColor = remember {
Color(
App.ThemeDelegate.getColorByAttr(
LocalContext.current,
context,
R.attr.colorOnAccent,
item
)
)
val onBackgroundColor = Color(
}
val onBackgroundColor = remember {
Color(
App.ThemeDelegate.getColorByAttr(
LocalContext.current,
context,
R.attr.colorOnBackground,
item
)
)
}
if (index == 0) {
Spacer(modifier = Modifier.size(16.dp))
}
@ -280,7 +462,6 @@ fun AppThemePage(
}
}
}
}
}
@Composable
@ -293,37 +474,50 @@ private fun ThemeItem(
selected: Boolean,
onClick: () -> Unit,
) {
Surface(
border = BorderStroke(12.dp, accentColor),
color = primaryColor,
contentColor = contentColor,
shape = CircleShape,
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.fillMaxWidth()
.height(IntrinsicSize.Min)
.clickable(
onClickLabel = themeName,
role = Role.Button,
onClick = onClick
),
)
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
contentAlignment = Alignment.Center
.size(36.dp)
.clip(CircleShape)
.background(
brush = Brush.radialGradient(
listOf(
primaryColor,
accentColor,
)
)
)
.padding(9.dp),
) {
if (ThemeUtil.isNightMode(themeValue)) {
Icon(
imageVector = Icons.Rounded.NightsStay,
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
)
} else if (ThemeUtil.isNightMode(themeValue)) {
Icon(
imageVector = Icons.Rounded.NightsStay,
contentDescription = stringResource(id = R.string.desc_night_theme),
)
}
}
}
}

View File

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