Write your own Android Pull-to-Refresh ListView


                   Of course, ListView is one of the mostly used GUI structure for displaying list of scrollable items, and Android Developers have put a lot of effort to make it beautiful, responsive, and memory efficient. Pull To Refresh is the recent, innovative and sleek UX design to refresh the list data. And it's Twitter who rendered this new way of refreshing list data instead of doing a button click. And it became viral, got a lot of attention. Twitter patented the gesture in 2010, this year (2013) Twitter officially announced that the patent will be used only for defensive purpose, anyone using it can continue the usage without any issues.

                This year Google updated their Apps (Gmail, G+) and integrated their own, a variant of pull to refresh design. But its touch interaction is broken in a way. You can't do pull to refresh while you are scrolling the list. List's first item needs to be positioned at the top to occur the pull to refresh gesture. ie, First you have to scroll the list to top, leave the touch (scroll gesture) and then start the pulling. Hope developers will soon fix it.
             
                 If you want to write your own Pull to Refresh, this blog will help you to achieve it. First you have to decide how / what will happen when user tries to pull the listview, whether something like Gmail app where pulling/refreshing ui is not connected to list (ie, only horiontal bar changes on top of the listview), or something like Twitter app where list scrolls along with the pulling, or any other style. Any way, for implementing these styles what you need is get Touch/Scroll events (listener) from ListView. For that you have to use customized ListView.
             
               Customization is about overriding the touch events from ListView. I hope you have basic idea about handling touch events (If you don't know, have a look at this blog: Handling TouchEvents).

2 ways of getting Pull-To-Refresh Gesture:

                 * Simplest way: something like Gmail does, if list is at top position, do get scroll listener from list and use the scroll event for creating Pulling gesture, else do nothing (Allow List to handle the touch events). For that what you have to do is just check whether the list is at top. You can check whether the list is at top by following code.

public boolean isOnTop(){ 
  if (listView.getChildCount()>0 && listview.getChildAt(0).getTop()==0
                             && listview.getFirstVisiblePosition() == 0){
      return true;
  }
  return false; 
} 


                  Here we will check whether the position/index of first visible item of list is zero & its top layout value is zero. If "isOnTop()" method returns True, then we can conclude that list is at top, and any try to scrolling down(Over scroll) can be considered as "pull to refresh". Then take the touch events from listView and do pull to refresh ui interaction. (Note: this method will break touch interaction, ie if the list is not at the top position, first you have place it to top, leave the touch, then pull).

PullToRefresh in Friday App
        


      * Better way: something like twitter does, without breaking touch interaction. To do this we have to customize android listview, a small tweak to provide touch events outside listview. All we need we need to know while pulling/swiping whether list is on the top or not. If you can get listview's overscroll listener then we will know when list reaches top . Officially listview doesn't provide overscroll listener. So we have to tweak listview a bit as follows

We have to override Listview's onOverScrolled() method.

 
 
 
 
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
            boolean clampedY) {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) {
            super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
            if(getFirstVisiblePosition()==0){
                // Of course list is at top, can perform pull to refresh
            }else{
                // List reached bottom end, but we don't need to know.
            }
        }
    }   

By adding more code listview will look like this:

public class PullToRefreshListView extends ListView {
    private boolean mTouchDown = false, mOverScroll = false,
            mRefreshing = false, mBottomOverScroll = false;
    private int mDist = 0, mLevel = 0, mLimit = -1; 

    private PullToRefresh mPullToRefresh;
  
    // Gesture listener, which will listen touch events w/o interrupting listview operations 
    private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD
                    && !mRefreshing) {
                if (getFirstVisiblePosition() == 0 && distanceY < 0) {
                    setSelection(0);
                    mOverScroll = true;
                    mOverScrollListener.onOverScrollStarted();
                }
            }
            if (distanceY > 0 && mOverScroll && !mRefreshing && mTouchDown) {
                mDist -= (distanceY);
                if (mDist < 0) {
                    mDist = 0;
                    setOverScrollCancel();
                }
                if (mPullToRefresh != null)
                    mPullToRefresh.onPull((mDist * 100 / mLimit));
            } else if (distanceY < 0 && mOverScroll && !mRefreshing
                    && mTouchDown) {
                mDist += (-distanceY);
                if (mDist > mLimit)
                    mDist = mLimit;
                if (mPullToRefresh != null)
                    mPullToRefresh.onPull((mDist * 100 / mLimit));
                if (mDist >= mLimit)
                    setOverScrollDone();

            }
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            if (!mRefreshing)
                setOverScrollCancel();
            return true;
        }

        public boolean onDown(MotionEvent e) {
            mBottomOverScroll = false;
            mTouchDown = true;
            return true;
        };
    };
 
    private GestureDetector mGestureDetector;
    private OnScrollEndListener mOnScrollEndListener;
 
    //Overscroll listener for internal use, will check list is at top.
    private OnOverScrollListener mOverScrollListener = new OnOverScrollListener() {
        @Override
        public void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
                boolean clampedY) {
            if (getFirstVisiblePosition() == 0)
                mOverScroll = true;
            if (getAdapter() != null
                    && getLastVisiblePosition() == getAdapter().getCount() - 1
                    && !mBottomOverScroll
                    && getChildAt(getChildCount() - 1).getBottom() == getBottom()) {
                mBottomOverScroll = true;
                if (mOnScrollEndListener != null) {
                    mOnScrollEndListener.onScrollEnd();
                }
            }
        }

        @Override
        public void onOverScrollStarted() {
            if (mPullToRefresh != null)
                mPullToRefresh.onPullStarted();
        }

        @Override
        public void onOverScollDone() {
            if (mPullToRefresh != null)
                mPullToRefresh.onRefresh();
        }

        @Override
        public void onReverseScroll(float distanceY) {
            mDist = 0;
            mLevel = 0;
            mOverScroll = false;
            /*
             * if (mPullToRefresh != null) { mPullToRefresh.onCancel(); }
             */
        }
    };

    public PullToRefreshListView(Context context) {
        super(context);
        init();
    }

    public PullToRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PullToRefreshListView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void setOverScrollDone() {
        mRefreshing = true;
        mDist = 0;
        mLevel = 0;
        if (mOverScrollListener != null && mOverScroll) {
            mOverScrollListener.onOverScollDone();
        }
        mOverScroll = false;
    }

    private void setOverScrollCancel() {
        mRefreshing = false;
        mTouchDown = false;
        mDist = 0;
        mLevel = 0;
        if (mPullToRefresh != null && mOverScroll) {
            mPullToRefresh.onCancel();
        }
        mOverScroll = false;
    }

    private void init() {
        setScrollingCacheEnabled(false);
        mGestureDetector = new GestureDetector(getContext(), mOnGestureListener);
    }

    public void setPullToRefresh(int maxLimit, PullToRefresh pullToRefresh) {
        mLimit = maxLimit;
        mPullToRefresh = pullToRefresh;
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
            boolean clampedY) {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.FROYO) {
            super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
            if (mOverScrollListener != null && !mRefreshing && mTouchDown) {
                if (scrollY == 0 && !mOverScroll) {
                    mOverScroll = true;
                    mOverScrollListener.onOverScrollStarted();
                }
                mOverScrollListener.onOverScrolled(scrollX, scrollY, clampedX,
                        clampedY);
            }
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mGestureDetector.onTouchEvent(ev);
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mGestureDetector.onTouchEvent(ev);
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mBottomOverScroll = false;
            mTouchDown = true;
            break;
        case MotionEvent.ACTION_MOVE:
            if (getChildCount() < 1)
                mOverScroll = true;
            else {
                if (getChildAt(0).getTop() == 0
                        && getFirstVisiblePosition() == 0)
                    mOverScroll = true;
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mBottomOverScroll = false;
            if (!mRefreshing && mOverScroll) {
                setOverScrollCancel();
            } else {
                mDist = 0;
                mLevel = 0;
                mOverScroll = false;
            }
            mTouchDown = false;
            break;
        }
        boolean ret = super.onTouchEvent(ev);
        return ret;
    }

    public void setRefreshDone() {
        mRefreshing = false;
        mDist = 0;
        mLevel = 0;
        mOverScroll = false;
    }

    public void setOnScrollEndListener(OnScrollEndListener onScrollEndListener) {
        mOnScrollEndListener = onScrollEndListener;
    }

    public interface PullToRefresh {
        public void onPullStarted();

        public void onPull(int progress);

        public void onRefresh();

        public void onCancel();
    }

    public interface OnScrollEndListener {
        public void onScrollEnd();
    }

    private interface OnOverScrollListener {
        public void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
                boolean clampedY);

        public void onOverScrollStarted();

        public void onOverScollDone();

        public void onReverseScroll(float distanceY);
    }
}
 
Holla, we are done..

Well you can get sample code here Android-PullToRefresh

Thanks !!!

Comments

Post a Comment