Skip to content

EPUB Navigator: overridable drag gestures #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

### Deprecated

Expand Down
65 changes: 65 additions & 0 deletions readium/navigator/src/main/assets/_scripts/src/gestures.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { handleDecorationClickEvent } from "./decorator";

window.addEventListener("DOMContentLoaded", function () {
document.addEventListener("click", onClick, false);
bindDragGesture(document);
});

function onClick(event) {
Expand Down Expand Up @@ -39,6 +40,70 @@ function onClick(event) {
}
}

function bindDragGesture(element) {
// passive: false is necessary to be able to prevent the default behavior.
element.addEventListener("touchstart", onStart, { passive: false });
element.addEventListener("touchend", onEnd, { passive: false });
element.addEventListener("touchmove", onMove, { passive: false });

var state = undefined;
var isStartingDrag = false;
const pixelRatio = window.devicePixelRatio;

function onStart(event) {
isStartingDrag = true;

const startX = event.touches[0].clientX * pixelRatio;
const startY = event.touches[0].clientY * pixelRatio;
state = {
defaultPrevented: event.defaultPrevented,
startX: startX,
startY: startY,
currentX: startX,
currentY: startY,
offsetX: 0,
offsetY: 0,
interactiveElement: nearestInteractiveElement(event.target),
};
}

function onMove(event) {
if (!state) return;

state.currentX = event.touches[0].clientX * pixelRatio;
state.currentY = event.touches[0].clientY * pixelRatio;
state.offsetX = state.currentX - state.startX;
state.offsetY = state.currentY - state.startY;

var shouldPreventDefault = false;
// Wait for a movement of at least 6 pixels before reporting a drag.
if (isStartingDrag) {
if (Math.abs(state.offsetX) >= 6 || Math.abs(state.offsetY) >= 6) {
isStartingDrag = false;
shouldPreventDefault = Android.onDragStart(JSON.stringify(state));
}
} else {
shouldPreventDefault = Android.onDragMove(JSON.stringify(state));
}

if (shouldPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
}

function onEnd(event) {
if (!state) return;

const shouldPreventDefault = Android.onDragEnd(JSON.stringify(state));
if (shouldPreventDefault) {
event.stopPropagation();
event.preventDefault();
}
state = undefined;
}
}

// See. https://github.com/JayPanoz/architecture/tree/touch-handling/misc/touch-handling
function nearestInteractiveElement(element) {
var interactiveTags = [
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,33 @@ interface VisualNavigator : Navigator {
* The [point] is relative to the navigator's view.
*/
fun onTap(point: PointF): Boolean = false

/**
* Called when the user starts dragging the content, but nothing handled the event
* internally.
*
* The points are relative to the navigator's view.
*/
@ExperimentalDragGesture
fun onDragStart(startPoint: PointF, offset: PointF): Boolean = false

/**
* Called when the user continues dragging the content, but nothing handled the event
* internally.
*
* The points are relative to the navigator's view.
*/
@ExperimentalDragGesture
fun onDragMove(startPoint: PointF, offset: PointF): Boolean = false

/**
* Called when the user stops dragging the content, but nothing handled the event
* internally.
*
* The points are relative to the navigator's view.
*/
@ExperimentalDragGesture
fun onDragEnd(startPoint: PointF, offset: PointF): Boolean = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ annotation class ExperimentalDecorator
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
annotation class ExperimentalAudiobook

@RequiresOptIn(
level = RequiresOptIn.Level.WARNING,
message = "The new dragging gesture is still experimental. The API may be changed in the future without notice."
)
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS, AnnotationTarget.PROPERTY)
annotation class ExperimentalDragGesture
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
fun onPageEnded(end: Boolean)
fun onScroll()
fun onTap(point: PointF): Boolean
fun onDragStart(event: DragEvent): Boolean
fun onDragMove(event: DragEvent): Boolean
fun onDragEnd(event: DragEvent): Boolean
fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean = false
fun onProgressionChanged()
fun onHighlightActivated(id: String)
Expand Down Expand Up @@ -362,6 +365,71 @@ open class R2BasicWebView(context: Context, attrs: AttributeSet) : WebView(conte
return true
}

@android.webkit.JavascriptInterface
fun onDragStart(eventJson: String): Boolean {
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
?: return false

return runBlocking(uiScope.coroutineContext) { listener.onDragStart(event) }
}

@android.webkit.JavascriptInterface
fun onDragMove(eventJson: String): Boolean {
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
?: return false

return runBlocking(uiScope.coroutineContext) { listener.onDragMove(event) }
}

@android.webkit.JavascriptInterface
fun onDragEnd(eventJson: String): Boolean {
val event = DragEvent.fromJSON(eventJson)?.takeIf { it.isValid }
?: return false

return runBlocking(uiScope.coroutineContext) { listener.onDragEnd(event) }
}

/** Produced by gestures.js */
data class DragEvent(
val defaultPrevented: Boolean,
val startPoint: PointF,
val currentPoint: PointF,
val offset: PointF,
val interactiveElement: String?
) {
internal val isValid: Boolean get() =
!defaultPrevented && (interactiveElement == null)

companion object {
fun fromJSONObject(obj: JSONObject?): DragEvent? {
obj ?: return null

val x = obj.optDouble("x").toFloat()
val y = obj.optDouble("y").toFloat()

return DragEvent(
defaultPrevented = obj.optBoolean("defaultPrevented"),
startPoint = PointF(
obj.optDouble("startX").toFloat(),
obj.optDouble("startY").toFloat()
),
currentPoint = PointF(
obj.optDouble("currentX").toFloat(),
obj.optDouble("currentY").toFloat()
),
offset = PointF(
obj.optDouble("offsetX").toFloat(),
obj.optDouble("offsetY").toFloat()
),
interactiveElement = obj.optNullableString("interactiveElement")
)
}

fun fromJSON(json: String): DragEvent? =
fromJSONObject(tryOrNull { JSONObject(json) })
}
}

@android.webkit.JavascriptInterface
fun getViewportWidth(): Int = width

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,6 @@ class R2WebView(context: Context, attrs: AttributeSet) : R2BasicWebView(context,
val pointerIndex = ev.findPointerIndex(mActivePointerId)
val x = ev.getX(pointerIndex)
val xDiff = abs(x - mLastMotionX)
if (DEBUG) Timber.v("Moved x to $x diff=$xDiff")

if (xDiff > mTouchSlop) {
if (DEBUG) Timber.v("Starting drag!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ class EpubNavigatorFragment private constructor(
?: return null

val rect = json.optRectF("rect")
?.apply { adjustToViewport() }
?.run { adjustedToViewport() }

return Selection(
locator = currentLocator.value.copy(
Expand All @@ -387,17 +387,15 @@ class EpubNavigatorFragment private constructor(
run(viewModel.clearSelection())
}

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

private fun RectF.adjustToViewport() {
currentFragment?.paddingTop?.let { top ->
this.top += top
}
}
private fun RectF.adjustedToViewport(): RectF =
currentFragment?.paddingTop?.let { topOffset ->
RectF(left, top + topOffset, right, bottom)
} ?: this

// DecorableNavigator

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

internal val webViewListener: R2BasicWebView.Listener = WebViewListener()

@OptIn(ExperimentalDragGesture::class)
private inner class WebViewListener : R2BasicWebView.Listener {

override val readingProgression: ReadingProgression
Expand Down Expand Up @@ -472,16 +471,34 @@ class EpubNavigatorFragment private constructor(
}
}

override fun onTap(point: PointF): Boolean {
point.adjustToViewport()
return listener?.onTap(point) ?: false
}

override fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean {
rect.adjustToViewport()
point.adjustToViewport()
return viewModel.onDecorationActivated(id, group, rect, point)
}
override fun onTap(point: PointF): Boolean =
listener?.onTap(point.adjustedToViewport()) ?: false

override fun onDragStart(event: R2BasicWebView.DragEvent): Boolean =
listener?.onDragStart(
startPoint = event.startPoint.adjustedToViewport(),
offset = event.offset
) ?: false

override fun onDragMove(event: R2BasicWebView.DragEvent): Boolean =
listener?.onDragMove(
startPoint = event.startPoint.adjustedToViewport(),
offset = event.offset
) ?: false

override fun onDragEnd(event: R2BasicWebView.DragEvent): Boolean =
listener?.onDragEnd(
startPoint = event.startPoint.adjustedToViewport(),
offset = event.offset
) ?: false

override fun onDecorationActivated(id: DecorationId, group: String, rect: RectF, point: PointF): Boolean =
viewModel.onDecorationActivated(
id = id,
group = group,
rect = rect.adjustedToViewport(),
point = point.adjustedToViewport()
)

override fun onProgressionChanged() {
notifyCurrentLocation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,10 @@ class R2ViewPager : R2RTLViewPager {
}

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
if (DEBUG) Timber.d("onInterceptTouchEvent ev.action ${ev.action}")
if (type == Publication.TYPE.EPUB) {
when (ev.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// prevent swipe from view pager directly
if (DEBUG) Timber.d("onInterceptTouchEvent ACTION_DOWN")
return false
}
}
Expand Down