Skip to content

Commit

Permalink
Enable swipe-to-refresh for browser tabs (#864)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Usiel committed Jul 9, 2020
1 parent 6f27234 commit be89ddb
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 13 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
29 changes: 27 additions & 2 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Expand Up @@ -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)
Expand Down Expand Up @@ -269,6 +269,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
configureObservers()
configurePrivacyGrade()
configureWebView()
configureSwipeRefresh()
viewModel.registerWebViewListener(webViewClient, webChromeClient)
configureOmnibarTextInput()
configureFindInPage()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -829,6 +830,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
false
}

it.setEnableSwipeRefreshCallback { enable ->
swipeRefreshContainer?.isEnabled = enable
}

registerForContextMenu(it)

it.setFindListener(this)
Expand All @@ -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
*/
Expand Down Expand Up @@ -1463,6 +1483,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi
createTrackersAnimation()
}
}

if (!viewState.isLoading) {
swipeRefreshContainer.isRefreshing = false
webView?.detectOverscrollBehavior()
}
}
}

Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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)
Expand Down Expand Up @@ -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 {

/*
Expand Down
@@ -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
}
}
27 changes: 17 additions & 10 deletions app/src/main/res/layout/fragment_browser_tab.xml
Expand Up @@ -71,16 +71,23 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.duckduckgo.app.browser.BrowserActivity">

<FrameLayout
android:id="@+id/webViewContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#4F00" />
<com.duckduckgo.app.browser.ui.ScrollAwareRefreshLayout
android:id="@+id/swipeRefreshContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">

<FrameLayout
android:id="@+id/webViewContainer"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="#4F00" />

</com.duckduckgo.app.browser.ui.ScrollAwareRefreshLayout>

<View
android:id="@+id/focusDummy"
Expand Down
Expand Up @@ -29,6 +29,7 @@
android:scrollbarThumbVertical="@color/webViewScrollbarThumbColor"
android:scrollbarTrackHorizontal="@color/webViewScrollbarTrackColor"
android:scrollbarTrackVertical="@color/webViewScrollbarTrackColor"
android:overScrollMode="always"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:visibility="visible">
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/themes.xml
Expand Up @@ -31,6 +31,7 @@
<item name="autoCompleteTextViewStyle">@style/AutoCompleteTextViewStyle</item>
<item name="android:statusBarColor">?toolbarBgColor</item>
<item name="snackbarButtonStyle">@style/snackbarButtonStyle</item>
<item name="android:colorEdgeEffect">@color/newBlack</item>
</style>

<style name="AppTheme.BaseDark">
Expand Down

0 comments on commit be89ddb

Please sign in to comment.