初步用Compose重构关于界面

增加了以后会使用的一些自定义Compose控件
尝试让机器人自动发布预发布版本
修复透明模式下搜索框"删除所有"文字不明显 #354
忙忙忙咩咩咩咩
This commit is contained in:
HapeLee
2025-11-22 02:35:11 +08:00
parent 158b472bef
commit 58893de5c2
13 changed files with 1018 additions and 176 deletions
@@ -0,0 +1,148 @@
package io.legado.app.base
import android.graphics.BitmapFactory
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.core.graphics.drawable.toDrawable
import androidx.core.graphics.scale
import androidx.core.view.WindowCompat
import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.disableAutoFill
import io.legado.app.utils.fullScreen
import io.legado.app.utils.getPrefString
import io.legado.app.utils.observeEvent
import io.legado.app.utils.setStatusBarColorAuto
import io.legado.app.utils.themeColor
import io.legado.app.utils.windowSize
import java.io.File
abstract class BaseComposeActivity(
val fullScreen: Boolean = true,
private val toolBarTheme: Theme = Theme.Auto,
private val transparent: Boolean = false,
private val imageBg: Boolean = true
) : AppCompatActivity() {
@Composable
protected abstract fun Content()
override fun onCreate(savedInstanceState: Bundle?) {
initTheme()
window.decorView.disableAutoFill()
AppContextWrapper.applyLocaleAndFont(this)
super.onCreate(savedInstanceState)
// Compose 入口
setContent {
AppTheme {
Surface(color = MaterialTheme.colorScheme.background) {
Content()
}
}
}
setupSystemBar()
if (imageBg) {
upBackgroundImage()
}
observeLiveBus()
}
open fun setupSystemBar() {
WindowCompat.setDecorFitsSystemWindows(window, false)
if (fullScreen) fullScreen()
setStatusBarColorAuto(
themeColor(com.google.android.material.R.attr.colorSurface),
true,
fullScreen
)
}
open fun upBackgroundImage() {
try {
ThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
window.setBackgroundDrawable(it.toDrawable(resources))
}
} catch (_: Exception) {}
}
open fun observeLiveBus() {
observeEvent<String>(EventBus.RECREATE) {
recreate()
}
}
open fun initTheme() {
when (getPrefString("app_theme", "0")) {
"0" -> {
DynamicColors.applyToActivitiesIfAvailable(application)
}
"1" -> setTheme(R.style.Theme_Base_GR)
"2" -> setTheme(R.style.Theme_Base_Lemon)
"3" -> setTheme(R.style.Theme_Base_WH)
"4" -> setTheme(R.style.Theme_Base_Elink)
"5" -> setTheme(R.style.Theme_Base_Sora)
"6" -> setTheme(R.style.Theme_Base_August)
"7" -> setTheme(R.style.Theme_Base_Carlotta)
"8" -> setTheme(R.style.Theme_Base_Koharu)
"9" -> setTheme(R.style.Theme_Base_Yuuka)
"10" -> setTheme(R.style.Theme_Base_Phoebe)
"11" -> setTheme(R.style.Theme_Base_Mujika)
"12" -> {
if (AppConfig.customMode == "accent")
setTheme(R.style.ThemeOverlay_WhiteBackground)
val colorImagePath = getPrefString(PreferKey.colorImage)
if (!colorImagePath.isNullOrBlank()) {
val file = File(colorImagePath)
if (file.exists()) {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
if (bitmap != null) {
val colorAccuracy = true
val targetWidth = if (colorAccuracy) (bitmap.width / 4).coerceAtMost(256) else 16
val targetHeight = if (colorAccuracy) (bitmap.height / 4).coerceAtMost(256) else 16
val scaledBitmap = bitmap.scale(targetWidth, targetHeight, false)
val options = DynamicColorsOptions.Builder()
.setContentBasedSource(scaledBitmap)
.build()
DynamicColors.applyToActivitiesIfAvailable(application, options)
bitmap.recycle()
}
}
}else{
DynamicColors.applyToActivitiesIfAvailable(
application,
DynamicColorsOptions.Builder()
.setContentBasedSource(application.primaryColor)
.build()
)
}
}
"13" -> setTheme(R.style.AppTheme_Transparent)
}
if (AppConfig.pureBlack)
setTheme(R.style.ThemeOverlay_PureBlack)
}
}
@@ -0,0 +1,37 @@
package io.legado.app.base
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.foundation.isSystemInDarkTheme
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val context = LocalContext.current
val colorScheme = when {
dynamicColor && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S ->
if (darkTheme) dynamicDarkColorScheme(context)
else dynamicLightColorScheme(context)
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
shapes = AppShapes,
content = content
)
}
// Material3 Typography
val AppTypography = Typography()
// Material3 Shapes
val AppShapes = Shapes()
@@ -1,7 +1,5 @@
package io.legado.app.ui.about
//import io.legado.app.lib.theme.accentColor
//import io.legado.app.lib.theme.filletBackground
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -9,21 +7,16 @@ import androidx.activity.compose.setContent
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.MaterialTheme
import androidx.core.app.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.constant.AppConst.appInfo
import io.legado.app.base.BaseComposeActivity
import io.legado.app.constant.AppLog
import io.legado.app.databinding.ActivityAboutBinding
import io.legado.app.help.CrashHandler
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.update.AppUpdate
import io.legado.app.help.update.AppUpdateGitHub
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.FileDoc
@@ -40,18 +33,24 @@ import io.legado.app.utils.openUrl
import io.legado.app.utils.share
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.init.appCtx
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class AboutActivity : AppCompatActivity() {
class AboutActivity : BaseComposeActivity() {
@Composable
override fun Content() {
AboutScreen(
onCheckUpdate = { checkUpdate() },
onOpenUrl = { openUrl(it) },
onShowMdFile = { title, file -> showMdFile(title, file) },
onSaveLog = { saveLog() },
onCreateHeapDump = { createHeapDump() },
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() },
)
}
private val waitDialog by lazy {
WaitDialog(this).setText(R.string.checking_update)
@@ -60,18 +59,6 @@ class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
AboutScreen(
onCheckUpdate = { checkUpdate() },
onOpenUrl = { openUrl(it) },
onShowMdFile = { title, file -> showMdFile(title, file) },
onSaveLog = { saveLog() },
onCreateHeapDump = { createHeapDump() },
onShowCrashLogs = { showDialogFragment<CrashLogsDialog>() }
)
}
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -100,9 +87,9 @@ class AboutActivity : AppCompatActivity() {
private fun checkUpdate() {
waitDialog.show()
AppUpdate.gitHubUpdate?.run {
kotlin.check(lifecycleScope)
check(lifecycleScope)
.onSuccess {
showDialogFragment(UpdateDialog(it))
showDialogFragment(UpdateDialog(it, UpdateDialog.Mode.UPDATE))
}.onError {
appCtx.toastOnUi("${getString(R.string.check_update)}\n${it.localizedMessage}")
}.onFinally {
@@ -119,17 +106,17 @@ class AboutActivity : AppCompatActivity() {
private fun saveLog() {
Coroutine.async {
val backupPath = AppConfig.backupPath ?: let {
appCtx.toastOnUi("未设置备份目录")
toastOnUi("未设置备份目录")
return@async
}
if (!AppConfig.recordLog) {
appCtx.toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
toastOnUi("未开启日志记录,请去其他设置里打开记录日志")
delay(3000)
}
val doc = FileDoc.fromUri(backupPath.toUri(), true)
copyLogs(doc)
copyHeapDump(doc)
appCtx.toastOnUi("已保存至备份目录")
toastOnUi("已保存至备份目录")
}.onError {
AppLog.put("保存日志出错\n${it.localizedMessage}", it, true)
}
@@ -138,21 +125,21 @@ class AboutActivity : AppCompatActivity() {
private fun createHeapDump() {
Coroutine.async {
val backupPath = AppConfig.backupPath ?: let {
appCtx.toastOnUi("未设置备份目录")
toastOnUi("未设置备份目录")
return@async
}
if (!AppConfig.recordHeapDump) {
appCtx.toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
toastOnUi("未开启堆转储记录,请去其他设置里打开记录堆转储")
delay(3000)
}
appCtx.toastOnUi("开始创建堆转储")
toastOnUi("开始创建堆转储")
System.gc()
CrashHandler.doHeapDump(true)
val doc = FileDoc.fromUri(backupPath.toUri(), true)
if (!copyHeapDump(doc)) {
appCtx.toastOnUi("未找到堆转储文件")
toastOnUi("未找到堆转储文件")
} else {
appCtx.toastOnUi("已保存至备份目录")
toastOnUi("已保存至备份目录")
}
}.onError {
AppLog.put("保存堆转储失败\n${it.localizedMessage}", it)
@@ -160,7 +147,7 @@ class AboutActivity : AppCompatActivity() {
}
private fun copyLogs(doc: FileDoc) {
val cacheDir = appCtx.externalCache
val cacheDir = externalCache
val logFiles = File(cacheDir, "logs")
val crashFiles = File(cacheDir, "crash")
val logcatFile = File(cacheDir, "logcat.txt")
@@ -182,7 +169,7 @@ class AboutActivity : AppCompatActivity() {
}
private fun copyHeapDump(doc: FileDoc): Boolean {
val heapFile = FileDoc.fromFile(File(appCtx.externalCache, "heapDump")).list()
val heapFile = FileDoc.fromFile(File(externalCache, "heapDump")).list()
?.firstOrNull() ?: return false
doc.find("heapDump")?.delete()
val heapDumpDoc = doc.createFolderIfNotExist("heapDump")
@@ -1,47 +1,147 @@
package io.legado.app.ui.about
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ButtonGroup
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.base.AppTheme
import io.legado.app.constant.AppConst.appInfo
import io.legado.app.ui.widget.compose.SettingItem
import io.legado.app.ui.widget.compose.SplicedColumnGroup
import io.legado.app.ui.widget.compose.TextCard
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
@Composable
fun PreviewAboutScreen() {
AppTheme {
AboutScreen(
versionName = "1.0",
onCheckUpdate = {},
onOpenUrl = {},
onShowMdFile = { _, _ -> },
onSaveLog = {},
onCreateHeapDump = {},
onShowCrashLogs = {}
)
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ButtonGroup(
onOpenUrl: (String) -> Unit,
onCheckUpdate: () -> Unit
) {
ButtonGroup(
modifier = Modifier.fillMaxWidth(),
overflowIndicator = { menuState ->
ButtonGroupDefaults.OverflowIndicator(menuState = menuState)
},
verticalAlignment = Alignment.CenterVertically
) {
clickableItem(
onClick = { onOpenUrl("https://example.com") },
weight = 1f,
icon = {
Icon(
painter = painterResource(R.drawable.ic_web_outline),
contentDescription = stringResource(R.string.back),
tint = colorScheme.onSurface
)
},
label = ""
)
clickableItem(
onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") },
weight = 1f,
icon = {
Icon(
painter = painterResource(R.drawable.ic_github),
contentDescription = stringResource(R.string.back),
tint = colorScheme.onSurface
)
},
label = ""
)
clickableItem(
onClick = { onCheckUpdate() },
weight = 1f,
icon = {
Icon(
painter = painterResource(R.drawable.ic_import),
contentDescription = stringResource(R.string.back),
tint = colorScheme.onSurface
)
},
label = ""
)
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AboutScreen(
versionName: String = appInfo.versionName,
onNavigateBack: () -> Unit = {},
onCheckUpdate: () -> Unit = {},
onOpenUrl: (String) -> Unit = {},
onShowMdFile: (String, String) -> Unit = { _, _ -> },
onSaveLog: () -> Unit = {},
onCreateHeapDump: () -> Unit = {},
onShowCrashLogs: () -> Unit = {},
versionName: String = "1.0.0"
onShowCrashLogs: () -> Unit = {}
) {
val context = LocalContext.current
val version = remember { appInfo.versionName }
LocalContext.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.about)) }
title = { Text(stringResource(R.string.about)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
}
)
}
) { padding ->
@@ -51,57 +151,109 @@ fun AboutScreen(
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Text(text = "版本:$version", style = MaterialTheme.typography.bodyLarge)
Spacer(Modifier.height(8.dp))
Button(onClick = onCheckUpdate) {
Text(stringResource(R.string.check_update))
}
Spacer(Modifier.height(8.dp))
Button(onClick = { onOpenUrl(context.getString(R.string.github_url)) }) {
Text(stringResource(R.string.github_url))
}
Button(onClick = { onOpenUrl(context.getString(R.string.legado_url)) }) {
Text(stringResource(R.string.web_service))
}
HorizontalDivider(
Modifier.padding(vertical = 8.dp),
DividerDefaults.Thickness,
DividerDefaults.color
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
Text(text = "文档与政策", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(4.dp))
TextButton(onClick = { onShowMdFile(context.getString(R.string.privacy_policy), "privacyPolicy.md") }) {
Text(stringResource(R.string.privacy_policy))
}
TextButton(onClick = { onShowMdFile(context.getString(R.string.license), "LICENSE.md") }) {
Text(stringResource(R.string.license))
}
TextButton(onClick = { onShowMdFile(context.getString(R.string.disclaimer), "disclaimer.md") }) {
Text(stringResource(R.string.disclaimer))
}
HorizontalDivider(
Modifier.padding(vertical = 8.dp),
DividerDefaults.Thickness,
DividerDefaults.color
Text(
text = stringResource(R.string.app_name), style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally))
TextCard(
text = versionName,
backgroundColor = colorScheme.tertiaryContainer,
contentColor = colorScheme.onTertiaryContainer,
cornerRadius = 8.dp,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
Text(
text = stringResource(R.string.about_description), style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally))
Row (modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center){
FilledTonalIconButton ( onClick = { onOpenUrl("https://example.com") } ) {
Icon(
painter = painterResource(R.drawable.ic_web_outline),
contentDescription = stringResource(R.string.back)
)
}
Text(text = "开发工具", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(4.dp))
TextButton(onClick = onShowCrashLogs) {
Text(stringResource(R.string.crash_log))
}
TextButton(onClick = onSaveLog) {
Text(stringResource(R.string.save_log))
}
TextButton(onClick = onCreateHeapDump) {
Text(stringResource(R.string.create_heap_dump))
FilledTonalIconButton (onClick = { onOpenUrl("https://github.com/HapeLee/legado-with-MD3") }) {
Icon(
painter = painterResource(R.drawable.ic_github),
contentDescription = stringResource(R.string.back)
)
}
FilledTonalIconButton (onClick = { onCheckUpdate }) {
Icon(
painter = painterResource(R.drawable.ic_import),
contentDescription = stringResource(R.string.back)
)
}
}
SplicedColumnGroup(
title = stringResource(R.string.about),
content = listOf(
{
SettingItem(
modifier = Modifier.background(colorScheme.surfaceBright),
title = stringResource(R.string.contributors),
description = "",
onClick = {
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
}
)
SettingItem(
title = stringResource(R.string.privacy_policy),
description = "",
onClick = {
onShowMdFile("隐私政策", "privacyPolicy.md")
}
)
SettingItem(
title = stringResource(R.string.license),
description = "",
onClick = {
onShowMdFile("许可证", "LICENSE.md")
}
)
SettingItem(
title = stringResource(R.string.disclaimer),
description = "",
onClick = {
onShowMdFile("免责声明", "disclaimer.md")
}
)
SettingItem(
title = stringResource(R.string.crash_log),
description = "",
onClick = onShowCrashLogs
)
SettingItem(
title = stringResource(R.string.save_log),
description = "",
onClick = onSaveLog
)
SettingItem(
title = stringResource(R.string.create_heap_dump),
description = "",
onClick = onCreateHeapDump
)
}
)
)
}
}
}
}
@@ -0,0 +1,152 @@
package io.legado.app.ui.widget.compose
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
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.graphics.Color
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
@Composable
fun AnimatedText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
style: TextStyle = LocalTextStyle.current
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
for (i in text.indices) {
val char = text[i]
Box {
AnimatedContent(
targetState = char,
transitionSpec = {
(slideInVertically(initialOffsetY = { it })).togetherWith(
slideOutVertically(targetOffsetY = { -it })
)
},
label = ""
) {
Text(
style = style,
color = color,
softWrap = softWrap,
text = it.toString(),
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
maxLines = maxLines,
minLines = minLines,
onTextLayout = onTextLayout
)
}
}
}
}
}
/**
* 动画文本控件
*
* 区别于 AnimatedText, 该控件在文本变化时,提供整行的滑动动画效果
*/
@Composable
fun AnimatedTextLine(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
style: TextStyle = LocalTextStyle.current
) {
var currentText by remember { mutableStateOf(text) }
SideEffect { currentText = text }
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedContent(
targetState = currentText,
transitionSpec = {
(slideInVertically(initialOffsetY = { it })).togetherWith(
slideOutVertically(targetOffsetY = { -it })
)
},
label = ""
) { text ->
Text(
text = text,
modifier = modifier,
style = style,
color = color,
softWrap = softWrap,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
maxLines = maxLines,
minLines = minLines,
onTextLayout = onTextLayout
)
}
}
}
@@ -0,0 +1,92 @@
package io.legado.app.ui.widget.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.base.AppTypography
@Composable
fun SettingItem(
modifier: Modifier = Modifier,
painter: Painter? = null,
title: String,
option: String? = null,
trailingContent: (@Composable () -> Unit)? = null,
description: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.then(modifier)
.fillMaxWidth()
.clickable { onClick() }
.padding(horizontal = 8.dp)
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
painter?.let {
Icon(
modifier = Modifier.padding(end = 8.dp).size(24.dp),
painter = it,
tint = colorScheme.onSurfaceVariant,
contentDescription = "Icon"
)
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(3.dp)
) {
Text(
text = title,
color = colorScheme.onSurface,
style = AppTypography.titleMedium,
fontWeight = FontWeight.Normal
)
Text(
text = description,
color = colorScheme.onSurfaceVariant,
style = AppTypography.labelMedium
)
option?.let {
AnimatedText(
text = it,
style = AppTypography.labelMedium,
color = colorScheme.primary,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
trailingContent?.let { composable ->
Box(
modifier = Modifier.fillMaxHeight()
.width(55.dp),
contentAlignment = Alignment.Center
) {
composable()
}
}
}
}
@@ -0,0 +1,84 @@
package io.legado.app.ui.widget.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
/**
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
*
* @param title the title of the group
* @param content a list of composable that will be displayed in the group
* @param modifier Modifier
*/
@Composable
fun SplicedColumnGroup(
modifier: Modifier = Modifier,
title: String = "",
content: List<@Composable () -> Unit>,
) {
if (content.isEmpty()) return
val cornerRadius = 16.dp
val connectionRadius = 5.dp
// Define shapes for different positions.
val topShape = RoundedCornerShape(
topStart = cornerRadius,
topEnd = cornerRadius,
bottomStart = connectionRadius,
bottomEnd = connectionRadius
)
val middleShape = RoundedCornerShape(connectionRadius)
val bottomShape = RoundedCornerShape(
topStart = connectionRadius,
topEnd = connectionRadius,
bottomStart = cornerRadius,
bottomEnd = cornerRadius
)
val singleShape = RoundedCornerShape(cornerRadius)
Column(modifier = modifier.padding(vertical = 8.dp)) {
// Group title
if (title != "")
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
)
// The container for setting items.
Column(
modifier = Modifier.clip(
// Clip the whole column to ensure content stays within the rounded bounds.
if (content.size == 1) singleShape else RoundedCornerShape(cornerRadius)
),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
content.forEachIndexed { index, itemContent ->
// Determine the shape based on the pkg's position.
val shape = when {
content.size == 1 -> singleShape
index == 0 -> topShape
index == content.size - 1 -> bottomShape
else -> middleShape
}
// Apply background with the correct shape to the pkg.
Column(
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceBright, shape)
) {
itemContent()
}
}
}
}
}
@@ -0,0 +1,67 @@
package io.legado.app.ui.widget.compose
import androidx.compose.ui.graphics.Color
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Preview(showBackground = true)
@Composable
fun PreviewTextCard() {
MaterialTheme {
TextCard(
text = "v1.0.0"
)
}
}
@Composable
fun TextCard(
text: String,
modifier: Modifier = Modifier,
backgroundColor: Color = colorScheme.tertiaryContainer,
contentColor: Color = colorScheme.onTertiaryContainer,
cornerRadius: Dp = 12.dp,
paddingHorizontal: Dp = 8.dp,
paddingVertical: Dp = 0.dp,
textSize: TextUnit = 10.sp,
bold: Boolean = true,
onClick: (() -> Unit)? = null,
) {
Card(
modifier = modifier
.then(
if (onClick != null) Modifier.clickable { onClick() }
else Modifier
),
colors = CardDefaults.cardColors(
containerColor = backgroundColor,
contentColor = contentColor
),
shape = RoundedCornerShape(cornerRadius),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
) {
Text(
text = text,
modifier = Modifier.padding(
horizontal = paddingHorizontal,
vertical = paddingVertical
),
fontSize = textSize,
fontWeight = if (bold) FontWeight.Bold else FontWeight.Normal
)
}
}