Skip to content

Commit 3670f7c

Browse files
authored
Improve loading of EPUB reflowable resources (#10)
1 parent f78df70 commit 3670f7c

File tree

6 files changed

+136
-80
lines changed

6 files changed

+136
-80
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ captures/
5252
.idea/modules.xml
5353
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
5454
.idea/navEditor.xml
55+
# Additional IntelliJ files
56+
.idea/misc.xml
57+
.idea/deploymentTargetDropDown.xml
5558

5659
# Keystore files
5760
*.jks

CHANGELOG.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,20 @@ All notable changes to this project will be documented in this file. Take a look
66

77
## [Unreleased]
88

9-
### Navigator
9+
### Changed
10+
11+
#### Navigator
12+
13+
* Improve loading of EPUB reflowable resources.
14+
* Resources are hidden until fully loaded and positioned.
15+
* Intermediary locators are not broadcasted as `currentLocator` anymore while loading a resource.
16+
* Improved accuracy when jumping to the middle of a large resource.
17+
* `EpubNavigatorFragment.PaginationListener.onPageLoaded()` is now called only a single time, for the currently visible page.
18+
* `VisualNavigator.Listener.onTap()` is called even when a resource is not fully loaded.
1019

11-
#### Fixed
20+
### Fixed
21+
22+
#### Navigator
1223

1324
* `EpubNavigatorFragment`'s `goForward()` and `goBackward()` are now jumping to the previous or next pages instead of resources.
1425

readium/navigator/src/main/java/org/readium/r2/navigator/R2BasicWebView.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
206206
}
207207
}
208208

209-
210209
/**
211210
* Called from the JS code when a tap is detected.
212211
* If the JS indicates the tap is being handled within the web view, don't take action,
@@ -416,10 +415,7 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
416415
}
417416

418417
fun setProperty(key: String, value: String) {
419-
runJavaScript("readium.setProperty(\"$key\", \"$value\");") {
420-
// Used to redraw highlights when user settings changed.
421-
listener.onPageLoaded()
422-
}
418+
runJavaScript("readium.setProperty(\"$key\", \"$value\");")
423419
}
424420

425421
fun removeProperty(key: String) {

readium/navigator/src/main/java/org/readium/r2/navigator/epub/EpubNavigatorFragment.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import org.readium.r2.shared.publication.epub.EpubLayout
4747
import org.readium.r2.shared.publication.presentation.presentation
4848
import org.readium.r2.shared.publication.services.isRestricted
4949
import org.readium.r2.shared.publication.services.positions
50+
import org.readium.r2.shared.publication.services.positionsByReadingOrder
5051
import kotlin.math.ceil
5152
import kotlin.reflect.KClass
5253

@@ -96,6 +97,7 @@ class EpubNavigatorFragment private constructor(
9697
EpubNavigatorViewModel.createFactory(config.decorationTemplates.copy())
9798
}
9899

100+
internal lateinit var positionsByReadingOrder: List<List<Locator>>
99101
internal lateinit var positions: List<Locator>
100102
lateinit var resourcePager: R2ViewPager
101103

@@ -126,17 +128,19 @@ class EpubNavigatorFragment private constructor(
126128
_binding = ActivityR2ViewpagerBinding.inflate(inflater, container, false)
127129
val view = binding.root
128130

129-
positions = runBlocking { publication.positions() }
131+
positionsByReadingOrder = runBlocking { publication.positionsByReadingOrder() }
132+
positions = positionsByReadingOrder.flatten()
130133
publicationIdentifier = publication.metadata.identifier ?: publication.metadata.title
131134

132135
resourcePager = binding.resourcePager
133136
resourcePager.type = Publication.TYPE.EPUB
134137

135138
if (publication.metadata.presentation.layout == EpubLayout.REFLOWABLE) {
136-
resourcesSingle = publication.readingOrder.map { link ->
139+
resourcesSingle = publication.readingOrder.mapIndexed { index, link ->
137140
PageResource.EpubReflowable(
138141
link = link,
139-
url = link.withBaseUrl(baseUrl).href
142+
url = link.withBaseUrl(baseUrl).href,
143+
positionCount = positionsByReadingOrder.getOrNull(index)?.size ?: 0
140144
)
141145
}
142146

@@ -258,7 +262,6 @@ class EpubNavigatorFragment private constructor(
258262
internal var pendingLocator: Locator? = null
259263

260264
override fun go(locator: Locator, animated: Boolean, completion: () -> Unit): Boolean {
261-
pendingLocator = locator
262265

263266
val href = locator.href
264267
// Remove anchor
@@ -288,6 +291,7 @@ class EpubNavigatorFragment private constructor(
288291
resourcePager.adapter = adapter
289292

290293
if (publication.metadata.presentation.layout == EpubLayout.REFLOWABLE) {
294+
pendingLocator = locator
291295
setCurrent(resourcesSingle)
292296
} else {
293297

@@ -394,6 +398,7 @@ class EpubNavigatorFragment private constructor(
394398
override fun onPageLoaded() {
395399
r2Activity?.onPageLoaded()
396400
paginationListener?.onPageLoaded()
401+
notifyCurrentLocation()
397402
}
398403

399404
override fun onPageChanged(pageIndex: Int, totalPages: Int, url: String) {
@@ -618,6 +623,10 @@ class EpubNavigatorFragment private constructor(
618623
debounceLocationNotificationJob = launch {
619624
delay(100L)
620625

626+
if (pendingLocator != null) {
627+
return@launch
628+
}
629+
621630
// The transition has stabilized, so we can ask the web view to refresh its current
622631
// item to reflect the current scroll position.
623632
currentFragment?.webView?.updateCurrentItem()

readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2EpubPageFragment.kt

Lines changed: 104 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package org.readium.r2.navigator.pager
1212
import android.annotation.SuppressLint
1313
import android.content.Context
1414
import android.content.SharedPreferences
15+
import android.graphics.PointF
1516
import android.os.Bundle
1617
import android.util.Base64
1718
import android.util.DisplayMetrics
@@ -23,10 +24,8 @@ import androidx.core.view.ViewCompat
2324
import androidx.fragment.app.Fragment
2425
import androidx.lifecycle.lifecycleScope
2526
import androidx.webkit.WebViewClientCompat
26-
import kotlinx.coroutines.delay
2727
import kotlinx.coroutines.flow.collectLatest
2828
import kotlinx.coroutines.launch
29-
import org.readium.r2.navigator.Navigator
3029
import org.readium.r2.navigator.R
3130
import org.readium.r2.navigator.R2BasicWebView
3231
import org.readium.r2.navigator.R2WebView
@@ -49,6 +48,9 @@ class R2EpubPageFragment : Fragment() {
4948
internal val link: Link?
5049
get() = requireArguments().getParcelable("link")
5150

51+
private val positionCount: Long
52+
get() = requireArguments().getLong("positionCount")
53+
5254
var webView: R2WebView? = null
5355
private set
5456

@@ -58,6 +60,8 @@ class R2EpubPageFragment : Fragment() {
5860
private var _binding: ViewpagerFragmentEpubBinding? = null
5961
private val binding get() = _binding!!
6062

63+
private var isLoading: Boolean = false
64+
6165
@SuppressLint("SetJavaScriptEnabled")
6266
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
6367
val navigatorFragment = parentFragment as EpubNavigatorFragment
@@ -68,6 +72,7 @@ class R2EpubPageFragment : Fragment() {
6872
val webView = binding.webView
6973
this.webView = webView
7074

75+
webView.visibility = View.INVISIBLE
7176
webView.navigator = navigatorFragment
7277
webView.listener = navigatorFragment.webViewListener
7378
webView.preferences = preferences
@@ -127,71 +132,13 @@ class R2EpubPageFragment : Fragment() {
127132

128133
webView.listener.onResourceLoaded(link, webView, url)
129134

130-
val epubNavigator = (webView.navigator as? EpubNavigatorFragment)
131-
val currentFragment: R2EpubPageFragment? =
132-
(epubNavigator?.resourcePager?.adapter as? R2PagerAdapter)?.getCurrentFragment() as? R2EpubPageFragment
133-
134-
if (currentFragment != null && this@R2EpubPageFragment.tag == currentFragment.tag) {
135-
val locator = epubNavigator.pendingLocator
136-
epubNavigator.pendingLocator = null
137-
var locations = locator?.locations
138-
139-
// TODO this seems to be needed, will need to test more
140-
if (url != null && url.indexOf("#") > 0) {
141-
val id = url.substringAfterLast("#")
142-
locations = Locator.Locations(fragments = listOf(id))
135+
// To make sure the page is properly laid out before jumping to the target locator,
136+
// we execute a dummy JavaScript and wait for the callback result.
137+
webView.evaluateJavascript("true") {
138+
viewLifecycleOwner.lifecycleScope.launchWhenCreated {
139+
onLoadPage()
143140
}
144-
145-
val currentWebView = currentFragment.webView
146-
if (currentWebView != null && locations != null) {
147-
148-
lifecycleScope.launchWhenStarted {
149-
// FIXME: We need a better way to wait, because if the value is too low it fails
150-
delay(200)
151-
152-
val text = locator?.text
153-
if (text?.highlight != null) {
154-
if (currentWebView.scrollToText(text)) {
155-
return@launchWhenStarted
156-
}
157-
158-
// The delay is necessary before falling back on the other
159-
// locations, because scrollToText() is moving the scroll position
160-
// while looking for the text snippet.
161-
delay(100)
162-
}
163-
164-
val htmlId = locations.htmlId
165-
if (htmlId != null && currentWebView.scrollToId(htmlId)) {
166-
return@launchWhenStarted
167-
}
168-
169-
var progression = locations.progression
170-
if (progression != null) {
171-
// We need to reverse the progression with RTL because the Web View
172-
// always scrolls from left to right, no matter the reading direction.
173-
progression =
174-
if (webView.scrollMode || navigatorFragment.readingProgression == ReadingProgression.LTR) progression
175-
else 1 - progression
176-
177-
if (webView.scrollMode) {
178-
currentWebView.scrollToPosition(progression)
179-
180-
} else {
181-
// Figure out the target web view "page" from the requested
182-
// progression.
183-
var item = (progression * currentWebView.numPages).roundToInt()
184-
if (navigatorFragment.readingProgression == ReadingProgression.RTL && item > 0) {
185-
item -= 1
186-
}
187-
currentWebView.setCurrentItem(item, false)
188-
}
189-
}
190-
}
191-
}
192-
193141
}
194-
webView.listener.onPageLoaded()
195142
}
196143

197144
// prevent favicon.ico to be loaded, this was causing NullPointerException in NanoHttp
@@ -237,10 +184,19 @@ class R2EpubPageFragment : Fragment() {
237184
false
238185
}
239186

240-
resourceUrl?.let { webView.loadUrl(it) }
187+
resourceUrl?.let {
188+
isLoading = true
189+
webView.loadUrl(it)
190+
}
241191

242192
setupPadding()
243193

194+
// Forward a tap event when the web view is not ready to propagate the taps. This allows
195+
// to toggle a navigation UI while a page is loading, for example.
196+
binding.root.setOnClickListenerWithPoint { _, point ->
197+
webView.listener.onTap(point)
198+
}
199+
244200
return containerView
245201
}
246202

@@ -312,15 +268,96 @@ class R2EpubPageFragment : Fragment() {
312268
internal val paddingTop: Int get() = containerView.paddingTop
313269
internal val paddingBottom: Int get() = containerView.paddingBottom
314270

271+
private val isCurrentResource: Boolean get() {
272+
val epubNavigator = webView?.navigator as? EpubNavigatorFragment ?: return false
273+
val currentFragment = (epubNavigator.resourcePager.adapter as? R2PagerAdapter)?.getCurrentFragment() as? R2EpubPageFragment ?: return false
274+
return tag == currentFragment.tag
275+
}
276+
277+
private suspend fun onLoadPage() {
278+
if (!isLoading) return
279+
isLoading = false
280+
281+
val webView = requireNotNull(webView)
282+
webView.visibility = View.VISIBLE
283+
284+
if (isCurrentResource) {
285+
val epubNavigator = requireNotNull(webView.navigator as? EpubNavigatorFragment)
286+
val locator = epubNavigator.pendingLocator
287+
epubNavigator.pendingLocator = null
288+
if (locator != null) {
289+
loadLocator(locator)
290+
}
291+
292+
webView.listener.onPageLoaded()
293+
}
294+
}
295+
296+
private suspend fun loadLocator(locator: Locator) {
297+
val webView = requireNotNull(webView)
298+
val epubNavigator = requireNotNull(webView.navigator as? EpubNavigatorFragment)
299+
300+
val text = locator.text
301+
if (text.highlight != null) {
302+
if (webView.scrollToText(text)) {
303+
return
304+
}
305+
}
306+
307+
val htmlId = locator.locations.htmlId
308+
if (htmlId != null && webView.scrollToId(htmlId)) {
309+
return
310+
}
311+
312+
var progression = locator.locations.progression
313+
if (progression != null) {
314+
// We need to reverse the progression with RTL because the Web View
315+
// always scrolls from left to right, no matter the reading direction.
316+
progression =
317+
if (webView.scrollMode || epubNavigator.readingProgression == ReadingProgression.LTR) progression
318+
else 1 - progression
319+
320+
if (webView.scrollMode) {
321+
webView.scrollToPosition(progression)
322+
323+
} else {
324+
// Figure out the target web view "page" from the requested
325+
// progression.
326+
var item = (progression * webView.numPages).roundToInt()
327+
if (epubNavigator.readingProgression == ReadingProgression.RTL && item > 0) {
328+
item -= 1
329+
}
330+
webView.setCurrentItem(item, false)
331+
}
332+
}
333+
}
334+
315335
companion object {
316-
fun newInstance(url: String, link: Link? = null): R2EpubPageFragment =
336+
fun newInstance(url: String, link: Link? = null, positionCount: Int = 0): R2EpubPageFragment =
317337
R2EpubPageFragment().apply {
318338
arguments = Bundle().apply {
319339
putString("url", url)
320340
putParcelable("link", link)
341+
putLong("positionCount", positionCount.toLong())
321342
}
322343
}
323344
}
324345
}
325346

347+
/**
348+
* Same as setOnClickListener, but will also report the tap point in the view.
349+
*/
350+
private fun View.setOnClickListenerWithPoint(action: (View, PointF) -> Unit) {
351+
var point = PointF()
326352

353+
setOnTouchListener { v, event ->
354+
if (event.action == MotionEvent.ACTION_DOWN) {
355+
point = PointF(event.x, event.y)
356+
}
357+
false
358+
}
359+
360+
setOnClickListener {
361+
action(it, point)
362+
}
363+
}

readium/navigator/src/main/java/org/readium/r2/navigator/pager/R2PagerAdapter.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import org.readium.r2.shared.publication.Link
1919
class R2PagerAdapter internal constructor(val fm: FragmentManager, private val resources: List<PageResource>) : R2FragmentPagerAdapter(fm) {
2020

2121
internal sealed class PageResource {
22-
data class EpubReflowable(val link: Link, val url: String) : PageResource()
22+
data class EpubReflowable(val link: Link, val url: String, val positionCount: Int) : PageResource()
2323
data class EpubFxl(val url1: String, val url2: String? = null) : PageResource()
2424
data class Cbz(val link: Link) : PageResource()
2525
}
@@ -52,7 +52,7 @@ class R2PagerAdapter internal constructor(val fm: FragmentManager, private val r
5252
override fun getItem(position: Int): Fragment =
5353
when (val resource = resources[position]) {
5454
is PageResource.EpubReflowable -> {
55-
R2EpubPageFragment.newInstance(resource.url, resource.link)
55+
R2EpubPageFragment.newInstance(resource.url, resource.link, positionCount = resource.positionCount)
5656
}
5757
is PageResource.EpubFxl -> {
5858
R2FXLPageFragment.newInstance(resource.url1, resource.url2)

0 commit comments

Comments
 (0)