MangaScreen: Ditch the expanded app bar (#7470)
Animating the content padding that's used for the lazy list is heavy. A simple fix to *just* offset the list is blocked by a Compose fling issue (b/179417109). So I decided to go with the previous layout of this screen by putting everything in the list. MangaInfoHeader is split into separate composables to avoid jank when the item is being inflated.
This commit is contained in:
@@ -85,179 +85,185 @@ import kotlin.math.roundToInt
|
||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
||||
|
||||
@Composable
|
||||
fun MangaInfoHeader(
|
||||
fun MangaInfoBox(
|
||||
modifier: Modifier = Modifier,
|
||||
windowWidthSizeClass: WindowWidthSizeClass,
|
||||
appBarPadding: Dp,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
coverDataProvider: () -> Manga,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
fromSource: Boolean,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
onCoverClick: () -> Unit,
|
||||
doSearch: (query: String, global: Boolean) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
Box {
|
||||
// Backdrop
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background,
|
||||
)
|
||||
AsyncImage(
|
||||
model = coverDataProvider(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
MangaAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
} else {
|
||||
MangaAndSourceTitlesLarge(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = context,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
Box(modifier = modifier) {
|
||||
// Backdrop
|
||||
val backdropGradientColors = listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.background,
|
||||
)
|
||||
AsyncImage(
|
||||
model = coverDataProvider(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = Brush.verticalGradient(colors = backdropGradientColors),
|
||||
)
|
||||
}
|
||||
.alpha(.2f),
|
||||
)
|
||||
|
||||
// Manga & source info
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
||||
if (windowWidthSizeClass == WindowWidthSizeClass.Compact) {
|
||||
MangaAndSourceTitlesSmall(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
} else {
|
||||
MangaAndSourceTitlesLarge(
|
||||
appBarPadding = appBarPadding,
|
||||
coverDataProvider = coverDataProvider,
|
||||
onCoverClick = onCoverClick,
|
||||
title = title,
|
||||
context = LocalContext.current,
|
||||
doSearch = doSearch,
|
||||
author = author,
|
||||
artist = artist,
|
||||
status = status,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
Row(modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
@Composable
|
||||
fun MangaActionRow(
|
||||
modifier: Modifier = Modifier,
|
||||
favorite: Boolean,
|
||||
trackingCount: Int,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onEditCategory: (() -> Unit)?,
|
||||
) {
|
||||
Row(modifier = modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp)) {
|
||||
val defaultActionButtonColor = MaterialTheme.colorScheme.onSurface.copy(alpha = .38f)
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
} else {
|
||||
stringResource(R.string.add_to_library)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
)
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (favorite) {
|
||||
stringResource(R.string.in_library)
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(R.string.manga_tracking_tab)
|
||||
} else {
|
||||
stringResource(R.string.add_to_library)
|
||||
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (favorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
|
||||
color = if (favorite) MaterialTheme.colorScheme.primary else defaultActionButtonColor,
|
||||
onClick = onAddToLibraryClicked,
|
||||
onLongClick = onEditCategory,
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
if (onTrackingClicked != null) {
|
||||
MangaActionButton(
|
||||
title = if (trackingCount == 0) {
|
||||
stringResource(R.string.manga_tracking_tab)
|
||||
} else {
|
||||
quantityStringResource(id = R.plurals.num_trackers, quantity = trackingCount, trackingCount)
|
||||
},
|
||||
icon = if (trackingCount == 0) Icons.Default.Sync else Icons.Default.Done,
|
||||
color = if (trackingCount == 0) defaultActionButtonColor else MaterialTheme.colorScheme.primary,
|
||||
onClick = onTrackingClicked,
|
||||
)
|
||||
}
|
||||
if (onWebViewClicked != null) {
|
||||
MangaActionButton(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onWebViewClicked != null) {
|
||||
MangaActionButton(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Default.Public,
|
||||
color = defaultActionButtonColor,
|
||||
onClick = onWebViewClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expandable description-tags
|
||||
Column {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(fromSource || windowWidthSizeClass != WindowWidthSizeClass.Compact)
|
||||
}
|
||||
val desc =
|
||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
|
||||
val trimmedDescription = remember(desc) {
|
||||
desc
|
||||
.replace(whitespaceLineRegex, "\n")
|
||||
.trimEnd()
|
||||
}
|
||||
MangaSummary(
|
||||
expandedDescription = desc,
|
||||
shrunkDescription = trimmedDescription,
|
||||
expanded = expanded,
|
||||
@Composable
|
||||
fun ExpandableMangaDescription(
|
||||
modifier: Modifier = Modifier,
|
||||
defaultExpandState: Boolean,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
onTagClicked: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier = modifier) {
|
||||
val (expanded, onExpanded) = rememberSaveable {
|
||||
mutableStateOf(defaultExpandState)
|
||||
}
|
||||
val desc =
|
||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(id = R.string.description_placeholder)
|
||||
val trimmedDescription = remember(desc) {
|
||||
desc
|
||||
.replace(whitespaceLineRegex, "\n")
|
||||
.trimEnd()
|
||||
}
|
||||
MangaSummary(
|
||||
expandedDescription = desc,
|
||||
shrunkDescription = trimmedDescription,
|
||||
expanded = expanded,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(desc, desc) },
|
||||
onClick = { onExpanded(!expanded) },
|
||||
),
|
||||
)
|
||||
val tags = tagsProvider()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickableNoIndication(
|
||||
onLongClick = { context.copyToClipboard(desc, desc) },
|
||||
onClick = { onExpanded(!expanded) },
|
||||
),
|
||||
)
|
||||
val tags = tagsProvider()
|
||||
if (!tags.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
mainAxisSpacing = 4.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
if (expanded) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
mainAxisSpacing = 4.dp,
|
||||
crossAxisSpacing = 8.dp,
|
||||
) {
|
||||
tags.forEach {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items(items = tags) {
|
||||
TagsChip(
|
||||
text = it,
|
||||
onClick = { onTagClicked(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package eu.kanade.presentation.manga.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.manga.DownloadAction
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun MangaTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
author: String?,
|
||||
artist: String?,
|
||||
description: String?,
|
||||
tagsProvider: () -> List<String>?,
|
||||
coverDataProvider: () -> Manga,
|
||||
sourceName: String,
|
||||
isStubSource: Boolean,
|
||||
favorite: Boolean,
|
||||
status: Long,
|
||||
trackingCount: Int,
|
||||
chapterCount: Int?,
|
||||
chapterFiltered: Boolean,
|
||||
incognitoMode: Boolean,
|
||||
downloadedOnlyMode: Boolean,
|
||||
fromSource: Boolean,
|
||||
onBackClicked: () -> Unit,
|
||||
onCoverClick: () -> Unit,
|
||||
onTagClicked: (String) -> Unit,
|
||||
onAddToLibraryClicked: () -> Unit,
|
||||
onWebViewClicked: (() -> Unit)?,
|
||||
onTrackingClicked: (() -> Unit)?,
|
||||
onFilterButtonClicked: () -> Unit,
|
||||
onShareClicked: (() -> Unit)?,
|
||||
onDownloadClicked: ((DownloadAction) -> Unit)?,
|
||||
onEditCategoryClicked: (() -> Unit)?,
|
||||
onMigrateClicked: (() -> Unit)?,
|
||||
doGlobalSearch: (query: String, global: Boolean) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
// For action mode
|
||||
actionModeCounter: Int,
|
||||
onSelectAll: () -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
onSmallAppBarHeightChanged: (Int) -> Unit,
|
||||
) {
|
||||
val scrollPercentageProvider = { scrollBehavior?.scrollFraction?.coerceIn(0f, 1f) ?: 0f }
|
||||
val inverseScrollPercentageProvider = { 1f - scrollPercentageProvider() }
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
val (smallHeightPx, onSmallHeightPxChanged) = remember { mutableStateOf(0) }
|
||||
Column(modifier = Modifier.layoutId("mangaInfo")) {
|
||||
MangaInfoHeader(
|
||||
windowWidthSizeClass = WindowWidthSizeClass.Compact,
|
||||
appBarPadding = with(LocalDensity.current) { smallHeightPx.toDp() },
|
||||
title = title,
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
tagsProvider = tagsProvider,
|
||||
sourceName = sourceName,
|
||||
isStubSource = isStubSource,
|
||||
coverDataProvider = coverDataProvider,
|
||||
favorite = favorite,
|
||||
status = status,
|
||||
trackingCount = trackingCount,
|
||||
fromSource = fromSource,
|
||||
onAddToLibraryClicked = onAddToLibraryClicked,
|
||||
onWebViewClicked = onWebViewClicked,
|
||||
onTrackingClicked = onTrackingClicked,
|
||||
onTagClicked = onTagClicked,
|
||||
onEditCategory = onEditCategoryClicked,
|
||||
onCoverClick = onCoverClick,
|
||||
doSearch = doGlobalSearch,
|
||||
)
|
||||
ChapterHeader(
|
||||
chapterCount = chapterCount,
|
||||
isChapterFiltered = chapterFiltered,
|
||||
onFilterButtonClicked = onFilterButtonClicked,
|
||||
)
|
||||
}
|
||||
|
||||
MangaSmallAppBar(
|
||||
modifier = Modifier
|
||||
.layoutId("topBar")
|
||||
.onSizeChanged {
|
||||
onSmallHeightPxChanged(it.height)
|
||||
onSmallAppBarHeightChanged(it.height)
|
||||
},
|
||||
title = title,
|
||||
titleAlphaProvider = { if (actionModeCounter == 0) scrollPercentageProvider() else 1f },
|
||||
incognitoMode = incognitoMode,
|
||||
downloadedOnlyMode = downloadedOnlyMode,
|
||||
onBackClicked = onBackClicked,
|
||||
onShareClicked = onShareClicked,
|
||||
onDownloadClicked = onDownloadClicked,
|
||||
onEditCategoryClicked = onEditCategoryClicked,
|
||||
onMigrateClicked = onMigrateClicked,
|
||||
actionModeCounter = actionModeCounter,
|
||||
onSelectAll = onSelectAll,
|
||||
onInvertSelection = onInvertSelection,
|
||||
)
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val mangaInfoPlaceable = measurables
|
||||
.first { it.layoutId == "mangaInfo" }
|
||||
.measure(constraints.copy(maxHeight = Constraints.Infinity))
|
||||
val topBarPlaceable = measurables
|
||||
.first { it.layoutId == "topBar" }
|
||||
.measure(constraints)
|
||||
val mangaInfoHeight = mangaInfoPlaceable.height
|
||||
val topBarHeight = topBarPlaceable.height
|
||||
val mangaInfoSansTopBarHeightPx = mangaInfoHeight - topBarHeight
|
||||
val layoutHeight = topBarHeight +
|
||||
(mangaInfoSansTopBarHeightPx * inverseScrollPercentageProvider()).roundToInt()
|
||||
|
||||
layout(constraints.maxWidth, layoutHeight) {
|
||||
val mangaInfoY = (-mangaInfoSansTopBarHeightPx * scrollPercentageProvider()).roundToInt()
|
||||
mangaInfoPlaceable.place(0, mangaInfoY)
|
||||
topBarPlaceable.place(0, 0)
|
||||
|
||||
// Update offset limit
|
||||
val offsetLimit = -mangaInfoSansTopBarHeightPx.toFloat()
|
||||
if (scrollBehavior?.state?.offsetLimit != offsetLimit) {
|
||||
scrollBehavior?.state?.offsetLimit = offsetLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user