[优化] Compose界面的顶栏模糊和渐变模糊,可在外观处体验。初始版本,需要重启应用,可能存在bug,仅在发现页可用

This commit is contained in:
HapeLee
2026-02-26 01:31:02 +08:00
parent 8751f62cce
commit fb020356b6
2 changed files with 197 additions and 122 deletions
@@ -42,17 +42,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Grid4x4
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.FilterAlt
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
@@ -61,7 +57,9 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -82,28 +80,34 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.model.BookShelfState
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.Cover
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.button.AnimatedTextButton
import io.legado.app.ui.widget.components.button.SmallTopBarButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import org.koin.androidx.compose.koinViewModel
@SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class,
ExperimentalMaterial3ExpressiveApi::class
)
@Composable
fun ExploreShowScreen(
title: String,
@@ -133,6 +137,10 @@ fun ExploreShowScreen(
val isGridMode = layoutState == 1
var showGridCountSheet by remember { mutableStateOf(false) }
val gridColumnCount by viewModel.gridCount.collectAsState()
val isRefreshing by viewModel.isRefreshing.collectAsState()
val pullToRefreshState = rememberPullToRefreshState()
val hazeState = remember { HazeState() }
val shouldLoadMoreList = remember {
derivedStateOf {
@@ -149,6 +157,7 @@ fun ExploreShowScreen(
total > 0 && last >= total - 1
}
}
var showMenu by remember { mutableStateOf(false) }
LaunchedEffect(shouldLoadMoreList.value, isGridMode) {
if (!isGridMode && shouldLoadMoreList.value) viewModel.loadMore()
@@ -298,30 +307,130 @@ fun ExploreShowScreen(
Scaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.hazeSource(hazeState),
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
ExploreTopBar(
modifier = Modifier.hazeEffect(hazeState) {
progressive =
HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f)
GlassMediumFlexibleTopAppBar(
modifier = Modifier.responsiveHazeEffect(
state = hazeState
),
title = {
AnimatedTextLine(
selectedTitle ?: title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
colors = GlassTopAppBarDefaults.glassColors(),
navigationIcon = {
TopbarNavigationButton(onClick = onBack)
},
actions = {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.animateContentSize(tween(300))
) {
TopBarActionButton(
onClick = { showMenu = true },
imageVector = Icons.Default.FilterList,
contentDescription = "Filter"
)
TopBarActionButton(
onClick = { showKindSheet = true },
imageVector = Icons.Outlined.FilterAlt,
contentDescription = "分类"
)
AnimatedVisibility(
visible = isGridMode,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(300))
) {
TopBarActionButton(
onClick = { showGridCountSheet = true },
imageVector = Icons.AutoMirrored.Outlined.FormatListBulleted,
contentDescription = "列数设置"
)
}
}
TopBarActionButton(
onClick = { viewModel.setLayout() },
imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
contentDescription = "切换布局"
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
RoundDropdownMenuItem(
text = { Text("全部显示") },
onClick = {
viewModel.setFilterState(BookFilterState.SHOW_ALL)
showMenu = false
},
trailingIcon = {
if (filterState == BookFilterState.SHOW_ALL)
Icon(Icons.Default.Check, null)
}
)
RoundDropdownMenuItem(
text = { Text("隐藏已在书架的同源书籍") },
onClick = {
viewModel.setFilterState(BookFilterState.HIDE_IN_SHELF)
showMenu = false
},
trailingIcon = {
if (filterState == BookFilterState.HIDE_IN_SHELF)
Icon(Icons.Default.Check, null)
}
)
RoundDropdownMenuItem(
text = { Text("隐藏已在书架的非同源书籍") },
onClick = {
viewModel.setFilterState(BookFilterState.HIDE_SAME_NAME_AUTHOR)
showMenu = false
},
trailingIcon = {
if (filterState == BookFilterState.HIDE_SAME_NAME_AUTHOR)
Icon(Icons.Default.Check, null)
}
)
RoundDropdownMenuItem(
text = { Text("只显示不在书架的书籍") },
onClick = {
viewModel.setFilterState(BookFilterState.SHOW_NOT_IN_SHELF_ONLY)
showMenu = false
},
trailingIcon = {
if (filterState == BookFilterState.SHOW_NOT_IN_SHELF_ONLY)
Icon(Icons.Default.Check, null)
}
)
}
},
title = selectedTitle ?: title,
filterState = filterState,
onBack = onBack,
onFilterSelect = viewModel::setFilterState,
onSelectKindClick = { showKindSheet = true },
onToggleGridMode = { viewModel.setLayout() },
isGridMode = isGridMode,
onGridCountClick = { showGridCountSheet = true },
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
PullToRefreshBox(
modifier = Modifier.fillMaxSize(),
isRefreshing = isRefreshing,
state = pullToRefreshState,
onRefresh = { viewModel.loadMore(isRefresh = true) },
indicator = {
PullToRefreshDefaults.LoadingIndicator(
state = pullToRefreshState,
isRefreshing = isRefreshing,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = paddingValues.calculateTopPadding())
)
}
) {
Crossfade(
targetState = isGridMode,
@@ -331,9 +440,16 @@ fun ExploreShowScreen(
if (isGrid) {
LazyVerticalGrid(
state = gridState,
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState),
columns = GridCells.Fixed(gridColumnCount),
contentPadding = PaddingValues(12.dp),
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 12.dp,
bottom = paddingValues.calculateBottomPadding() + 12.dp,
start = 12.dp,
end = 12.dp
),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -360,8 +476,14 @@ fun ExploreShowScreen(
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState),
state = listState,
contentPadding = PaddingValues(bottom = 16.dp)
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 16.dp
)
) {
items(
items = books,
@@ -419,97 +541,6 @@ fun KindGridItem(
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ExploreTopBar(
modifier: Modifier = Modifier,
title: String,
filterState: BookFilterState,
onBack: () -> Unit,
onFilterSelect: (BookFilterState) -> Unit,
onSelectKindClick: () -> Unit,
onToggleGridMode: () -> Unit,
isGridMode: Boolean,
onGridCountClick: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null
) {
var showMenu by remember { mutableStateOf(false) }
GlassMediumFlexibleTopAppBar(
modifier = modifier,
title = { AnimatedTextLine(title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
navigationIcon = {
SmallTopBarButton(onClick = onBack)
},
actions = {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.animateContentSize(tween(300))
) {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.FilterList, contentDescription = "Filter")
}
IconButton(onClick = { onSelectKindClick() }) {
Icon(Icons.Outlined.FilterAlt, contentDescription = "分类")
}
AnimatedVisibility(
visible = isGridMode,
enter = fadeIn(tween(300)),
exit = fadeOut(tween(300))
) {
IconButton(onClick = onGridCountClick) {
Icon(Icons.Default.Grid4x4, contentDescription = "列数设置")
}
}
}
IconButton(onClick = { onToggleGridMode() }) {
Icon(
imageVector = if (!isGridMode) Icons.AutoMirrored.Outlined.FormatListBulleted else Icons.Default.GridView,
contentDescription = "切换布局"
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text("全部显示") },
onClick = {
onFilterSelect(BookFilterState.SHOW_ALL)
showMenu = false
},
trailingIcon = { if(filterState == BookFilterState.SHOW_ALL) Icon(Icons.Default.Check, null) }
)
DropdownMenuItem(
text = { Text("隐藏已在书架的同源书籍") },
onClick = {
onFilterSelect(BookFilterState.HIDE_IN_SHELF)
showMenu = false
},
trailingIcon = { if(filterState == BookFilterState.HIDE_IN_SHELF) Icon(Icons.Default.Check, null) }
)
DropdownMenuItem(
text = { Text("隐藏已在书架的非同源书籍") },
onClick = {
onFilterSelect(BookFilterState.HIDE_SAME_NAME_AUTHOR)
showMenu = false
},
trailingIcon = { if(filterState == BookFilterState.HIDE_SAME_NAME_AUTHOR) Icon(Icons.Default.Check, null) }
)
DropdownMenuItem(
text = { Text("只显示不在书架的书籍") },
onClick = {
onFilterSelect(BookFilterState.SHOW_NOT_IN_SHELF_ONLY)
showMenu = false
},
trailingIcon = { if(filterState == BookFilterState.SHOW_NOT_IN_SHELF_ONLY) Icon(Icons.Default.Check, null) }
)
}
},
scrollBehavior = scrollBehavior
)
}
@Composable
fun ExploreBookItem(
book: SearchBook,
@@ -0,0 +1,44 @@
package io.legado.app.ui.theme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import io.legado.app.ui.config.themeConfig.ThemeConfig
/**
* 自动感知全局配置的 HazeSource
*/
fun Modifier.responsiveHazeSource(state: HazeState): Modifier = this.then(
if (ThemeConfig.enableBlur) Modifier.hazeSource(state) else Modifier
)
/**
* 自动感知全局配置的 HazeEffect
*/
@OptIn(ExperimentalHazeMaterialsApi::class)
@Composable
fun Modifier.responsiveHazeEffect(
state: HazeState
): Modifier {
val enableBlur = ThemeConfig.enableBlur
val enableProgressiveBlur = ThemeConfig.enableProgressiveBlur
if (!enableBlur) return this
return this.hazeEffect(
state = state,
style = HazeMaterials.ultraThin()
) {
if (enableProgressiveBlur) {
progressive = HazeProgressive.verticalGradient(
startIntensity = 1f,
endIntensity = 0f
)
}
}
}