Skip to content

Commit d24f1d2

Browse files
authored
EPUB Navigator: overridable drag gestures (#106)
1 parent 75b5f80 commit d24f1d2

File tree

10 files changed

+208
-25
lines changed

10 files changed

+208
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ All notable changes to this project will be documented in this file. Take a look
3838
* See the [pull request #80](https://github.com/readium/kotlin-toolkit/pull/80) for the differences with the previous audiobook navigator.
3939
* This navigator is located in its own module `readium-navigator-media2`. You will need to add it to your dependencies to use it.
4040
* The Test App demonstrates how to use the new audiobook navigator, see `MediaService` and `AudioReaderFragment`.
41+
* (*experimental*) The EPUB navigator now supports overridable drag gestures. See `VisualNavigator.Listener`.
4142

4243
### Deprecated
4344

readium/navigator/src/main/assets/_scripts/src/gestures.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { handleDecorationClickEvent } from "./decorator";
88

99
window.addEventListener("DOMContentLoaded", function () {
1010
document.addEventListener("click", onClick, false);
11+
bindDragGesture(document);
1112
});
1213

1314
function onClick(event) {
@@ -39,6 +40,70 @@ function onClick(event) {
3940
}
4041
}
4142

43+
function bindDragGesture(element) {
44+
// passive: false is necessary to be able to prevent the default behavior.
45+
element.addEventListener("touchstart", onStart, { passive: false });
46+
element.addEventListener("touchend", onEnd, { passive: false });
47+
element.addEventListener("touchmove", onMove, { passive: false });
48+
49+
var state = undefined;
50+
var isStartingDrag = false;
51+
const pixelRatio = window.devicePixelRatio;
52+
53+
function onStart(event) {
54+
isStartingDrag = true;
55+
56+
const startX = event.touches[0].clientX * pixelRatio;
57+
const startY = event.touches[0].clientY * pixelRatio;
58+
state = {
59+
defaultPrevented: event.defaultPrevented,
60+
startX: startX,
61+
startY: startY,
62+
currentX: startX,
63+
currentY: startY,
64+
offsetX: 0,
65+
offsetY: 0,
66+
interactiveElement: nearestInteractiveElement(event.target),
67+
};
68+
}
69+
70+
function onMove(event) {
71+
if (!state) return;
72+
73+
state.currentX = event.touches[0].clientX * pixelRatio;
74+
state.currentY = event.touches[0].clientY * pixelRatio;
75+
state.offsetX = state.currentX - state.startX;
76+
state.offsetY = state.currentY - state.startY;
77+
78+
var shouldPreventDefault = false;
79+
// Wait for a movement of at least 6 pixels before reporting a drag.
80+
if (isStartingDrag) {
81+
if (Math.abs(state.offsetX) >= 6 || Math.abs(state.offsetY) >= 6) {
82+
isStartingDrag = false;
83+
shouldPreventDefault = Android.onDragStart(JSON.stringify(state));
84+
}
85+
} else {
86+
shouldPreventDefault = Android.onDragMove(JSON.stringify(state));
87+
}
88+
89+
if (shouldPreventDefault) {
90+
event.stopPropagation();
91+
event.preventDefault();
92+
}
93+
}
94+
95+
function onEnd(event) {
96+
if (!state) return;
97+
98+
const shouldPreventDefault = Android.onDragEnd(JSON.stringify(state));
99+
if (shouldPreventDefault) {
100+
event.stopPropagation();
101+
event.preventDefault();
102+
}
103+
state = undefined;
104+
}
105+
}
106+
42107
// See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling
43108
function nearestInteractiveElement(element) {
44109
var interactiveTags = [

readium/navigator/src/main/assets/readium/scripts/readium-fixed.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

readium/navigator/src/main/assets/readium/scripts/readium-reflowable.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,33 @@ interface VisualNavigator : Navigator {
120120
* The [point] is relative to the navigator's view.
121121
*/
122122
fun onTap(point: PointF): Boolean = false
123+
124+
/**
125+
* Called when the user starts dragging the content, but nothing handled the event
126+
* internally.
127+
*
128+
* The points are relative to the navigator's view.
129+
*/
130+
@ExperimentalDragGesture
131+
fun onDragStart(startPoint: PointF, offset: PointF): Boolean = false
132+
133+
/**
134+
* Called when the user continues dragging the content, but nothing handled the event
135+
* internally.
136+
*
137+
* The points are relative to the navigator's view.
138+
*/
139+
@ExperimentalDragGesture
140+
fun onDragMove(startPoint: PointF, offset: PointF): Boolean = false
141+
142+
/**
143+
* Called when the user stops dragging the content, but nothing handled the event
144+
* internally.
145+
*
146+
* The points are relative to the navigator's view.
147+
*/
148+
@ExperimentalDragGesture
149+
fun onDragEnd(startPoint: PointF, offset: PointF): Boolean = false
123150
}
124151
}
125152

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ annotation class ExperimentalDecorator
2121
@Retention(AnnotationRetention.BINARY)
2222
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
2323
annotation class ExperimentalAudiobook
24+
25+
@RequiresOptIn(
26+
level = RequiresOptIn.Level.WARNING,
27+
message = "The new dragging gesture is still experimental. The API may be changed in the future without notice."
28+
)
29+
@Retention(AnnotationRetention.BINARY)
30+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
31+
annotation class ExperimentalDragGesture

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
5353
fun onPageEnded(end: Boolean)
5454
fun onScroll()
5555
fun onTap(point: PointF): Boolean
56+
fun onDragStart(event: DragEvent): Boolean
57+
fun onDragMove(event: DragEvent): Boolean
58+
fun onDragEnd(event: DragEvent): Boolean
5659
fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean = false
5760
fun onProgressionChanged()
5861
fun onHighlightActivated(id: String)
@@ -362,6 +365,71 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
362365
return true
363366
}
364367

368+
@android.webkit.JavascriptInterface
369+
fun onDragStart(eventJson: String): Boolean {
370+
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
371+
?: return false
372+
373+
return runBlocking(uiScope.coroutineContext) { listener.onDragStart(event) }
374+
}
375+
376+
@android.webkit.JavascriptInterface
377+
fun onDragMove(eventJson: String): Boolean {
378+
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
379+
?: return false
380+
381+
return runBlocking(uiScope.coroutineContext) { listener.onDragMove(event) }
382+
}
383+
384+
@android.webkit.JavascriptInterface
385+
fun onDragEnd(eventJson: String): Boolean {
386+
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
387+
?: return false
388+
389+
return runBlocking(uiScope.coroutineContext) { listener.onDragEnd(event) }
390+
}
391+
392+
/** Produced by gestures.js */
393+
data class DragEvent(
394+
val defaultPrevented: Boolean,
395+
val startPoint: PointF,
396+
val currentPoint: PointF,
397+
val offset: PointF,
398+
val interactiveElement: String?
399+
) {
400+
internal val isValid: Boolean get() =
401+
!defaultPrevented && (interactiveElement == null)
402+
403+
companion object {
404+
fun fromJSONObject(obj: JSONObject?): DragEvent? {
405+
obj ?: return null
406+
407+
val x = obj.optDouble("x").toFloat()
408+
val y = obj.optDouble("y").toFloat()
409+
410+
return DragEvent(
411+
defaultPrevented = obj.optBoolean("defaultPrevented"),
412+
startPoint = PointF(
413+
obj.optDouble("startX").toFloat(),
414+
obj.optDouble("startY").toFloat()
415+
),
416+
currentPoint = PointF(
417+
obj.optDouble("currentX").toFloat(),
418+
obj.optDouble("currentY").toFloat()
419+
),
420+
offset = PointF(
421+
obj.optDouble("offsetX").toFloat(),
422+
obj.optDouble("offsetY").toFloat()
423+
),
424+
interactiveElement = obj.optNullableString("interactiveElement")
425+
)
426+
}
427+
428+
fun fromJSON(json: String): DragEvent? =
429+
fromJSONObject(tryOrNull { JSONObject(json) })
430+
}
431+
}
432+
365433
@android.webkit.JavascriptInterface
366434
fun getViewportWidth(): Int = width
367435

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context,
698698
val pointerIndex = ev.findPointerIndex(mActivePointerId)
699699
val x = ev.getX(pointerIndex)
700700
val xDiff = abs(x - mLastMotionX)
701-
if (DEBUG) Timber.v("Moved x to $x diff=$xDiff")
702701

703702
if (xDiff > mTouchSlop) {
704703
if (DEBUG) Timber.v("Starting drag!")

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

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ class EpubNavigatorFragment private constructor(
373373
?: return null
374374

375375
val rect = json.optRectF("rect")
376-
?.apply { adjustToViewport() }
376+
?.run { adjustedToViewport() }
377377

378378
return Selection(
379379
locator = currentLocator.value.copy(
@@ -387,17 +387,15 @@ class EpubNavigatorFragment private constructor(
387387
run(viewModel.clearSelection())
388388
}
389389

390-
private fun PointF.adjustToViewport() {
390+
private fun PointF.adjustedToViewport(): PointF =
391391
currentFragment?.paddingTop?.let { top ->
392-
y += top
393-
}
394-
}
392+
PointF(x, y + top)
393+
} ?: this
395394

396-
private fun RectF.adjustToViewport() {
397-
currentFragment?.paddingTop?.let { top ->
398-
this.top += top
399-
}
400-
}
395+
private fun RectF.adjustedToViewport(): RectF =
396+
currentFragment?.paddingTop?.let { topOffset ->
397+
RectF(left, top + topOffset, right, bottom)
398+
} ?: this
401399

402400
// DecorableNavigator
403401

@@ -420,6 +418,7 @@ class EpubNavigatorFragment private constructor(
420418

421419
internal val webViewListener: R2BasicWebView.Listener = WebViewListener()
422420

421+
@OptIn(ExperimentalDragGesture::class)
423422
private inner class WebViewListener : R2BasicWebView.Listener {
424423

425424
override val readingProgression: ReadingProgression
@@ -472,16 +471,34 @@ class EpubNavigatorFragment private constructor(
472471
}
473472
}
474473

475-
override fun onTap(point: PointF): Boolean {
476-
point.adjustToViewport()
477-
return listener?.onTap(point) ?: false
478-
}
479-
480-
override fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean {
481-
rect.adjustToViewport()
482-
point.adjustToViewport()
483-
return viewModel.onDecorationActivated(id, group, rect, point)
484-
}
474+
override fun onTap(point: PointF): Boolean =
475+
listener?.onTap(point.adjustedToViewport()) ?: false
476+
477+
override fun onDragStart(event: R2BasicWebView.DragEvent): Boolean =
478+
listener?.onDragStart(
479+
startPoint = event.startPoint.adjustedToViewport(),
480+
offset = event.offset
481+
) ?: false
482+
483+
override fun onDragMove(event: R2BasicWebView.DragEvent): Boolean =
484+
listener?.onDragMove(
485+
startPoint = event.startPoint.adjustedToViewport(),
486+
offset = event.offset
487+
) ?: false
488+
489+
override fun onDragEnd(event: R2BasicWebView.DragEvent): Boolean =
490+
listener?.onDragEnd(
491+
startPoint = event.startPoint.adjustedToViewport(),
492+
offset = event.offset
493+
) ?: false
494+
495+
override fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean =
496+
viewModel.onDecorationActivated(
497+
id = id,
498+
group = group,
499+
rect = rect.adjustedToViewport(),
500+
point = point.adjustedToViewport()
501+
)
485502

486503
override fun onProgressionChanged() {
487504
notifyCurrentLocation()

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,10 @@ class R2ViewPager : R2RTLViewPager {
5353
}
5454

5555
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
56-
if (DEBUG) Timber.d("onInterceptTouchEvent ev.action ${ev.action}")
5756
if (type == Publication.TYPE.EPUB) {
5857
when (ev.action and MotionEvent.ACTION_MASK) {
5958
MotionEvent.ACTION_DOWN -> {
6059
// prevent swipe from view pager directly
61-
if (DEBUG) Timber.d("onInterceptTouchEvent ACTION_DOWN")
6260
return false
6361
}
6462
}

0 commit comments

Comments
 (0)