共享元素动画优化
This commit is contained in:
@@ -5,6 +5,9 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -82,6 +85,7 @@ import io.legado.app.ui.widget.components.book.SearchBookListItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
@@ -89,7 +93,7 @@ import org.koin.compose.koinInject
|
||||
@SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class,
|
||||
ExperimentalMaterial3ExpressiveApi::class
|
||||
ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class
|
||||
)
|
||||
@Composable
|
||||
fun ExploreShowScreen(
|
||||
@@ -98,7 +102,9 @@ fun ExploreShowScreen(
|
||||
exploreUrl: String?,
|
||||
onBack: () -> Unit,
|
||||
onBookClick: (SearchBook) -> Unit,
|
||||
viewModel: ExploreShowViewModel = koinViewModel()
|
||||
viewModel: ExploreShowViewModel = koinViewModel(),
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
|
||||
LaunchedEffect(sourceUrl, exploreUrl, viewModel) {
|
||||
@@ -448,7 +454,9 @@ fun ExploreShowScreen(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = { onBookClick(item.book) },
|
||||
modifier = Modifier.animateItem()
|
||||
modifier = Modifier.animateItem(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -480,7 +488,9 @@ fun ExploreShowScreen(
|
||||
book = item.book,
|
||||
shelfState = item.shelfState,
|
||||
onClick = { onBookClick(item.book) },
|
||||
modifier = Modifier.animateItem()
|
||||
modifier = Modifier.animateItem(),
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -500,33 +510,45 @@ fun ExploreShowScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun ExploreBookItem(
|
||||
book: SearchBook,
|
||||
shelfState: BookShelfState,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
SearchBookListItem(
|
||||
book = book,
|
||||
shelfState = shelfState,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun ExploreBookGridItem(
|
||||
book: SearchBook,
|
||||
onClick: () -> Unit,
|
||||
shelfState: BookShelfState,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
SearchBookGridItem(
|
||||
book = book,
|
||||
shelfState = shelfState,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package io.legado.app.ui.book.info
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -54,6 +57,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -501,6 +505,12 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
}
|
||||
}
|
||||
|
||||
val backdropAlpha by animateFloatAsState(
|
||||
targetValue = if (showBackdropImage) 1f else 0f,
|
||||
animationSpec = tween(800),
|
||||
label = "BackdropFade"
|
||||
)
|
||||
|
||||
val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) {
|
||||
buildCoverImageRequest(
|
||||
context = context,
|
||||
@@ -516,15 +526,16 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
0.42f
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (!cover.isNullOrBlank() && showBackdropImage) {
|
||||
if (!cover.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = backdropRequest,
|
||||
imageLoader = imageLoader,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(360.dp)
|
||||
.blur(24.dp),
|
||||
.height(480.dp)
|
||||
.blur(24.dp)
|
||||
.alpha(backdropAlpha),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
@@ -540,10 +551,10 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
Brush.verticalGradient(
|
||||
colorStops = arrayOf(
|
||||
0f to Color.Transparent,
|
||||
0.18f to seedOverlay.copy(alpha = 0.10f),
|
||||
0.34f to seedOverlay.copy(alpha = 0.18f),
|
||||
0.52f to LegadoTheme.colorScheme.surface.copy(alpha = 0.82f),
|
||||
0.72f to LegadoTheme.colorScheme.surface,
|
||||
0.20f to seedOverlay.copy(alpha = 0.10f),
|
||||
0.40f to seedOverlay.copy(alpha = 0.18f),
|
||||
0.60f to LegadoTheme.colorScheme.surface.copy(alpha = 0.85f),
|
||||
0.80f to LegadoTheme.colorScheme.surface,
|
||||
1f to LegadoTheme.colorScheme.surface,
|
||||
)
|
||||
)
|
||||
@@ -687,25 +698,16 @@ private fun BookInfoHeader(
|
||||
.width(112.dp)
|
||||
.combinedClickable(onClick = onCoverClick, onLongClick = onCoverLongClick)
|
||||
) {
|
||||
val coverModifier = with(sharedTransitionScope) {
|
||||
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
|
||||
Modifier
|
||||
.width(112.dp)
|
||||
.sharedElement(
|
||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
} else {
|
||||
Modifier.width(112.dp)
|
||||
}
|
||||
}
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.getDisplayCover(),
|
||||
sourceOrigin = book.origin,
|
||||
modifier = coverModifier,
|
||||
modifier = Modifier.width(112.dp).aspectRatio(5f / 7f),
|
||||
showLoadingPlaceholder = sharedCoverKey == null,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.legado.app.ui.book.search
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -91,18 +94,21 @@ import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
viewModel: SearchViewModel,
|
||||
onBack: () -> Unit,
|
||||
onOpenBookInfo: (name: String, author: String, bookUrl: String) -> Unit,
|
||||
onOpenSourceManage: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
@@ -362,7 +368,10 @@ fun SearchScreen(
|
||||
shelfState = item.shelfState,
|
||||
onClick = {
|
||||
viewModel.onIntent(SearchIntent.OpenSearchBook(item.book))
|
||||
}
|
||||
},
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(item.book.bookUrl)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -609,7 +609,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteSearch> { route ->
|
||||
entry<MainRouteSearch>(
|
||||
metadata = NavDisplay.transitionSpec {
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
} + NavDisplay.popTransitionSpec {
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
} + NavDisplay.predictivePopTransitionSpec { _ ->
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
}
|
||||
) { route ->
|
||||
val searchViewModel = koinViewModel<SearchViewModel>()
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
@@ -657,7 +668,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
},
|
||||
onOpenSourceManage = {
|
||||
this@MainActivity.startActivity<BookSourceActivity>()
|
||||
}
|
||||
},
|
||||
sharedTransitionScope = this@SharedTransitionLayout,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -730,7 +743,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteExploreShow> { route ->
|
||||
entry<MainRouteExploreShow>(
|
||||
metadata = NavDisplay.transitionSpec {
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
} + NavDisplay.popTransitionSpec {
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
} + NavDisplay.predictivePopTransitionSpec { _ ->
|
||||
fadeIn(animationSpec = tween(300)) togetherWith
|
||||
fadeOut(animationSpec = tween(300))
|
||||
}
|
||||
) { route ->
|
||||
ExploreShowScreen(
|
||||
title = route.title ?: "探索",
|
||||
sourceUrl = route.sourceUrl,
|
||||
@@ -745,7 +769,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
bookUrl = book.bookUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
sharedTransitionScope = this@SharedTransitionLayout,
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +349,13 @@ fun MainScreen(
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
}
|
||||
),
|
||||
userScrollEnabled = true,
|
||||
beyondViewportPageCount = 1
|
||||
) { page ->
|
||||
|
||||
@@ -536,28 +536,21 @@ fun BookItem(
|
||||
}
|
||||
} else null,
|
||||
cover = { modifier ->
|
||||
val coverModifier = with(sharedTransitionScope) {
|
||||
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
|
||||
Modifier.fillMaxWidth().sharedElement(
|
||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
}
|
||||
}
|
||||
BookshelfCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.getDisplayCover(),
|
||||
isUpdating = isUpdating,
|
||||
modifier = modifier,
|
||||
coverModifier = coverModifier,
|
||||
coverModifier = Modifier.fillMaxWidth().aspectRatio(5f / 7f),
|
||||
sourceOrigin = book.origin,
|
||||
badgeText = if (layoutMode != 0) unreadText else null,
|
||||
showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
|
||||
leftBottomText = matchedSourceLabel ?: bookTypeLabel,
|
||||
showLoadingPlaceholder = sharedCoverKey == null,
|
||||
showLoadingPlaceholder = true,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
},
|
||||
title = book.name,
|
||||
|
||||
@@ -632,7 +632,12 @@ fun BookshelfScreen(
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
}
|
||||
),
|
||||
contentPadding = adaptiveContentPaddingBookshelf(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp,
|
||||
@@ -717,7 +722,13 @@ fun BookshelfScreen(
|
||||
} else {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
}
|
||||
),
|
||||
beyondViewportPageCount = 2,
|
||||
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
|
||||
) { pageIndex ->
|
||||
@@ -1078,82 +1089,98 @@ fun BookshelfPage(
|
||||
onDragFinished()
|
||||
}
|
||||
}
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(columns.coerceAtLeast(1)),
|
||||
state = gridState,
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPaddingBookshelf(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp,
|
||||
horizontal = if (isGridMode) 8.dp else 4.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
items(displayBooks, key = { it.bookUrl }) { book ->
|
||||
val isSelected = selectedBookUrls.contains(book.bookUrl)
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = book.bookUrl,
|
||||
enabled = canReorderBooks
|
||||
) {
|
||||
BookItem(
|
||||
book = book,
|
||||
modifier = Modifier.then(
|
||||
if (canReorderBooks) {
|
||||
Modifier.longPressDraggableHandle(
|
||||
onDragStarted = {
|
||||
onDragStarted(displayBooks)
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureThresholdActivate
|
||||
)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureEnd
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
layoutMode = bookshelfLayoutMode,
|
||||
isSelected = isSelected,
|
||||
gridStyle = BookshelfConfig.bookshelfGridLayout,
|
||||
isCompact = BookshelfConfig.bookshelfLayoutCompact,
|
||||
isUpdating = uiState.updatingBooks.contains(book.bookUrl),
|
||||
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
||||
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
||||
isSearchMode = uiState.isSearch,
|
||||
searchKey = uiState.searchKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
|
||||
onClick = {
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
} else {
|
||||
onBookClick(book)
|
||||
}
|
||||
},
|
||||
onLongClick = if (canReorderBooks) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(columns.coerceAtLeast(1)),
|
||||
state = gridState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
||||
}
|
||||
),
|
||||
contentPadding = adaptiveContentPaddingBookshelf(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp,
|
||||
horizontal = if (isGridMode) 8.dp else 4.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||
) {
|
||||
items(displayBooks, key = { it.bookUrl }) { book ->
|
||||
val isSelected = selectedBookUrls.contains(book.bookUrl)
|
||||
ReorderableItem(
|
||||
state = reorderableState,
|
||||
key = book.bookUrl,
|
||||
enabled = canReorderBooks
|
||||
) {
|
||||
BookItem(
|
||||
book = book,
|
||||
modifier = Modifier.then(
|
||||
if (canReorderBooks) {
|
||||
Modifier.longPressDraggableHandle(
|
||||
onDragStarted = {
|
||||
onDragStarted(displayBooks)
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureThresholdActivate
|
||||
)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(
|
||||
HapticFeedbackType.GestureEnd
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
layoutMode = bookshelfLayoutMode,
|
||||
isSelected = isSelected,
|
||||
gridStyle = BookshelfConfig.bookshelfGridLayout,
|
||||
isCompact = BookshelfConfig.bookshelfLayoutCompact,
|
||||
isUpdating = uiState.updatingBooks.contains(book.bookUrl),
|
||||
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
||||
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
||||
isSearchMode = uiState.isSearch,
|
||||
searchKey = uiState.searchKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(book.bookUrl) else null,
|
||||
onClick = {
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
} else {
|
||||
onBookLongClick(book)
|
||||
onBookClick(book)
|
||||
}
|
||||
},
|
||||
onLongClick = if (canReorderBooks) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
} else {
|
||||
onBookLongClick(book)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package io.legado.app.ui.widget.components.book
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -18,8 +21,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Shuffle
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -34,7 +35,7 @@ import androidx.compose.ui.unit.sp
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.domain.model.BookShelfState
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.cover.Cover
|
||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -54,47 +55,33 @@ fun SearchBookListItem(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun SearchBookListItem(
|
||||
book: SearchBook,
|
||||
shelfState: BookShelfState,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
val badgeContent: (@Composable RowScope.() -> Unit)? = when (shelfState) {
|
||||
BookShelfState.IN_SHELF -> {
|
||||
{
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "已在书架",
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BookShelfState.SAME_NAME_AUTHOR -> {
|
||||
{
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Icons.Default.Shuffle,
|
||||
contentDescription = "同名书籍",
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BookShelfState.NOT_IN_SHELF -> null
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Cover(
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.coverUrl,
|
||||
modifier = Modifier.width(72.dp),
|
||||
badgeContent = badgeContent,
|
||||
modifier = Modifier.width(72.dp).aspectRatio(5f / 7f),
|
||||
sourceOrigin = book.origin,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
showLoadingPlaceholder = sharedCoverKey == null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
@@ -180,45 +167,35 @@ fun SearchBookGridItem(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun SearchBookGridItem(
|
||||
book: SearchBook,
|
||||
shelfState: BookShelfState,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
val badgeText: String? = when (shelfState) {
|
||||
BookShelfState.IN_SHELF -> "已在书架"
|
||||
BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
|
||||
BookShelfState.NOT_IN_SHELF -> null
|
||||
}
|
||||
|
||||
val badgeContent: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) {
|
||||
{
|
||||
AppText(
|
||||
text = badgeText,
|
||||
style = LegadoTheme.typography.labelSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 9.sp,
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(IntrinsicSize.Min)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Cover(
|
||||
CoilBookCover(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
path = book.coverUrl,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(12 / 17f),
|
||||
badgeContent = badgeContent,
|
||||
.aspectRatio(5f / 7f),
|
||||
sourceOrigin = book.origin,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
showLoadingPlaceholder = sharedCoverKey == null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package io.legado.app.ui.widget.components.cover
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -14,6 +17,7 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun BookshelfCover(
|
||||
name: String?,
|
||||
@@ -28,6 +32,9 @@ fun BookshelfCover(
|
||||
sourceOrigin: String? = null,
|
||||
onLoadFinish: (() -> Unit)? = null,
|
||||
showLoadingPlaceholder: Boolean = true,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
CoilBookCover(
|
||||
@@ -38,6 +45,9 @@ fun BookshelfCover(
|
||||
sourceOrigin = sourceOrigin,
|
||||
onLoadFinish = onLoadFinish,
|
||||
showLoadingPlaceholder = showLoadingPlaceholder,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = sharedCoverKey,
|
||||
)
|
||||
|
||||
if (!badgeText.isNullOrEmpty()) {
|
||||
|
||||
@@ -6,11 +6,13 @@ import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.text.TextUtils
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -27,6 +29,7 @@ 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.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
@@ -34,17 +37,15 @@ import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.withSave
|
||||
import coil.compose.AsyncImage
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koin.compose.koinInject
|
||||
import io.legado.app.model.BookCover as BookCoverModel
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun CoilBookCover(
|
||||
name: String?,
|
||||
@@ -55,6 +56,9 @@ fun CoilBookCover(
|
||||
onLoadFinish: (() -> Unit)? = null,
|
||||
ignoreUseDefaultCover: Boolean = false,
|
||||
showLoadingPlaceholder: Boolean = true,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
sharedCoverKey: String? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isNight = isSystemInDarkTheme()
|
||||
@@ -62,11 +66,7 @@ fun CoilBookCover(
|
||||
val useDefault = !ignoreUseDefaultCover && CoverConfig.useDefaultCover
|
||||
val finalPath = if (useDefault) null else path
|
||||
|
||||
val randomPath = remember(
|
||||
name, author, path, isNight,
|
||||
CoverConfig.defaultCover,
|
||||
CoverConfig.defaultCoverDark
|
||||
) {
|
||||
val randomPath = remember(name, author, path, isNight) {
|
||||
BookCoverModel.getRandomDefaultPath(
|
||||
seed = name ?: author ?: path ?: "",
|
||||
isNight = isNight
|
||||
@@ -74,57 +74,46 @@ fun CoilBookCover(
|
||||
}
|
||||
|
||||
val hasCustomDefault = !randomPath.isNullOrBlank()
|
||||
|
||||
var isOnlineCoverLoaded by remember(path) { mutableStateOf(false) }
|
||||
var isFinalPathLoaded by remember(path) { mutableStateOf(false) }
|
||||
var isFinalPathLoadFinished by remember(path) { mutableStateOf(false) }
|
||||
var isPlaceholderAllowed by remember(path, showLoadingPlaceholder) {
|
||||
mutableStateOf(showLoadingPlaceholder)
|
||||
}
|
||||
LaunchedEffect(finalPath) {
|
||||
isOnlineCoverLoaded = false
|
||||
isFinalPathLoaded = false
|
||||
isFinalPathLoadFinished = finalPath == null
|
||||
}
|
||||
LaunchedEffect(finalPath, showLoadingPlaceholder) {
|
||||
if (showLoadingPlaceholder) {
|
||||
isPlaceholderAllowed = true
|
||||
} else {
|
||||
isPlaceholderAllowed = false
|
||||
delay(600)
|
||||
isPlaceholderAllowed = true
|
||||
}
|
||||
}
|
||||
|
||||
NormalCard(
|
||||
cornerRadius = 4.dp,
|
||||
containerColor = if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
LegadoTheme.colorScheme.surfaceContainerLow
|
||||
} else
|
||||
LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0f)
|
||||
){
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.aspectRatio(5f / 7f)
|
||||
.then(
|
||||
if (CoverConfig.coverShowShadow) {
|
||||
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||
Box(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (CoverConfig.coverShowShadow) {
|
||||
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||
} else Modifier
|
||||
)
|
||||
.background(
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
LegadoTheme.colorScheme.surfaceContainerLow
|
||||
} else Color.Transparent,
|
||||
RoundedCornerShape(4.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
) {
|
||||
val imageContentModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
with(sharedTransitionScope) {
|
||||
if (this != null && animatedVisibilityScope != null && sharedCoverKey != null) {
|
||||
Modifier.sharedElement(
|
||||
sharedContentState = rememberSharedContentState(sharedCoverKey),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
renderInOverlayDuringTransition = true
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val iconSizeDp = with(density) {
|
||||
(minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (hasCustomDefault && !isFinalPathLoaded) {
|
||||
Box(modifier = imageContentModifier) {
|
||||
if (hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
AsyncImage(
|
||||
model = buildCoverImageRequest(
|
||||
context = context,
|
||||
data = randomPath,
|
||||
sourceOrigin = null,
|
||||
loadOnlyWifi = false,
|
||||
crossfade = true,
|
||||
crossfade = showLoadingPlaceholder,
|
||||
),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
@@ -140,7 +129,7 @@ fun CoilBookCover(
|
||||
data = finalPath,
|
||||
sourceOrigin = sourceOrigin,
|
||||
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
||||
crossfade = true,
|
||||
crossfade = showLoadingPlaceholder,
|
||||
),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
@@ -148,13 +137,10 @@ fun CoilBookCover(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onSuccess = {
|
||||
isOnlineCoverLoaded = true
|
||||
isFinalPathLoaded = true
|
||||
isFinalPathLoadFinished = true
|
||||
onLoadFinish?.invoke()
|
||||
},
|
||||
onError = {
|
||||
isOnlineCoverLoaded = false
|
||||
isFinalPathLoadFinished = true
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
)
|
||||
@@ -163,28 +149,24 @@ fun CoilBookCover(
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showPlaceholder = isPlaceholderAllowed && (showLoadingPlaceholder || isFinalPathLoadFinished)
|
||||
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded && showPlaceholder) {
|
||||
if (showLoadingPlaceholder && !isOnlineCoverLoaded) {
|
||||
if (!hasCustomDefault) {
|
||||
Icon(
|
||||
Icons.Default.Book,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(iconSizeDp)
|
||||
.fillMaxSize(0.35f)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isOnlineCoverLoaded && showPlaceholder) {
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
author = author,
|
||||
isNight = isNight
|
||||
)
|
||||
}
|
||||
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
author = author,
|
||||
isNight = isNight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,8 +178,7 @@ private fun CoverTextOverlay(
|
||||
isNight: Boolean
|
||||
) {
|
||||
val showName = if (isNight) CoverConfig.coverShowNameN else CoverConfig.coverShowName
|
||||
val showAuthor =
|
||||
(if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
|
||||
val showAuthor = (if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
|
||||
|
||||
if (!showName && !showAuthor) return
|
||||
|
||||
@@ -231,9 +212,7 @@ private fun CoverTextOverlay(
|
||||
|
||||
if (isHorizontal) {
|
||||
val maxWidth = (viewWidth * 0.8f).toInt()
|
||||
val textPaint = TextPaint(paint).apply {
|
||||
textAlign = Paint.Align.LEFT
|
||||
}
|
||||
val textPaint = TextPaint(paint).apply { textAlign = Paint.Align.LEFT }
|
||||
val layout = StaticLayout.Builder
|
||||
.obtain(name, 0, name.length, textPaint, maxWidth)
|
||||
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||
@@ -296,12 +275,7 @@ private fun CoverTextOverlay(
|
||||
}
|
||||
}
|
||||
if (isHorizontal) {
|
||||
val authorText = TextUtils.ellipsize(
|
||||
author,
|
||||
TextPaint(paint),
|
||||
viewWidth * 0.9f,
|
||||
TextUtils.TruncateAt.END
|
||||
)
|
||||
val authorText = TextUtils.ellipsize(author, TextPaint(paint), viewWidth * 0.9f, TextUtils.TruncateAt.END)
|
||||
if (CoverConfig.coverShowStroke) {
|
||||
val strokePaint = Paint(paint).apply {
|
||||
color = Color.White.toArgb()
|
||||
@@ -309,19 +283,9 @@ private fun CoverTextOverlay(
|
||||
strokeWidth = paint.textSize / 10
|
||||
clearShadowLayer()
|
||||
}
|
||||
nativeCanvas.drawText(
|
||||
authorText.toString(),
|
||||
viewWidth / 2,
|
||||
viewHeight * 0.75f,
|
||||
strokePaint
|
||||
)
|
||||
nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, strokePaint)
|
||||
}
|
||||
nativeCanvas.drawText(
|
||||
authorText.toString(),
|
||||
viewWidth / 2,
|
||||
viewHeight * 0.75f,
|
||||
paint
|
||||
)
|
||||
nativeCanvas.drawText(authorText.toString(), viewWidth / 2, viewHeight * 0.75f, paint)
|
||||
} else {
|
||||
val startX = viewWidth * 0.84f
|
||||
val fm = paint.fontMetrics
|
||||
|
||||
Reference in New Issue
Block a user