Skip to content

Commit 202fee1

Browse files
committed
Use JavaScript to improve pull-to-refresh
Detect nested scroll from JS to avoid triggering unwanted refresh. Close #797.
1 parent 7a32e39 commit 202fee1

8 files changed

Lines changed: 139 additions & 7 deletions

File tree

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,8 @@ mezzanine {
483483
"src/main/html/private.html",
484484
"src/main/js/InvertPage.js",
485485
"src/main/js/SetMetaViewport.js",
486-
"src/main/js/ThemeColor.js"
486+
"src/main/js/ThemeColor.js",
487+
"src/main/js/NestedScrollDetect.js"
487488
)
488489
}
489490

app/src/main/java/fulguris/di/AppModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import fulguris.html.ListPageReader
99
import fulguris.html.bookmark.BookmarkPageReader
1010
import fulguris.html.homepage.HomePageReader
1111
import fulguris.js.InvertPage
12+
import fulguris.js.NestedScrollDetect
1213
import fulguris.js.TextReflow
1314
import fulguris.js.ThemeColor
1415
import fulguris.js.SetMetaViewport
@@ -195,6 +196,9 @@ class AppModule {
195196

196197
@Provides
197198
fun providesSetMetaViewport(): SetMetaViewport = mezzanine<SetMetaViewport>()
199+
200+
@Provides
201+
fun providesNestedScrollDetect(): NestedScrollDetect = mezzanine<NestedScrollDetect>()
198202
}
199203

200204
@Qualifier

app/src/main/java/fulguris/di/EntryPoint.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import fulguris.download.DownloadHandler
1212
import fulguris.favicon.FaviconModel
1313
import fulguris.html.homepage.HomePageFactory
1414
import fulguris.js.InvertPage
15+
import fulguris.js.NestedScrollDetect
1516
import fulguris.js.SetMetaViewport
1617
import fulguris.js.TextReflow
1718
import fulguris.js.ThemeColor
@@ -58,6 +59,7 @@ interface HiltEntryPoint {
5859
val invertPageJs: InvertPage
5960
val setMetaViewport: SetMetaViewport
6061
val themeColorJs: ThemeColor
62+
val nestedScrollDetectJs: NestedScrollDetect
6163
val homePageFactory: HomePageFactory
6264
val abpBlockerManager: AbpBlockerManager
6365
val noopBlocker: NoOpAdBlocker
@@ -75,5 +77,3 @@ interface HiltEntryPoint {
7577
var clipboardManager: ClipboardManager
7678

7779
}
78-
79-
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package fulguris.js
2+
3+
import com.anthonycr.mezzanine.FileStream
4+
5+
/**
6+
* Detects whether a touch event targets a nested CSS scrollable element.
7+
* Notifies the native side via the _fulgurisScroll JavaScript interface
8+
* so that pull-to-refresh can be suppressed when appropriate.
9+
*/
10+
@FileStream("src/main/js/NestedScrollDetect.js")
11+
interface NestedScrollDetect {
12+
fun provideJs(): String
13+
}

app/src/main/java/fulguris/view/PullRefreshLayout.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,21 @@ class PullRefreshLayout(context: Context, attrs: AttributeSet?) : SwipeRefreshLa
7272
return iFieldTarget.get(this) as View?
7373
}
7474

75+
/**
76+
* Prevents pull-to-refresh from triggering when the user is scrolling inside
77+
* a nested CSS scrollable element (e.g. overflow:auto sidebar) within the web page.
78+
* When JavaScript detects the touch is on such an element, [WebViewEx.isTouchOnNestedScrollable]
79+
* is set to true, and we report the child as scrollable to block interception.
80+
*/
81+
override fun canChildScrollUp(): Boolean {
82+
callEnsureTarget()
83+
val target = getTarget()
84+
if (target is WebViewEx && target.isTouchOnNestedScrollable) {
85+
return true
86+
}
87+
return super.canChildScrollUp()
88+
}
89+
7590
/**
7691
*
7792
*/

app/src/main/java/fulguris/view/WebPageClient.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import kotlinx.coroutines.launch as coroutineLaunch
2525
import kotlinx.coroutines.withContext
2626
import fulguris.html.homepage.HomePageFactory
2727
import fulguris.js.InvertPage
28+
import fulguris.js.NestedScrollDetect
2829
import fulguris.js.SetMetaViewport
2930
import fulguris.js.TextReflow
3031
import fulguris.permissions.PermissionsManager
@@ -95,6 +96,7 @@ class WebPageClient(
9596
val textReflowJs: TextReflow = hiltEntryPoint.textReflowJs
9697
val invertPageJs: InvertPage = hiltEntryPoint.invertPageJs
9798
val setMetaViewport: SetMetaViewport = hiltEntryPoint.setMetaViewport
99+
val nestedScrollDetectJs: NestedScrollDetect = hiltEntryPoint.nestedScrollDetectJs
98100
val homePageFactory: HomePageFactory = hiltEntryPoint.homePageFactory
99101
val abpBlockerManager: AbpBlockerManager = hiltEntryPoint.abpBlockerManager
100102
val noopBlocker: NoOpAdBlocker = hiltEntryPoint.noopBlocker
@@ -337,7 +339,7 @@ class WebPageClient(
337339
iResourceCount++
338340
Timber.d("$ihs : onLoadResource - ${if (isForMainFrame) "Main frame" else "Resource"} - $iResourceCount - $url")
339341
}
340-
342+
341343
/**
342344
*
343345
*/
@@ -347,7 +349,7 @@ class WebPageClient(
347349
webBrowser.onTabChangedUrl(webPageTab)
348350
}
349351
}
350-
352+
351353

352354
/**
353355
* Overrides [WebViewClient.onPageFinished].
@@ -379,6 +381,14 @@ class WebPageClient(
379381
// Flag that we have called onPageFinished
380382
onPageFinishedDone = true
381383

384+
// Inject nested scroll detection so that pull-to-refresh is suppressed when
385+
// the user scrolls inside a CSS overflow:auto/scroll element (e.g. a sidebar).
386+
// Only inject if pull-to-refresh is enabled and JavaScript is enabled.
387+
// Though if the config changes we could be missing it...
388+
if (view.context.configPrefs.pullToRefresh && view.settings.javaScriptEnabled) {
389+
view.evaluateJavascript(nestedScrollDetectJs.provideJs(), null)
390+
}
391+
382392
// Execute and clear callback registered with loadUrl
383393
webPageTab.onLoadCompleteCallback?.invoke()
384394
webPageTab.onLoadCompleteCallback = null
@@ -1228,4 +1238,3 @@ class WebPageClient(
12281238
super.onSafeBrowsingHit(view, request, threatType, callback)
12291239
}
12301240
}
1231-

app/src/main/java/fulguris/view/WebViewEx.kt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package fulguris.view
22

3+
import android.annotation.SuppressLint
34
import android.content.Context
45
import android.print.PrintAttributes
56
import android.print.PrintDocumentAdapter
@@ -9,6 +10,7 @@ import android.util.AttributeSet
910
import android.view.KeyEvent
1011
import android.view.MotionEvent
1112
import android.view.View
13+
import android.webkit.JavascriptInterface
1214
import android.webkit.WebView
1315
import androidx.annotation.ColorInt
1416
import fulguris.extensions.ihs
@@ -23,15 +25,45 @@ import timber.log.Timber
2325
*/
2426
class WebViewEx : WebView {
2527
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
28+
initNestedScrollDetection()
2629
}
2730

2831
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
29-
32+
initNestedScrollDetection()
3033
}
3134

3235
//
3336
lateinit var proxy: WebPageTab
3437

38+
/**
39+
* True when the current touch gesture started on a nested CSS scrollable element
40+
* (e.g. a div with overflow:auto/scroll whose content overflows).
41+
* Set from JavaScript via [NestedScrollBridge] on `touchstart`, reset on `touchend`/`touchcancel`.
42+
* Read from [PullRefreshLayout.canChildScrollUp] on the UI thread.
43+
*/
44+
@Volatile
45+
var isTouchOnNestedScrollable: Boolean = false
46+
47+
@SuppressLint("JavascriptInterface")
48+
private fun initNestedScrollDetection() {
49+
addJavascriptInterface(NestedScrollBridge(), NESTED_SCROLL_JS_INTERFACE)
50+
}
51+
52+
/**
53+
* JavaScript interface that lets injected page scripts tell us whether the current
54+
* touch gesture started on a nested scrollable CSS element.
55+
*/
56+
inner class NestedScrollBridge {
57+
@JavascriptInterface
58+
fun setNestedScrollable(value: Boolean) {
59+
isTouchOnNestedScrollable = value
60+
}
61+
}
62+
63+
companion object {
64+
const val NESTED_SCROLL_JS_INTERFACE = "_fulgurisScroll"
65+
}
66+
3567
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
3668

3769
/*
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* The contents of this file are subject to the Common Public Attribution License Version 1.0.
3+
* (the "License"); you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at:
5+
* https://github.com/Slion/Fulguris/blob/main/LICENSE.CPAL-1.0.
6+
* The License is based on the Mozilla Public License Version 1.1, but Sections 14 and 15 have been
7+
* added to cover use of software over a computer network and provide for limited attribution for
8+
* the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
9+
*
10+
* Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF
11+
* ANY KIND, either express or implied. See the License for the specific language governing rights
12+
* and limitations under the License.
13+
*
14+
* The Original Code is Fulguris.
15+
*
16+
* The Original Developer is the Initial Developer.
17+
* The Initial Developer of the Original Code is Stéphane Lenclud.
18+
*
19+
* All portions of the code written by Stéphane Lenclud are Copyright © 2020 Stéphane Lenclud.
20+
* All Rights Reserved.
21+
*/
22+
23+
(function() {
24+
if (window._fulgurisScrollInstalled) return;
25+
window._fulgurisScrollInstalled = true;
26+
27+
function isScrollableElement(el) {
28+
if (!el || el === document.documentElement || el === document.body) return false;
29+
var style = window.getComputedStyle(el);
30+
var overflowY = style.overflowY;
31+
if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
32+
return true;
33+
}
34+
return false;
35+
}
36+
37+
function hasScrollableAncestor(el) {
38+
while (el) {
39+
if (isScrollableElement(el)) return true;
40+
el = el.parentElement;
41+
}
42+
return false;
43+
}
44+
45+
document.addEventListener('touchstart', function(e) {
46+
try {
47+
window._fulgurisScroll.setNestedScrollable(hasScrollableAncestor(e.target));
48+
} catch(ex) {}
49+
}, true);
50+
51+
document.addEventListener('touchend', function() {
52+
try { window._fulgurisScroll.setNestedScrollable(false); } catch(ex) {}
53+
}, true);
54+
55+
document.addEventListener('touchcancel', function() {
56+
try { window._fulgurisScroll.setNestedScrollable(false); } catch(ex) {}
57+
}, true);
58+
})();

0 commit comments

Comments
 (0)