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
|
|
@ -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,25 +116,22 @@ 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 isAtBottom = (currentScrollY + currentClientHeight) >= (currentScrollHeight - 2)
|
||||
val isNext = keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|
||||
|
||||
Timber.d("Dir: $direction, AtBottom: $isAtBottom, Y: $currentScrollY")
|
||||
if (renderMode == RenderMode.VERTICAL_SCROLL) {
|
||||
val direction = if (isNext) 1 else -1
|
||||
val isAtBottom = (currentScrollY + currentClientHeight) >= (currentScrollHeight - 2)
|
||||
|
||||
if (direction == -1 && currentScrollY == 0) {
|
||||
// Top -> Prev Chapter
|
||||
if (currentChapterIndex > 0) {
|
||||
onNavigateChapter(-1, ChapterScrollPosition.END)
|
||||
}
|
||||
} else if (direction == 1 && isAtBottom) {
|
||||
// Bottom -> Next Chapter
|
||||
if (currentChapterIndex < totalChapters - 1) {
|
||||
onNavigateChapter(1, ChapterScrollPosition.START)
|
||||
if (direction == -1 && currentScrollY == 0) {
|
||||
if (currentChapterIndex > 0) onNavigateChapter(-1, ChapterScrollPosition.END)
|
||||
} else if (direction == 1 && isAtBottom) {
|
||||
if (currentChapterIndex < totalChapters - 1) onNavigateChapter(1, ChapterScrollPosition.START)
|
||||
} else {
|
||||
val scrollAmount = (currentClientHeight * 0.25).toInt() * direction
|
||||
onScrollBy(scrollAmount)
|
||||
}
|
||||
} else {
|
||||
// Scroll
|
||||
val scrollAmount = (currentClientHeight * 0.25).toInt() * direction
|
||||
onScrollBy(scrollAmount)
|
||||
if (isNext) onNextPage() else onPrevPage()
|
||||
}
|
||||
}
|
||||
true
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue