-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPullToFireListView.java
402 lines (332 loc) · 12.6 KB
/
PullToFireListView.java
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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package costbook.activity.main;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;
import android.widget.AdapterView;
import android.widget.ListView;
/**
* Derived from this <a href="https://github.com/erikwt/PullToRefresh-ListView">project</a> <br>
* <br>
* Generic pull-to-do something list view, the header view being pulled out is 100%
* customisable. A couple of events are triggered at appropriate time to allow customisation of
* header view, mainly for animation.<br>
* <br>
* Quick use:
* Create a layout xml as header container which has at least one child view (real header).
* Subclass this, customise the header in appropriate methods, see details in those methods.
* Refresh graphic editor (eclipse), add the concrete subclass view in your layout.
* Implement OnFireListener, set it on this view to listen to pull event.
*/
public abstract class PullToFireListView extends ListView {
@SuppressWarnings("unused")
private static final String TAG = "PullList";
// adjust these numbers to control animation
private static final float PULL_RESISTANCE_WEAK = 1.2f;
private static final float PULL_RESISTANCE_STRONG = 7.5f;
private static final int BOUNCE_ANIMATION_DURATION = 600;
private static final int BOUNCE_ANIMATION_DELAY = 100;
private static final float BOUNCE_OVERSHOOT_TENSION = 1.4f;
private static enum State {
PULL_TO_FIRE,
RELEASE_TO_FIRE,
FIRING
}
public interface OnFireListener {
public void onFire();
}
private static int measuredHeaderHeight;
private boolean bounceBackHeader;
private boolean lockScrollWhileFiring;
private boolean scrollbarEnabled;
private float previousY;
private int headerPadding;
private boolean hasResetHeader;
private State state;
private ViewGroup headerContainer;
private View header;
private OnItemClickListener onItemClickListener;
private OnItemLongClickListener onItemLongClickListener;
private OnFireListener onFireListener;
private float mScrollStartY;
private final int IDLE_DISTANCE = 5;
public PullToFireListView(Context context) {
super(context);
init();
}
public PullToFireListView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PullToFireListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
@Override
public void setOnItemClickListener(OnItemClickListener onItemClickListener){
this.onItemClickListener = onItemClickListener;
}
@Override
public void setOnItemLongClickListener(OnItemLongClickListener onItemLongClickListener){
this.onItemLongClickListener = onItemLongClickListener;
}
public void setOnFireListener(OnFireListener onFireListener) {
this.onFireListener = onFireListener;
}
public boolean isFiring() {
return state == State.FIRING;
}
public void setLockScrollWhileFIRING(boolean lockScrollWhileFiring){
this.lockScrollWhileFiring= lockScrollWhileFiring;
}
public void setFiring() {
state = State.FIRING;
scrollTo(0, 0);
setHeaderPadding(0);
}
public void onFireComplete() {
state = State.PULL_TO_FIRE;
resetHeader();
//TODO: add event probably
}
private void init() {
setVerticalFadingEdgeEnabled(false);
headerContainer = onCreateHeaderContainer(LayoutInflater.from(getContext()), this);
header = headerContainer.findViewById(onFindHeaderView());
addHeaderView(headerContainer);
setState(State.PULL_TO_FIRE);
scrollbarEnabled = isVerticalScrollBarEnabled();
ViewTreeObserver vto = header.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new PTFOnGlobalLayoutListener());
super.setOnItemClickListener(new PTFOnItemClickListener());
super.setOnItemLongClickListener(new PTFOnItemLongClickListener());
}
private void setHeaderPadding(int newPadding) {
headerPadding = newPadding;
MarginLayoutParams params = (ViewGroup.MarginLayoutParams) header.getLayoutParams();
params.setMargins(0, Math.round(newPadding), 0, 0);
header.setLayoutParams(params);
}
@SuppressWarnings("incomplete-switch")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (lockScrollWhileFiring && (state == State.FIRING || getAnimation() != null && !getAnimation().hasEnded())) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (getFirstVisiblePosition() == 0) {
previousY = event.getY();
} else {
previousY = -1;
}
// remember starting Y
mScrollStartY = event.getY();
break;
case MotionEvent.ACTION_UP:
if (previousY != -1 && (state == State.RELEASE_TO_FIRE || getFirstVisiblePosition() == 0)) {
switch (state) {
case RELEASE_TO_FIRE:
setState(State.FIRING);
bounceBackHeader();
if (onFireListener != null) {
onFireListener.onFire();
onFireSuccess();
} else {
setState(State.PULL_TO_FIRE);
}
break;
case PULL_TO_FIRE:
resetHeader();
onFireDiscard();
break;
}
}
break;
case MotionEvent.ACTION_MOVE:
if (previousY != -1 && getFirstVisiblePosition() == 0 && Math.abs(mScrollStartY-event.getY()) > IDLE_DISTANCE) {
float y = event.getY();
float diff = y - previousY;
if (diff > 0) {
if (headerPadding < 0) diff = diff / PULL_RESISTANCE_WEAK;
else diff = diff / PULL_RESISTANCE_STRONG;
}
previousY = y;
int newHeaderPadding = Math.max(Math.round(headerPadding + diff), -header.getHeight());
if (newHeaderPadding != headerPadding && state != State.FIRING) {
setHeaderPadding(newHeaderPadding);
// report percentage of header shown
float percent = (float) (newHeaderPadding + measuredHeaderHeight) / measuredHeaderHeight;
onPulling(percent);
// change state when header is completely revealed
if(state == State.PULL_TO_FIRE && headerPadding >= 0){
setState(State.RELEASE_TO_FIRE);
}else if(state == State.RELEASE_TO_FIRE && headerPadding < 0){
setState(State.PULL_TO_FIRE);
}
}
}
}
return super.onTouchEvent(event);
}
private void bounceBackHeader() {
int yTranslate;
if (state == State.FIRING) {
yTranslate = header.getHeight() - headerContainer.getHeight();
} else {
yTranslate = -headerContainer.getHeight() - headerContainer.getTop() + getPaddingTop();
}
TranslateAnimation bounceAnim = new TranslateAnimation(
TranslateAnimation.ABSOLUTE, 0,
TranslateAnimation.ABSOLUTE, 0,
TranslateAnimation.ABSOLUTE, 0,
TranslateAnimation.ABSOLUTE, yTranslate);
bounceAnim.setDuration(BOUNCE_ANIMATION_DURATION);
bounceAnim.setFillEnabled(true);
bounceAnim.setFillAfter(false);
bounceAnim.setFillBefore(true);
bounceAnim.setInterpolator(new OvershootInterpolator(BOUNCE_OVERSHOOT_TENSION));
bounceAnim.setAnimationListener(new HeaderAnimationListener(yTranslate));
startAnimation(bounceAnim);
}
private void resetHeader(){
if(getFirstVisiblePosition() > 0){
setHeaderPadding(-header.getHeight());
setState(State.PULL_TO_FIRE);
return;
}
if(getAnimation() != null && !getAnimation().hasEnded()){
bounceBackHeader = true;
}else{
bounceBackHeader();
}
}
private void setState(State state) {
this.state = state;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt){
super.onScrollChanged(l, t, oldl, oldt);
if(!hasResetHeader){
if(measuredHeaderHeight > 0 && state != State.FIRING){
setHeaderPadding(-measuredHeaderHeight);
}
hasResetHeader = true;
}
}
private class HeaderAnimationListener implements AnimationListener{
private int height, translation;
private State stateAtAnimationStart;
public HeaderAnimationListener(int translation){
this.translation = translation;
}
@Override
public void onAnimationStart(Animation animation){
stateAtAnimationStart = state;
ViewGroup.LayoutParams params = getLayoutParams();
height = params.height;
params.height = getHeight() - translation;
setLayoutParams(params);
if(scrollbarEnabled){
setVerticalScrollBarEnabled(false);
}
}
@Override
public void onAnimationEnd(Animation animation){
setHeaderPadding(stateAtAnimationStart == State.FIRING ? 0 : -measuredHeaderHeight - headerContainer.getTop());
setSelection(0);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = height;
setLayoutParams(params);
if(scrollbarEnabled){
setVerticalScrollBarEnabled(true);
}
if(bounceBackHeader){
bounceBackHeader = false;
postDelayed(new Runnable(){
@Override
public void run(){
resetHeader();
}
}, BOUNCE_ANIMATION_DELAY);
}else if(stateAtAnimationStart != State.FIRING){
setState(State.PULL_TO_FIRE);
}
}
@Override
public void onAnimationRepeat(Animation animation){}
}
private class PTFOnGlobalLayoutListener implements OnGlobalLayoutListener{
@Override
public void onGlobalLayout(){
int initialHeaderHeight = header.getHeight();
if(initialHeaderHeight > 0){
measuredHeaderHeight = initialHeaderHeight;
if(measuredHeaderHeight > 0 && state != State.FIRING){
setHeaderPadding(-measuredHeaderHeight);
requestLayout();
}
}
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
private class PTFOnItemClickListener implements OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
hasResetHeader = false;
if (onItemClickListener != null && state == State.PULL_TO_FIRE && view != headerContainer) {
// passing up onItemClick. correct position with the number of header views
onItemClickListener.onItemClick(adapterView, view, position - getHeaderViewsCount(), id);
}
}
}
private class PTFOnItemLongClickListener implements OnItemLongClickListener {
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id) {
hasResetHeader = false;
if (onItemLongClickListener != null && state == State.PULL_TO_FIRE && view != headerContainer) {
// passing up onItemLongClick. correct position with the number of header views
return onItemLongClickListener.onItemLongClick(adapterView, view, position - getHeaderViewsCount(), id);
}
return false;
}
}
// TODO: override below
/**
* Called when creating the header container
*
* @param inflater
* @param parent
* @return container ViewGroup that contains a header child
*/
protected abstract ViewGroup onCreateHeaderContainer(LayoutInflater inflater, ViewGroup parent);
/**
* Called after creating the header container, the ListView will call findViewById() to get the header child
*
* @return resource id of the header child inside header container, this id must be in the layout file of header container
*/
protected abstract int onFindHeaderView();
/**
* Called continuously while the ListView is being pulled
*
* @param percent
* the percentage of header child revealed, 0-1 = partially revealed, >1 = fully revealed
*/
protected abstract void onPulling(float percent);
/**
* Called when pull action is released, header is fully revealed, fire event is triggered
*/
protected abstract void onFireSuccess();
/**
* Called when pull action is released, however header is not fully revealed, fire event is discarded
*/
protected abstract void onFireDiscard();
}