I’ve implemented SwipeRefreshLayout
into my app but it can only hold one direct child which should be the listview. I’m trying to figure out how to add an empty textview to the following working XML file:
<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" >
<ListView
android:id="@+id/listViewConversation"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:dividerHeight="1dp" />
</android.support.v4.widget.SwipeRefreshLayout>
Wrapping it in a Linear/Relative layout makes it buggy because the listview will always update when you want to slide back up the listview. One way I can think of is doing this programmatically but I guess that’s not the best option.
You can learn how to implement it using this tutorial: Swipe to refresh GUIDE
So basically it all works fine but I would like to add an empty view that shows a message when the listview is empty.
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.
Answer:
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(…)” 🙂
Answer:
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).
Answer:
Here’s what I did :
I disabled the swipe to refresh, unless my listView is up.
mBookedProductsListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int i) {
}
@Override
public void onScroll(AbsListView absListView, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (firstVisibleItem == 0)
mSwipeRefreshLayout.setEnabled(true);
else
mSwipeRefreshLayout.setEnabled(false);
}
});
My 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/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<RelativeLayout
android:id="@+id/productListLL"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<EditText
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentTop="true"
android:background="#a4aeb8"
android:drawableRight="@drawable/search_icon"
android:paddingRight="15dp"
android:textColor="@android:color/white"
android:paddingLeft="15dp"
android:hint="Rechercher"
android:textColorHint="@android:color/white"
android:inputType="textCapWords|textNoSuggestions"
/>
<include android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/filter_by_categories_buttons" android:id="@+id/categoriesTree" android:layout_below="@id/search" />
<ListView
android:id="@+id/productListLV"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@drawable/product_listview_divider"
android:dividerHeight="1dp"
android:scrollbars="none"
android:overScrollMode="never"
android:choiceMode="multipleChoiceModal"
android:background="#e7eaef"
android:visibility="gone"
android:layout_below="@id/categoriesTree"
/>
</RelativeLayout>
</android.support.v4.widget.SwipeRefreshLayout>
Works like a charm.
Answer:
I encountered the same problem. It is annoying that SwipeRefreshLayout can only have one AdpaterView child.
If an empty view is set for an AdpaterView, it will be set as hidden if no data is available. However, SwipeRefreshLayout needs an AdpaterView to work. So, I extend AdpaterView to make it still shown, even if it is empty.
@Override
public void setVisibility(int visibility) {
if (visibility == View.GONE && getCount() == 0) {
return;
}
super.setVisibility(visibility);
}
Maybe you need to set the background of adapter view as transparent as well. But in my case, it is not needed, as the empty view is a simple TextView.
Answer:
Based on some answers here and the source code of SwipeRefreshLayout, I have subclassed the view to specifically handle having a RecyclerView (or ListView) and also an “empty” view inside a container which is the child.
It expects a layout such as
<SwipeRefreshLayoutWithEmpty ...>
<FrameLayout ...>
<TextView android:text="List is Empty" ...>
<RecyclerView ...>
</FrameLayout>
</SwipeRefreshLayoutWithEmpty>
The code is:
public class SwipeRefreshLayoutWithEmpty extends SwipeRefreshLayout {
private ViewGroup container;
public SwipeRefreshLayoutWithEmpty(Context context) {
super(context);
}
public SwipeRefreshLayoutWithEmpty(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean canChildScrollUp() {
// The swipe refresh layout has 2 children; the circle refresh indicator
// and the view container. The container is needed here
ViewGroup container = getContainer();
if (container == null) {
return false;
}
// The container has 2 children; the empty view and the scrollable view.
// Use whichever one is visible and test that it can scroll
View view = container.getChildAt(0);
if (view.getVisibility() != View.VISIBLE) {
view = container.getChildAt(1);
}
return ViewCompat.canScrollVertically(view, -1);
}
private ViewGroup getContainer() {
// Cache this view
if (container != null) {
return container;
}
// The container may not be the first view. Need to iterate to find it
for (int i=0; i<getChildCount(); i++) {
if (getChildAt(i) instanceof ViewGroup) {
container = (ViewGroup) getChildAt(i);
if (container.getChildCount() != 2) {
throw new RuntimeException("Container must have an empty view and content view");
}
break;
}
}
if (container == null) {
throw new RuntimeException("Container view not found");
}
return container;
}
}
Full gist: https://gist.github.com/grennis/16cb2b0c7f798418284dd2d754499b43
Answer:
I have tried following thing. In both empty view, and case of a list, swipe will refresh the data, also fastscroll is working fine without any code change required in activity.
I have put emptyview before the listview and marked its visiblity to gone.
and listview is put after that inside SwipeToRefresh block.
Sample Code –
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/tv_empty_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
android:gravity="center_horizontal"
android:text="No item found"
android:visibility="gone"/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lv_product"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
Answer:
Probably late, But if you’re still facing this issue, Here is the clean solution! There is no need of creating another SwipeRefreshLayout
.
wrap empty view
into a ScrollView
. Stick both AdapterView
and the ScrollView
into a ViewGroup
and put it into the SwipeRefreshLayout
Sample:
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" />
</ScrollView>
</FrameLayout>
</android.support.v4.widget.SwipeRefreshLayout>
EDIT
Here is much more simpler way.
check if your list is empthy. if yes then make it invisible. Sample:
if(lst.size() > 0) {
mRecyclerView.setVisibility(View.VISIBLE);
//set adapter
}else
{
mRecyclerView.setVisibility(View.INVISIBLE);
findViewById(R.id.txtNodata).setVisibility(View.VISIBLE);
}
this will make mRecyclerView
still there and all swipes will be working fine now even there is no data!
Answer:
Put SwipeRefreshLayout into a FrameLayout and other views behind it.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="@+id/your_message_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="No result"/>
</LinearLayout>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ListView
android:id="@+id/your_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
</android.support.v4.widget.SwipeRefreshLayout>
</FrameLayout>
Answer:
Why not wrap everything inside SwipeRefreshLayout
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"/>
<RelativeLayout
android:id="@android:id/empty"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</RelativeLayout>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>
Answer:
You may be just use NestedScrollView in SwipeRefreshLayout with single container.
Below is the list of buggy use mRecyclerView.setNestedScrollingEnabled(false);
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- some toolbar -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/conversationFragment_swipeRefresh"
android:layout_alignParentBottom="true"
android:layout_below="@+id/some_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/conversationFragment_noResultText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/FragmentConversations_empty"
android:layout_centerHorizontal="true" />
<android.support.v7.widget.RecyclerView
android:id="@+id/conversationFragment_recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
</RelativeLayout>
Answer:
Another option that works nicely in case that your empty view doesn’t need any interaction. For example, it is a simple textview saying “No data in here.”
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="visible"
android:layout_centerInParent="true"
android:text="@string/no_earnable_available"
android:textSize="18dp"
android:textColor="@color/black"
android:background="@color/white"
/>
<some.app.RecyclerViewSwipeToRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="4dp"
android:background="@android:color/transparent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
/>
</some.app.RecyclerViewSwipeToRefreshLayout>
</RelativeLayout>
This puts the empty view behind the SwipeToRefreshLayout which is transparent and which contains also a transparent RecyclerView.
Then, in the code, in the place where you add the items to the recycler view adapter, you check if the adapter is empty, and if so you set the visibility of the empty view to “visible”. And vice versa.
The trick is that the view is behind the transparent recycler view, which means that the recycler view and his parent, the SwipeToRefreshLayout, will handle the scroll when there are no items in the recycler. The empty view behind won’t even be touched so the touch event will be consumed by the SwipeTorefreshLayout.
The custom RecyclerSwipeTorefreshLayout should handle the canChildScrollUp method in the following way
@Override
public boolean canChildScrollUp() {
if (recycler == null) throw new IllegalArgumentException("recycler not set!");
else if (recycler.getAdapter().getItemCount() == 0){ // this check could be done in a more optimised way by setting a flag from the same place where you change the visibility of the empty view
return super.canChildScrollUp();
} else {
RecyclerView.LayoutManager layoutManager = recycler.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
return ((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() != 0 ||
(((LinearLayoutManager) recycler.getLayoutManager()).findFirstVisibleItemPosition() == 0
&& recycler.getChildAt(0) != null && recycler.getChildAt(0).getY() < 0);
//...
This will do the trick.
UPDATE:
Of course, the recycler view doesn’t have to be transparent all the time. You can update the transparency to be active only when the adapter is empty.
Cheers!
Answer:
This was a very frustrating issue for me but after a few hours with try and fail I came up with this solution.
With this I can refresh even with empty view visible (and RecyclerView too of course)
In my layout file I have this structure:
SwipeRefreshLayout
FrameLayout
RecyclerView
my_empty_layout // Doesnt have to be ScrollView it can be whatever ViewGroup you want, I used LinearLayout with a single TextView child
In code:
...
adapter.notifyDataSetChanged()
if (adapter.getItemCount() == 0) {
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
else {
recyclerView.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
}
Answer:
This worked for me
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/lighter_white">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@color/silver"
android:layout_marginTop="10dp"
android:divider="@android:color/transparent"
android:dividerHeight="8dp"
android:padding="4dp"/>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/empty"
android:text="@string/strNoRecordsFound"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textColor="@android:color/black"
android:alpha="0.5"
android:gravity="center">
</TextView>
</FrameLayout>
Answer:
As i mentioned here, i have done this for RecyclerView
:
I have used clickable="true"
like below with empty RecyclerView
:
mRecyclerView.setVisibility(View.VISIBLE);
mRecyclerView.setClickable(true);
myText.setVisibility(View.VISIBLE);
xml layout:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshKartvizitKisilerim"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/myText"
android:visibility="gone"
android:text="@string/noListItem"
android:textSize="18sp"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
RecyclerView
has height match_parent
and SwipeRefresh
has wrap_content
. When there is item in list, don’t forget to make text gone
.
Tags: androidandroid, layout, text, view