SwipeRefreshLayout + WebView when scroll position is at top
Asked Answered
U

19

39

I'm trying to use SwipeRefreshLayout with WebView.

I'm facing the problem where in the middle of page, when user scrolls down, unwanted refresh kicks in.

How do I make the refresh event only happen when webview's scroll position is at the top. (ie, he's looking at the top portion of the page)?

Unrighteous answered 9/7, 2014 at 15:56 Comment(1)
I did some testing and I can confirm that this issue still exists on some web pages. Hence fix is required.Side
T
38

I've managed to solve it without having to extend anything. Have a look at this snippet (Fragment-specific):

private ViewTreeObserver.OnScrollChangedListener mOnScrollChangedListener;

@Override
public void onStart() {
    super.onStart();

    swipeLayout.getViewTreeObserver().addOnScrollChangedListener(mOnScrollChangedListener =
            new ViewTreeObserver.OnScrollChangedListener() {
                @Override
                public void onScrollChanged() {
                    if (mWebView.getScrollY() == 0)
                        swipeLayout.setEnabled(true);
                    else
                        swipeLayout.setEnabled(false);

                }
            });
}

@Override
public void onStop() {
    swipeLayout.getViewTreeObserver().removeOnScrollChangedListener(mOnScrollChangedListener);
    super.onStop();
}

For a broader context, have a look at my answer to Android - SwipeRefreshLayout with empty textview.

Tephra answered 13/1, 2015 at 7:5 Comment(3)
One-liner of the poor man (with lambda): swipeLayout.getViewTreeObserver().addOnScrollChangedListener(() -> swipeLayout.setEnabled((webView.getScrollY() == 0)));Iambic
Free one liner for Kotlin swipeRefreshLayout.isEnabled = webView.scrollY == 0Bonnette
Using this solution sometimes flickers the webview while scrolling or fling.Emblematize
L
25

I think the recommended way of doing this is to extend the SwipeRefreshLayout and override canChildScrollUp() - https://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout.html#canChildScrollUp()

This is how it's done in the Google IO 2014 Schedule app - https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/ui/widget/MultiSwipeRefreshLayout.java#L76

The CustomSwipeRefreshLayout will probably look like:

public class CustomSwipeRefreshLayout extends SwipeRefreshLayout {

    private CanChildScrollUpCallback mCanChildScrollUpCallback;

    public interface CanChildScrollUpCallback {
        boolean canSwipeRefreshChildScrollUp();
    }

    public void setCanChildScrollUpCallback(CanChildScrollUpCallback canChildScrollUpCallback) {
        mCanChildScrollUpCallback = canChildScrollUpCallback;
    }

    @Override
    public boolean canChildScrollUp() {
        if (mCanChildScrollUpCallback != null) {
            return mCanChildScrollUpCallback.canSwipeRefreshChildScrollUp();
        }
        return super.canChildScrollUp();
    }
}

And in your Activity that has the WebView,

public class FooActivity
        implements CustomSwipeRefreshLayout.CanChildScrollUpCallback {

    private CustomSwipeRefreshLayout mRefreshLayout;
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // after initialization
        mRefreshLayout.setCanChildScrollUpCallback(this);
    }

    @Override
    public boolean canSwipeRefreshChildScrollUp() {
        return mWebview.getScrollY() > 0;
    }
}
Leund answered 5/2, 2015 at 22:59 Comment(6)
The real issue is why does SwipeRefreshLayout intercepts touch events event when requested not to by scrolling child views. Removing that "feature" would solve all these issues automatically. See the following answer for details: #25978962Zither
mWebView.getScrollY is always 0 for me.Tadich
@JohnShelley Check your web layout. "position: relative;" property in your CSS might be the source of your problem. Relative positioning causes troubles with scrolling in the WebView.Nefarious
@Nefarious but what if we are loading some othe website? we can't have control over other websites, so this is not a global solutionChampignon
Unable to start activity, android.view.InflateExceptionSublunar
@PabloCegarra You need to provide some constructors for the custom view. hope it helps othersAllegory
S
12

Just implement your Fragment or Activity with ViewTreeObserver.OnScrollChangedListener then set Listener like webview.getViewTreeObserver().addOnScrollChangedListener(this);

In onScrollChanged() method do like this

    @Override
    public void onScrollChanged() {
        if (webview.getScrollY() == 0) {
            swipeLayout.setEnabled(true);
        } else {
            swipeLayout.setEnabled(false);
        }
    }
Sialkot answered 4/1, 2016 at 10:3 Comment(1)
this should be the accepted answer, thanks mateSanctum
S
5

Most recent versions of support library have SwipeRefreshLayout#setOnChildScrollUpCallback method which allows you to avoid subclassing SwipeRefreshLayout.

mBinding.swipeRefreshLayout.setOnChildScrollUpCallback((parent, child) -> 
            mBinding.webView.getScrollY() > 10);
Styrax answered 13/9, 2018 at 8:13 Comment(1)
I used swipeRefreshLayout.setOnChildScrollUpCallback((p, c) -> webview.getScrollY() > 0); and there's no need to extend SwipeRefreshLayout for androidx.swiperefreshlayout:swiperefreshlayout:1.1.0Freemanfreemartin
S
4

You can try this:

It works for API >= 23 Android Marshmallow

webView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
        @Override
        public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
            swipeRefreshLayout.setEnabled(scrollY == 0);
        }
    });

Edit 1: This is not working if web page has nested scrolling content. So instead of using SwipeRefreshLayout I decided to use simple ImageButton to reload web page.

Side answered 11/3, 2021 at 8:42 Comment(1)
Dont use this method... override function -> onScrollChange are not called in some scenarios so it keep stuck in middle of web page.Mania
P
2

You can solve this by extending SwipeRefreshLayout with a custom SwipeRefreshLayout that takes in the WebView and use the WebView to check the scrollY position.

Note: This will work for scrolling the whole page, but it may not work with nested scrolls. Also, if you have horizontal scrolling you may also want to add the logic found in this answer: https://stackoverflow.com/a/24453194

public class CustomSwipeToRefresh extends SwipeRefreshLayout {

    private final int yScrollBuffer = 5;
    private WebView mWebView;

    public CustomSwipeToRefresh(Context context, WebView webView) {
        super(context);

        mWebView = webView;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        if (mWebView.getScrollY() > yScrollBuffer)
            return false;

        return super.onInterceptTouchEvent(event);
    }
}
Playful answered 23/7, 2014 at 18:1 Comment(1)
How would you purpose it work with nested scrolls? Is it possible?Tadich
B
2

I am answering the same question but using Kotlin. By enabling swipeRefreshLayout when the scrollY of webView gets 0:

Make this variable in your fragment or activity:

private val onScrollChangedListener = ViewTreeObserver.OnScrollChangedListener{ swipeRefreshLayout.isEnabled = webView.scrollY == 0 }

Then onViewCreated:

swipeRefreshLayout.viewTreeObserver.addOnScrollChangedListener(onScrollChangedListener)

Then onStop:

swipeRefreshLayout.viewTreeObserver.removeOnScrollChangedListener(onScrollChangedListener)

From here everything will work well. But if the webpage your are using has a fixed header that does not scroll scrollY will always be equal to 0. And this solution together with other solution in this page may not work.

Bonnette answered 28/9, 2018 at 15:6 Comment(1)
Your comment about fixed header definitely saved me a lot of time debugging!! Thank you.Kex
P
1

I was facing Similar issue but none of the answers given here works for me. So, I got a solution from this answer.

Step 1 : Create Class CustomWebView

public class CustomWebView extends WebView{

private OnScrollChangedCallback mOnScrollChangedCallback;

public CustomWebView(final Context context)
{
    super(context);
}

public CustomWebView(final Context context, final AttributeSet attrs)
{
    super(context, attrs);
}

public CustomWebView(final Context context, final AttributeSet attrs, final int defStyle)
{
    super(context, attrs, defStyle);
}

@Override
protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt)
{
    super.onScrollChanged(l, t, oldl, oldt);
    if(mOnScrollChangedCallback != null) mOnScrollChangedCallback.onScroll(l, t);
}

public OnScrollChangedCallback getOnScrollChangedCallback()
{
    return mOnScrollChangedCallback;
}

public void setOnScrollChangedCallback(final OnScrollChangedCallback onScrollChangedCallback)
{
    mOnScrollChangedCallback = onScrollChangedCallback;
}

/**
 * Impliment in the activity/fragment/view that you want to listen to the webview
 */
public static interface OnScrollChangedCallback
{
    public void onScroll(int horizontal, int vertical);
}}

Step 2 : In xml file use webview like this-

 <com.yourpackagename.CustomWebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

Step 3 : In your MainActivity use setOnScrollChangedCallback -

  mWebView.setOnScrollChangedCallback(new CustomWebView.OnScrollChangedCallback(){
        public void onScroll(int horizontal, int vertical){
            System.out.println("=="+horizontal+"---"+vertical);
   //this is to check webview scroll behaviour
            if (vertical<50){
                swipeRefreshLayout.setEnabled(true);
            }else{
                swipeRefreshLayout.setEnabled(false);
            }
        }
    });
}
Presidentelect answered 23/1, 2017 at 11:8 Comment(0)
L
0

The answer provided by hiBrianLee almost worked for me, but as John Shelley wrote, getScrollY always returned 0 in my case (I used a ListView as the nested scrolling child). What worked straight away, however, was to use canScrollVertically instead (which is a View method, so it's available both for ListViews and WebViews, and also for any other kind of scrolling child).

My layout class looks as follows:

public class NestedScrollSwipeLayout extends SwipeRefreshLayout {

    private ListView scrollChild;

    public NestedScrollSwipeLayout(Context context) {
        super(context);
    }

    public NestedScrollSwipeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setScrollChild(ListView child) {
        this.scrollChild = child;
    }

    @Override
    public boolean canChildScrollUp() {
        return (scrollChild != null && scrollChild.canScrollVertically(-1)) ||
            super.canChildScrollUp();
    }
}

(As mentioned, canScrollVertically is a View method, so the ListView can be safely replaced by a general View or any other specialized view class.)

Launalaunce answered 26/11, 2015 at 11:50 Comment(0)
F
0

Nothing above worked for me.Here is my approach

 @Override
 public boolean dispatchTouchEvent(MotionEvent event)
 {
super.dispatchTouchEvent(event);
int x = (int)event.getX();
int y = (int)event.getY();
 if(y>800){
     swipeRefreshLayout.setEnabled(false);
 }else {
     swipeRefreshLayout.setEnabled(true);
 }
 Log.e("yy",""+y);

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_MOVE:
    case MotionEvent.ACTION_UP:
}

return false;

}

Forereach answered 2/4, 2020 at 9:35 Comment(0)
L
0

The most reliable approach to solve conflict between SwipeRefreshLayout and WebView is to bound touch/swipe gesture to certain space at the top of the screen.

Previous answers base on detecting webview.scrollY may be not reliable, because some pages has fixed header position, therefore even if there is scrollable list webview.scrollY will alwyas returns 0

SOURCE

  1. Create custom SwipeRefreshLayout
class MySwipeRefreshLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : SwipeRefreshLayout(context, attrs) {

    private var mDeclined = false
    private var mPrevY: Float = 0f

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mPrevY = event.y

                mDeclined = false
                if (mPrevY > resources.getDimension(R.dimen.disable_swipe_after)){
                    mDeclined = true
                }
            }
            MotionEvent.ACTION_MOVE -> {
                if (mDeclined) {
                    return false
                }
            }
        }
        return super.onInterceptTouchEvent(event)
    }
}
  1. Apply it in the view file
    <yourpackage.MyLimitedSwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:windowSoftInputMode="adjustResize"
            >

            <WebView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                />
        </FrameLayout>
    </yourpackage.MySwipeRefreshLayout>
Liquesce answered 4/12, 2020 at 14:31 Comment(0)
D
0

Finally, I find a solution, this will solve the case webView.scrollY == 0 or the web page has nested scrolling content. The SwipeRefreshLayout is allowed to intercept the touch event only when the webview is overscroll. Use clampedY to check if the webview is overscrolled.

Step1. Create Class CustomWebView

class CustomWebView : WebView {
    
    ....

    override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean) {
        

        // Log.d(TAG, "clampedX $clampedX clampedY $clampedY")
        intercept = clampedY
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        // Log.d(TAG, "onTouchEvent $event")
        when(event?.action) {
           MotionEvent.ACTION_DOWN-> {
              intercept = false
              requestDisallowInterceptTouchEvent(true)
           }
           MotionEvent.ACTION_MOVE-> {
              requestDisallowInterceptTouchEvent(!intercept)
           }
           else-> {
              requestDisallowInterceptTouchEvent(false)
           }
        }
        return super.onTouchEvent(event)
     }
...
}

Step2.Create Class CustomSwipeRefreshLayoutUI

class CustomSwipeRefreshLayoutUI: SwipeRefreshLayout {
    var disallowIntercept = false
    ....
    
    override fun requestDisallowInterceptTouchEvent(b: Boolean) {
        disallowIntercept = b
        parent.requestDisallowInterceptTouchEvent(b)
        super.requestDisallowInterceptTouchEvent(b)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        // 不允许拦截
        if (disallowIntercept) {
            return false
        }
        return super.onInterceptTouchEvent(ev)
    }
    ....
    

}

The layout file looks like:

<yourpackage.CustomSwipeRefreshLayoutUI
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    
    <yourpackage.CustomWebView 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />
   
</yourpackage.MySwipeRefreshLayout>
Danelle answered 19/10, 2023 at 8:31 Comment(0)
M
0

Here is my unique perfect solution

binding.webView.getSettings().setJavaScriptEnabled(true);
binding.webView.addJavascriptInterface(new JavaScriptInterface(this,binding.webView), "Android");
    
public void onPageFinished(WebView view, String url) {

     view.loadUrl("javascript: window.addEventListener('touchstart', startTouch, false);" +
    
          "function startTouch(event) {" +
          "Android.getHeightValues(window.scrollY);" +
          "}");
    
    }

My Custom JavaScriptInterface

public class JavaScriptInterface {
        
   private Context context;
        
   public JavaScriptInterface(Context context) {
                this.context = context;
        
            }
    
   @JavascriptInterface
   public void getHeightValues(int topYPositionValue) {
           
      Boolean shouldSwipeRefleshActive = false;
    
    
      if(topYPositionValue < 1){
          shouldSwipeRefleshActive = true;
      }else{
           shouldSwipeRefleshActive = false;
      }
            ((WebviewActivity)context).shouldActiveSwipeRefleshLayout(shouldSwipeRefleshActive);
    
        }
        
 }


 public  void shouldActiveSwipeRefleshLayout(Boolean shouldActiveSwipeRefleshLayout ){
        binding.swipeRefreshLayout.setEnabled(shouldActiveSwipeRefleshLayout);
    }

WARNING------------------------------- DONT USE BELOW METHOD FOR THIS ISSUE, this listener are not prompt up in some scenarios so that your app can stuck on middle of page

binding.webView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
                @Override
                public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                    //binding.swipeRefreshLayout.setEnabled(scrollY == 0);

                    /*Log.d("YHY90","scrool X -> " + scrollX);
                    Log.d("YHY90","scrool Y"+ scrollY);
                    Log.d("YHY90","old scrool Y"+ oldScrollY);

                }
            });
Mania answered 4/5, 2024 at 13:45 Comment(0)
B
-1

Use SwipeRefreshLayout normally as you do with other views but enable nested scrolling.

swipeRefreshLayout.setNestedScrollingEnabled(true);
Burroughs answered 21/10, 2016 at 19:37 Comment(1)
Thing is once user swipe down too fast it will invoke SwipeReferesh if the user scrolls normally or slowly it is good.Burroughs
T
-1

i find a solution for it. Just use the CoordinatorLayout as the root layout and add layout_behavior="@string/appbar_scrolling_view_behavior" to your SwipeRefreshLayout.It's perfect for me.

Trust answered 12/6, 2017 at 8:26 Comment(0)
P
-1

I found disabling nested scrolling on the webview (not the SwipeRefreshLayout itself) sorted this problem for me. It won't be possible to do this in all situations, but was in mine.

webView.setNestedScrollingEnabled(false);
Prickle answered 6/9, 2017 at 16:36 Comment(0)
S
-1
// swipe - active only when WebView Y==0        
swipeView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
            @Override
            public void onScrollChanged() {
                if (webView.getScrollY() == 0) {
                    Log.d(TAG, "onScrollChanged - y=0 - make swipe refresh active");
                    swipeView.setEnabled(true);
                }else {
                    swipeView.setEnabled(false);
                    Log.d(TAG, "onScrollChanged - y is not null - swipe refresh not active");
                }
            }
        });

// swipe - after refresh do work (see above for condition)
    swipeView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {

        @Override
        public void onRefresh() {

            Log.d(TAG, "Swipe - refresh");

            // do work ... your work method here
            getSchedule();

            // hide spinner of swiperefreshlayout
            swipeView.setRefreshing(false);

        }

    });
Stramonium answered 20/6, 2020 at 13:48 Comment(1)
Dont use this method... override function -> onScrollChange are not called in some scenarios so it keep stuck in middle of web page.Mania
W
-1

It works for me :

swipeRefreshLayout.getViewTreeObserver().addOnScrollChangedListener(() -> {
    swipeRefreshLayout.setEnabled(webView.getScrollY() == 0);
});
Webfooted answered 3/4, 2023 at 14:53 Comment(1)
Dont use this method... override function -> onScrollChange are not called in some scenarios so it keep stuck in middle of web page.Mania
M
-1

This Kotlin code snippet is setting up a SwipeRefreshLayout to enable pull-to-refresh functionality for a WebView in an Android app. It checks the scroll position of the WebView and enables or disables the refresh action based on whether the WebView is scrolled to the top. When the user initiates a refresh, it reloads the WebView content and stops the refreshing animation when done.

    binding.swipeRefreshLayout.viewTreeObserver.addOnScrollChangedListener {
        binding.swipeRefreshLayout.isEnabled = binding.webView.scrollY === 0
    }

    binding.swipeRefreshLayout.setOnRefreshListener {
        binding.swipeRefreshLayout.isRefreshing = false
        binding.webView.reload()
    }
Machinegun answered 25/9, 2023 at 17:50 Comment(7)
Dont use this method... override function -> onScrollChange are not called in some scenarios so it keep stuck in middle of web page.Mania
Could you explain in which scenarios this method is not called?Machinegun
it depend on website, in some scroll inside "WebView" this function not called and app stuck on middle of pageMania
Okay, can you share the URL for testing purposes? It could be an issue with the website content and webview not reaching the top. If your app says so, it clearly means that the website content is not loading or responding.Machinegun
This issue happen when website is fully loaded.Mania
Share your code and web URL, and I will assist you further.Machinegun
You can test on some websites and you will see thatMania

© 2022 - 2025 — McMap. All rights reserved.