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)?

eugene
  • 39,839
  • 68
  • 255
  • 489

16 Answers16

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.

Community
  • 1
  • 1
vizZ
  • 1,530
  • 14
  • 14
  • 4
    One-liner of the poor man (with lambda): `swipeLayout.getViewTreeObserver().addOnScrollChangedListener(() -> swipeLayout.setEnabled((webView.getScrollY() == 0)));` – Ek1noX Nov 25 '16 at 15:12
  • 2
    Free one liner for Kotlin `swipeRefreshLayout.isEnabled = webView.scrollY == 0` – Xenolion Sep 28 '18 at 14:49
  • Using this solution sometimes flickers the webview while scrolling or fling. – Smeet Sep 12 '21 at 02:30
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;
    }
}
hiBrianLee
  • 1,847
  • 16
  • 19
  • 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: http://stackoverflow.com/questions/25978462/swiperefreshlayout-viewpager-limit-horizontal-scroll-only/25983397#25983397 – corsair992 Feb 06 '15 at 00:17
  • 3
    mWebView.getScrollY is always 0 for me. – John Shelley Oct 29 '15 at 20:04
  • 1
    @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. – petrnohejl Feb 16 '16 at 23:32
  • 2
    @petrnohejl but what if we are loading some othe website? we can't have control over other websites, so this is not a global solution – Lalit Poptani Dec 01 '16 at 11:52
  • Unable to start activity, android.view.InflateException – Pablo Cegarra Jun 30 '17 at 07:29
  • @PabloCegarra You need to provide some constructors for the custom view. hope it helps others – KYHSGeekCode Dec 21 '21 at 07:19
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);
        }
    }
Pranav
  • 4,172
  • 3
  • 30
  • 31
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);
Alexey
  • 7,262
  • 4
  • 48
  • 68
  • I used `swipeRefreshLayout.setOnChildScrollUpCallback((p, c) -> webview.getScrollY() > 0);` and there's no need to extend SwipeRefreshLayout for `androidx.swiperefreshlayout:swiperefreshlayout:1.1.0` – wMaN May 12 '21 at 08:16
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.

Chaitanya Karmarkar
  • 1,425
  • 2
  • 7
  • 18
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);
    }
}
Community
  • 1
  • 1
Isaac
  • 136
  • 1
  • 4
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.

Xenolion
  • 12,035
  • 7
  • 33
  • 48
  • 1
    Your comment about fixed header definitely saved me a lot of time debugging!! Thank you. – katie May 10 '19 at 14:56
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);
            }
        }
    });
}
Community
  • 1
  • 1
karanatwal.github.io
  • 3,613
  • 3
  • 25
  • 57
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.)

doertsch
  • 121
  • 1
  • 7
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;

}

Akash Jp
  • 162
  • 1
  • 10
0
// 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);

        }

    });
Josef Vancura
  • 1,053
  • 10
  • 18
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>
murt
  • 3,790
  • 4
  • 37
  • 48
0

It works for me :

swipeRefreshLayout.getViewTreeObserver().addOnScrollChangedListener(() -> {
    swipeRefreshLayout.setEnabled(webView.getScrollY() == 0);
});
Arman Download
  • 174
  • 1
  • 2
  • 14
-1

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

swipeRefreshLayout.setNestedScrollingEnabled(true);
Maher Abuthraa
  • 17,493
  • 11
  • 81
  • 103
Tej
  • 564
  • 4
  • 10
  • Thing is once user swipe down too fast it will invoke SwipeReferesh if the user scrolls normally or slowly it is good. – Tej Oct 24 '16 at 14:31
-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.

星星瑶
  • 159
  • 2
  • 7
-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);
Breeno
  • 3,007
  • 2
  • 31
  • 30