SwipeRefreshLayout: How to use

Last support library update has included an interesting and unexpected layout: SwipeRefresLayout. It is a standard way to implement the common Pull to Refresh pattern in Android.

Using this new layout is really easy, but here it is a simple guide to make it work in a few seconds.

What do you need to know?

Almost nothing. Swipe refresh layout is a ViewGroup with the particularity that it can only hold one scrollable view as a children. It is basically a layout decorator that manages touch events and shows an indeterminate progress animation below the action bar when the user swipes down. The effect is similar to the one seen in Google Now app.

SwipeRefreshLayout

Methods are quite few:

setOnRefreshListener(OnRefreshListener): adds a listener to let other parts of the code know when refreshing begins.
setRefreshing(boolean): enables or disables progress visibility.
isRefreshing(): checks whether the view is refreshing.
setColorScheme(): it receive four different colors that will be used to colorize the animation.

SwipeRefreshLayout: The layout

As said before, you only need to decorate the swipable content (probable the whole layout) with this new layout. This view must be scrollable, such a ScrollView or a ListView. As a simple example:

[xml]
<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”>

<ScrollView
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<TextView
android:text=”@string/hello_world”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginTop=”16dp”
android:gravity=”center”/>
</ScrollView>

</android.support.v4.widget.SwipeRefreshLayout>
[/xml]

The code

We just need to get the layout, and assign some colours and the listener. The refreshing listener is a post delayed handler.

[java]
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container);
swipeLayout.setOnRefreshListener(this);
swipeLayout.setColorScheme(android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
}

@Override public void onRefresh() {
new Handler().postDelayed(new Runnable() {
@Override public void run() {
swipeLayout.setRefreshing(false);
}
}, 5000);
}
[/java]

A convenient activity and a trick

In order to simplify the use of this layout, I created a SwipeRefreshActivity which adds this layout to any activity. It can by found at my Github Gist.

As a trick, you may be found interesting to disable manual swipe gesture, maybe temporarily or because you only want to show progress animation programmatically. What you need to do is to use the method setEnabled() and set it to false.

Conclusion

Finally there is a standard way to create this common design pattern, and be sure we’ll begin to see it more and more in future apps and updates. Unfortunately I didn’t find a way to change the animation to make it similar to other Apps such as Gmail or Google+, but maybe some subclassing and overriding would do the trick. Keep you informed if I get into it.

55 thoughts on “SwipeRefreshLayout: How to use”

  1. TheRedPillSumit

    With a ListView I am not able to scroll the list view up. It starts the refresh instead of scrolling the list back to the top. Is there anything extra we need to do to make the listview also scroll up?

    1. @TheRedPillSumit, Just create a class which extends SwipeRefreshLayout and override the method canChildScrollUp(). return true when you want scroll down for listview. ex
      @override.
      boolean canChildScrollUp()
      {
      return Listview.getFirstVisibleItemPostion()!=0;
      }

      1. This is the current implementation that worked for me (I am using a ListFragment):

        @Override
        public boolean canChildScrollUp() {
        ListView mListView = (ListView) this.findViewById(android.R.id.list);
        //Log.d(“swipe_refresh”, String.format(“%d”,mListView.getFirstVisiblePosition()));
        return mListView.getFirstVisiblePosition() != 0;
        }

  2. Ok I have got it working. If the SwipeRefreshLayout is the root of the layout and the ListView resides deep into the hierarchy (I had put the ListView inside a RelativeLayout along with the empty TextView) and not the direct child of the SwipeRefreshLayout, it won’t detect a swipe up on the list view properly.

    When I put the ListView as the direct child of the SwipeRefreshLayout, it started working as intended.

    1. Antonio Leiva

      Thanks for your clarification. I only tried some simple cases till now, but I think we’ll need to find some workarounds for more complex ones.

      1. @Antonio Leiva Just create a class which extends SwipeRefreshLayout and override the method canChildScrollUp(). return true when you want scroll down for listview. ex
        @override.
        boolean canChildScrollUp()
        {
        return Listview.getFirstVisibleItemPostion()!=0;
        }

    2. Thanks Sumit, it worked, As switeRefreshLayout must have direct scrollable child in case of scrolling view, itherwise it won’t work

  3. The SwipeRefreshLayout sample at support4demo running is ok,but when i use SwipeRefreshLayout to my listview ,and use BucketListAdapter for the adapter . while the listview scrolling up ,the listview top view canit scroll down again,why? is the SwipeRefreshLayout ‘s bug?(sorry,my english is not very well),thanks.

    1. Antonio Leiva

      Maybe it has something to do with previous comment, SwipeRefreshLayout must be the immediate parent of listview. I didn’t try it with more complex layouts, sorry.

      1. Says so right in the docs: “This layout should be made the parent of the view that will be refreshed as a
        * result of the gesture and can only support one direct child.”

      2. When im trying to scroll down the list view,swipe refresh gets called.How to avoid it?

  4. My code :
    ———————————————————
    The Activity:

    package com.ksharkapps.fragments;

    import java.util.ArrayList;
    import java.util.List;

    import android.app.Activity;
    import android.content.Context;
    import android.os.AsyncTask;
    import android.os.Bundle;
    import android.os.Handler;
    import android.support.v4.app.Fragment;
    import android.support.v4.widget.SwipeRefreshLayout;
    import android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.view.ViewGroup;
    import android.widget.CheckBox;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import android.widget.ListView;
    import android.widget.ProgressBar;
    import android.widget.TextView;
    import android.widget.Toast;

    import com.actionbarsherlock.app.SherlockFragment;
    import com.actionbarsherlock.view.MenuItem;
    import com.kshark.adapters.BucketListAdapter;
    import com.ksharkapps.PlaycardGrid.R;

    public class WeBucketGridFragment extends SherlockFragment implements OnRefreshListener {

    private CustomAdapter adapter;
    private Fragment mContent;
    private ArrayList listitems;
    private ListView cardList;
    private static ProgressBar progress;
    private Context mContext;
    private static final String ARG_POSITION = “position”;

    private SwipeRefreshLayout mSwipeRefreshWidget;

    private Handler mHandler = new Handler();
    private final Runnable mRefreshDone = new Runnable() {

    @Override
    public void run() {
    mSwipeRefreshWidget.setRefreshing(false);
    }

    };

    public static WeBucketGridFragment newInstance(int position) {
    WeBucketGridFragment f = new WeBucketGridFragment();
    Bundle b = new Bundle();
    b.putInt(ARG_POSITION, position);
    f.setArguments(b);
    return f;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup group,
    Bundle saved) {
    View v = inflater.inflate(R.layout.play_listview, group, false);
    cardList = (ListView) v.findViewById(R.id.card_list);
    progress = (ProgressBar) v.findViewById(R.id.progressBar);

    mSwipeRefreshWidget = (SwipeRefreshLayout) v.findViewById(R.id.swipe_refresh_widget);
    mSwipeRefreshWidget.setColorScheme(R.color.color1, R.color.color2, R.color.color3,
    R.color.color4);
    mSwipeRefreshWidget.setOnRefreshListener(this);
    return v;
    }

    private void showMessage(String message) {
    Toast.makeText(getActivity(), message, Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    //yangjiantong add
    View head = LayoutInflater.from(getActivity()).inflate(R.layout.play_giditem, null);
    cardList.addHeaderView(head);
    head.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View arg0) {
    // TODO Auto-generated method stub
    Toast.makeText(getActivity(), “click head…”, 0).show();
    }
    });

    //end………..

    new AsyncTask() {

    @Override
    protected Long doInBackground(String[]… params) {

    listitems = new ArrayList();
    for (int i = 0; i < 1000; i++) {
    listitems.add("Temple run");
    }

    return null;
    }

    protected void onPreExecute() {

    progress.setVisibility(View.VISIBLE);
    cardList.setVisibility(View.GONE);

    }

    @Override
    public void onProgressUpdate(Long… value) {

    }

    @Override
    protected void onPostExecute(Long result) {

    adapter = new CustomAdapter(getActivity(), listitems);
    adapter.enableAutoMeasure(150);
    cardList.setAdapter(adapter);
    cardList.setVisibility(View.VISIBLE);
    progress.setVisibility(View.GONE);

    }

    }.execute();

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
    if (savedInstanceState != null)
    mContent = getActivity().getSupportFragmentManager().getFragment(
    savedInstanceState, "mContent");
    super.onCreate(savedInstanceState);
    }

    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {

    case android.R.id.home:
    getFragmentManager().popBackStack();
    return true;

    default:
    return super.onOptionsItemSelected(item);
    }
    }

    public class CustomAdapter extends BucketListAdapter {

    private Activity mActivity;
    private List items;

    public CustomAdapter(Activity ctx, List elements) {
    super(ctx, elements);
    this.mActivity = ctx;
    this.items = elements;
    }

    @Override
    protected View getBucketElement(final int position,
    String currentElement) {
    // TODO Auto-generated method stub
    ViewHolder holder;
    View bucketElement = null;

    LayoutInflater inflater = mActivity.getLayoutInflater();
    bucketElement = inflater.inflate(R.layout.play_giditem, null);
    holder = new ViewHolder(bucketElement);
    bucketElement.setTag(holder);
    holder.name.setText(currentElement);

    bucketElement.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    showMessage(“position :” + position);
    }
    });

    return bucketElement;
    }

    }

    class ViewHolder {
    public TextView name = null;
    public TextView info = null;
    public TextView modified = null;
    public CheckBox select = null;
    public ImageView image = null, app_icon = null;
    public LinearLayout rowlayout;

    ViewHolder(View row) {
    name = (TextView) row.findViewById(R.id.projectName);
    info = (TextView) row.findViewById(R.id.companyName);
    modified = (TextView) row.findViewById(R.id.Name);
    image = (ImageView) row.findViewById(R.id.listicon);

    }

    void populateFrom(String s) {
    name.setText(s);
    }
    }

    @Override
    public void onRefresh() {
    // TODO Auto-generated method stub
    mHandler.removeCallbacks(mRefreshDone);
    mHandler.postDelayed(mRefreshDone, 1000);
    }

    }

    ——————————————————-
    The BucketListAdapter:
    package com.kshark.adapters;

    import java.util.List;

    import com.ksharkapps.PlaycardGrid.R;

    import android.app.Activity;
    import android.util.DisplayMetrics;
    import android.view.View;
    import android.view.ViewGroup;
    import android.view.WindowManager;
    import android.widget.BaseAdapter;
    import android.widget.CheckBox;
    import android.widget.FrameLayout;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import android.widget.TextView;

    /**
    * A bucket adapter presenting rows of buckets.
    *
    * @author Scythe
    *
    * @param
    */
    public abstract class BucketListAdapter extends BaseAdapter {

    protected List elements;
    protected Activity ctx;
    protected Integer bucketSize;

    /**
    * Basic constructor, takes an Activity context and the list of elements.
    * Assumes a 1 column view by default.
    *
    * @param ctx
    * The Activity context.
    * @param elements
    * The list of elements to present.
    */
    public BucketListAdapter(Activity ctx, List elements) {
    this(ctx, elements, 1);
    }

    /**
    * Extended constructor, takes an Activity context, the list of elements and
    * the exact number of columns.
    *
    * @param ctx
    * The Activity context.
    * @param elements
    * The list of elements to present.
    * @param bucketSize
    * The exact number of columns.
    *
    */
    public BucketListAdapter(Activity ctx, List elements, Integer bucketSize) {
    this.elements = elements;
    this.ctx = ctx;
    this.bucketSize = bucketSize;
    }

    /**
    * Calculates the required number of columns based on the actual screen
    * width (in DIP) and the given minimum element width (in DIP).
    *
    * @param minBucketElementWidthDip
    * The minimum width in DIP of an element.
    */
    public void enableAutoMeasure(float minBucketElementWidthDip) {
    float screenWidth = getScreenWidthInDip();

    if (minBucketElementWidthDip >= screenWidth) {
    bucketSize = 1;
    } else {
    bucketSize = (int) (screenWidth / minBucketElementWidthDip);
    }
    }

    @Override
    public int getCount() {
    return (elements.size() + bucketSize – 1) / bucketSize;
    }

    @Override
    public Object getItem(int position) {
    return null;
    }

    @Override
    public long getItemId(int position) {
    return position;
    }

    @Override
    public View getView(int bucketPosition, View convertView, ViewGroup parent) {

    ViewGroup bucket = (ViewGroup) View.inflate(ctx, R.layout.bucket, null);

    for (int i = (bucketPosition * bucketSize); i < ((bucketPosition * bucketSize) + bucketSize); i++) {
    FrameLayout bucketElementFrame = new FrameLayout(ctx);
    bucketElementFrame.setLayoutParams(new LinearLayout.LayoutParams(0,
    LinearLayout.LayoutParams.WRAP_CONTENT, 1));
    bucketElementFrame.setPadding(3, 0, 3, 0);

    if (i < elements.size()) {
    View current = getBucketElement(i, elements.get(i));

    bucketElementFrame.addView(current);
    }

    bucket.addView(bucketElementFrame);
    }

    return bucket;
    }

    /**
    * Extending classes should return a bucket-element with this method. Each
    * row in the list contains bucketSize total elements.
    *
    * @param position
    * The absolute, global position of the current item.
    * @param currentElement
    * The current element for which the View should be constructed
    * @return The View that should be presented in the corresponding bucket.
    */
    protected abstract View getBucketElement(final int position,
    T currentElement);

    protected float getScreenWidthInDip() {
    WindowManager wm = ctx.getWindowManager();
    DisplayMetrics dm = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(dm);
    int screenWidth_in_pixel = dm.widthPixels;
    float screenWidth_in_dip = screenWidth_in_pixel / dm.density;

    return screenWidth_in_dip;
    }
    }

    class ViewHolder{

    private static FrameLayout card;

    ViewHolder(View row){

    }
    }

    ————————————————–

    The layout:

    The resuorce and code refer to this Demo:https://github.com/kshark27/PlayCardGrid

  5. ok,thanks,i re-try it。

    my before layout is:

    when i change layout to this :

    That’s OK.
    but iI also do not know why?
    Thanks!

    1. Antonio Leiva

      I don’t think is easy. It’s controlled by private constants in SwipeRefreshLayout class, and there are not public nor protected methods you can override to manage it. It will be hard to customize this class, only can think on reflection at the moment, or copying full code and changing what you need.

    1. Antonio Leiva

      Thanks! I haven’t used it yet, but will probably solve some problems I’ve been reading.

  6. Nguyễn Trần Hoài Trung

    How can I use SwipeRefreshLayout with ListView (@android:id/list) and EmptyView (@android:id/empty) ? SwipeRefreshLayout can host only one child.

    1. @Nguyễn Trần Hoài Trung, This is a bug in SwipeRefreshLayout. But it is simple to fix. Put your Listview and EmptyView inside LinearLayout and add it to SwipeRefreshLayout. Thats it. Make sure every component in the Empty view is clickable(set andriod:clickable=”true”).

      1. Fabian Frank

        Swipe to refresh only works when the view that is set using ListView.setEmptyView() is not clickable, by setting android:clickable=”false”.

      2. Have you tried what you have stated..?. When we set clickable false. its just ignore intercept event. Then the empty view which is on the top wont support touch event.

      3. Fabian Frank

        Yes, but if your empty view contains a button, then you are right, my suggestion is not feasible.

      4. I put a FrameLayout (with the ListView and EmptyView) inside the SwipeRefreshLayout and works great.
        The detail is set the android:clickable=”true” in the FrameLayout.

        Thanks again!

  7. How to show text “Swipe to refresh” like Gmail app. My layout contains a listview so its not intuitive for the user.

    1. Antonio Leiva

      New gmail update no longer uses that refresh. It switched to this one. If you want to achieve that appearance you will need to use another library.

  8. Can i Change the swipe direction from left to right .The default is from middle.

    1. Antonio Leiva

      No, the layout doesn’t allow changing animation. You would need to extend and override onDraw.

    1. Antonio Leiva

      You don’t need it at all. SwipeRefreshLayout is a ViewGroup, so you can use it wherever you want.

      1. So when I’m not using ActionBar, where the colored feedback will be displayed? Can I use SwipeRefreshLayout to have iOS like pull to refresh listview.

      2. Antonio Leiva

        It is shown at the top of the layout, it’s independent from the ActionBar. So if it fills the screen, it will appear at the very top.

      3. Antonio Leiva

        The best way is to try it yourself and see if it matches what you are looking for.

    1. Antonio Leiva

      That’s not supported by this layout directly. You would need to implement it by yourself. Not an easy task by the way.

  9. Please Help me…. With a WebViewI am not able to scroll the list view up. It starts the refresh instead of scrolling the WebView back to the top. Is there anything extra we need to do to make the WebView also scroll up? I dont want to use external library..

  10. Does anyone achived to implement it on a Fragment? I constatly get NoClassDefFoundError

  11. Can anyone Suggest how to implement SwipeRefreshLayout on a ListView which is inside a RelativeLayout with some other ImageViews and TextViews?

  12. where i can specify colors for progress bar ?
    i mean according to my app theme i want different colors so how can i change them ?

    Thanks for tuts.

  13. How can I use SwipeRefreshLayout to refresh my listview scrolling up and scrolling down too?

  14. I am facing the weird problem implementing the SwipeRefreshLayout.
    a. It is working perfectly when implemented in activity.
    b. The problem occurs when implementing in PagerSlidingTabs

    When the first element of the list have height grater than the size of screen, it don’t load other elements of the list. if 1st element is less or equal to height of screen, regardless the size of other elements of the screen, all works as expected.

    for further description:
    https://stackoverflow.com/questions/40323294/swiperefreshlayout-in-tabbed-view

Comments are closed.