I want to implement scroll to refresh functionality with a listView. Also there are other view elements in the same layout file which are displayed if the list is empty. Her
I had a similar problem where the child of my SwipeRefreshLayout
was a FrameLayout
which had a TextView
and ListView
as children and when I scrolled up on the ListView
it would try to do a refresh.
I fixed it by using a custom FrameLayout
which overrides the canScrollVertically()
method
package com.wi.director.ui.common;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
/**
* Created by devansh on 9/22/15.
*/
public class FrameLayoutForSwipeRefresh extends FrameLayout {
public FrameLayoutForSwipeRefresh(Context context) {
super(context);
}
public FrameLayoutForSwipeRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FrameLayoutForSwipeRefresh(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public boolean canScrollVertically(int direction) {
if (super.canScrollVertically(direction)) {
return true;
}
int cc = getChildCount();
for (int i = 0; i < cc; i++) {
if (getChildAt(i).canScrollVertically(direction)) {
return true;
}
}
return false;
}
}
Basically, we want to scroll something scrollable. We can put all views in ScrollView.
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
But scrolling ListView may require some modifications - wrap_content ListView
Make your own implementation of SwipeRefreshLayout and override the canChildScrollUp in this way:
@Override
public boolean canChildScrollUp() {
if (scrollView != null)
return scrollView.canScrollVertically(-1);
return false;
}
just replace with any subclass of ScrollView.
If RecyclerView
or ListView
is not the direct child of SwipeRefreshLayout
then this issue occurs.
Simplest solution is to provide OnChildScrollUpCallback
implementation and return the results appropriately. In Kotlin code below, refreshLayout
is SwipeRefreshLayout
and recyclerView
is RecyclerView
as can be seen in xml
layout code as well.
refreshLayout.setOnChildScrollUpCallback(object : SwipeRefreshLayout.OnChildScrollUpCallback {
override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?): Boolean {
if (recyclerView != null) {
return recyclerView.canScrollVertically(-1)
}
return false
}
})
While xml
layout is something like this,
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
...
lots of other views i.e TextView, ImageView, MotionLayout
...
...
...
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView".../>
...
...
...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
In order for SwipeRefreshLayout to work, it needs to be the direct parent of your ListView, and the ListView should be the first active child view of the SwipeRefreshLayout.
The documentation for SwipeRefreshLayout says that the ListView should be the only child, but it is okay if it has more than one child as long as the ListView is first. This means, for instance, that SwipeRefreshLayout will work fine if you are using an adapter with a view for "empty". For example:
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NearbyJobsActivity$PlaceholderFragment">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@color/list_divider"
android:dividerHeight="1dp"
android:listSelector="@drawable/list_row_selector" />
<TextView
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center" />
</android.support.v4.widget.SwipeRefreshLayout>
If you can manage this sort of layout, then SwipeRefreshLayout will work fine and you won't need any of the workarounds listed in other answers.
My own problem was that that I was loading my ListView as a Fragment, so I actually had:
<SwipeRefreshLayout>
<FrameLayout> )
<ListView/> \ fragment
<TextView/> /
</FrameLayout> )
</SwipeRefreshLayout>
So the SwipeRefreshLayout was choosing the FrameLayout as it's "target" and its default canChildScrollUp()
implementation was always returning false. Once I moved the SwipeRefreshLayout inside the Fragment, everything started working correctly.
<FrameLayout>
<SwipeRefreshLayout> )
<ListView/> \ fragment
<TextView/> /
</SwipeRefreshLayout> )
</FrameLayout>
Even simpler just set SwipeRefreshLayout
enabled if firstVisibleItem
or disabled if not:
yourList.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
yourSwipeRefreshLayout.setEnabled(firstVisibleItem == 0);
}
});