Fixed: WebView was not occupying full height when the BottomAppBar was hidden.

* Moved the scrolling logic of the WebView into the Compose UI, as custom scroll handling in the WebView was causing issues.
* Removed ToolbarScrollingKiwixWebView since it's no longer needed—scrolling is now managed entirely within Compose.
* Added animations for opening and closing the tab switcher view.
This commit is contained in:
MohitMaliFtechiz 2025-06-25 01:50:22 +05:30
parent 3ebb37d5cc
commit db22e245b7
5 changed files with 107 additions and 264 deletions

View File

@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.nav.destination.reader
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.View import android.view.View
@ -48,8 +47,6 @@ import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.extensions.update import org.kiwix.kiwixmobile.core.extensions.update
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.CoreWebViewClient
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment import org.kiwix.kiwixmobile.core.main.reader.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin
import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromExternalLaunch
@ -57,6 +54,7 @@ import org.kiwix.kiwixmobile.core.main.reader.RestoreOrigin.FromSearchScreen
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import org.kiwix.kiwixmobile.core.utils.DimenUtils.getToolbarHeight
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_FILE import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_FILE
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
@ -299,19 +297,14 @@ class KiwixReaderFragment : CoreReaderFragment() {
} }
} }
@Throws(IllegalArgumentException::class) override fun updateNavigationBarHeight(toolbarOffset: Float) {
override fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? { // if no activity exist simply return.
return ToolbarScrollingKiwixWebView( if (activity == null) return
requireContext(), activity?.findViewById<BottomNavigationView>(R.id.bottom_nav_view)?.let { view ->
this, val toolbarHeightPx = activity?.getToolbarHeight() ?: 0f
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"), val offsetFactor = view.height / toolbarHeightPx.toFloat()
requireNotNull(readerScreenState.value.fullScreenItem.second), view.translationY = -1 * toolbarOffset * offsetFactor
CoreWebViewClient(this, requireNotNull(zimReaderContainer)), }
onToolbarOffsetChanged = { offsetY -> toolbarOffsetY.value = offsetY },
onBottomAppBarOffsetChanged = { bottomOffsetY -> bottomAppBarOffsetY.value = bottomOffsetY },
sharedPreferenceUtil = requireNotNull(sharedPreferenceUtil),
parentNavigationBar = requireActivity().findViewById(R.id.bottom_nav_view)
)
} }
override fun onFullscreenVideoToggled(isFullScreen: Boolean) { override fun onFullscreenVideoToggled(isFullScreen: Boolean) {

View File

@ -13,7 +13,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID> <ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID> <ID>LongParameterList:Repository.kt$Repository$( private val libkiwixBookOnDisk: LibkiwixBookOnDisk, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, videoView: ViewGroup?, webViewClient: CoreWebViewClient, private val onToolbarOffsetChanged: ((Float) -> Unit)? = null, private val onBottomAppBarOffsetChanged: ((Float) -> Unit)? = null, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID> <ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, videoView: ViewGroup?, webViewClient: CoreWebViewClient, sharedPreferenceUtil: SharedPreferenceUtil )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID> <ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID> <ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID> <ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>

View File

@ -1,136 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.COMPOSE_BOTTOM_APP_BAR_DEFAULT_HEIGHT
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.COMPOSE_TOOLBAR_DEFAULT_HEIGHT
import org.kiwix.kiwixmobile.core.utils.DimenUtils.dpToPx
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import kotlin.math.max
import kotlin.math.min
@SuppressLint("ViewConstructor")
@Suppress("UnusedPrivateProperty")
class ToolbarScrollingKiwixWebView @JvmOverloads constructor(
context: Context,
callback: WebViewCallback,
attrs: AttributeSet,
videoView: ViewGroup?,
webViewClient: CoreWebViewClient,
private val onToolbarOffsetChanged: ((Float) -> Unit)? = null,
private val onBottomAppBarOffsetChanged: ((Float) -> Unit)? = null,
sharedPreferenceUtil: SharedPreferenceUtil,
private val parentNavigationBar: View? = null
) : KiwixWebView(
context,
callback,
attrs,
videoView,
webViewClient,
sharedPreferenceUtil
) {
private val toolbarHeight = context.dpToPx(COMPOSE_TOOLBAR_DEFAULT_HEIGHT)
private val bottomAppBarHeightPx = context.dpToPx(COMPOSE_BOTTOM_APP_BAR_DEFAULT_HEIGHT)
private var startY = 0f
private var currentOffset = 0f
init {
fixInitalScrollingIssue()
}
/**
* Adjusts the internal offset of the WebView based on scroll delta.
*
* Positive scrollDelta = user scrolling down (hide UI)
* Negative scrollDelta = user scrolling up (show UI)
*/
private fun moveToolbar(scrollDelta: Int): Boolean {
val newOffset = when {
scrollDelta > 0 -> max(-toolbarHeight.toFloat(), currentOffset - scrollDelta)
else -> min(0f, currentOffset - scrollDelta)
}
if (newOffset != currentOffset) {
currentOffset = newOffset
notifyOffsetChanged(newOffset)
return true
}
return false
}
/**
* Notifies Compose UI about toolbar offset.
*/
private fun notifyOffsetChanged(offset: Float) {
onToolbarOffsetChanged?.invoke(offset)
// Compute offset for bottomAppBar using height ratio
val bottomOffset = offset * -1 * (bottomAppBarHeightPx.toFloat() / toolbarHeight)
onBottomAppBarOffsetChanged?.invoke(bottomOffset)
// Optional: Animate parent navigation bar (if still using it)
parentNavigationBar?.let { view ->
val offsetFactor = view.height / toolbarHeight.toFloat()
view.translationY = offset * -1 * offsetFactor
}
// Adjust WebView position to prevent layout jump
this.translationY = offset
}
/**
* The webview needs to be scrolled with 0 to not be slightly hidden on startup.
* See https://github.com/kiwix/kiwix-android/issues/2304 for issue description.
*/
private fun fixInitalScrollingIssue() {
moveToolbar(0)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
if (sharedPreferenceUtil.prefFullScreen) return super.onTouchEvent(event)
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
startY = event.rawY
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1) {
val diffY = (event.rawY - startY).toInt()
startY = event.rawY
if (moveToolbar(-diffY)) {
event.offsetLocation(0f, -diffY.toFloat())
return super.onTouchEvent(event)
}
}
}
}
return super.onTouchEvent(event)
}
}

View File

@ -148,7 +148,6 @@ import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.TableClickListener
import org.kiwix.kiwixmobile.core.main.TabsAdapter import org.kiwix.kiwixmobile.core.main.TabsAdapter
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.WebViewCallback import org.kiwix.kiwixmobile.core.main.WebViewCallback
import org.kiwix.kiwixmobile.core.main.WebViewProvider import org.kiwix.kiwixmobile.core.main.WebViewProvider
@ -226,9 +225,6 @@ abstract class CoreReaderFragment :
var drawerLayout: DrawerLayout? = null var drawerLayout: DrawerLayout? = null
protected var tableDrawerRightContainer: NavigationView? = null protected var tableDrawerRightContainer: NavigationView? = null
var contentFrame: FrameLayout? = null
var tabSwitcherRoot: View? = null var tabSwitcherRoot: View? = null
var activityMainRoot: View? = null var activityMainRoot: View? = null
@ -318,8 +314,6 @@ abstract class CoreReaderFragment :
private var isReadSelection = false private var isReadSelection = false
private var isReadAloudServiceRunning = false private var isReadAloudServiceRunning = false
private var libkiwixBook: Book? = null private var libkiwixBook: Book? = null
val toolbarOffsetY = mutableStateOf(0f)
val bottomAppBarOffsetY = mutableStateOf(0f)
protected var readerMenuState: ReaderMenuState? = null protected var readerMenuState: ReaderMenuState? = null
private var composeView: ComposeView? = null private var composeView: ComposeView? = null
@ -502,6 +496,9 @@ abstract class CoreReaderFragment :
ReaderScreen( ReaderScreen(
state = readerScreenState.value, state = readerScreenState.value,
actionMenuItems = readerMenuState?.menuItems.orEmpty(), actionMenuItems = readerMenuState?.menuItems.orEmpty(),
onBottomScrollOffsetChanged = { offset ->
updateNavigationBarHeight(offset)
},
navigationIcon = { navigationIcon = {
NavigationIcon( NavigationIcon(
iconItem = navigationIcon(), iconItem = navigationIcon(),
@ -509,9 +506,7 @@ abstract class CoreReaderFragment :
onClick = { navigationIconClick() }, onClick = { navigationIconClick() },
iconTint = navigationIconTint() iconTint = navigationIconTint()
) )
}, }
toolbarOffsetY = toolbarOffsetY,
bottomAppBarOffsetY = bottomAppBarOffsetY
) )
DialogHost(alertDialogShower as AlertDialogShower) DialogHost(alertDialogShower as AlertDialogShower)
} }
@ -615,6 +610,17 @@ abstract class CoreReaderFragment :
) )
} }
/**
* This method is for hiding the KiwixMainActivity bottomNavigationView.
* In custom apps we do not have the bottomnavigationView so that's why this method is empty here.
*
* See the implementation in KiwixReaderFragment.
* TODO refactore this when migrating the KiwixMainActivity in compose.
*/
open fun updateNavigationBarHeight(toolbarOffset: Float) {
// Do nothing since in custom apps we do not have the bottomNavigationView.
}
private fun getVideoView() = context?.let { private fun getVideoView() = context?.let {
FrameLayout(it).apply { FrameLayout(it).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
@ -698,7 +704,6 @@ abstract class CoreReaderFragment :
fragmentReaderBinding?.let { readerBinding -> fragmentReaderBinding?.let { readerBinding ->
with(readerBinding.root) { with(readerBinding.root) {
activityMainRoot = findViewById(R.id.activity_main_root) activityMainRoot = findViewById(R.id.activity_main_root)
contentFrame = findViewById(R.id.activity_main_content_frame)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
tabSwitcherRoot = findViewById(R.id.activity_main_tab_switcher) tabSwitcherRoot = findViewById(R.id.activity_main_tab_switcher)
tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view) tabRecyclerView = findViewById(R.id.tab_switcher_recycler_view)
@ -921,29 +926,6 @@ abstract class CoreReaderFragment :
} }
} }
/**
* Sets a top margin to the web views.
*
* @param topMargin The top margin to be applied to the web views.
* Use 0 to remove the margin.
*/
protected open fun setTopMarginToWebViews(topMargin: Int) {
for (webView in webViewList) {
if (webView.parent == null) {
// Ensure that the web view has a parent before modifying its layout parameters
// This check is necessary to prevent adding the margin when the web view is not attached to a layout
// Adding the margin without a parent can cause unintended layout issues or empty
// space on top of the webView in the tabs adapter.
val frameLayout = FrameLayout(requireActivity())
// Add the web view to the frame layout
frameLayout.addView(webView)
}
val layoutParams = webView.layoutParams as FrameLayout.LayoutParams?
layoutParams?.topMargin = topMargin
webView.requestLayout()
}
}
protected fun startAnimation( protected fun startAnimation(
view: View?, view: View?,
@AnimRes anim: Int @AnimRes anim: Int
@ -969,9 +951,6 @@ abstract class CoreReaderFragment :
showSearchPlaceHolderInToolbar(false) showSearchPlaceHolderInToolbar(false)
readerMenuState?.showWebViewOptions(urlIsValid()) readerMenuState?.showWebViewOptions(urlIsValid())
selectTab(currentWebViewIndex) selectTab(currentWebViewIndex)
// Reset the top margin of web views to 0 to remove any previously set margin
// This ensures that the web views are displayed without any additional top margin for kiwix custom apps.
// setTopMarginToWebViews(0)
} }
open fun setUpDrawerToggle() { open fun setUpDrawerToggle() {
@ -1360,7 +1339,6 @@ abstract class CoreReaderFragment :
activityMainRoot = null activityMainRoot = null
tabRecyclerView = null tabRecyclerView = null
tabSwitcherRoot = null tabSwitcherRoot = null
contentFrame = null
compatCallback?.finish() compatCallback?.finish()
compatCallback = null compatCallback = null
toolbar?.setOnTouchListener(null) toolbar?.setOnTouchListener(null)
@ -1422,15 +1400,13 @@ abstract class CoreReaderFragment :
} }
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
protected open fun createWebView(attrs: AttributeSet?): ToolbarScrollingKiwixWebView? { protected open fun createWebView(attrs: AttributeSet?): KiwixWebView? {
return ToolbarScrollingKiwixWebView( return KiwixWebView(
requireContext(), requireContext(),
this, this,
attrs ?: throw IllegalArgumentException("AttributeSet must not be null"), attrs ?: throw IllegalArgumentException("AttributeSet must not be null"),
requireNotNull(readerScreenState.value.fullScreenItem.second), requireNotNull(readerScreenState.value.fullScreenItem.second),
CoreWebViewClient(this, requireNotNull(zimReaderContainer)), CoreWebViewClient(this, requireNotNull(zimReaderContainer)),
onToolbarOffsetChanged = { offsetY -> toolbarOffsetY.value = offsetY },
onBottomAppBarOffsetChanged = { bottomOffsetY -> bottomAppBarOffsetY.value = bottomOffsetY },
requireNotNull(sharedPreferenceUtil) requireNotNull(sharedPreferenceUtil)
) )
} }
@ -1505,7 +1481,6 @@ abstract class CoreReaderFragment :
private fun reopenBook() { private fun reopenBook() {
hideNoBookOpenViews() hideNoBookOpenViews()
contentFrame?.visibility = VISIBLE
readerMenuState?.showBookSpecificMenuItems() readerMenuState?.showBookSpecificMenuItems()
} }
@ -1517,7 +1492,6 @@ abstract class CoreReaderFragment :
readerScreenTitle = context?.getString(R.string.reader).orEmpty() readerScreenTitle = context?.getString(R.string.reader).orEmpty()
) )
} }
contentFrame?.visibility = GONE
hideProgressBar() hideProgressBar()
readerMenuState?.hideBookSpecificMenuItems() readerMenuState?.hideBookSpecificMenuItems()
if (shouldCloseZimBook) { if (shouldCloseZimBook) {
@ -1796,16 +1770,16 @@ abstract class CoreReaderFragment :
setUpDrawerToggle() setUpDrawerToggle()
setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
sharedPreferenceUtil?.putPrefFullScreen(false) sharedPreferenceUtil?.putPrefFullScreen(false)
updateBottomToolbarVisibility()
val window = requireActivity().window
window.decorView.closeFullScreenMode(window)
getCurrentWebView()?.requestLayout()
readerScreenState.update { readerScreenState.update {
copy( copy(
shouldShowBottomAppBar = true, shouldShowBottomAppBar = true,
shouldShowFullScreenMode = false shouldShowFullScreenMode = false
) )
} }
updateBottomToolbarVisibility()
val window = requireActivity().window
window.decorView.closeFullScreenMode(window)
getCurrentWebView()?.requestLayout()
} }
override fun openExternalUrl(intent: Intent) { override fun openExternalUrl(intent: Intent) {

View File

@ -22,9 +22,14 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.compose.animation.core.animateDpAsState 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.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -37,11 +42,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
@ -56,6 +59,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.BottomAppBarScrollBehavior
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@ -97,9 +101,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
@ -149,12 +153,15 @@ const val CONTENT_LOADING_PROGRESSBAR_TESTING_TAG = "contentLoadingProgressBarTe
fun ReaderScreen( fun ReaderScreen(
state: ReaderScreenState, state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
toolbarOffsetY: MutableState<Float>, onBottomScrollOffsetChanged: (Float) -> Unit,
bottomAppBarOffsetY: MutableState<Float>,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() } val bottomNavHeightInDp = with(LocalDensity.current) { state.bottomNavigationHeight.toDp() }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior()
LaunchedEffect(bottomAppBarScrollBehavior.state.heightOffset) {
onBottomScrollOffsetChanged(scrollBehavior.state.heightOffset)
}
KiwixDialogTheme { KiwixDialogTheme {
Scaffold( Scaffold(
snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) }, snackbarHost = { KiwixSnackbarHost(snackbarHostState = state.snackBarHostState) },
@ -162,7 +169,6 @@ fun ReaderScreen(
ReaderTopBar( ReaderTopBar(
state, state,
actionMenuItems, actionMenuItems,
toolbarOffsetY,
scrollBehavior, scrollBehavior,
navigationIcon navigationIcon
) )
@ -171,12 +177,13 @@ fun ReaderScreen(
modifier = Modifier modifier = Modifier
.systemBarsPadding() .systemBarsPadding()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection)
.nestedScroll(bottomAppBarScrollBehavior.nestedScrollConnection)
.padding(bottom = bottomNavHeightInDp) .padding(bottom = bottomNavHeightInDp)
) { paddingValues -> ) { paddingValues ->
ReaderContentLayout( ReaderContentLayout(
state, state,
Modifier.padding(paddingValues), Modifier.padding(paddingValues),
bottomAppBarOffsetY bottomAppBarScrollBehavior
) )
} }
} }
@ -188,69 +195,87 @@ fun ReaderScreen(
private fun ReaderTopBar( private fun ReaderTopBar(
state: ReaderScreenState, state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
toolbarOffsetY: MutableState<Float>,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
navigationIcon: @Composable () -> Unit, navigationIcon: @Composable () -> Unit,
) { ) {
if (!state.shouldShowFullScreenMode && !state.fullScreenItem.first) { if (!state.shouldShowFullScreenMode && !state.fullScreenItem.first) {
val animatedOffsetY by animateDpAsState(
targetValue = with(LocalDensity.current) { toolbarOffsetY.value.toDp() },
label = "ToolbarScrollOffset"
)
KiwixAppBar( KiwixAppBar(
title = if (state.showTabSwitcher) "" else state.readerScreenTitle, title = if (state.showTabSwitcher) "" else state.readerScreenTitle,
navigationIcon = navigationIcon, navigationIcon = navigationIcon,
actionMenuItems = actionMenuItems, actionMenuItems = actionMenuItems,
topAppBarScrollBehavior = scrollBehavior, topAppBarScrollBehavior = scrollBehavior,
searchBar = searchBar =
searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps), searchPlaceHolderIfActive(state.searchPlaceHolderItemForCustomApps)
modifier = Modifier.offset { IntOffset(x = ZERO, y = animatedOffsetY.roundToPx()) }
) )
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ReaderContentLayout( private fun ReaderContentLayout(
state: ReaderScreenState, state: ReaderScreenState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
bottomAppBarOffsetY: MutableState<Float> bottomAppBarScrollBehavior: BottomAppBarScrollBehavior
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
when { TabSwitcherAnimated(state)
state.showTabSwitcher -> TabSwitcherView( if (!state.showTabSwitcher) {
state.kiwixWebViewList, when {
state.currentWebViewPosition, state.isNoBookOpenInReader -> NoBookOpenView(state.onOpenLibraryButtonClicked)
state.onTabClickListener, state.fullScreenItem.first -> ShowFullScreenView(state)
state.onCloseAllTabs,
state.darkModeViewPainter
)
state.isNoBookOpenInReader -> NoBookOpenView(state.onOpenLibraryButtonClicked) else -> {
state.fullScreenItem.first -> ShowFullScreenView(state) ShowZIMFileContent(state)
ShowProgressBarIfZIMFilePageIsLoading(state)
else -> { Column(Modifier.align(Alignment.BottomCenter)) {
ShowZIMFileContent(state, bottomAppBarOffsetY) TtsControls(state)
ShowProgressBarIfZIMFilePageIsLoading(state) BottomAppBarOfReaderScreen(
Column(Modifier.align(Alignment.BottomCenter)) { state.bookmarkButtonItem,
TtsControls(state) state.previousPageButtonItem,
BottomAppBarOfReaderScreen( state.onHomeButtonClick,
state.bookmarkButtonItem, state.nextPageButtonItem,
state.previousPageButtonItem, state.tocButtonItem,
state.onHomeButtonClick, state.shouldShowBottomAppBar,
state.nextPageButtonItem, bottomAppBarScrollBehavior
state.tocButtonItem, )
state.shouldShowBottomAppBar, }
bottomAppBarOffsetY CloseFullScreenImageButton(
state.shouldShowFullScreenMode,
state.onExitFullscreenClick
) )
} }
CloseFullScreenImageButton(
state.shouldShowFullScreenMode,
state.onExitFullscreenClick
)
} }
ShowDonationLayout(state)
} }
}
}
ShowDonationLayout(state) @Composable
private fun TabSwitcherAnimated(state: ReaderScreenState) {
val transitionSpec = remember {
slideInVertically(
initialOffsetY = { -it },
animationSpec = tween(durationMillis = 300)
) + fadeIn() togetherWith
slideOutVertically(
targetOffsetY = { -it },
animationSpec = tween(durationMillis = 300)
) + fadeOut()
}
AnimatedVisibility(
visible = state.showTabSwitcher,
enter = transitionSpec.targetContentEnter,
exit = transitionSpec.initialContentExit,
modifier = Modifier.zIndex(1f),
) {
TabSwitcherView(
state.kiwixWebViewList,
state.currentWebViewPosition,
state.onTabClickListener,
state.onCloseAllTabs,
state.darkModeViewPainter
)
} }
} }
@ -325,15 +350,7 @@ private fun BoxScope.CloseFullScreenImageButton(
} }
@Composable @Composable
private fun ShowZIMFileContent( private fun ShowZIMFileContent(state: ReaderScreenState) {
state: ReaderScreenState,
bottomAppBarOffsetY: MutableState<Float>
) {
val density = LocalDensity.current
val bottomNavHeightDp = with(density) { state.bottomNavigationHeight.toDp() }
val bottomAppBarOffsetDp = with(density) { -bottomAppBarOffsetY.value.toDp() }
val totalBottomPadding = (bottomNavHeightDp + bottomAppBarOffsetDp).coerceAtLeast(ZERO.dp)
state.selectedWebView?.let { selectedWebView -> state.selectedWebView?.let { selectedWebView ->
key(selectedWebView) { key(selectedWebView) {
AndroidView( AndroidView(
@ -346,10 +363,10 @@ private fun ShowZIMFileContent(
} }
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxSize()
.fillMaxHeight() .verticalScroll(rememberScrollState())
.padding(bottom = totalBottomPadding)
) )
// TODO handle the unnecessary vertical scroll when webView page chnaged.
} }
} }
} }
@ -458,18 +475,13 @@ private fun BottomAppBarOfReaderScreen(
nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>, nextPageButtonItem: Triple<() -> Unit, () -> Unit, Boolean>,
tocButtonItem: Pair<Boolean, () -> Unit>, tocButtonItem: Pair<Boolean, () -> Unit>,
shouldShowBottomAppBar: Boolean, shouldShowBottomAppBar: Boolean,
bottomAppBarOffsetY: MutableState<Float> bottomAppBarScrollBehavior: BottomAppBarScrollBehavior
) { ) {
if (!shouldShowBottomAppBar) return if (!shouldShowBottomAppBar) return
val animatedOffsetY by animateDpAsState(
targetValue = with(LocalDensity.current) { bottomAppBarOffsetY.value.toDp() },
label = "BottomAppBarOffset"
)
BottomAppBar( BottomAppBar(
containerColor = Black, containerColor = Black,
contentColor = White, contentColor = White,
scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior(), scrollBehavior = bottomAppBarScrollBehavior,
modifier = Modifier.offset { IntOffset(ZERO, animatedOffsetY.roundToPx()) }
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier