Touch up manga grid/list items (#8307)

* Touch up library item touch indicator

Now the touch indicator has the same coverage as the selection indicator.
Experimental Modifier.Node API is used to draw the selection indicator

* Unify library and browse source list item layouts
This commit is contained in:
Ivan Iskandar
2022-10-28 22:46:10 +07:00
committed by GitHub
parent e36d31bf0f
commit 87391832ba
12 changed files with 470 additions and 679 deletions

View File

@@ -0,0 +1,317 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.modifierElementOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.kanade.presentation.util.selectedBackground
object CommonMangaItemDefaults {
val GridHorizontalSpacer = 4.dp
val GridVerticalSpacer = 4.dp
const val BrowseFavoriteCoverAlpha = 0.34f
}
private const val GridSelectedCoverAlpha = 0.76f
/**
* Layout of grid list item with title overlaying the cover.
* Accepts null [title] for a cover-only view.
*/
@Composable
fun MangaCompactGridItem(
isSelected: Boolean = false,
title: String? = null,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
GridItemSelectable(
isSelected = isSelected,
onClick = onClick,
onLongClick = onLongClick,
) {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
badgesStart = coverBadgeStart,
badgesEnd = coverBadgeEnd,
content = {
if (title != null) {
CoverTextOverlay(title = title)
}
},
)
}
}
/**
* Title overlay for [MangaCompactGridItem]
*/
@Composable
private fun BoxScope.CoverTextOverlay(title: String) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
.background(
Brush.verticalGradient(
0f to Color.Transparent,
1f to Color(0xAA000000),
),
)
.fillMaxHeight(0.33f)
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
GridItemTitle(
modifier = Modifier
.padding(8.dp)
.align(Alignment.BottomStart),
title = title,
style = MaterialTheme.typography.titleSmall.copy(
color = Color.White,
shadow = Shadow(
color = Color.Black,
blurRadius = 4f,
),
),
)
}
/**
* Layout of grid list item with title below the cover.
*/
@Composable
fun MangaComfortableGridItem(
isSelected: Boolean = false,
title: String,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
coverBadgeStart: (@Composable RowScope.() -> Unit)? = null,
coverBadgeEnd: (@Composable RowScope.() -> Unit)? = null,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
GridItemSelectable(
isSelected = isSelected,
onClick = onClick,
onLongClick = onLongClick,
) {
Column {
MangaGridCover(
cover = {
MangaCover.Book(
modifier = Modifier
.fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
data = coverData,
)
},
badgesStart = coverBadgeStart,
badgesEnd = coverBadgeEnd,
)
GridItemTitle(
modifier = Modifier.padding(4.dp),
title = title,
style = MaterialTheme.typography.titleSmall,
)
}
}
}
/**
* Common cover layout to add contents to be drawn on top of the cover.
*/
@Composable
private fun MangaGridCover(
modifier: Modifier = Modifier,
cover: @Composable BoxScope.() -> Unit = {},
badgesStart: (@Composable RowScope.() -> Unit)? = null,
badgesEnd: (@Composable RowScope.() -> Unit)? = null,
content: @Composable (BoxScope.() -> Unit)? = null,
) {
Box(
modifier = modifier
.fillMaxWidth()
.aspectRatio(MangaCover.Book.ratio),
) {
cover()
content?.invoke(this)
if (badgesStart != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopStart),
content = badgesStart,
)
}
if (badgesEnd != null) {
BadgeGroup(
modifier = Modifier
.padding(4.dp)
.align(Alignment.TopEnd),
content = badgesEnd,
)
}
}
}
@Composable
private fun GridItemTitle(
modifier: Modifier,
title: String,
style: TextStyle,
) {
Text(
modifier = modifier,
text = title,
fontSize = 12.sp,
lineHeight = 18.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = style,
)
}
/**
* Wrapper for grid items to handle selection state, click and long click.
*/
@Composable
private fun GridItemSelectable(
modifier: Modifier = Modifier,
isSelected: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
content: @Composable () -> Unit,
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary)
.padding(4.dp),
) {
val contentColor = if (isSelected) {
MaterialTheme.colorScheme.onSecondary
} else {
LocalContentColor.current
}
CompositionLocalProvider(LocalContentColor provides contentColor) {
content()
}
}
}
/**
* @see GridItemSelectable
*/
private fun Modifier.selectedOutline(
isSelected: Boolean,
color: Color,
): Modifier {
class SelectedOutlineNode(var selected: Boolean, var color: Color) : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
if (selected) drawRect(color)
drawContent()
}
}
return this then modifierElementOf(
params = isSelected.hashCode() + color.hashCode(),
create = { SelectedOutlineNode(isSelected, color) },
update = {
it.selected = isSelected
it.color = color
},
definitions = {
name = "selectionOutline"
properties["isSelected"] = isSelected
properties["color"] = color
},
)
}
/**
* Layout of list item.
*/
@Composable
fun MangaListItem(
isSelected: Boolean = false,
title: String,
coverData: eu.kanade.domain.manga.model.MangaCover,
coverAlpha: Float = 1f,
badge: @Composable RowScope.() -> Unit,
onLongClick: () -> Unit,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.selectedBackground(isSelected)
.height(56.dp)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover.Square(
modifier = Modifier
.fillMaxHeight()
.alpha(coverAlpha),
data = coverData,
)
Text(
text = title,
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
BadgeGroup(content = badge)
}
}