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
|
google-services.json
|
||||||
.kotlin/
|
.kotlin/
|
||||||
.idea/
|
.idea/
|
||||||
|
CLA.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
cla.yml
|
||||||
|
|
@ -25,8 +25,8 @@ android {
|
||||||
applicationId = "com.aryan.reader"
|
applicationId = "com.aryan.reader"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 34
|
versionCode = 35
|
||||||
versionName = "1.0.33"
|
versionName = "1.0.34"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
|
|
|
||||||
|
|
@ -136,12 +136,14 @@ fun EpubReaderTopBar(
|
||||||
isTtsActive: Boolean,
|
isTtsActive: Boolean,
|
||||||
tapToNavigateEnabled: Boolean,
|
tapToNavigateEnabled: Boolean,
|
||||||
volumeScrollEnabled: Boolean,
|
volumeScrollEnabled: Boolean,
|
||||||
|
isPageTurnAnimationEnabled: Boolean,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onCloseSearch: () -> Unit,
|
onCloseSearch: () -> Unit,
|
||||||
onChangeRenderMode: (RenderMode) -> Unit,
|
onChangeRenderMode: (RenderMode) -> Unit,
|
||||||
onToggleBookmark: () -> Unit,
|
onToggleBookmark: () -> Unit,
|
||||||
onToggleTapToNavigate: (Boolean) -> Unit,
|
onToggleTapToNavigate: (Boolean) -> Unit,
|
||||||
onToggleVolumeScroll: (Boolean) -> Unit,
|
onToggleVolumeScroll: (Boolean) -> Unit,
|
||||||
|
onTogglePageTurnAnimation: (Boolean) -> Unit,
|
||||||
onStartAutoScroll: () -> Unit,
|
onStartAutoScroll: () -> Unit,
|
||||||
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -232,8 +234,13 @@ fun EpubReaderTopBar(
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Volume Button Scrolling") },
|
text = {
|
||||||
enabled = currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
Text(
|
||||||
|
if (currentRenderMode == RenderMode.VERTICAL_SCROLL) "Volume Button Scrolling"
|
||||||
|
else "Volume Button Page Turn"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
enabled = true,
|
||||||
onClick = {
|
onClick = {
|
||||||
onToggleVolumeScroll(!volumeScrollEnabled)
|
onToggleVolumeScroll(!volumeScrollEnabled)
|
||||||
showMoreMenu = false
|
showMoreMenu = false
|
||||||
|
|
@ -241,6 +248,18 @@ fun EpubReaderTopBar(
|
||||||
trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") }
|
trailingIcon = { if (volumeScrollEnabled) Icon(Icons.Default.Check, contentDescription = "Enabled") }
|
||||||
)
|
)
|
||||||
HorizontalDivider()
|
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(
|
DropdownMenuItem(
|
||||||
text = { Text("Auto Scroll") },
|
text = { Text("Auto Scroll") },
|
||||||
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
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_USE_SLIDER_KEY = "auto_scroll_use_slider"
|
||||||
private const val AUTO_SCROLL_MIN_SPEED_KEY = "auto_scroll_min_speed"
|
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 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) {
|
private fun saveAutoScrollMinSpeed(context: Context, speed: Float) {
|
||||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||||
|
|
@ -309,6 +321,10 @@ fun EpubReaderHost(
|
||||||
mutableStateOf(loadTapToNavigateSetting(context))
|
mutableStateOf(loadTapToNavigateSetting(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPageTurnAnimationEnabled by remember {
|
||||||
|
mutableStateOf(loadPageTurnAnimationSetting(context))
|
||||||
|
}
|
||||||
|
|
||||||
val locatorConverter = remember(context) {
|
val locatorConverter = remember(context) {
|
||||||
LocatorConverter(
|
LocatorConverter(
|
||||||
bookCacheDao = BookCacheDatabase.getDatabase(context).bookCacheDao(),
|
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) {
|
BackHandler(enabled = true) {
|
||||||
if (isPageSliderVisible) {
|
if (isPageSliderVisible) {
|
||||||
isPageSliderVisible = false
|
isPageSliderVisible = false
|
||||||
|
|
@ -1467,9 +1491,31 @@ fun EpubReaderHost(
|
||||||
scope.launch {
|
scope.launch {
|
||||||
initialScrollTargetForChapter = target
|
initialScrollTargetForChapter = target
|
||||||
if (target == ChapterScrollPosition.START) currentScrollYPosition = 0
|
if (target == ChapterScrollPosition.START) currentScrollYPosition = 0
|
||||||
|
|
||||||
currentChapterIndex += offset
|
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,
|
textAlign = currentTextAlign,
|
||||||
activeHighlightPalette = currentHighlightPalette,
|
activeHighlightPalette = currentHighlightPalette,
|
||||||
onUpdatePalette = onUpdateHighlightPalette,
|
onUpdatePalette = onUpdateHighlightPalette,
|
||||||
|
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||||
ttsHighlightInfo = TtsHighlightInfo(
|
ttsHighlightInfo = TtsHighlightInfo(
|
||||||
text = ttsState.currentText ?: "",
|
text = ttsState.currentText ?: "",
|
||||||
cfi = ttsState.sourceCfi ?: "",
|
cfi = ttsState.sourceCfi ?: "",
|
||||||
|
|
@ -2182,9 +2229,15 @@ fun EpubReaderHost(
|
||||||
},
|
},
|
||||||
onTap = { tapOffset ->
|
onTap = { tapOffset ->
|
||||||
Timber.d("PaginatedReaderScreen onTap called with offset: $tapOffset")
|
Timber.d("PaginatedReaderScreen onTap called with offset: $tapOffset")
|
||||||
|
|
||||||
|
if (volumeScrollEnabled) {
|
||||||
|
containerFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
|
||||||
if (tapOffset == null || !tapToNavigateEnabled) {
|
if (tapOffset == null || !tapToNavigateEnabled) {
|
||||||
Timber.d("Condition met: Toggling bars. tapOffset is null: ${tapOffset == null}, tapToNavigateEnabled: $tapToNavigateEnabled")
|
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
|
if (volumeScrollEnabled) containerFocusRequester.requestFocus()
|
||||||
|
|
||||||
if (showBars || showFormatAdjustmentBars) {
|
if (showBars || showFormatAdjustmentBars) {
|
||||||
showBars = false
|
showBars = false
|
||||||
showFormatAdjustmentBars = false
|
showFormatAdjustmentBars = false
|
||||||
|
|
@ -2192,37 +2245,34 @@ fun EpubReaderHost(
|
||||||
showBars = true
|
showBars = true
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
val oneQuarterWidthPx = constraints.maxWidth / 4f
|
||||||
when {
|
when {
|
||||||
tapOffset.x < oneQuarterWidthPx -> {
|
tapOffset.x < oneQuarterWidthPx -> {
|
||||||
// Left 25%: go to previous page
|
|
||||||
Timber.d("Tapped left zone. Navigating to previous page.")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val targetPage = (paginatedPagerState.currentPage - 1).coerceAtLeast(0)
|
val targetPage = (paginatedPagerState.currentPage - 1).coerceAtLeast(0)
|
||||||
if (targetPage != paginatedPagerState.currentPage) {
|
if (targetPage != paginatedPagerState.currentPage) {
|
||||||
paginatedPagerState.scrollToPage(targetPage)
|
if (isPageTurnAnimationEnabled) {
|
||||||
|
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||||
|
} else paginatedPagerState.scrollToPage(targetPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tapOffset.x > (constraints.maxWidth - oneQuarterWidthPx) -> {
|
tapOffset.x > (constraints.maxWidth - oneQuarterWidthPx) -> {
|
||||||
// Right 25%: go to next page
|
|
||||||
Timber.d("Tapped right zone. Navigating to next page.")
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val pageCount = paginatedPagerState.pageCount
|
val pageCount = paginatedPagerState.pageCount
|
||||||
if (pageCount > 0) {
|
if (pageCount > 0) {
|
||||||
val targetPage = (paginatedPagerState.currentPage + 1).coerceAtMost(pageCount - 1)
|
val targetPage = (paginatedPagerState.currentPage + 1).coerceAtMost(pageCount - 1)
|
||||||
if (targetPage != paginatedPagerState.currentPage) {
|
if (targetPage != paginatedPagerState.currentPage) {
|
||||||
paginatedPagerState.scrollToPage(targetPage)
|
if (isPageTurnAnimationEnabled) {
|
||||||
|
paginatedPagerState.animateScrollToPage(targetPage, animationSpec = tween(700))
|
||||||
|
} else paginatedPagerState.scrollToPage(targetPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Middle 50%: toggle bars
|
|
||||||
Timber.d("Tapped middle zone. Toggling bars.")
|
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
|
if (volumeScrollEnabled) containerFocusRequester.requestFocus()
|
||||||
if (showBars || showFormatAdjustmentBars) {
|
if (showBars || showFormatAdjustmentBars) {
|
||||||
showBars = false
|
showBars = false
|
||||||
showFormatAdjustmentBars = false
|
showFormatAdjustmentBars = false
|
||||||
|
|
@ -2635,6 +2685,7 @@ fun EpubReaderHost(
|
||||||
isTtsActive = isTtsSessionActive,
|
isTtsActive = isTtsSessionActive,
|
||||||
tapToNavigateEnabled = tapToNavigateEnabled,
|
tapToNavigateEnabled = tapToNavigateEnabled,
|
||||||
volumeScrollEnabled = volumeScrollEnabled,
|
volumeScrollEnabled = volumeScrollEnabled,
|
||||||
|
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
|
||||||
onNavigateBack = { triggerSaveAndExit() },
|
onNavigateBack = { triggerSaveAndExit() },
|
||||||
onCloseSearch = {
|
onCloseSearch = {
|
||||||
searchState.isSearchActive = false
|
searchState.isSearchActive = false
|
||||||
|
|
@ -2675,6 +2726,10 @@ fun EpubReaderHost(
|
||||||
tapToNavigateEnabled = enabled
|
tapToNavigateEnabled = enabled
|
||||||
saveTapToNavigateSetting(context, enabled)
|
saveTapToNavigateSetting(context, enabled)
|
||||||
},
|
},
|
||||||
|
onTogglePageTurnAnimation = { enabled ->
|
||||||
|
isPageTurnAnimationEnabled = enabled
|
||||||
|
savePageTurnAnimationSetting(context, enabled)
|
||||||
|
},
|
||||||
onToggleVolumeScroll = { enabled ->
|
onToggleVolumeScroll = { enabled ->
|
||||||
volumeScrollEnabled = enabled
|
volumeScrollEnabled = enabled
|
||||||
saveVolumeScrollSetting(context, enabled)
|
saveVolumeScrollSetting(context, enabled)
|
||||||
|
|
|
||||||
|
|
@ -100,10 +100,11 @@ fun Modifier.volumeScrollHandler(
|
||||||
currentChapterIndex: Int,
|
currentChapterIndex: Int,
|
||||||
totalChapters: Int,
|
totalChapters: Int,
|
||||||
onScrollBy: (Int) -> Unit,
|
onScrollBy: (Int) -> Unit,
|
||||||
onNavigateChapter: (offset: Int, scrollTarget: ChapterScrollPosition) -> Unit
|
onNavigateChapter: (offset: Int, scrollTarget: ChapterScrollPosition) -> Unit,
|
||||||
|
onNextPage: () -> Unit = {},
|
||||||
|
onPrevPage: () -> Unit = {}
|
||||||
): Modifier = this.onPreviewKeyEvent { keyEvent ->
|
): Modifier = this.onPreviewKeyEvent { keyEvent ->
|
||||||
val shouldHandle = volumeScrollEnabled &&
|
val shouldHandle = volumeScrollEnabled &&
|
||||||
renderMode == RenderMode.VERTICAL_SCROLL &&
|
|
||||||
!isTtsActive &&
|
!isTtsActive &&
|
||||||
!isMusicActive
|
!isMusicActive
|
||||||
|
|
||||||
|
|
@ -115,25 +116,22 @@ fun Modifier.volumeScrollHandler(
|
||||||
if (!isVolumeKey) return@onPreviewKeyEvent false
|
if (!isVolumeKey) return@onPreviewKeyEvent false
|
||||||
|
|
||||||
if (keyEvent.type == KeyEventType.KeyDown) {
|
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
|
||||||
val isAtBottom = (currentScrollY + currentClientHeight) >= (currentScrollHeight - 2)
|
|
||||||
|
|
||||||
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) {
|
if (direction == -1 && currentScrollY == 0) {
|
||||||
// Top -> Prev Chapter
|
if (currentChapterIndex > 0) onNavigateChapter(-1, ChapterScrollPosition.END)
|
||||||
if (currentChapterIndex > 0) {
|
} else if (direction == 1 && isAtBottom) {
|
||||||
onNavigateChapter(-1, ChapterScrollPosition.END)
|
if (currentChapterIndex < totalChapters - 1) onNavigateChapter(1, ChapterScrollPosition.START)
|
||||||
}
|
} else {
|
||||||
} else if (direction == 1 && isAtBottom) {
|
val scrollAmount = (currentClientHeight * 0.25).toInt() * direction
|
||||||
// Bottom -> Next Chapter
|
onScrollBy(scrollAmount)
|
||||||
if (currentChapterIndex < totalChapters - 1) {
|
|
||||||
onNavigateChapter(1, ChapterScrollPosition.START)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Scroll
|
if (isNext) onNextPage() else onPrevPage()
|
||||||
val scrollAmount = (currentClientHeight * 0.25).toInt() * direction
|
|
||||||
onScrollBy(scrollAmount)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue