From be89ddb202bd5e8d82730df1a938812e29eddddb Mon Sep 17 00:00:00 2001 From: Usiel Riedl Date: Thu, 9 Jul 2020 20:20:20 +0800 Subject: [PATCH] Enable swipe-to-refresh for browser tabs (#864) * Enable swipe-to-refresh for browser tabs - Including SwipeRefreshLayout dependencies & adapted fragment - Minor fix for DuckDuckGoWebView to work with SwipeRefreshLayout Resolves: #33 * Fix for lint * Replaced single quotes with double quotes in gradle file * Swipe-to-refresh fixes for draggable content - Added callback to allow WebView control over SwipeRefreshLayout - Added logic to disable swipe refresh when WebView is not clampedY before * Always show overScrollMode to make swipe-refresh behavior more clear * Changed overscroll effect to black for all themes to highlight swipe-refresh * Disable swipe to refresh on pages that define overscrollBehavior - Added JavaScript interface to retrieve the css overscrollBehaviorY value - Added switch on custom WebView to allow disabling swipe to refresh completely - Minor fix for lastClampedY issue on pages where height <= window to detect whether we actually are clampedY at the top * Replaced JS interface with evaluateJavascript call * Minor refactor, moved webView related logic into webView class --- app/build.gradle | 2 + .../app/browser/BrowserTabFragment.kt | 29 +++++++++- .../app/browser/DuckDuckGoWebView.kt | 53 ++++++++++++++++++- .../browser/ui/ScrollAwareRefreshLayout.kt | 38 +++++++++++++ .../main/res/layout/fragment_browser_tab.xml | 27 ++++++---- .../include_duckduckgo_browser_webview.xml | 1 + app/src/main/res/values/themes.xml | 1 + 7 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/browser/ui/ScrollAwareRefreshLayout.kt diff --git a/app/build.gradle b/app/build.gradle index 130422340621..523c07b25fc7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,6 +99,7 @@ android { ext { androidX = "1.2.0-beta01" materialDesign = "1.2.0-alpha05" + swipeRefreshLayout = "1.0.0" architectureComponents = "1.1.1" architectureComponentsExtensions = "1.1.1" androidKtxCore = "1.2.0" @@ -146,6 +147,7 @@ dependencies { implementation "androidx.appcompat:appcompat:$androidX" implementation "com.google.android.material:material:$materialDesign" implementation "androidx.constraintlayout:constraintlayout:$constraintLayout" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swipeRefreshLayout" implementation "androidx.webkit:webkit:$webkit" implementation "com.squareup.okhttp3:okhttp:$okHttp" implementation "com.squareup.retrofit2:retrofit:$retrofit" diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 79bba56e4bdb..3fb25d15fd11 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -214,7 +214,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private val menuButton: ViewGroup? get() = appBarLayout.browserMenu - private var webView: WebView? = null + private var webView: DuckDuckGoWebView? = null private val errorSnackbar: Snackbar by lazy { Snackbar.make(browserLayout, R.string.crashedWebViewErrorMessage, Snackbar.LENGTH_INDEFINITE) @@ -269,6 +269,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi configureObservers() configurePrivacyGrade() configureWebView() + configureSwipeRefresh() viewModel.registerWebViewListener(webViewClient, webChromeClient) configureOmnibarTextInput() configureFindInPage() @@ -798,7 +799,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi R.layout.include_duckduckgo_browser_webview, webViewContainer, true - ).findViewById(R.id.browserWebView) as WebView + ).findViewById(R.id.browserWebView) as DuckDuckGoWebView webView?.let { it.webViewClient = webViewClient @@ -829,6 +830,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi false } + it.setEnableSwipeRefreshCallback { enable -> + swipeRefreshContainer?.isEnabled = enable + } + registerForContextMenu(it) it.setFindListener(this) @@ -840,6 +845,21 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } + private fun configureSwipeRefresh() { + swipeRefreshContainer.setColorSchemeColors(ContextCompat.getColor(requireContext(), R.color.cornflowerBlue)) + + swipeRefreshContainer.setOnRefreshListener { + onRefreshRequested() + } + + swipeRefreshContainer.setCanChildScrollUpCallback { + webView?.canScrollVertically(-1) ?: false + } + + // avoids progressView from showing under toolbar + swipeRefreshContainer.progressViewStartOffset = swipeRefreshContainer.progressViewStartOffset - 15 + } + /** * Explicitly disable database to try protect against Magellan WebSQL/SQLite vulnerability */ @@ -1463,6 +1483,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi createTrackersAnimation() } } + + if (!viewState.isLoading) { + swipeRefreshContainer.isRefreshing = false + webView?.detectOverscrollBehavior() + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt index 9daace06ea9f..52b6a27caed2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt @@ -35,7 +35,12 @@ import androidx.core.view.ViewCompat * Originally based on https://github.com/takahirom/webview-in-coordinatorlayout for scrolling behaviour */ class DuckDuckGoWebView : WebView, NestedScrollingChild { + private var lastClampedTopY: Boolean = false + private var contentAllowsSwipeToRefresh: Boolean = true + private var enableSwipeRefreshCallback: ((Boolean) -> Unit)? = null + private var lastY: Int = 0 + private var lastDeltaY: Int = 0 private val scrollOffset = IntArray(2) private val scrollConsumed = IntArray(2) private var nestedOffsetY: Int = 0 @@ -74,6 +79,10 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild { MotionEvent.ACTION_MOVE -> { var deltaY = lastY - eventY + if (deltaY > 0) { + lastClampedTopY = false + } + if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) { deltaY -= scrollConsumed[1] lastY = eventY - scrollOffset[1] @@ -83,14 +92,24 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild { returnValue = super.onTouchEvent(event) - if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) { + if (scrollY == 0 && lastClampedTopY) { + // we have reached the top and are clamped -> enable swipeRefresh (by default always disabled) + enableSwipeRefresh(true) + + stopNestedScroll() + } else if (dispatchNestedScroll(0, scrollOffset[1], 0, deltaY, scrollOffset)) { event.offsetLocation(0f, scrollOffset[1].toFloat()) nestedOffsetY += scrollOffset[1] lastY -= scrollOffset[1] } + + lastDeltaY = deltaY } MotionEvent.ACTION_DOWN -> { + // disable swipeRefresh until we can be sure it should be enabled + enableSwipeRefresh(false) + returnValue = super.onTouchEvent(event) lastY = eventY startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL) @@ -131,6 +150,38 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild { override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean = nestedScrollHelper.dispatchNestedPreFling(velocityX, velocityY) + override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) { + // taking into account lastDeltaY since we are only interested whether we clamped at the top + lastClampedTopY = clampedY && lastDeltaY <= 0 + enableSwipeRefresh(clampedY && scrollY == 0) + super.onOverScrolled(scrollX, scrollY, clampedX, clampedY) + } + + fun setEnableSwipeRefreshCallback(callback: (Boolean) -> Unit) { + enableSwipeRefreshCallback = callback + } + + /** + * Allows us to determine whether to (de)activate Swipe to Refresh behavior for the current page content, e.g. if page implements a swipe behavior of its + * own already (see twitter.com). + */ + fun detectOverscrollBehavior() { + evaluateJavascript("(function() { return getComputedStyle(document.querySelector('body')).overscrollBehaviorY; })();") { behavior -> + setContentAllowsSwipeToRefresh(behavior.replace("\"", "") == "auto") + } + } + + private fun enableSwipeRefresh(enable: Boolean) { + enableSwipeRefreshCallback?.invoke(enable && contentAllowsSwipeToRefresh) + } + + private fun setContentAllowsSwipeToRefresh(allowed: Boolean) { + contentAllowsSwipeToRefresh = allowed + if (!allowed) { + enableSwipeRefresh(false) + } + } + companion object { /* diff --git a/app/src/main/java/com/duckduckgo/app/browser/ui/ScrollAwareRefreshLayout.kt b/app/src/main/java/com/duckduckgo/app/browser/ui/ScrollAwareRefreshLayout.kt new file mode 100644 index 000000000000..2705dae691fe --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/browser/ui/ScrollAwareRefreshLayout.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2020 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.browser.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +class ScrollAwareRefreshLayout(context: Context, attrs: AttributeSet?) : SwipeRefreshLayout(context, attrs) { + + private var canChildScrollUpCallback: (() -> Boolean)? = null + + fun setProgressViewStartOffset(start: Int) { + mOriginalOffsetTop = start + } + + override fun canChildScrollUp(): Boolean { + return canChildScrollUpCallback?.invoke() ?: super.canChildScrollUp() + } + + fun setCanChildScrollUpCallback(callback: () -> Boolean) { + canChildScrollUpCallback = callback + } +} diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index bc9965e3dd01..7efa36945dcb 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -71,16 +71,23 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context="com.duckduckgo.app.browser.BrowserActivity"> - + + + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 512ee71bb9c7..bc7e94480cd5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -31,6 +31,7 @@ @style/AutoCompleteTextViewStyle ?toolbarBgColor @style/snackbarButtonStyle + @color/newBlack