├── .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 | [](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 |
--------------------------------------------------------------------------------