├── .gitignore ├── DemoAPK ├── OverScrollListViewDemo.apk └── OverScrollListViewQRCode.png ├── OverScrollListViewDemo ├── AndroidManifest.xml ├── build.xml ├── project.properties ├── res │ ├── drawable │ │ ├── down_arrow.png │ │ ├── footer_view_selector.xml │ │ └── icon.png │ ├── layout │ │ ├── footer.xml │ │ ├── header.xml │ │ ├── item.xml │ │ └── main.xml │ └── values │ │ ├── dimens.xml │ │ └── strings.xml └── src │ └── net │ └── neevek │ └── android │ └── demo │ └── ptr │ ├── MainActivity.java │ ├── PullToLoadMoreFooterView.java │ └── PullToRefreshHeaderView.java ├── OverScrollListViewLib ├── AndroidManifest.xml ├── ant.properties ├── build.xml ├── proguard-project.txt ├── project.properties └── src │ └── net │ └── neevek │ └── android │ └── lib │ └── ptr │ └── OverScrollListView.java ├── README.md └── license.txt /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | obj/ 4 | proguard/ 5 | .classpath 6 | .project 7 | .DS_Store 8 | .settings 9 | local.properties 10 | OverScrollListViewDemo/OverScrollListViewDemo.iml 11 | OverScrollListViewLib/OverScrollListViewLib.iml 12 | .idea/ 13 | out 14 | *.out 15 | -------------------------------------------------------------------------------- /DemoAPK/OverScrollListViewDemo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/Easy-PullToRefresh-Android/27fccf0078e550ec4e19e5ae6c2eb62369aa5748/DemoAPK/OverScrollListViewDemo.apk -------------------------------------------------------------------------------- /DemoAPK/OverScrollListViewQRCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/Easy-PullToRefresh-Android/27fccf0078e550ec4e19e5ae6c2eb62369aa5748/DemoAPK/OverScrollListViewQRCode.png -------------------------------------------------------------------------------- /OverScrollListViewDemo/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-18 15 | android.library.reference.1=../OverScrollListViewLib 16 | proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 17 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/drawable/down_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/Easy-PullToRefresh-Android/27fccf0078e550ec4e19e5ae6c2eb62369aa5748/OverScrollListViewDemo/res/drawable/down_arrow.png -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/drawable/footer_view_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/Easy-PullToRefresh-Android/27fccf0078e550ec4e19e5ae6c2eb62369aa5748/OverScrollListViewDemo/res/drawable/icon.png -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/layout/footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 27 | 28 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/layout/header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 16 | 23 | 28 | 35 | 36 | 44 | 45 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/layout/item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 70dp 4 | 5 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | OverScrollListViewDemo 4 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/src/net/neevek/android/demo/ptr/MainActivity.java: -------------------------------------------------------------------------------- 1 | package net.neevek.android.demo.ptr; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.os.SystemClock; 6 | import android.view.View; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.Toast; 9 | import net.neevek.android.lib.ptr.OverScrollListView; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class MainActivity extends Activity implements OverScrollListView.OnRefreshListener, OverScrollListView.OnLoadMoreListener { 15 | private final static String TAG = MainActivity.class.getSimpleName(); 16 | 17 | private OverScrollListView mListView; 18 | 19 | private List mDataList; 20 | private ArrayAdapter mAdapter; 21 | 22 | @Override 23 | public void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.main); 26 | 27 | mListView = (OverScrollListView)findViewById(R.id.listview); 28 | 29 | View header = getLayoutInflater().inflate(R.layout.header, null); 30 | View footer = getLayoutInflater().inflate(R.layout.footer, null); 31 | 32 | mListView.setPullToRefreshHeaderView(header); 33 | // mListView.addHeaderView(getLayoutInflater().inflate(R.layout.header, null)); 34 | 35 | // mListView.addFooterView(getLayoutInflater().inflate(R.layout.footer, null)); 36 | mListView.setPullToLoadMoreFooterView(footer); 37 | 38 | mListView.setOnRefreshListener(this); 39 | mListView.setOnLoadMoreListener(this); 40 | 41 | mDataList = new ArrayList(); 42 | mAdapter = new ArrayAdapter(this, R.layout.item, R.id.tv_item, mDataList); 43 | 44 | mListView.setAdapter(mAdapter); 45 | 46 | mListView.startRefreshManually(null); 47 | } 48 | 49 | private void initData() { 50 | if (mDataList == null) { 51 | mDataList = new ArrayList(); 52 | } else { 53 | mDataList.clear(); 54 | } 55 | 56 | for (int i = 0; i < 25; ++i) { 57 | mDataList.add("Item " + i); 58 | } 59 | 60 | mAdapter.notifyDataSetChanged(); 61 | 62 | if (mDataList.size() > 0 && !mListView.isLoadingMoreEnabled()) { 63 | mListView.enableLoadMore(true); 64 | } 65 | } 66 | 67 | @Override 68 | public void onLoadMore() { 69 | new Thread(){ 70 | @Override 71 | public void run() { 72 | SystemClock.sleep(1000); 73 | 74 | mListView.post(new Runnable() { 75 | @Override 76 | public void run() { 77 | for (int i = 0, j = mDataList.size(); i < 10; ++i, ++j) { 78 | mDataList.add("Item " + j); 79 | } 80 | mAdapter.notifyDataSetChanged(); 81 | 82 | boolean reachTheEnd = mDataList.size() >= 55; 83 | mListView.finishLoadingMore(); 84 | if (reachTheEnd) { 85 | mListView.enableLoadMore(false); 86 | Toast.makeText(MainActivity.this, "Reach the end of the list, no more data to load.", Toast.LENGTH_LONG).show(); 87 | } 88 | 89 | } 90 | }); 91 | } 92 | }.start(); 93 | } 94 | 95 | @Override 96 | public void onRefresh(Object bizContextObject) { 97 | new Thread(){ 98 | @Override 99 | public void run() { 100 | SystemClock.sleep(2000); 101 | 102 | mListView.post(new Runnable() { 103 | @Override 104 | public void run() { 105 | initData(); 106 | mListView.finishRefreshing(); 107 | mListView.resetLoadMoreFooterView(); 108 | } 109 | }); 110 | } 111 | }.start(); 112 | } 113 | 114 | @Override 115 | public void onRefreshAnimationEnd() { 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/src/net/neevek/android/demo/ptr/PullToLoadMoreFooterView.java: -------------------------------------------------------------------------------- 1 | package net.neevek.android.demo.ptr; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | import android.util.AttributeSet; 6 | import android.view.ViewTreeObserver; 7 | import android.widget.FrameLayout; 8 | import android.widget.ProgressBar; 9 | import android.widget.TextView; 10 | import net.neevek.android.lib.ptr.OverScrollListView; 11 | 12 | /** 13 | * @author neevek 14 | * 15 | * The default implementation of a pull-to-load-more footer view for OverScrollListView. 16 | * this can be taken as an implementation reference. 17 | */ 18 | public class PullToLoadMoreFooterView extends FrameLayout implements OverScrollListView.PullToLoadMoreCallback { 19 | private TextView mTvLoadMore; 20 | private ProgressBar mProgressBar; 21 | 22 | private String mPullText = "Pull to load more"; 23 | private String mClickText = "Click to load more"; 24 | private String mReleaseText = "Release to load more"; 25 | private String mLoadingText = "Loading..."; 26 | 27 | public PullToLoadMoreFooterView(Context context) { 28 | super(context); 29 | } 30 | 31 | public PullToLoadMoreFooterView(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | } 34 | 35 | public PullToLoadMoreFooterView(Context context, AttributeSet attrs, int defStyle) { 36 | super(context, attrs, defStyle); 37 | } 38 | 39 | @Override 40 | protected void onFinishInflate() { 41 | ensuresLoadMoreViewsAvailability(); 42 | } 43 | 44 | private void ensuresLoadMoreViewsAvailability() { 45 | if (mTvLoadMore == null) { 46 | mTvLoadMore = (TextView)findViewById(R.id.tv_load_more); 47 | mTvLoadMore.setText(mClickText); 48 | } 49 | 50 | if (mProgressBar == null) { 51 | mProgressBar = (ProgressBar)findViewById(R.id.pb_loading); 52 | } 53 | } 54 | 55 | @Override 56 | public void onReset() { 57 | ensuresLoadMoreViewsAvailability(); 58 | getChildAt(0).setVisibility(VISIBLE); 59 | } 60 | 61 | @Override 62 | public void onStartPulling() { 63 | ensuresLoadMoreViewsAvailability(); 64 | mTvLoadMore.setText(mPullText); 65 | } 66 | 67 | @Override 68 | public void onCancelPulling() { 69 | ensuresLoadMoreViewsAvailability(); 70 | mTvLoadMore.setText(mClickText); 71 | } 72 | 73 | @Override 74 | public void onReachAboveRefreshThreshold() { 75 | mTvLoadMore.setText(mReleaseText); 76 | } 77 | 78 | @Override 79 | public void onReachBelowRefreshThreshold() { 80 | onStartPulling(); 81 | } 82 | 83 | @Override 84 | public void onStartLoadingMore() { 85 | ensuresLoadMoreViewsAvailability(); 86 | mProgressBar.setVisibility(VISIBLE); 87 | mTvLoadMore.setText(mLoadingText); 88 | } 89 | 90 | @Override 91 | public void onEndLoadingMore() { 92 | mProgressBar.setVisibility(GONE); 93 | mTvLoadMore.setText(mClickText); 94 | } 95 | 96 | @Override 97 | public void setVisibility(int visibility) { 98 | super.setVisibility(visibility); 99 | getChildAt(0).setVisibility(visibility); 100 | } 101 | 102 | public void setPullText(String pullText) { 103 | mPullText = pullText; 104 | } 105 | 106 | public void setClickText(String clickText) { 107 | mClickText = clickText; 108 | } 109 | 110 | public void setReleaseText(String releaseText) { 111 | mReleaseText = releaseText; 112 | } 113 | 114 | public void setLoadingText(String loadingText) { 115 | mLoadingText = loadingText; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /OverScrollListViewDemo/src/net/neevek/android/demo/ptr/PullToRefreshHeaderView.java: -------------------------------------------------------------------------------- 1 | package net.neevek.android.demo.ptr; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.view.animation.Animation; 7 | import android.view.animation.RotateAnimation; 8 | import android.widget.LinearLayout; 9 | import android.widget.ProgressBar; 10 | import android.widget.TextView; 11 | import net.neevek.android.lib.ptr.OverScrollListView; 12 | 13 | /** 14 | * @author neevek 15 | * 16 | * The default implementation of a pull-to-load-more header view for OverScrollListView. 17 | * this can be taken as an implementation reference. 18 | */ 19 | public class PullToRefreshHeaderView extends LinearLayout implements OverScrollListView.PullToRefreshCallback { 20 | private final static int ROTATE_ANIMATION_DURATION = 300; 21 | 22 | private View mArrowView; 23 | private TextView mTvRefresh; 24 | private ProgressBar mProgressBar; 25 | 26 | private Animation mAnimRotateUp; 27 | private Animation mAnimRotateDown; 28 | 29 | private String mPullText = "Pull to refresh"; 30 | private String mReleaseText = "Release to refresh"; 31 | private String mRefreshText = "Refreshing..."; 32 | private String mFinishText = "Refresh complete"; 33 | 34 | public PullToRefreshHeaderView(Context context) { 35 | super(context); 36 | init(); 37 | } 38 | 39 | public PullToRefreshHeaderView(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | init(); 42 | } 43 | 44 | public void init() { 45 | mAnimRotateUp = new RotateAnimation(0, -180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 46 | mAnimRotateUp.setDuration(ROTATE_ANIMATION_DURATION); 47 | mAnimRotateUp.setFillAfter(true); 48 | mAnimRotateDown = new RotateAnimation(-180f, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 49 | mAnimRotateDown.setDuration(ROTATE_ANIMATION_DURATION); 50 | mAnimRotateDown.setFillAfter(true); 51 | } 52 | 53 | @Override 54 | protected void onFinishInflate() { 55 | mArrowView = findViewById(R.id.iv_down_arrow); 56 | mTvRefresh = (TextView)findViewById(R.id.tv_refresh); 57 | mProgressBar = (ProgressBar)findViewById(R.id.pb_refreshing); 58 | } 59 | 60 | @Override 61 | public void onStartPulling() { 62 | mProgressBar.setVisibility(GONE); 63 | mArrowView.setVisibility(VISIBLE); 64 | mTvRefresh.setVisibility(VISIBLE); 65 | mTvRefresh.setText(mPullText); 66 | } 67 | 68 | /** 69 | * @param scrollY [screenHeight, 0] 70 | */ 71 | @Override 72 | public void onPull(int scrollY) { 73 | } 74 | 75 | @Override 76 | public void onReachAboveHeaderViewHeight() { 77 | mProgressBar.setVisibility(GONE); 78 | mTvRefresh.setText(mReleaseText); 79 | mArrowView.startAnimation(mAnimRotateUp); 80 | } 81 | 82 | @Override 83 | public void onReachBelowHeaderViewHeight() { 84 | mProgressBar.setVisibility(GONE); 85 | mTvRefresh.setText(mPullText); 86 | mArrowView.startAnimation(mAnimRotateDown); 87 | } 88 | 89 | @Override 90 | public void onStartRefreshing() { 91 | mArrowView.clearAnimation(); 92 | mArrowView.setVisibility(GONE); 93 | mProgressBar.setVisibility(VISIBLE); 94 | mTvRefresh.setText(mRefreshText); 95 | } 96 | 97 | @Override 98 | public void onEndRefreshing() { 99 | mProgressBar.setVisibility(GONE); 100 | mTvRefresh.setVisibility(GONE); 101 | } 102 | 103 | @Override 104 | public void onFinishRefreshing() { 105 | mTvRefresh.setText(mFinishText); 106 | } 107 | 108 | public void setPullText(String pullText) { 109 | mPullText = pullText; 110 | } 111 | 112 | public void setReleaseText(String releaseText) { 113 | mReleaseText = releaseText; 114 | } 115 | 116 | public void setRefreshText(String refreshText) { 117 | mRefreshText = refreshText; 118 | } 119 | 120 | public void setFinishText(String finishText) { 121 | mFinishText = finishText; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /OverScrollListViewLib/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /OverScrollListViewLib/ant.properties: -------------------------------------------------------------------------------- 1 | # This file is used to override default values used by the Ant build system. 2 | # 3 | # This file must be checked into Version Control Systems, as it is 4 | # integral to the build system of your project. 5 | 6 | # This file is only used by the Ant script. 7 | 8 | # You can use this to override default values such as 9 | # 'source.dir' for the location of your java source folder and 10 | # 'out.dir' for the location of your output folder. 11 | 12 | # You can also use it define how the release builds are signed by declaring 13 | # the following properties: 14 | # 'key.store' for the location of your keystore and 15 | # 'key.alias' for the name of the key to use. 16 | # The password will be asked during the build when you use the 'release' target. 17 | 18 | -------------------------------------------------------------------------------- /OverScrollListViewLib/build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | 56 | 57 | 69 | 70 | 71 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /OverScrollListViewLib/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /OverScrollListViewLib/project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | android.library=true 14 | # Project target. 15 | target=android-18 16 | -------------------------------------------------------------------------------- /OverScrollListViewLib/src/net/neevek/android/lib/ptr/OverScrollListView.java: -------------------------------------------------------------------------------- 1 | package net.neevek.android.lib.ptr; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.*; 8 | import android.view.animation.DecelerateInterpolator; 9 | import android.widget.*; 10 | 11 | /** 12 | * @author neevek 13 | * @version v1.0.0 finished on Nov. 24, 2013 (a rainy Sunday in GuangZhou) 14 | * @version v1.0.3 finished at 2:49 a.m. on Dec. 5, 2013 15 | * 16 | * This class implements the bounce effect & pull-to-refresh feature for 17 | * ListView(the implementation can also be applied to ExpandableListView). 18 | * 19 | * For the bounce effect, the implementation simply intercepts touch events 20 | * and detects if the scrolling has reached the top or bottom edge, if so, we 21 | * call scrollTo() to scroll the entire the ListView off the screen, and then 22 | * with a Scroller, we compute the Y scroll positions and create a smooth 23 | * bounce effect. 24 | * 25 | * For pull-to-refresh, the implementation uses a header view which implements 26 | * the PullToRefreshCallback interface as the indicator view for displaying 27 | * "Pull to refresh", "Release to refresh", "Loading..." and an arrow image. 28 | * Of course, you can implement PullToRefreshCallback and write your own 29 | * PullToRefreshHeaderView, as long as you follow some requirements for 30 | * the layout of the header view, take the default PullToRefreshHeaderView 31 | * as a reference. 32 | * 33 | * NOTE: If you do not want the pull-to-refresh feature, you can still use 34 | * OverScrollListView, in that case, OverScrollListView only offers 35 | * you the bounce effect, and that is why it has the name. just remember 36 | * not to call setPullToRefreshHeaderView() 37 | */ 38 | public class OverScrollListView extends ListView { 39 | private final static int DEFAULT_MAX_OVER_SCROLL_DURATION = 350; 40 | private final static int DEFAULT_FINISH_DELAYED_DURATION = 800; 41 | 42 | // boucing for a normal touch scroll gesture(happens right after the finger leaves the screen) 43 | private Scroller mScroller; 44 | 45 | private float mLastY; 46 | private boolean mIsTouching; 47 | private boolean mIsBeingTouchScrolled; 48 | private int mLoadingMorePullDistanceThreshold; 49 | private float mScreenDensity; 50 | 51 | // a threshold to tell whether the user is touch-scrolling 52 | private int mTouchSlop; 53 | private int mMinimumVelocity; 54 | private int mMaximumVelocity; 55 | 56 | // the top-level layout of the header view 57 | private PullToRefreshCallback mOrigHeaderView; 58 | 59 | // the layout, of which we will do adjust the height, and on which 60 | // we call requestLayout() to cause the view hierarchy to be redrawn 61 | private View mHeaderView; 62 | // for convenient adjustment of the header view height 63 | private ViewGroup.LayoutParams mHeaderViewLayoutParams; 64 | // the original height of the header view 65 | private int mHeaderViewHeight; 66 | 67 | // user of this pull-to-refresh ListView may register a a listener, 68 | // which will be called when a "refresh" action should be initiated. 69 | private OnRefreshListener mOnRefreshListener; 70 | private boolean mIsRefreshing; 71 | // is finishRefreshing() has just been called? 72 | private boolean mCancellingRefreshing; 73 | private boolean mHideHeaderViewWithoutAnimation; 74 | 75 | private PullToLoadMoreCallback mSavedFooterView; 76 | private PullToLoadMoreCallback mFooterView; 77 | private boolean mIsLoadingMore; 78 | private OnLoadMoreListener mOnLoadMoreListener; 79 | 80 | private boolean mMarkAutoRefresh; 81 | private Object mBizContextForRefresh; 82 | 83 | private VelocityTracker mVelocityTracker; 84 | 85 | public OverScrollListView(Context context) { 86 | super(context); 87 | init(context); 88 | } 89 | 90 | public OverScrollListView(Context context, AttributeSet attrs) { 91 | super(context, attrs); 92 | init(context); 93 | } 94 | 95 | public OverScrollListView(Context context, AttributeSet attrs, int defStyle) { 96 | super(context, attrs, defStyle); 97 | init(context); 98 | } 99 | 100 | private void init(Context context) { 101 | mScreenDensity = context.getResources().getDisplayMetrics().density; 102 | mLoadingMorePullDistanceThreshold = (int)(mScreenDensity * 50); 103 | 104 | mScroller = new Scroller(context, new DecelerateInterpolator(1.3f)); 105 | 106 | // on Android 2.3.3, disabling overscroll makes ListView behave weirdly 107 | if (Build.VERSION.SDK_INT > 10) { 108 | // disable the glow effect at the edges when overscrolling. 109 | setOverScrollMode(OVER_SCROLL_NEVER); 110 | } 111 | 112 | final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 113 | 114 | mTouchSlop = configuration.getScaledTouchSlop(); 115 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 116 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 117 | 118 | mVelocityTracker = VelocityTracker.obtain(); 119 | } 120 | 121 | public void setPullToRefreshHeaderView(View headerView) { 122 | if (mOrigHeaderView != null) { 123 | return; 124 | } 125 | 126 | if (!(headerView instanceof PullToRefreshCallback)) { 127 | throw new IllegalArgumentException("Pull-to-refresh header view must implement PullToRefreshCallback"); 128 | } 129 | 130 | mOrigHeaderView = (PullToRefreshCallback)headerView; 131 | 132 | if (headerView instanceof ViewGroup) { 133 | mHeaderView = ((ViewGroup) headerView).getChildAt(0); // pay attention to this 134 | if (mHeaderView == null || (!(mHeaderView instanceof LinearLayout) && !(mHeaderView instanceof RelativeLayout))) { 135 | throw new IllegalArgumentException("Pull-to-refresh header view must have " + 136 | "the following layout hierachy: LinearLayout->LinearLayout->[either a LinearLayout or RelativeLayout]"); 137 | } 138 | } else { 139 | throw new IllegalArgumentException("Pull-to-refresh header view must have " + 140 | "the following layout hierarchy: LinearLayout->LinearLayout->[either a LinearLayout or RelativeLayout]"); 141 | } 142 | addHeaderView(headerView, null, false); 143 | } 144 | 145 | @Override 146 | protected void layoutChildren() { 147 | try { 148 | super.layoutChildren(); 149 | } catch (RuntimeException e) { 150 | e.printStackTrace(); 151 | ListAdapter listAdapter = getAdapter(); 152 | if (listAdapter instanceof HeaderViewListAdapter) { 153 | listAdapter = ((HeaderViewListAdapter) listAdapter).getWrappedAdapter(); 154 | throw new RuntimeException(e.getMessage() + ", adapter=["+ listAdapter.getClass() +"]", e); 155 | } 156 | throw e; 157 | } 158 | } 159 | 160 | @Override 161 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 162 | super.onLayout(changed, l, t, r, b); 163 | 164 | if (mHeaderViewHeight == 0 && mHeaderView != null) { 165 | mHeaderViewLayoutParams = mHeaderView.getLayoutParams(); 166 | // after the first "laying-out", we get the original height of header view 167 | mHeaderViewHeight = mHeaderViewLayoutParams.height; 168 | 169 | if (mMarkAutoRefresh) { 170 | mMarkAutoRefresh = false; 171 | post(new Runnable() { 172 | @Override 173 | public void run() { 174 | startRefreshingManually(mBizContextForRefresh); 175 | } 176 | }); 177 | } else { 178 | // set the header height to 0 in advance. "post(Runnable)" below is queued up 179 | // to run in the main thread, which may delay for some time 180 | mHeaderViewLayoutParams.height = 0; 181 | // hide the header view 182 | post(new Runnable() { 183 | @Override 184 | public void run() { 185 | setHeaderViewHeightInternal(0); 186 | } 187 | }); 188 | } 189 | } 190 | } 191 | 192 | public void setPullToLoadMoreFooterView(View footerView) { 193 | if (!(footerView instanceof PullToLoadMoreCallback)) { 194 | throw new IllegalArgumentException("Pull-to-load-more footer view must implement PullToLoadMoreCallback"); 195 | } 196 | 197 | mSavedFooterView = (PullToLoadMoreCallback)footerView; 198 | ((View)mSavedFooterView).setVisibility(GONE); 199 | 200 | addFooterView(footerView); 201 | 202 | footerView.setOnClickListener(new OnClickListener() { 203 | @Override 204 | public void onClick(View v) { 205 | if (!mIsLoadingMore && mFooterView != null) { 206 | mIsLoadingMore = true; 207 | mFooterView.onStartLoadingMore(); 208 | 209 | if (mOnLoadMoreListener != null) { 210 | mOnLoadMoreListener.onLoadMore(); 211 | } 212 | } 213 | } 214 | }); 215 | } 216 | 217 | public void setOnRefreshListener(OnRefreshListener listener) { 218 | mOnRefreshListener = listener; 219 | } 220 | 221 | public void setOnLoadMoreListener(OnLoadMoreListener listener) { 222 | mOnLoadMoreListener = listener; 223 | } 224 | 225 | public boolean isRefreshing() { 226 | return mIsRefreshing; 227 | } 228 | 229 | public void finishRefreshing() { 230 | if (mIsRefreshing) { 231 | mCancellingRefreshing = true; 232 | mIsRefreshing = false; 233 | 234 | mScroller.forceFinished(true); 235 | 236 | // hide the header view, with a smooth bouncing effect 237 | springBack(-mHeaderViewHeight + getScrollY()); 238 | // setSelection(0); 239 | } 240 | } 241 | 242 | public void finishRefreshing(Runnable runnable) { 243 | finishRefreshing(runnable, DEFAULT_FINISH_DELAYED_DURATION); 244 | } 245 | 246 | public void finishRefreshing(final Runnable runnable, int delayedDuration) { 247 | if (mOrigHeaderView != null) { 248 | mOrigHeaderView.onFinishRefreshing(); 249 | } 250 | postDelayed(new Runnable() { 251 | @Override 252 | public void run() { 253 | finishRefreshing(); 254 | if (runnable != null) { 255 | postDelayed(runnable, DEFAULT_MAX_OVER_SCROLL_DURATION); 256 | } 257 | } 258 | }, delayedDuration); 259 | } 260 | 261 | public void finishRefreshingAndHideHeaderViewWithoutAnimation() { 262 | if (mIsRefreshing) { 263 | mCancellingRefreshing = true; 264 | mHideHeaderViewWithoutAnimation = true; 265 | mIsRefreshing = false; 266 | 267 | mScroller.forceFinished(true); 268 | // hide the header view, with a smooth bouncing effect 269 | springBack(getScrollY()); 270 | } 271 | } 272 | 273 | public boolean isLoadingMore() { 274 | return mIsLoadingMore; 275 | } 276 | 277 | public void finishLoadingMore() { 278 | if (mIsLoadingMore) { 279 | mIsLoadingMore = false; 280 | 281 | if (mFooterView != null) { 282 | mFooterView.onEndLoadingMore(); 283 | } 284 | } 285 | } 286 | 287 | public void resetLoadMoreFooterView() { 288 | if (mSavedFooterView != null) { 289 | mFooterView = mSavedFooterView; 290 | } 291 | 292 | if (mFooterView != null) { 293 | mFooterView.onReset(); 294 | } 295 | } 296 | 297 | public void enableLoadMore(boolean enable) { 298 | if (enable) { 299 | if (mSavedFooterView != null) { 300 | mFooterView = mSavedFooterView; 301 | ((View)mFooterView).setVisibility(VISIBLE); 302 | } 303 | } else if (mFooterView != null) { 304 | ((View)mFooterView).setVisibility(GONE); 305 | mSavedFooterView = mFooterView; 306 | mFooterView = null; 307 | } 308 | } 309 | 310 | public boolean isLoadingMoreEnabled() { 311 | return mFooterView != null; 312 | } 313 | 314 | protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { 315 | if (!isTouchEvent && mScroller.isFinished()) { 316 | mVelocityTracker.computeCurrentVelocity((int)(16 * mScreenDensity), mMaximumVelocity); 317 | int yVelocity = (int) mVelocityTracker.getYVelocity(0); 318 | 319 | if ((Math.abs(yVelocity) > mMinimumVelocity)) { 320 | mScroller.fling(0, getScrollY(), 0, -yVelocity, 0, 0, -mMaximumVelocity, mMaximumVelocity); 321 | postInvalidate(); 322 | } 323 | } 324 | return true; 325 | } 326 | 327 | @Override 328 | public boolean onInterceptTouchEvent(MotionEvent ev) { 329 | switch (ev.getAction()) { 330 | case MotionEvent.ACTION_DOWN: 331 | // for whatever reason, stop the scroller when the user *might* 332 | // start new touch-scroll gestures. 333 | mScroller.forceFinished(true); 334 | 335 | mLastY = ev.getRawY(); 336 | mIsTouching = true; 337 | mCancellingRefreshing = false; 338 | 339 | mVelocityTracker.clear(); 340 | mVelocityTracker.addMovement(ev); 341 | break; 342 | } 343 | return super.onInterceptTouchEvent(ev); 344 | } 345 | 346 | 347 | @Override 348 | public boolean onTouchEvent(MotionEvent ev) { 349 | mVelocityTracker.addMovement(ev); 350 | 351 | switch (ev.getAction()) { 352 | case MotionEvent.ACTION_MOVE: 353 | float y = ev.getRawY(); 354 | int deltaY = (int)(y - mLastY); 355 | 356 | if (deltaY == 0) { 357 | return true; 358 | } 359 | 360 | if (mIsBeingTouchScrolled) { 361 | if (getChildCount() > 0) { 362 | handleTouchScroll(deltaY); 363 | } 364 | 365 | mLastY = y; 366 | } else if (Math.abs(deltaY) > mTouchSlop) { 367 | // check if the delta-y has exceeded the threshold 368 | mIsBeingTouchScrolled = true; 369 | mLastY = y; 370 | break; 371 | } 372 | break; 373 | case MotionEvent.ACTION_UP: 374 | case MotionEvent.ACTION_CANCEL: 375 | mIsTouching = false; 376 | mIsBeingTouchScrolled = false; 377 | 378 | // 'getScrollY != 0' means that content of the ListView is off screen. 379 | // Or if it is not in "refreshing" state while height of the header view 380 | // is greater than 0, we must set it to 0 with a smooth bounce effect 381 | if ((getScrollY() != 0 || (!mIsRefreshing && getCurrentHeaderViewHeight() > 0))) { 382 | springBack(); 383 | 384 | // it is safe to digest the touch events here 385 | return true; 386 | } 387 | 388 | break; 389 | } 390 | 391 | int curScrollY = getScrollY(); 392 | 393 | // if not in 'refreshing' state or scrollY is less than zero, and height of 394 | // header view is greater than zero. we should keep the the first item of the 395 | // ListView always at the top(we are decreasing height of the header view, without 396 | // calling setSelection(0), we will decrease height of the header view and scroll 397 | // the ListView itself at the same time, which will cause scrolling too fast 398 | // when decreasing height of the header view) 399 | if ((!mIsRefreshing && getCurrentHeaderViewHeight() > 0) || curScrollY < 0) { 400 | // setSelection(0); 401 | return true; 402 | } else if (curScrollY > 0) { 403 | // setSelection(getCount() - 1); 404 | return true; 405 | } 406 | 407 | try { 408 | // let the original ListView handle the touch events 409 | return super.onTouchEvent(ev); 410 | } catch (IndexOutOfBoundsException e) { 411 | e.printStackTrace(); 412 | } catch (IllegalStateException e) { 413 | e.printStackTrace(); 414 | ListAdapter listAdapter = getAdapter(); 415 | if (listAdapter instanceof HeaderViewListAdapter) { 416 | listAdapter = ((HeaderViewListAdapter) listAdapter).getWrappedAdapter(); 417 | throw new IllegalStateException(e.getMessage() + ", adapter=["+ listAdapter.getClass() +"]", e); 418 | } 419 | throw e; 420 | } 421 | 422 | return false; 423 | } 424 | 425 | private void handleTouchScroll(int deltaY) { 426 | boolean reachTopEdge = reachTopEdge(); 427 | boolean reachBottomEdge = reachBottomEdge(); 428 | if (!reachTopEdge && !reachBottomEdge) { 429 | // since we are at the middle of the ListView, we don't 430 | // need to handle any touch events 431 | return; 432 | } 433 | 434 | final int scrollY = getScrollY(); 435 | 436 | int listViewHeight = getHeight(); 437 | // 0.4f is just a number that gives OK effect out of many tests. it means nothing special 438 | float scale = ((float)listViewHeight - Math.abs(scrollY) - getCurrentHeaderViewHeight()) / getHeight() * 0.4f; 439 | 440 | int newDeltaY = Math.round(deltaY * scale); 441 | 442 | if (newDeltaY != 0) { 443 | deltaY = newDeltaY; 444 | } 445 | 446 | if (reachTopEdge) { 447 | if (deltaY > 0) { 448 | scrollDown(deltaY); 449 | } else { 450 | scrollUp(deltaY); 451 | } 452 | } else { 453 | if (deltaY > 0) { 454 | if (scrollY > 0) { 455 | // when scrollY is greater than 0, it means we reach the bottom of the list 456 | // and the ListView is scrolled off the screen from the bottom, now we 457 | // scrollDown() to scroll it back, otherwise, we just let the original ListView 458 | // handle the scroll_down events 459 | scrollDown(Math.min(deltaY, scrollY)); 460 | } 461 | } else { 462 | scrollUp(deltaY); 463 | } 464 | } 465 | } 466 | 467 | private boolean reachTopEdge() { 468 | int childCount = getChildCount(); 469 | if (childCount > 0) { 470 | return (getFirstVisiblePosition() == 0) && (getChildAt(0).getTop() == 0); 471 | } else { 472 | return true; 473 | } 474 | } 475 | 476 | private boolean reachBottomEdge() { 477 | int childCount = getChildCount(); 478 | if (childCount > 0) { 479 | return (getLastVisiblePosition() == getCount() - 1) && 480 | (getChildAt(childCount - 1).getBottom() <= getHeight()); 481 | } 482 | return true; 483 | } 484 | 485 | private void springBack() { 486 | int scrollY = getScrollY(); 487 | 488 | int curHeaderViewHeight = getCurrentHeaderViewHeight(); 489 | if (curHeaderViewHeight == mHeaderViewHeight && mHeaderViewHeight > 0) { 490 | if (!mIsRefreshing && mOrigHeaderView != null) { 491 | triggerRefreshing(); 492 | } 493 | } else { 494 | scrollY -= curHeaderViewHeight; 495 | } 496 | 497 | if (scrollY != 0) { 498 | if (mFooterView != null && !mIsLoadingMore) { 499 | if (scrollY >= mLoadingMorePullDistanceThreshold) { 500 | mIsLoadingMore = true; 501 | mFooterView.onStartLoadingMore(); 502 | 503 | if (mOnLoadMoreListener != null) { 504 | mOnLoadMoreListener.onLoadMore(); 505 | } 506 | } else if (scrollY > 0) { 507 | mFooterView.onCancelPulling(); 508 | } 509 | } 510 | 511 | if (!mCancellingRefreshing) { 512 | springBack(scrollY); 513 | } 514 | } 515 | } 516 | 517 | private void triggerRefreshing() { 518 | mIsRefreshing = true; 519 | mOrigHeaderView.onStartRefreshing(); 520 | 521 | if (mOnRefreshListener != null) { 522 | mOnRefreshListener.onRefresh(mBizContextForRefresh); 523 | mBizContextForRefresh = null; 524 | } 525 | } 526 | 527 | /** 528 | * @deprecated 529 | * use startRefreshingManually() instead 530 | */ 531 | public void startRefreshManually(Object bizContextForRefresh) { 532 | startRefreshingManually(bizContextForRefresh); 533 | } 534 | 535 | public void startRefreshingManually(Object bizContextForRefresh) { 536 | mBizContextForRefresh = bizContextForRefresh; 537 | 538 | if (!mIsRefreshing) { 539 | if (mOrigHeaderView != null && mHeaderViewHeight > 0) { 540 | mMarkAutoRefresh = false; 541 | setHeaderViewHeight(mHeaderViewHeight); 542 | 543 | triggerRefreshing(); 544 | } else { 545 | mMarkAutoRefresh = true; 546 | } 547 | } 548 | } 549 | 550 | public void startLoadingMoreManually() { 551 | if (!isLoadingMoreEnabled()) { 552 | enableLoadMore(true); 553 | } 554 | 555 | if (mFooterView != null) { 556 | mIsLoadingMore = true; 557 | mFooterView.onStartLoadingMore(); 558 | 559 | if (mOnLoadMoreListener != null) { 560 | mOnLoadMoreListener.onLoadMore(); 561 | } 562 | } 563 | } 564 | 565 | private void springBack(int scrollY) { 566 | mScroller.startScroll(0, scrollY, 0, -scrollY, DEFAULT_MAX_OVER_SCROLL_DURATION); 567 | postInvalidate(); 568 | } 569 | 570 | @Override 571 | public void computeScroll() { 572 | if (mScroller.computeScrollOffset()) { 573 | int scrollY = getScrollY(); 574 | 575 | // if not in "refreshing" state, we must decrease height of the 576 | // header view to 0 577 | if (!mHideHeaderViewWithoutAnimation && !mIsRefreshing && getCurrentHeaderViewHeight() > 0) { 578 | scrollY -= getCurrentHeaderViewHeight(); 579 | } 580 | 581 | final int deltaY = mScroller.getCurrY() - scrollY; 582 | 583 | if (deltaY != 0) { 584 | if (deltaY < 0) { 585 | scrollDown(-deltaY); 586 | 587 | } else { 588 | scrollUp(-deltaY); 589 | } 590 | } else if (mCancellingRefreshing && scrollY == 0) { 591 | if (mHideHeaderViewWithoutAnimation) { 592 | mHideHeaderViewWithoutAnimation = false; 593 | 594 | mHeaderViewLayoutParams.height = 0; 595 | requestLayout(); 596 | } 597 | 598 | if (mOrigHeaderView != null) { 599 | mOrigHeaderView.onEndRefreshing(); 600 | } 601 | 602 | notifyRefreshAnimationEnd(); 603 | } 604 | 605 | postInvalidate(); 606 | 607 | } else if (!mIsTouching && (getScrollY() != 0 || (!mIsRefreshing && getCurrentHeaderViewHeight() != 0))) { 608 | springBack(); 609 | } 610 | 611 | super.computeScroll(); 612 | } 613 | 614 | /** 615 | * scrollDown() does 2 things: 616 | * 617 | * 1. check if height of the header view is greater than 0, if so, decrease it to 0 618 | * 619 | * 2. scroll content of the ListView off the screen any there's any deltaY left(i.e. 620 | * deltaY is not 0) 621 | */ 622 | private void scrollDown(int deltaY) { 623 | if (!mIsRefreshing && getScrollY() <= 0 && reachTopEdge()) { 624 | final int curHeaderViewHeight = getCurrentHeaderViewHeight(); 625 | if (curHeaderViewHeight < mHeaderViewHeight) { 626 | int newHeaderViewHeight = curHeaderViewHeight + deltaY; 627 | if (newHeaderViewHeight < mHeaderViewHeight) { 628 | setHeaderViewHeight(newHeaderViewHeight); 629 | return ; 630 | } else { 631 | setHeaderViewHeight(mHeaderViewHeight); 632 | deltaY = newHeaderViewHeight - mHeaderViewHeight; 633 | } 634 | } 635 | } 636 | 637 | scrollBy(0, -deltaY); 638 | } 639 | 640 | /** 641 | * scrollUp() does 3 things: 642 | * 643 | * 1. if scrollY is less than 0, it means we have scrolled the list off the screen 644 | * from the top, now we scroll back and make the list to reach the top edge of 645 | * the screen. 646 | * 647 | * 2. check height of the header view and see if it is greater than 0, if so, we 648 | * decrease it and make it zero. 649 | * 650 | * 3. now check if we have scrolled the list to reach the bottom of the screen, if so 651 | * we scroll the list off the screen from the bottom. 652 | */ 653 | private void scrollUp(int deltaY) { 654 | final int scrollY = getScrollY(); 655 | if (scrollY < 0) { 656 | if (scrollY < deltaY) { // both scrollY and deltaY are less than 0 657 | scrollBy(0, -deltaY); 658 | return; 659 | } else { 660 | scrollTo(0, 0); 661 | deltaY -= scrollY; 662 | 663 | if (deltaY == 0) { 664 | return; 665 | } 666 | } 667 | } 668 | 669 | if (!mIsRefreshing) { 670 | int curHeaderViewHeight = getCurrentHeaderViewHeight(); 671 | if (curHeaderViewHeight > 0) { 672 | 673 | int newHeaderViewHeight = curHeaderViewHeight + deltaY; 674 | if (newHeaderViewHeight > 0) { 675 | setHeaderViewHeight(newHeaderViewHeight); 676 | 677 | return; 678 | } else { 679 | setHeaderViewHeight(0); 680 | 681 | deltaY = newHeaderViewHeight; 682 | } 683 | } 684 | } 685 | 686 | if (reachBottomEdge()) { 687 | scrollBy(0, -deltaY); 688 | } 689 | } 690 | 691 | @Override 692 | public void scrollTo(int x, int y) { 693 | int oldScrollY = getScrollY(); 694 | 695 | super.scrollTo(x, y); 696 | 697 | if (mOrigHeaderView != null && y < 0 && !mIsRefreshing) { 698 | int curTotalScrollY = getCurrentHeaderViewHeight() + (-y); 699 | mOrigHeaderView.onPull(curTotalScrollY); 700 | } else if (mFooterView != null && !mIsLoadingMore) { 701 | int halfPullDistanceThreshold = mLoadingMorePullDistanceThreshold / 2; 702 | if (y > halfPullDistanceThreshold) { 703 | if (oldScrollY <= halfPullDistanceThreshold) { 704 | mFooterView.onStartPulling(); 705 | } else if (oldScrollY < mLoadingMorePullDistanceThreshold && y >= mLoadingMorePullDistanceThreshold) { 706 | mFooterView.onReachAboveRefreshThreshold(); 707 | } else if (oldScrollY >= mLoadingMorePullDistanceThreshold && y < mLoadingMorePullDistanceThreshold) { 708 | mFooterView.onReachBelowRefreshThreshold(); 709 | } 710 | } else { 711 | mFooterView.onCancelPulling(); 712 | } 713 | } 714 | } 715 | 716 | private void setHeaderViewHeight(int height) { 717 | if (mHeaderViewLayoutParams != null && (mHeaderViewLayoutParams.height != 0 || height != 0)) { 718 | setHeaderViewHeightInternal(height); 719 | } 720 | } 721 | 722 | private void setHeaderViewHeightInternal(int height) { 723 | int oldHeight = mHeaderViewLayoutParams.height; 724 | 725 | mHeaderViewLayoutParams.height = height; 726 | 727 | // if mHeaderView is visible(I mean within the confines of the visible screen), we should 728 | // request the mHeaderView to re-layout itself, if mHeaderView is not visible, we should 729 | // redraw the ListView itself, which ensures correct scroll position of the ListView. 730 | if (mHeaderView.isShown()) { 731 | mHeaderView.requestLayout(); 732 | } else { 733 | invalidate(); 734 | } 735 | 736 | if (mOrigHeaderView != null && !mIsRefreshing && !mCancellingRefreshing) { 737 | if (oldHeight == 0 && height > 0) { 738 | mOrigHeaderView.onStartPulling(); 739 | } 740 | mOrigHeaderView.onPull(height); 741 | 742 | if (oldHeight < mHeaderViewHeight && height == mHeaderViewHeight) { 743 | mOrigHeaderView.onReachAboveHeaderViewHeight(); 744 | } else if (oldHeight == mHeaderViewHeight && height < mHeaderViewHeight) { 745 | if (height != 0) { // initial setup 746 | mOrigHeaderView.onReachBelowHeaderViewHeight(); 747 | } 748 | } 749 | } else if (mCancellingRefreshing && height == 0) { 750 | notifyRefreshAnimationEnd(); 751 | } 752 | } 753 | 754 | private void notifyRefreshAnimationEnd() { 755 | mCancellingRefreshing = false; 756 | if (mOnRefreshListener != null) { 757 | mOnRefreshListener.onRefreshAnimationEnd(); 758 | } 759 | } 760 | 761 | private int getCurrentHeaderViewHeight() { 762 | if (mHeaderViewLayoutParams != null) { 763 | return mHeaderViewLayoutParams.height; 764 | } 765 | return 0; 766 | } 767 | 768 | // see http://stackoverflow.com/a/9173866/668963 769 | @Override 770 | protected void onDetachedFromWindow() { 771 | try { 772 | super.onDetachedFromWindow(); 773 | } catch(IllegalArgumentException iae) { 774 | // Workaround for http://code.google.com/p/android/issues/detail?id=22751 775 | } 776 | } 777 | 778 | // see http://stackoverflow.com/a/8433777/668963 779 | @Override 780 | protected void dispatchDraw(Canvas canvas) { 781 | try { 782 | super.dispatchDraw(canvas); 783 | } catch (IndexOutOfBoundsException e) { 784 | e.printStackTrace(); 785 | // ignore this exception 786 | } 787 | } 788 | 789 | /** 790 | * The listener to be registered through OverScrollListView.setOnRefreshListener() 791 | */ 792 | public static interface OnRefreshListener { 793 | void onRefresh(Object bizContext); 794 | void onRefreshAnimationEnd(); 795 | } 796 | 797 | /** 798 | * The interface to be implemented by header view to be used with OverScrollListView 799 | */ 800 | public interface PullToRefreshCallback { 801 | void onStartPulling(); 802 | 803 | // scrollY = how far have we pulled? 804 | void onPull(int scrollY); 805 | 806 | void onReachAboveHeaderViewHeight(); 807 | void onReachBelowHeaderViewHeight(); 808 | 809 | void onStartRefreshing(); 810 | void onEndRefreshing(); 811 | void onFinishRefreshing(); 812 | } 813 | 814 | /** 815 | * The listener to be registered through OverScrollListView.setOnLoadMoreListener() 816 | * see the demo project(OverScrollListViewDemo) for a reference implementation 817 | */ 818 | public static interface OnLoadMoreListener { 819 | void onLoadMore(); 820 | } 821 | 822 | /** 823 | * The interface to be implemented by footer view to be used with OverScrollListView 824 | * see the demo project(OverScrollListViewDemo) for a reference implementation 825 | */ 826 | public interface PullToLoadMoreCallback { 827 | void onReset(); 828 | void onStartPulling(); 829 | 830 | void onReachAboveRefreshThreshold(); 831 | void onReachBelowRefreshThreshold(); 832 | 833 | void onStartLoadingMore(); 834 | void onEndLoadingMore(); 835 | 836 | void onCancelPulling(); 837 | } 838 | } 839 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Demo 2 | ==== 3 | 4 | Install the Demo APK and get a feel of **how smooth it is**. 5 | 6 | [![OverScrollListView Demo](https://github.com/neevek/Easy-PullToRefresh-Android/raw/master/DemoAPK/OverScrollListViewQRCode.png)](https://github.com/neevek/Easy-PullToRefresh-Android/raw/master/DemoAPK/OverScrollListViewDemo.apk) 7 | 8 | Easy-PullToRefresh-Android 9 | ========================== 10 | 11 | **OverScrollListView** is a drop-in replacement for `ListView`. 12 | 13 | The **OverScrollListView** class implements the bounce effect & pull-to-refresh feature for `ListView`(this implementation can also be applied to `ExpandableListView`). 14 | 15 | This pull-to-refresh implementation is inspired by [XListView](https://github.com/Maxwin-z/XListView-Android) with the idea of adjusting height of the header view while pulling the `ListView`. 16 | 17 | For the bounce effect, it simply intercepts touch events and detects if the scrolling has reached the top or bottom edge, if so, we call scrollTo() to scroll the entire the `ListView` off the screen, and then with a `Scroller`, we compute the Y scroll positions and create a smooth bounce effect. 18 | 19 | For pull-to-refresh, it uses a header view which implements the `PullToRefreshCallback` interface as the indicator view for displaying "Pull to refresh", "Release to refresh", "Loading..." and an arrow image. Of course, you can implement `PullToRefreshCallback` and write your own `PullToRefreshHeaderView`, as long as you follow requirements for the layout of the header view, take the default `PullToRefreshHeaderView` as a referenece. 20 | 21 | **NOTE**: If you do not want the pull-to-refresh feature, you can still use **OverScrollListView**, in that case, **OverScrollListView** only offers you the bounce effect, and that is why it has the name. Just remember not to call `setPullToRefreshHeaderView()`. 22 | 23 | Release Notes 24 | ============= 25 | * v1.1.0 - Added OverScrollListView.finishRefreshingAndHideHeaderViewWithoutAnimation(), which produces more desired effect when used in situations that needs to use "pull down & release" to load more, such as in a conversation ListView where we pull down & release to load the conversation history. 26 | * v1.0.5 - Bugfixes 27 | * v1.0.4 - Disabled by default the "pull to load more" feature, which must be manually enabled or disabled. Fixed a few bugs. 28 | * v1.0.3 - Added support for "pull to load more" with a footer view. 29 | * v1.0.2 - Rewrite the code for handling over-scroll, and some bugfixes. 30 | * v1.0.1 - Some bugfixes. 31 | * v1.0.0 - Implemented "pull to refresh". 32 | 33 | Under MIT license 34 | ================= 35 | 36 | Copyright (c) 2013 neevek 37 | 38 | See the file license.txt for copying permission. 39 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 neevek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | --------------------------------------------------------------------------------