18034727885
2026-06-26 3f97e780654d2709d32fb815d6398440dbfb12c5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/**
 * @file XListView.java
 * @package me.maxwin.view
 * @create Mar 18, 2012 6:28:41 PM
 * @author Maxwin
 * @description An ListView support (a) Pull down to refresh, (b) Pull up to load more.
 *         Implement IWaterDropListViewListener, and see stopRefresh() / stopLoadMore().
 */
package medusa.theone.waterdroplistview.view;
 
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.Scroller;
 
public class WaterDropListView extends ListView implements OnScrollListener,WaterDropListViewHeader.IStateChangedListener {
 
    private float mLastY = -1; // save event y
    private Scroller mScroller; // used for scroll back
    private OnScrollListener mScrollListener; // user's scroll listener
 
    // the interface to trigger refresh and load more.
    private IWaterDropListViewListener mListViewListener;
 
    // -- header view
    private WaterDropListViewHeader mHeaderView;
    // header view content, use it to calculate the Header's height. And hide it
    // when disable pull refresh.
//    private RelativeLayout mHeaderViewContent;
    private boolean mEnablePullRefresh = true;
//    private boolean mPullRefreshing = false; // is refreashing.
 
    // -- footer view
    private WaterDropListViewFooter mFooterView;
    private boolean mEnablePullLoad;
    private boolean mPullLoading;
    private boolean mIsFooterReady = false;
 
    // total list items, used to detect is at the bottom of listview.
    private int mTotalItemCount;
 
    // for mScroller, scroll back from header or footer.
    private ScrollBack mScrollBack;
    private boolean isTouchingScreen = false;//手指是否触摸屏幕
 
//    private int mStretchHeight; // view开始变形的高度
//    private int mReadyHeight; // view由stretch变成ready的高度
    private final static int SCROLL_DURATION = 400; // scroll back duration
    private final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >= 50px
                                                        // at bottom, trigger
                                                        // load more.
    private final static float OFFSET_RADIO = 1.8f; // support iOS like pull
 
    private enum ScrollBack{
        header,
        footer
    }                                                // feature.
 
    /**
     * @param context
     */
    public WaterDropListView(Context context) {
        super(context);
        initWithContext(context);
    }
 
    public WaterDropListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initWithContext(context);
    }
 
    public WaterDropListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initWithContext(context);
    }
 
    private void initWithContext(Context context) {
        mScroller = new Scroller(context, new DecelerateInterpolator());
        // XListView need the scroll event, and it will dispatch the event to
        // user's listener (as a proxy).
        super.setOnScrollListener(this);
 
        // init header view
        mHeaderView = new WaterDropListViewHeader(context);
        mHeaderView.setStateChangedListener(this);
        addHeaderView(mHeaderView);
 
        // init footer view
        mFooterView = new WaterDropListViewFooter(context);
    }
 
    @Override
    public void setAdapter(ListAdapter adapter) {
        // make sure XListViewFooter is the last footer view, and only add once.
        if (mIsFooterReady == false) {
            mIsFooterReady = true;
            addFooterView(mFooterView);
        }
        super.setAdapter(adapter);
    }
 
    /**
     * enable or disable pull down refresh feature.
     * 
     * @param enable
     */
    /*public void setPullRefreshEnable(boolean enable) {
        mEnablePullRefresh = enable;
        if (!mEnablePullRefresh) { // disable, hide the content
            mHeaderViewContent.setVisibility(View.INVISIBLE);
        } else {
            mHeaderViewContent.setVisibility(View.VISIBLE);
        }
    }*/
 
    /**
     * enable or disable pull up load more feature.
     *
     * @param enable
     */
    public void setPullLoadEnable(boolean enable) {
        mEnablePullLoad = enable;
        if (!mEnablePullLoad) {
            mFooterView.hide();
            mFooterView.setOnClickListener(null);
        } else {
            mPullLoading = false;
            mFooterView.show();
            mFooterView.setState(WaterDropListViewFooter.STATE.normal);
            // both "pull up" and "click" will invoke load more.
            mFooterView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    mFooterView.setEnabled(false);
                    startLoadMore();
                }
            });
        }
    }
 
    /**
     * stop refresh, reset header view.
     */
    public void stopRefresh() {
        if (mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.refreshing) {
            mHeaderView.updateState(WaterDropListViewHeader.STATE.end);
            if(!isTouchingScreen){
                resetHeaderHeight();
            }
        }else{
            throw  new IllegalStateException("can not stop refresh while it is not refreshing!");
        }
    }
 
    /**
     * stop load more, reset footer view.
     */
    public void stopLoadMore() {
        if (mPullLoading == true) {
            mPullLoading = false;
            mFooterView.setState(WaterDropListViewFooter.STATE.normal);
        }
        mFooterView.setEnabled(true);
    }
 
 
    private void invokeOnScrolling() {
        if (mScrollListener instanceof OnXScrollListener) {
            OnXScrollListener l = (OnXScrollListener) mScrollListener;
            l.onXScrolling(this);
        }
    }
 
    private void updateHeaderHeight(int height){
        if (mEnablePullRefresh) {
            if (mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.normal && height >= mHeaderView.getStretchHeight()) {
                //由normal变成stretch的逻辑:1、当前状态是normal;2、下拉头达到了stretchheight的高度
                mHeaderView.updateState(WaterDropListViewHeader.STATE.stretch);
            } else if(mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.stretch && height >= mHeaderView.getReadyHeight()){
                //由stretch变成ready的逻辑:1、当前状态是stretch;2、下拉头达到了readyheight的高度
                mHeaderView.updateState(WaterDropListViewHeader.STATE.ready);
            }else if (mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.stretch && height < mHeaderView.getStretchHeight()){
                // 由stretch变成normal的逻辑:1、当前状态是stretch;2、下拉头高度小于stretchheight的高度
                mHeaderView.updateState(WaterDropListViewHeader.STATE.normal);
            }else if(mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.end && height < 2){
                //由end变成normal的逻辑:1、当前状态是end;2、下拉头高度小于一个极小值
                mHeaderView.updateState(WaterDropListViewHeader.STATE.normal);
            }
            /*else{
                throw new IllegalStateException("WaterDropListView's state is illegal!");
            }*/
        }
        mHeaderView.setVisiableHeight(height);//动态设置HeaderView的高度
    }
 
    private void updateHeaderHeight(float delta) {
        int newHeight = (int) delta + mHeaderView.getVisiableHeight();
        updateHeaderHeight(newHeight);
    }
 
    /**
     * reset header view's height.
     * 重置headerheight的高度
     * 逻辑:1、如果状态处于非refreshing,则回滚到height=0状态2;2、如果状态处于refreshing,则回滚到stretchheight高度
     */
    private void resetHeaderHeight() {
        int height = mHeaderView.getVisiableHeight();
        if (height == 0) {
        // not visible.
            return;
        }
        // refreshing and header isn't shown fully. do nothing.
        if (mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.refreshing && height <= mHeaderView.getStretchHeight()) {
            return;
        }
        int finalHeight = 0; // default: scroll back to dismiss header.
        // is refreshing, just scroll back to show all the header.
        if ((mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.ready ||mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.refreshing )&& height > mHeaderView.getStretchHeight()) {
            finalHeight = mHeaderView.getStretchHeight();
        }
 
        mScrollBack = ScrollBack.header;
        mScroller.startScroll(0, height, 0, finalHeight - height, SCROLL_DURATION);
        // trigger computeScroll
        invalidate();
    }
 
    private void updateFooterHeight(float delta) {
        int height = mFooterView.getBottomMargin() + (int) delta;
        if (mEnablePullLoad && !mPullLoading) {
            if (height > PULL_LOAD_MORE_DELTA) { // height enough to invoke load
                                                    // more.
                mFooterView.setState(WaterDropListViewFooter.STATE.ready);
            } else {
                mFooterView.setState(WaterDropListViewFooter.STATE.normal);
            }
        }
        mFooterView.setBottomMargin(height);
 
        // setSelection(mTotalItemCount - 1); // scroll to bottom
    }
 
 
    private void resetFooterHeight() {
        int bottomMargin = mFooterView.getBottomMargin();
        if (bottomMargin > 0) {
            mScrollBack = ScrollBack.footer;
            mScroller.startScroll(0, bottomMargin, 0, -bottomMargin, SCROLL_DURATION);
            invalidate();
        }
    }
 
    private void startLoadMore() {
        mPullLoading = true;
        mFooterView.setState(WaterDropListViewFooter.STATE.loading);
        if (mListViewListener != null) {
            mListViewListener.onLoadMore();
        }
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }
 
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastY = ev.getRawY();
            isTouchingScreen = true;
            break;
        case MotionEvent.ACTION_MOVE:
            final float deltaY = ev.getRawY() - mLastY;
            mLastY = ev.getRawY();
            if (getFirstVisiblePosition() == 0 && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)) {
                // the first item is showing, header has shown or pull down.
                updateHeaderHeight(deltaY / OFFSET_RADIO);
                invokeOnScrolling();
            } else if (getLastVisiblePosition() == mTotalItemCount - 1 && (mFooterView.getBottomMargin() > 0 || deltaY < 0)) {
                // last item, already pulled up or want to pull up.
                updateFooterHeight(-deltaY / OFFSET_RADIO);
            }
            break;
        default:
            mLastY = -1; // reset
            isTouchingScreen = false;
            //TODO 存在bug:当两个if的条件都满足的时候,只能滚动一个,所以在reSetHeader的时候就不起作用了,一般就只会reSetFooter
            if (getFirstVisiblePosition() == 0) {
                resetHeaderHeight();
            }
            if (getLastVisiblePosition() == mTotalItemCount - 1) {
                // invoke load more.
                if (mEnablePullLoad && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA) {
                    startLoadMore();
                }
                resetFooterHeight();
            }
            break;
        }
        return super.onTouchEvent(ev);
    }
 
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            if (mScrollBack == ScrollBack.header) {
                updateHeaderHeight(mScroller.getCurrY());
                if(mScroller.getCurrY() < 2 && mHeaderView.getCurrentState() == WaterDropListViewHeader.STATE.end){
                    //停止滚动了
                    //逻辑:如果header范围进入了一个极小值内,且当前的状态是end,就把状态置成normal
                    mHeaderView.updateState(WaterDropListViewHeader.STATE.normal);
                }
            } else {
                mFooterView.setBottomMargin(mScroller.getCurrY());
            }
            postInvalidate();
            invokeOnScrolling();
        }
        super.computeScroll();
    }
 
    @Override
    public void setOnScrollListener(OnScrollListener l) {
        mScrollListener = l;
    }
 
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (mScrollListener != null) {
            mScrollListener.onScrollStateChanged(view, scrollState);
        }
    }
 
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        // send to user's listener
        mTotalItemCount = totalItemCount;
        if (mScrollListener != null) {
            mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
        }
    }
    @Override
    public void notifyStateChanged(WaterDropListViewHeader.STATE oldState, WaterDropListViewHeader.STATE newState) {
        if(newState == WaterDropListViewHeader.STATE.refreshing){
            if(mListViewListener != null){
                mListViewListener.onRefresh();
            }
        }
    }
    public void setWaterDropListViewListener(IWaterDropListViewListener l) {
        mListViewListener = l;
    }
 
    /**
     * you can listen ListView.OnScrollListener or this one. it will invoke onXScrolling when header/footer scroll back.
     */
    public interface OnXScrollListener extends OnScrollListener {
        public void onXScrolling(View view);
    }
 
    /**
     * implements this interface to get refresh/load more event.
     */
    public interface IWaterDropListViewListener {
        public void onRefresh();
 
        public void onLoadMore();
    }
}