Epub page turn animation (#23)
* Added realistic page turn animation option in epub pagination mode * Added volume key navigation support for paginated render mode and added page turn animation to "tap to change page" and "volume button to change page"
This commit is contained in:
parent
7d26e10c07
commit
1ce353ad44
6 changed files with 1203 additions and 926 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -18,3 +18,6 @@ local.properties
|
|||
google-services.json
|
||||
.kotlin/
|
||||
.idea/
|
||||
CLA.md
|
||||
CONTRIBUTING.md
|
||||
cla.yml
|
||||
|
|
@ -25,8 +25,8 @@ android {
|
|||
applicationId = "com.aryan.reader"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 34
|
||||
versionName = "1.0.33"
|
||||
versionCode = 35
|
||||
versionName = "1.0.34"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
|
|
|
|||
|
|
@ -136,12 +136,14 @@ fun EpubReaderTopBar(
|
|||
isTtsActive: Boolean,
|
||||
tapToNavigateEnabled: Boolean,
|
||||
volumeScrollEnabled: Boolean,
|
||||
isPageTurnAnimationEnabled: Boolean,
|
||||
onNavigateBack: () -> Unit,
|
||||
onCloseSearch: () -> Unit,
|
||||
onChangeRenderMode: (RenderMode) -> Unit,
|
||||
onToggleBookmark: () -> Unit,
|
||||
onToggleTapToNavigate: (Boolean) -> Unit,
|
||||
onToggleVolumeScroll: (Boolean) -> Unit,
|
||||
onTogglePageTurnAnimation: (Boolean) -> Unit,
|
||||
onStartAutoScroll: () -> Unit,
|
||||
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -232,8 +234,13 @@ fun EpubReaderTopBar(
|
|||
)
|
||||
HorizontalDivider()
|
||||
DropdownMenuItem(
|
||||
text = { Text("Volume Button Scrolling") },
|
||||
enabled = currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
||||
text = {
|
||||
Text(
|
||||
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) "Volume Button Scrolling"
|
||||
else "Volume Button Page Turn"
|
||||
)
|
||||
},
|
||||
enabled = true,
|
||||
onClick = {
|
||||
onToggleVolumeScroll(!volumeScrollEnabled)
|
||||
showMoreMenu = false
|
||||
|
|
@ -241,6 +248,18 @@ fun EpubReaderTopBar(
|
|||
trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") }
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Realistic Page Turns") },
|
||||
enabled = currentRenderMode == RenderMode.PAGINATED,
|
||||
onClick = {
|
||||
onTogglePageTurnAnimation(!isPageTurnAnimationEnabled)
|
||||
showMoreMenu = false
|
||||
},
|
||||
trailingIcon = { if (isPageTurnAnimationEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") }
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text("Auto Scroll") },
|
||||
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
|
|
@ -175,6 +176,17 @@ private const val AUTO_SCROLL_LOCKED_KEY = "auto_scroll_locked"
|
|||
private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider"
|
||||
private const val AUTO_SCROLL_MIN_SPEED_KEY = "auto_scroll_min_speed"
|
||||
private const val AUTO_SCROLL_MAX_SPEED_KEY = "auto_scroll_max_speed"
|
||||
private const val PAGE_TURN_ANIMATION_KEY = "page_turn_animation_enabled"
|
||||
|
||||
private fun savePageTurnAnimationSetting(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit { putBoolean(PAGE_TURN_ANIMATION_KEY, isEnabled) }
|
||||
}
|
||||
|
||||
private fun loadPageTurnAnimationSetting(context: Context): Boolean {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getBoolean(PAGE_TURN_ANIMATION_KEY, false)
|
||||
}
|
||||
|
||||
private fun saveAutoScrollMinSpeed(context: Context, speed: Float) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
|
|
@ -309,6 +321,10 @@ fun EpubReaderHost(
|
|||
mutableStateOf(loadTapToNavigateSetting(context))
|
||||
}
|
||||
|
||||
var isPageTurnAnimationEnabled by remember {
|
||||
mutableStateOf(loadPageTurnAnimationSetting(context))
|
||||
}
|
||||
|
||||
val locatorConverter = remember(context) {
|
||||
LocatorConverter(
|
||||
bookCacheDao = BookCacheDatabase.getDatabase(context).bookCacheDao(),
|
||||
|
|
@ -1109,6 +1125,14 @@ fun EpubReaderHost(
|
|||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(paginatedPagerState.currentPage, currentRenderMode) {
|
||||
if (currentRenderMode == RenderMode.PAGINATED && volumeScrollEnabled) {
|
||||
delay(200)
|
||||
containerFocusRequester.requestFocus()
|
||||
Timber.d("Paginated: Page changed to ${paginatedPagerState.currentPage}, re-requesting focus for volume keys.")
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler(enabled = true) {
|
||||
if (isPageSliderVisible) {
|
||||
isPageSliderVisible = false
|
||||
|
|
@ -1467,9 +1491,31 @@ fun EpubReaderHost(
|
|||
scope.launch {
|
||||
initialScrollTargetForChapter = target
|
||||
if (target == ChapterScrollPosition.START) currentScrollYPosition = 0
|
||||
|
||||
currentChapterIndex += offset
|
||||
}
|
||||
},
|
||||
onNextPage = {
|
||||
scope.launch {
|
||||
val pageCount = paginatedPagerState.pageCount
|
||||
if (pageCount > 0) {
|
||||
val targetPage = (paginatedPagerState.currentPage + 1).coerceAtMost(pageCount - 1)
|
||||
if (targetPage != paginatedPagerState.currentPage) {
|
||||
if (isPageTurnAnimationEnabled) {
|
||||
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||
} else paginatedPagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onPrevPage = {
|
||||
scope.launch {
|
||||
val targetPage = (paginatedPagerState.currentPage - 1).coerceAtLeast(0)
|
||||
if (targetPage != paginatedPagerState.currentPage) {
|
||||
if (isPageTurnAnimationEnabled) {
|
||||
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||
} else paginatedPagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
|
@ -2170,6 +2216,7 @@ fun EpubReaderHost(
|
|||
textAlign = currentTextAlign,
|
||||
activeHighlightPalette = currentHighlightPalette,
|
||||
onUpdatePalette = onUpdateHighlightPalette,
|
||||
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||
ttsHighlightInfo = TtsHighlightInfo(
|
||||
text = ttsState.currentText ?: "",
|
||||
cfi = ttsState.sourceCfi ?: "",
|
||||
|
|
@ -2182,9 +2229,15 @@ fun EpubReaderHost(
|
|||
},
|
||||
onTap = { tapOffset ->
|
||||
Timber.d("PaginatedReaderScreen onTap called with offset: $tapOffset")
|
||||
|
||||
if (volumeScrollEnabled) {
|
||||
containerFocusRequester.requestFocus()
|
||||
}
|
||||
|
||||
if (tapOffset == null || !tapToNavigateEnabled) {
|
||||
Timber.d("Condition met: Toggling bars. tapOffset is null: ${tapOffset == null}, tapToNavigateEnabled: $tapToNavigateEnabled")
|
||||
focusManager.clearFocus()
|
||||
if (volumeScrollEnabled) containerFocusRequester.requestFocus()
|
||||
|
||||
if (showBars || showFormatAdjustmentBars) {
|
||||
showBars = false
|
||||
showFormatAdjustmentBars = false
|
||||
|
|
@ -2192,37 +2245,34 @@ fun EpubReaderHost(
|
|||
showBars = true
|
||||
}
|
||||
} else {
|
||||
// Handle zoned navigation since tapOffset is not null and the feature is enabled.
|
||||
Timber.d("Condition met: Handling zoned navigation.")
|
||||
val oneQuarterWidthPx = constraints.maxWidth / 4f
|
||||
when {
|
||||
tapOffset.x < oneQuarterWidthPx -> {
|
||||
// Left 25%: go to previous page
|
||||
Timber.d("Tapped left zone. Navigating to previous page.")
|
||||
scope.launch {
|
||||
val targetPage = (paginatedPagerState.currentPage - 1).coerceAtLeast(0)
|
||||
if (targetPage != paginatedPagerState.currentPage) {
|
||||
paginatedPagerState.scrollToPage(targetPage)
|
||||
if (isPageTurnAnimationEnabled) {
|
||||
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||
} else paginatedPagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
tapOffset.x > (constraints.maxWidth - oneQuarterWidthPx) -> {
|
||||
// Right 25%: go to next page
|
||||
Timber.d("Tapped right zone. Navigating to next page.")
|
||||
scope.launch {
|
||||
val pageCount = paginatedPagerState.pageCount
|
||||
if (pageCount > 0) {
|
||||
val targetPage = (paginatedPagerState.currentPage + 1).coerceAtMost(pageCount - 1)
|
||||
if (targetPage != paginatedPagerState.currentPage) {
|
||||
paginatedPagerState.scrollToPage(targetPage)
|
||||
if (isPageTurnAnimationEnabled) {
|
||||
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||
} else paginatedPagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Middle 50%: toggle bars
|
||||
Timber.d("Tapped middle zone. Toggling bars.")
|
||||
focusManager.clearFocus()
|
||||
if (volumeScrollEnabled) containerFocusRequester.requestFocus()
|
||||
if (showBars || showFormatAdjustmentBars) {
|
||||
showBars = false
|
||||
showFormatAdjustmentBars = false
|
||||
|
|
@ -2635,6 +2685,7 @@ fun EpubReaderHost(
|
|||
isTtsActive = isTtsSessionActive,
|
||||
tapToNavigateEnabled = tapToNavigateEnabled,
|
||||
volumeScrollEnabled = volumeScrollEnabled,
|
||||
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||
onNavigateBack = { triggerSaveAndExit() },
|
||||
onCloseSearch = {
|
||||
searchState.isSearchActive = false
|
||||
|
|
@ -2675,6 +2726,10 @@ fun EpubReaderHost(
|
|||
tapToNavigateEnabled = enabled
|
||||
saveTapToNavigateSetting(context, enabled)
|
||||
},
|
||||
onTogglePageTurnAnimation = { enabled ->
|
||||
isPageTurnAnimationEnabled = enabled
|
||||
savePageTurnAnimationSetting(context, enabled)
|
||||
},
|
||||
onToggleVolumeScroll = { enabled ->
|
||||
volumeScrollEnabled = enabled
|
||||
saveVolumeScrollSetting(context, enabled)
|
||||
|
|
|
|||
|
|
@ -100,10 +100,11 @@ fun Modifier.volumeScrollHandler(
|
|||
currentChapterIndex: Int,
|
||||
totalChapters: Int,
|
||||
onScrollBy: (Int) -> Unit,
|
||||
onNavigateChapter: (offset: Int, scrollTarget: ChapterScrollPosition) -> Unit
|
||||
onNavigateChapter: (offset: Int, scrollTarget: ChapterScrollPosition) -> Unit,
|
||||
onNextPage: () -> Unit = {},
|
||||
onPrevPage: () -> Unit = {}
|
||||
): Modifier = this.onPreviewKeyEvent { keyEvent ->
|
||||
val shouldHandle = volumeScrollEnabled &&
|
||||
renderMode == RenderMode.VERTICAL_SCROLL &&
|
||||
!isTtsActive &&
|
||||
!isMusicActive
|
||||
|
||||
|
|
@ -115,26 +116,23 @@ fun Modifier.volumeScrollHandler(
|
|||
if (!isVolumeKey) return@onPreviewKeyEvent false
|
||||
|
||||
if (keyEvent.type == KeyEventType.KeyDown) {
|
||||
val direction = if (keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) 1 else -1
|
||||
val isNext = keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
|
||||
if (renderMode == RenderMode.VERTICAL_SCROLL) {
|
||||
val direction = if (isNext) 1 else -1
|
||||
val isAtBottom = (currentScrollY + currentClientHeight) >= (currentScrollHeight - 2)
|
||||
|
||||
Timber.d("Dir: $direction, AtBottom: $isAtBottom, Y: $currentScrollY")
|
||||
|
||||
if (direction == -1 && currentScrollY == 0) {
|
||||
// Top -> Prev Chapter
|
||||
if (currentChapterIndex > 0) {
|
||||
onNavigateChapter(-1, ChapterScrollPosition.END)
|
||||
}
|
||||
if (currentChapterIndex > 0) onNavigateChapter(-1, ChapterScrollPosition.END)
|
||||
} else if (direction == 1 && isAtBottom) {
|
||||
// Bottom -> Next Chapter
|
||||
if (currentChapterIndex < totalChapters - 1) {
|
||||
onNavigateChapter(1, ChapterScrollPosition.START)
|
||||
}
|
||||
if (currentChapterIndex < totalChapters - 1) onNavigateChapter(1, ChapterScrollPosition.START)
|
||||
} else {
|
||||
// Scroll
|
||||
val scrollAmount = (currentClientHeight * 0.25).toInt() * direction
|
||||
onScrollBy(scrollAmount)
|
||||
}
|
||||
} else {
|
||||
if (isNext) onNextPage() else onPrevPage()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
|
@ -84,16 +84,23 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
|
|
@ -138,6 +145,7 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.core.net.toUri
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
|
|
@ -478,6 +486,7 @@ fun PaginatedReaderScreen(
|
|||
book: EpubBook,
|
||||
isDarkTheme: Boolean,
|
||||
pagerState: PagerState,
|
||||
isPageTurnAnimationEnabled: Boolean,
|
||||
searchQuery: String,
|
||||
fontSizeMultiplier: Float,
|
||||
lineHeightMultiplier: Float,
|
||||
|
|
@ -736,6 +745,7 @@ fun PaginatedReaderScreen(
|
|||
PaginatedReaderContent(
|
||||
uiState = uiState,
|
||||
pagerState = pagerState,
|
||||
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||
searchQuery = searchQuery,
|
||||
ttsHighlightInfo = ttsHighlightInfo,
|
||||
textStyle = textStyle,
|
||||
|
|
@ -1292,6 +1302,7 @@ private fun TextWithEmphasis(
|
|||
internal fun PaginatedReaderContent(
|
||||
uiState: PaginatedReaderUiState,
|
||||
pagerState: PagerState,
|
||||
isPageTurnAnimationEnabled: Boolean,
|
||||
searchQuery: String,
|
||||
ttsHighlightInfo: TtsHighlightInfo?,
|
||||
textStyle: TextStyle,
|
||||
|
|
@ -1380,13 +1391,15 @@ internal fun PaginatedReaderContent(
|
|||
}, onHide = { menuState = null })
|
||||
}
|
||||
|
||||
var pageTurnTouchY by remember { mutableStateOf<Float?>(null) }
|
||||
|
||||
if (uiState.isLoading) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
if (uiState.totalPageCount > 0) {
|
||||
val generation = uiState.generation
|
||||
uiState.generation
|
||||
|
||||
val realClipboard: Clipboard = LocalClipboard.current
|
||||
var isForDictionary by remember { mutableStateOf(false) }
|
||||
|
|
@ -1430,17 +1443,42 @@ internal fun PaginatedReaderContent(
|
|||
LocalTextToolbar provides textToolbar, LocalClipboard provides dictionaryClipboard
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState, modifier = Modifier.fillMaxSize()
|
||||
state = pagerState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
val down = event.changes.firstOrNull { it.pressed }
|
||||
if (down != null) {
|
||||
pageTurnTouchY = down.position.y
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beyondViewportPageCount = 1
|
||||
) { pageIndex ->
|
||||
val pageOffset = (pageIndex - pagerState.currentPage) - pagerState.currentPageOffsetFraction
|
||||
val zIndex = -pageOffset
|
||||
|
||||
val pageModifier = if (isPageTurnAnimationEnabled) {
|
||||
Modifier
|
||||
.zIndex(zIndex)
|
||||
.realisticBookPage(pagerState, pageIndex, isDarkTheme, pageTurnTouchY) // UPDATED
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
var pageContent by remember { mutableStateOf<Page?>(null) }
|
||||
var currentChapterPath by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(pageIndex, generation) {
|
||||
LaunchedEffect(pageIndex, uiState.generation) {
|
||||
pageContent = onGetPage(pageIndex)
|
||||
onGetChapterPath(pageIndex)?.let { currentChapterPath = it }
|
||||
}
|
||||
|
||||
// SelectionContainer will now use our custom dictionaryClipboardManager
|
||||
Box(modifier = Modifier.fillMaxSize().then(pageModifier)) {
|
||||
SelectionContainer(modifier = Modifier.fillMaxSize()) {
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -2463,6 +2501,7 @@ internal fun PaginatedReaderContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
menuState?.let { state ->
|
||||
Popup(popupPositionProvider = remember(state.rect, density) {
|
||||
SmartPopupPositionProvider(state.rect, density)
|
||||
|
|
@ -3282,3 +3321,166 @@ private fun RenderFlexChildBlock(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.realisticBookPage(
|
||||
pagerState: PagerState,
|
||||
pageIndex: Int,
|
||||
isDarkTheme: Boolean,
|
||||
touchY: Float?
|
||||
): Modifier = composed {
|
||||
val frontPath = remember { Path() }
|
||||
val backPath = remember { Path() }
|
||||
val reflectedScreenPath = remember { Path() }
|
||||
|
||||
this
|
||||
.graphicsLayer {
|
||||
val pageOffset = (pageIndex - pagerState.currentPage) - pagerState.currentPageOffsetFraction
|
||||
|
||||
if (pageOffset <= 1f && pageOffset > -1f) {
|
||||
translationX = -pageOffset * size.width
|
||||
}
|
||||
|
||||
if (pageOffset != 0f) {
|
||||
shadowElevation = 10f
|
||||
shape = androidx.compose.ui.graphics.RectangleShape
|
||||
clip = false
|
||||
}
|
||||
}
|
||||
.drawWithContent {
|
||||
val pageOffset = (pageIndex - pagerState.currentPage) - pagerState.currentPageOffsetFraction
|
||||
val paperColor = if (isDarkTheme) Color(0xFF121212) else Color(0xFFFFFFFF)
|
||||
|
||||
if (abs(pageOffset) < 0.001f) {
|
||||
drawRect(color = paperColor)
|
||||
drawContent()
|
||||
}
|
||||
else if (pageOffset < 0f && pageOffset > -1f) {
|
||||
val progress = -pageOffset
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
|
||||
val startY = touchY ?: h
|
||||
val centerDist = ((startY - h / 2f) / (h / 2f)).coerceIn(-1f, 1f)
|
||||
|
||||
val cornerY = if (centerDist >= 0) h else 0f
|
||||
|
||||
val dragX = w - w * 2.2f * progress
|
||||
val dragY = cornerY - h * 0.5f * progress * centerDist
|
||||
|
||||
val midX = (w + dragX) / 2f
|
||||
val midY = (cornerY + dragY) / 2f
|
||||
|
||||
val dx = w - dragX
|
||||
val dy = cornerY - dragY
|
||||
val nLen = kotlin.math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (nLen > 0f) {
|
||||
val nx = dx / nLen
|
||||
val ny = dy / nLen
|
||||
|
||||
val huge = w * 3f
|
||||
val vx = -ny
|
||||
|
||||
val p1X = midX + vx * huge
|
||||
val p1Y = midY + nx * huge
|
||||
val p2X = midX - vx * huge
|
||||
val p2Y = midY - nx * huge
|
||||
|
||||
frontPath.rewind()
|
||||
frontPath.moveTo(p1X, p1Y)
|
||||
frontPath.lineTo(p2X, p2Y)
|
||||
frontPath.lineTo(p2X - nx * huge, p2Y - ny * huge)
|
||||
frontPath.lineTo(p1X - nx * huge, p1Y - ny * huge)
|
||||
frontPath.close()
|
||||
|
||||
clipPath(frontPath) {
|
||||
drawRect(color = paperColor)
|
||||
this@drawWithContent.drawContent()
|
||||
}
|
||||
|
||||
val shadowWidth = (40.dp.toPx() * (1f - progress)).coerceAtLeast(10.dp.toPx())
|
||||
backPath.rewind()
|
||||
backPath.moveTo(p1X, p1Y)
|
||||
backPath.lineTo(p2X, p2Y)
|
||||
backPath.lineTo(p2X + nx * huge, p2Y + ny * huge)
|
||||
backPath.lineTo(p1X + nx * huge, p1Y + ny * huge)
|
||||
backPath.close()
|
||||
|
||||
val dropShadowBrush = Brush.linearGradient(
|
||||
colors = listOf(Color.Black.copy(alpha = 0.4f), Color.Transparent),
|
||||
start = Offset(midX, midY),
|
||||
end = Offset(midX + nx * shadowWidth, midY + ny * shadowWidth)
|
||||
)
|
||||
clipRect(0f, 0f, w, h) {
|
||||
drawPath(backPath, dropShadowBrush)
|
||||
}
|
||||
|
||||
fun reflect(px: Float, py: Float): Offset {
|
||||
val vX = px - midX
|
||||
val vY = py - midY
|
||||
val dist = vX * nx + vY * ny
|
||||
return Offset(px - 2 * dist * nx, py - 2 * dist * ny)
|
||||
}
|
||||
|
||||
val rTL = reflect(0f, 0f)
|
||||
val rTR = reflect(w, 0f)
|
||||
val rBR = reflect(w, h)
|
||||
val rBL = reflect(0f, h)
|
||||
|
||||
reflectedScreenPath.rewind()
|
||||
reflectedScreenPath.moveTo(rTL.x, rTL.y)
|
||||
reflectedScreenPath.lineTo(rTR.x, rTR.y)
|
||||
reflectedScreenPath.lineTo(rBR.x, rBR.y)
|
||||
reflectedScreenPath.lineTo(rBL.x, rBL.y)
|
||||
reflectedScreenPath.close()
|
||||
|
||||
clipRect(0f, 0f, w, h) {
|
||||
clipPath(frontPath) {
|
||||
val flapColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0)
|
||||
drawPath(reflectedScreenPath, color = flapColor)
|
||||
|
||||
val innerShadowWidth = shadowWidth * 0.7f
|
||||
val innerShadowBrush = Brush.linearGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.25f),
|
||||
Color.Black.copy(alpha = 0.05f),
|
||||
Color.Transparent
|
||||
),
|
||||
start = Offset(midX, midY),
|
||||
end = Offset(midX - nx * innerShadowWidth, midY - ny * innerShadowWidth)
|
||||
)
|
||||
drawPath(reflectedScreenPath, innerShadowBrush)
|
||||
|
||||
drawPath(
|
||||
path = reflectedScreenPath,
|
||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.15f) else Color.Black.copy(alpha = 0.15f),
|
||||
style = Stroke(width = 1.dp.toPx())
|
||||
)
|
||||
}
|
||||
|
||||
drawLine(
|
||||
color = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.1f),
|
||||
start = Offset(p1X, p1Y),
|
||||
end = Offset(p2X, p2Y),
|
||||
strokeWidth = 1.dp.toPx()
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
drawRect(color = paperColor)
|
||||
drawContent()
|
||||
}
|
||||
}
|
||||
else if (pageOffset > 0f && pageOffset <= 1f) {
|
||||
drawRect(color = paperColor)
|
||||
drawContent()
|
||||
val dimAlpha = (0.25f * pageOffset).coerceIn(0f, 0.4f)
|
||||
drawRect(color = Color.Black.copy(alpha = dimAlpha))
|
||||
}
|
||||
else {
|
||||
drawRect(color = paperColor)
|
||||
drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue