Android - SwipeRefreshLayout with empty textview

I didn't liked the limitation to a single child. Furthermore the current implementation of the SwipeRefreshLayout has an hardcoded "magic" handling for ScrollView, ListView and GridView that trigger only if the view it's the direct child of your own view.

That said the good news it's that it is open source, so you can either copy the code and adapt to your needs or you can do what I did:

Use two DIFFERENT SwipeRefreshLayout, one for the Empty view and one for the ListView.

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.tobrun.example.swipetorefresh.MainActivity">

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ListView
            android:id="@+id/listView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </android.support.v4.widget.SwipeRefreshLayout>

    <android.support.v4.widget.SwipeRefreshLayout
        android:id="@+id/swipeRefreshLayout_emptyView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <TextView
                android:id="@+id/emptyView"
                android:text="@string/empty"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:gravity="center" />

        </ScrollView>

    </android.support.v4.widget.SwipeRefreshLayout>


</FrameLayout>

Then tell your listview that the empty list view is the swipe refresh layout of the empty view.

Now the empty refresh layout will be automatically hidden by your list view when you have data and will be shown when the list is empty.

The swipe refresh layout of the list shouldn't receive touch events cause the list is hidden.

Good luck.


Actually, the only think you are missing is having that empty TextView be wrapped with a scrollable container - for example ScrollView. For details, have a look at SwipeRefreshLayout.canChildScrollUp() method and its usage.

Anyway, back to the point. Here is a successful implementation:

activity_some.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/swipe_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:measureAllChildren="true">

        <WebView
            android:id="@+id/webview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <include layout="@layout/empty" />

    </FrameLayout>

</android.support.v4.widget.SwipeRefreshLayout>

Where your empty.xml is basically anything you wish wrapped with a ScrollView.

empty.xml

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/empty"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <TextView
        android:text="Nothing here..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</ScrollView>

Now in order to get rid of the famous SwipeRefreshLayout refresh-only-when-at-the-top issue, toggle the SwipeRefreshLayout when necessary (Fragment-specific):

private ViewTreeObserver.OnScrollChangedListener mOnScrollChangedListener;

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

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

        }
    };
    swipeLayout.getViewTreeObserver().addOnScrollChangedListener(mOnScrollChangedListener);
}

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

That's it! Hope it helps! ;)

Btw, why would you use SwipeRefreshLayout with FrameLayout this way? Because this way you can do smooth transition animations, like crossfade effects, and any of your state views can be swipeable (in case you want a unified fetch/refresh/retry mechanism).


There is no need for any workaround.

You can simply use this view hierarchy :

    <FrameLayout ...>

        <android.support.v4.widget.SwipeRefreshLayout ...>

            <ListView
                android:id="@android:id/list" ... />
        </android.support.v4.widget.SwipeRefreshLayout>

        <TextView
            android:id="@android:id/empty" ...
            android:text="@string/empty_list"/>
    </FrameLayout>

Then, in code, you just call:

_listView.setEmptyView(findViewById(android.R.id.empty));

That's it.


EDIT: If you wish to be able to swipe-to-refresh even when the empty view is shown, you will have to somehow avoid hiding the ListView, so you could use a customized ListView that has this function inside:

@Override
  public void setVisibility(final int visibility)
    {
    if(visibility!=View.GONE||getCount()!=0)
      super.setVisibility(visibility);
    }

Together with the solution I wrote, the swipe-to-refresh is shown no matter how many items you are showing.

Of course, if you really want to hide the ListView, you should change the code. Maybe add "setVisibilityForReal(...)" :)