42 | * @version 1.0.0
43 | */
44 | public class MultiColumnPullToRefreshListView extends MultiColumnListView {
45 |
46 | private static final float PULL_RESISTANCE = 1.7f;
47 | private static final int BOUNCE_ANIMATION_DURATION = 200;
48 | private static final int BOUNCE_ANIMATION_DELAY = 0;
49 | private static final int ROTATE_ARROW_ANIMATION_DURATION = 250;
50 |
51 | private static enum State{
52 | PULL_TO_REFRESH,
53 | RELEASE_TO_REFRESH,
54 | REFRESHING
55 | }
56 |
57 | /**
58 | * Interface to implement when you want to get notified of 'pull to refresh'
59 | * events.
60 | * Call setOnRefreshListener(..) to activate an OnRefreshListener.
61 | */
62 | public interface OnRefreshListener{
63 |
64 | /**
65 | * Method to be called when a refresh is requested
66 | */
67 | void onRefresh();
68 | }
69 |
70 | private static int measuredHeaderHeight;
71 |
72 | private boolean scrollbarEnabled;
73 | private boolean bounceBackHeader;
74 | private boolean lockScrollWhileRefreshing;
75 | private boolean showLastUpdatedText;
76 | private String pullToRefreshText;
77 | private String releaseToRefreshText;
78 | private String refreshingText;
79 | private String lastUpdatedText;
80 | private SimpleDateFormat lastUpdatedDateFormat = new SimpleDateFormat("dd/MM HH:mm");
81 |
82 | private float previousY;
83 | private int headerPadding;
84 | private boolean hasResetHeader;
85 | private long lastUpdated = -1;
86 | private State state;
87 | private LinearLayout headerContainer;
88 | private RelativeLayout header;
89 | private RotateAnimation flipAnimation;
90 | private RotateAnimation reverseFlipAnimation;
91 | private RotateAnimation refreshingAnimation;
92 | private ImageView image;
93 | private View refreshingIcon;
94 | private TextView text;
95 | private TextView lastUpdatedTextView;
96 | private OnRefreshListener onRefreshListener;
97 | private TranslateAnimation bounceAnimation;
98 |
99 | public MultiColumnPullToRefreshListView(Context context){
100 | super(context);
101 | init();
102 | }
103 |
104 | public MultiColumnPullToRefreshListView(Context context, AttributeSet attrs){
105 | super(context, attrs);
106 | init();
107 | }
108 |
109 | public MultiColumnPullToRefreshListView(Context context, AttributeSet attrs, int defStyle){
110 | super(context, attrs, defStyle);
111 | init();
112 | }
113 |
114 | /**
115 | * Activate an OnRefreshListener to get notified on 'pull to refresh'
116 | * events.
117 | *
118 | * @param onRefreshListener The OnRefreshListener to get notified
119 | */
120 | public void setOnRefreshListener(OnRefreshListener onRefreshListener){
121 | this.onRefreshListener = onRefreshListener;
122 | }
123 |
124 | /**
125 | * @return If the list is in 'Refreshing' state
126 | */
127 | public boolean isRefreshing(){
128 | return state == State.REFRESHING;
129 | }
130 |
131 | /**
132 | * Default is false. When lockScrollWhileRefreshing is set to true, the list
133 | * cannot scroll when in 'refreshing' mode. It's 'locked' on refreshing.
134 | *
135 | * @param lockScrollWhileRefreshing
136 | */
137 | public void setLockScrollWhileRefreshing(boolean lockScrollWhileRefreshing){
138 | this.lockScrollWhileRefreshing = lockScrollWhileRefreshing;
139 | }
140 |
141 | /**
142 | * Default is false. Show the last-updated date/time in the 'Pull ro Refresh'
143 | * header. See 'setLastUpdatedDateFormat' to set the date/time formatting.
144 | *
145 | * @param showLastUpdatedText
146 | */
147 | public void setShowLastUpdatedText(boolean showLastUpdatedText){
148 | this.showLastUpdatedText = showLastUpdatedText;
149 | if(!showLastUpdatedText) lastUpdatedTextView.setVisibility(View.GONE);
150 | }
151 |
152 | /**
153 | * Default: "dd/MM HH:mm". Set the format in which the last-updated
154 | * date/time is shown. Meaningless if 'showLastUpdatedText == false (default)'.
155 | * See 'setShowLastUpdatedText'.
156 | *
157 | * @param lastUpdatedDateFormat
158 | */
159 | public void setLastUpdatedDateFormat(SimpleDateFormat lastUpdatedDateFormat){
160 | this.lastUpdatedDateFormat = lastUpdatedDateFormat;
161 | }
162 |
163 | /**
164 | * Explicitly set the state to refreshing. This
165 | * is useful when you want to show the spinner and 'Refreshing' text when
166 | * the refresh was not triggered by 'pull to refresh', for example on start.
167 | */
168 | public void setRefreshing(){
169 | state = State.REFRESHING;
170 | setUiRefreshing();
171 | //setHeaderPadding(0);
172 | //scrollTo(0, 0);
173 | }
174 |
175 | /**
176 | * Set the state back to 'pull to refresh'. Call this method when refreshing
177 | * the data is finished.
178 | */
179 | public void onRefreshComplete(){
180 | state = State.PULL_TO_REFRESH;
181 | resetHeader();
182 | lastUpdated = System.currentTimeMillis();
183 | }
184 |
185 | /**
186 | * Change the label text on state 'Pull to Refresh'
187 | *
188 | * @param pullToRefreshText Text
189 | */
190 | public void setTextPullToRefresh(String pullToRefreshText){
191 | this.pullToRefreshText = pullToRefreshText;
192 | if(state == State.PULL_TO_REFRESH){
193 | text.setText(pullToRefreshText);
194 | }
195 | }
196 |
197 | /**
198 | * Change the label text on state 'Release to Refresh'
199 | *
200 | * @param releaseToRefreshText Text
201 | */
202 | public void setTextReleaseToRefresh(String releaseToRefreshText){
203 | this.releaseToRefreshText = releaseToRefreshText;
204 | if(state == State.RELEASE_TO_REFRESH){
205 | text.setText(releaseToRefreshText);
206 | }
207 | }
208 |
209 | /**
210 | * Change the label text on state 'Refreshing'
211 | *
212 | * @param refreshingText Text
213 | */
214 | public void setTextRefreshing(String refreshingText){
215 | this.refreshingText = refreshingText;
216 | if(state == State.REFRESHING){
217 | text.setText(refreshingText);
218 | }
219 | }
220 |
221 | private void init(){
222 | setVerticalFadingEdgeEnabled(false);
223 |
224 | headerContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.ptr_header, null);
225 | header = (RelativeLayout) headerContainer.findViewById(R.id.ptr_id_header);
226 | text = (TextView) header.findViewById(R.id.ptr_id_text);
227 | lastUpdatedTextView = (TextView) header.findViewById(R.id.ptr_id_last_updated);
228 | image = (ImageView) header.findViewById(R.id.ptr_id_image);
229 | refreshingIcon = header.findViewById(R.id.ptr_id_spinner);
230 |
231 | pullToRefreshText = getContext().getString(R.string.ptr_pull_to_refresh);
232 | releaseToRefreshText = getContext().getString(R.string.ptr_release_to_refresh);
233 | refreshingText = getContext().getString(R.string.ptr_refreshing);
234 | lastUpdatedText = getContext().getString(R.string.ptr_last_updated);
235 |
236 | flipAnimation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
237 | flipAnimation.setInterpolator(new LinearInterpolator());
238 | flipAnimation.setDuration(ROTATE_ARROW_ANIMATION_DURATION);
239 | flipAnimation.setFillAfter(true);
240 |
241 | reverseFlipAnimation = new RotateAnimation(-180, 0,
242 | RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
243 | reverseFlipAnimation.setInterpolator(new LinearInterpolator());
244 | reverseFlipAnimation.setDuration(ROTATE_ARROW_ANIMATION_DURATION);
245 | reverseFlipAnimation.setFillAfter(true);
246 |
247 | refreshingAnimation = new RotateAnimation(0, 720,
248 | RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
249 | refreshingAnimation.setDuration(1200);
250 | refreshingAnimation.setInterpolator(new LinearInterpolator());
251 | refreshingAnimation.setRepeatCount(Integer.MAX_VALUE);
252 | refreshingAnimation.setRepeatMode(Animation.RESTART);
253 |
254 | addHeaderView(headerContainer);
255 | setState(State.PULL_TO_REFRESH);
256 | scrollbarEnabled = isVerticalScrollBarEnabled();
257 |
258 | ViewTreeObserver vto = header.getViewTreeObserver();
259 | vto.addOnGlobalLayoutListener(new PTROnGlobalLayoutListener());
260 |
261 | //super.setOnItemClickListener(new PTROnItemClickListener());
262 | //super.setOnItemLongClickListener(new PTROnItemLongClickListener());
263 | }
264 |
265 | private void setHeaderPadding(int padding){
266 | headerPadding = padding;
267 |
268 | MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) header.getLayoutParams();
269 | mlp.setMargins(0, Math.round(padding), 0, 0);
270 | header.setLayoutParams(mlp);
271 | }
272 |
273 | private boolean enablePulling = true;
274 | private boolean enablePull(MotionEvent event){
275 | return enablePulling;
276 | }
277 |
278 | @Override
279 | public boolean onInterceptTouchEvent(MotionEvent event) {
280 | if(lockScrollWhileRefreshing
281 | && (state == State.REFRESHING || getAnimation() != null && !getAnimation().hasEnded())){
282 | return true; //consume touch event here..
283 | }
284 |
285 | switch(event.getAction()){
286 | case MotionEvent.ACTION_DOWN:
287 | if( getFirstVisiblePosition() == 0 )
288 | previousY = event.getY();
289 | break;
290 | case MotionEvent.ACTION_MOVE:
291 | if( getFirstVisiblePosition() == 0 && event.getY() - previousY > 0 ) {
292 | enablePulling = true;
293 | return true;
294 | }else{
295 | enablePulling = false;
296 | }
297 | break;
298 | case MotionEvent.ACTION_CANCEL:
299 | case MotionEvent.ACTION_UP:
300 | enablePulling = false;
301 | break;
302 | }
303 |
304 | return super.onInterceptTouchEvent(event);
305 | }
306 |
307 | @Override
308 | public boolean onTouchEvent(MotionEvent event){
309 | if(lockScrollWhileRefreshing
310 | && (state == State.REFRESHING || getAnimation() != null && !getAnimation().hasEnded())){
311 | return true;
312 | }
313 |
314 | switch(event.getAction()){
315 |
316 | case MotionEvent.ACTION_UP:
317 | if(enablePull(event) && (state == State.RELEASE_TO_REFRESH || getFirstVisiblePosition() == 0)){
318 | switch(state){
319 | case RELEASE_TO_REFRESH:
320 | setState(State.REFRESHING);
321 | bounceBackHeader();
322 | break;
323 | case PULL_TO_REFRESH:
324 | resetHeader();
325 | break;
326 | default:
327 | break;
328 | }
329 | }
330 | break;
331 |
332 | case MotionEvent.ACTION_MOVE:
333 | if(enablePull(event)){
334 | float y = event.getY();
335 | float diff = y - previousY;
336 | if(diff > 0) diff /= PULL_RESISTANCE;
337 | previousY = y;
338 |
339 | int newHeaderPadding = Math.max(Math.round(headerPadding + diff), -header.getHeight());
340 |
341 | if(newHeaderPadding != headerPadding && state != State.REFRESHING){
342 | setHeaderPadding(newHeaderPadding);
343 |
344 | if(state == State.PULL_TO_REFRESH && headerPadding > 0){
345 | setState(State.RELEASE_TO_REFRESH);
346 |
347 | image.clearAnimation();
348 | image.startAnimation(flipAnimation);
349 | }else if(state == State.RELEASE_TO_REFRESH && headerPadding < 0){
350 | setState(State.PULL_TO_REFRESH);
351 |
352 | image.clearAnimation();
353 | image.startAnimation(reverseFlipAnimation);
354 | }
355 | }
356 | }
357 |
358 | break;
359 | }
360 |
361 | return super.onTouchEvent(event);
362 | }
363 |
364 | private void bounceBackHeader(){
365 | int yTranslate = state == State.REFRESHING ?
366 | header.getHeight() - headerContainer.getHeight() :
367 | -headerContainer.getHeight() - headerContainer.getTop();
368 |
369 | bounceAnimation = new TranslateAnimation(
370 | TranslateAnimation.ABSOLUTE, 0,
371 | TranslateAnimation.ABSOLUTE, 0,
372 | TranslateAnimation.ABSOLUTE, 0,
373 | TranslateAnimation.ABSOLUTE, yTranslate);
374 |
375 | bounceAnimation.setDuration(BOUNCE_ANIMATION_DURATION);
376 | bounceAnimation.setFillEnabled(true);
377 | bounceAnimation.setFillAfter(false);
378 | bounceAnimation.setFillBefore(true);
379 | // bounceAnimation.setInterpolator(new OvershootInterpolator(BOUNCE_OVERSHOOT_TENSION));
380 | bounceAnimation.setAnimationListener(new HeaderAnimationListener(yTranslate));
381 | startAnimation(bounceAnimation);
382 | }
383 |
384 | private void resetHeader(){
385 |
386 | if(getFirstVisiblePosition() > 0){
387 | setHeaderPadding(-header.getHeight());
388 | setState(State.PULL_TO_REFRESH);
389 | return;
390 | }
391 |
392 | if(getAnimation() != null && !getAnimation().hasEnded()){
393 | bounceBackHeader = true;
394 | }else{
395 | bounceBackHeader();
396 | }
397 | }
398 |
399 | private void setUiRefreshing(){
400 | image.clearAnimation();
401 | image.setVisibility(View.GONE);
402 | refreshingIcon.setVisibility(View.VISIBLE);
403 | refreshingIcon.startAnimation(refreshingAnimation);
404 | text.setText(refreshingText);
405 | }
406 |
407 | private void stopRefreshing(){
408 | refreshingIcon.clearAnimation();
409 | refreshingIcon.setVisibility(View.GONE);
410 | }
411 |
412 | private void setState(State state){
413 | this.state = state;
414 | switch(state){
415 | case PULL_TO_REFRESH:
416 | stopRefreshing();
417 | image.setVisibility(View.VISIBLE);
418 | text.setText(pullToRefreshText);
419 |
420 | if(showLastUpdatedText && lastUpdated != -1){
421 | lastUpdatedTextView.setVisibility(View.VISIBLE);
422 | lastUpdatedTextView.setText(String.format(lastUpdatedText,
423 | lastUpdatedDateFormat.format(new Date(lastUpdated))));
424 | }
425 |
426 | break;
427 |
428 | case RELEASE_TO_REFRESH:
429 | stopRefreshing();
430 | image.setVisibility(View.VISIBLE);
431 | text.setText(releaseToRefreshText);
432 |
433 | break;
434 |
435 | case REFRESHING:
436 | setUiRefreshing();
437 | lastUpdated = System.currentTimeMillis();
438 | if(onRefreshListener == null){
439 | setState(State.PULL_TO_REFRESH);
440 | }else{
441 | onRefreshListener.onRefresh();
442 | }
443 |
444 | break;
445 | }
446 | }
447 |
448 |
449 |
450 |
451 | @Override
452 | protected void onScrollChanged(int l, int t, int oldl, int oldt){
453 | super.onScrollChanged(l, t, oldl, oldt);
454 |
455 | if(!hasResetHeader){
456 | if(measuredHeaderHeight > 0 && state != State.REFRESHING){
457 | setHeaderPadding(-measuredHeaderHeight);
458 | }
459 |
460 | hasResetHeader = true;
461 | }
462 |
463 | }
464 |
465 | private class HeaderAnimationListener implements AnimationListener{
466 |
467 | private int height, translation;
468 | private State stateAtAnimationStart;
469 |
470 | public HeaderAnimationListener(int translation){
471 | this.translation = translation;
472 | }
473 |
474 | @Override
475 | public void onAnimationStart(Animation animation){
476 | stateAtAnimationStart = state;
477 |
478 | android.view.ViewGroup.LayoutParams lp = getLayoutParams();
479 | height = lp.height;
480 | lp.height = getHeight() - translation;
481 | setLayoutParams(lp);
482 |
483 | if(scrollbarEnabled){
484 | setVerticalScrollBarEnabled(false);
485 | }
486 | }
487 |
488 | @Override
489 | public void onAnimationEnd(Animation animation){
490 | setHeaderPadding(stateAtAnimationStart == State.REFRESHING ? 0 : -measuredHeaderHeight - headerContainer.getTop());
491 | //setSelection(0);
492 |
493 | android.view.ViewGroup.LayoutParams lp = getLayoutParams();
494 | lp.height = height;
495 | setLayoutParams(lp);
496 |
497 | if(scrollbarEnabled){
498 | setVerticalScrollBarEnabled(true);
499 | }
500 |
501 | if(bounceBackHeader){
502 | bounceBackHeader = false;
503 |
504 | postDelayed(new Runnable(){
505 |
506 | @Override
507 | public void run(){
508 | resetHeader();
509 | }
510 | }, BOUNCE_ANIMATION_DELAY);
511 | }else if(stateAtAnimationStart != State.REFRESHING){
512 | setState(State.PULL_TO_REFRESH);
513 | }
514 | }
515 |
516 | @Override
517 | public void onAnimationRepeat(Animation animation){}
518 | }
519 |
520 | private class PTROnGlobalLayoutListener implements OnGlobalLayoutListener{
521 |
522 | @Override
523 | public void onGlobalLayout(){
524 | int initialHeaderHeight = header.getHeight();
525 |
526 | if(initialHeaderHeight > 0){
527 | measuredHeaderHeight = initialHeaderHeight;
528 |
529 | if(measuredHeaderHeight > 0 && state != State.REFRESHING){
530 | setHeaderPadding(-measuredHeaderHeight);
531 | requestLayout();
532 | }
533 | }
534 |
535 | getViewTreeObserver().removeGlobalOnLayoutListener(this);
536 | }
537 | }
538 |
539 | // private class PTROnItemClickListener implements OnItemClickListener {
540 | //
541 | // @Override
542 | // public void onItemClick(AdapterView> adapterView, View view, int position, long id){
543 | // hasResetHeader = false;
544 | //
545 | // if(onItemClickListener != null && state == State.PULL_TO_REFRESH){
546 | // // Passing up onItemClick. Correct position with the number of header views
547 | // onItemClickListener.onItemClick(adapterView, view, position - getHeaderViewsCount(), id);
548 | // }
549 | // }
550 | // }
551 | //
552 | // private class PTROnItemLongClickListener implements OnItemLongClickListener{
553 | //
554 | // @Override
555 | // public boolean onItemLongClick(AdapterView> adapterView, View view, int position, long id){
556 | // hasResetHeader = false;
557 | //
558 | // if(onItemLongClickListener != null && state == State.PULL_TO_REFRESH){
559 | // // Passing up onItemLongClick. Correct position with the number of header views
560 | // return onItemLongClickListener.onItemLongClick(adapterView, view, position - getHeaderViewsCount(), id);
561 | // }
562 | //
563 | // return false;
564 | // }
565 | // }
566 | }
567 |
--------------------------------------------------------------------------------
/src/com/huewu/pla/lib/internal/PLA_AdapterView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2006 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.huewu.pla.lib.internal;
18 |
19 | import android.content.Context;
20 | import android.database.DataSetObserver;
21 | import android.os.Parcelable;
22 | import android.os.SystemClock;
23 | import android.util.AttributeSet;
24 | import android.util.SparseArray;
25 | import android.view.ContextMenu;
26 | import android.view.ContextMenu.ContextMenuInfo;
27 | import android.view.SoundEffectConstants;
28 | import android.view.View;
29 | import android.view.ViewDebug;
30 | import android.view.ViewGroup;
31 | import android.view.accessibility.AccessibilityEvent;
32 | import android.widget.Adapter;
33 |
34 |
35 | /**
36 | * An AdapterView is a view whose children are determined by an {@link Adapter}.
37 | *
38 | *
39 | * See {@link ListView}, {@link GridView}, {@link Spinner} and
40 | * {@link Gallery} for commonly used subclasses of AdapterView.
41 | */
42 | public abstract class PLA_AdapterView extends ViewGroup {
43 |
44 | /**
45 | * The item view type returned by {@link Adapter#getItemViewType(int)} when
46 | * the adapter does not want the item's view recycled.
47 | */
48 | public static final int ITEM_VIEW_TYPE_IGNORE = -1;
49 |
50 | /**
51 | * The item view type returned by {@link Adapter#getItemViewType(int)} when
52 | * the item is a header or footer.
53 | */
54 | public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
55 |
56 | /**
57 | * The position of the first child displayed
58 | */
59 | @ViewDebug.ExportedProperty
60 | int mFirstPosition = 0;
61 |
62 | /**
63 | * The offset in pixels from the top of the AdapterView to the top
64 | * of the view to select during the next layout.
65 | */
66 | int mSpecificTop;
67 |
68 | /**
69 | * Position from which to start looking for mSyncRowId
70 | */
71 | int mSyncPosition;
72 |
73 | /**
74 | * Row id to look for when data has changed
75 | */
76 | long mSyncRowId = INVALID_ROW_ID;
77 |
78 | /**
79 | * Height of the view when mSyncPosition and mSyncRowId where set
80 | */
81 | long mSyncHeight;
82 |
83 | /**
84 | * True if we need to sync to mSyncRowId
85 | */
86 | boolean mNeedSync = false;
87 |
88 | /**
89 | * Indicates whether to sync based on the selection or position. Possible
90 | * values are {@link #SYNC_SELECTED_POSITION} or
91 | * {@link #SYNC_FIRST_POSITION}.
92 | */
93 | int mSyncMode;
94 |
95 | /**
96 | * Our height after the last layout
97 | */
98 | private int mLayoutHeight;
99 |
100 | /**
101 | * Sync based on the selected child
102 | */
103 | static final int SYNC_SELECTED_POSITION = 0;
104 |
105 | /**
106 | * Sync based on the first child displayed
107 | */
108 | static final int SYNC_FIRST_POSITION = 1;
109 |
110 | /**
111 | * Maximum amount of time to spend in {@link #findSyncPosition()}
112 | */
113 | static final int SYNC_MAX_DURATION_MILLIS = 100;
114 |
115 | /**
116 | * Indicates that this view is currently being laid out.
117 | */
118 | boolean mInLayout = false;
119 |
120 | /**
121 | * The listener that receives notifications when an item is selected.
122 | */
123 | OnItemSelectedListener mOnItemSelectedListener;
124 |
125 | /**
126 | * The listener that receives notifications when an item is clicked.
127 | */
128 | OnItemClickListener mOnItemClickListener;
129 |
130 | /**
131 | * The listener that receives notifications when an item is long clicked.
132 | */
133 | OnItemLongClickListener mOnItemLongClickListener;
134 |
135 | /**
136 | * True if the data has changed since the last layout
137 | */
138 | boolean mDataChanged;
139 |
140 | /**
141 | * View to show if there are no items to show.
142 | */
143 | private View mEmptyView;
144 |
145 | /**
146 | * The number of items in the current adapter.
147 | */
148 | @ViewDebug.ExportedProperty
149 | int mItemCount;
150 |
151 | /**
152 | * The number of items in the adapter before a data changed event occured.
153 | */
154 | int mOldItemCount;
155 |
156 | /**
157 | * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
158 | * number of items in the current adapter.
159 | */
160 | public static final int INVALID_POSITION = -1;
161 |
162 | /**
163 | * Represents an empty or invalid row id
164 | */
165 | public static final long INVALID_ROW_ID = Long.MIN_VALUE;
166 |
167 | /**
168 | * The last selected position we used when notifying
169 | */
170 | int mOldSelectedPosition = INVALID_POSITION;
171 |
172 | /**
173 | * The id of the last selected position we used when notifying
174 | */
175 | long mOldSelectedRowId = INVALID_ROW_ID;
176 |
177 | /**
178 | * Indicates what focusable state is requested when calling setFocusable().
179 | * In addition to this, this view has other criteria for actually
180 | * determining the focusable state (such as whether its empty or the text
181 | * filter is shown).
182 | *
183 | * @see #setFocusable(boolean)
184 | * @see #checkFocus()
185 | */
186 | private boolean mDesiredFocusableState;
187 | private boolean mDesiredFocusableInTouchModeState;
188 |
189 | /**
190 | * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
191 | * This is used to layout the children during a layout pass.
192 | */
193 | boolean mBlockLayoutRequests = false;
194 |
195 | public PLA_AdapterView(Context context) {
196 | super(context);
197 | }
198 |
199 | public PLA_AdapterView(Context context, AttributeSet attrs) {
200 | super(context, attrs);
201 | }
202 |
203 | public PLA_AdapterView(Context context, AttributeSet attrs, int defStyle) {
204 | super(context, attrs, defStyle);
205 | }
206 |
207 |
208 | /**
209 | * Interface definition for a callback to be invoked when an item in this
210 | * AdapterView has been clicked.
211 | */
212 | public interface OnItemClickListener {
213 |
214 | /**
215 | * Callback method to be invoked when an item in this AdapterView has
216 | * been clicked.
217 | *
218 | * Implementers can call getItemAtPosition(position) if they need
219 | * to access the data associated with the selected item.
220 | *
221 | * @param parent The AdapterView where the click happened.
222 | * @param view The view within the AdapterView that was clicked (this
223 | * will be a view provided by the adapter)
224 | * @param position The position of the view in the adapter.
225 | * @param id The row id of the item that was clicked.
226 | */
227 | void onItemClick(PLA_AdapterView> parent, View view, int position, long id);
228 | }
229 |
230 | /**
231 | * Register a callback to be invoked when an item in this AdapterView has
232 | * been clicked.
233 | *
234 | * @param listener The callback that will be invoked.
235 | */
236 | public void setOnItemClickListener(OnItemClickListener listener) {
237 | mOnItemClickListener = listener;
238 | }
239 |
240 | /**
241 | * @return The callback to be invoked with an item in this AdapterView has
242 | * been clicked, or null id no callback has been set.
243 | */
244 | public final OnItemClickListener getOnItemClickListener() {
245 | return mOnItemClickListener;
246 | }
247 |
248 | /**
249 | * Call the OnItemClickListener, if it is defined.
250 | *
251 | * @param view The view within the AdapterView that was clicked.
252 | * @param position The position of the view in the adapter.
253 | * @param id The row id of the item that was clicked.
254 | * @return True if there was an assigned OnItemClickListener that was
255 | * called, false otherwise is returned.
256 | */
257 | public boolean performItemClick(View view, int position, long id) {
258 | if (mOnItemClickListener != null) {
259 | playSoundEffect(SoundEffectConstants.CLICK);
260 | mOnItemClickListener.onItemClick(this, view, position, id);
261 | return true;
262 | }
263 |
264 | return false;
265 | }
266 |
267 | /**
268 | * Interface definition for a callback to be invoked when an item in this
269 | * view has been clicked and held.
270 | */
271 | public interface OnItemLongClickListener {
272 | /**
273 | * Callback method to be invoked when an item in this view has been
274 | * clicked and held.
275 | *
276 | * Implementers can call getItemAtPosition(position) if they need to access
277 | * the data associated with the selected item.
278 | *
279 | * @param parent The AbsListView where the click happened
280 | * @param view The view within the AbsListView that was clicked
281 | * @param position The position of the view in the list
282 | * @param id The row id of the item that was clicked
283 | *
284 | * @return true if the callback consumed the long click, false otherwise
285 | */
286 | boolean onItemLongClick(PLA_AdapterView> parent, View view, int position, long id);
287 | }
288 |
289 |
290 | /**
291 | * Register a callback to be invoked when an item in this AdapterView has
292 | * been clicked and held
293 | *
294 | * @param listener The callback that will run
295 | */
296 | public void setOnItemLongClickListener(OnItemLongClickListener listener) {
297 | if (!isLongClickable()) {
298 | setLongClickable(true);
299 | }
300 | mOnItemLongClickListener = listener;
301 | }
302 |
303 | /**
304 | * @return The callback to be invoked with an item in this AdapterView has
305 | * been clicked and held, or null id no callback as been set.
306 | */
307 | public final OnItemLongClickListener getOnItemLongClickListener() {
308 | return mOnItemLongClickListener;
309 | }
310 |
311 | /**
312 | * Interface definition for a callback to be invoked when
313 | * an item in this view has been selected.
314 | */
315 | public interface OnItemSelectedListener {
316 | /**
317 | * Callback method to be invoked when an item in this view has been
318 | * selected.
319 | *
320 | * Impelmenters can call getItemAtPosition(position) if they need to access the
321 | * data associated with the selected item.
322 | *
323 | * @param parent The AdapterView where the selection happened
324 | * @param view The view within the AdapterView that was clicked
325 | * @param position The position of the view in the adapter
326 | * @param id The row id of the item that is selected
327 | */
328 | void onItemSelected(PLA_AdapterView> parent, View view, int position, long id);
329 |
330 | /**
331 | * Callback method to be invoked when the selection disappears from this
332 | * view. The selection can disappear for instance when touch is activated
333 | * or when the adapter becomes empty.
334 | *
335 | * @param parent The AdapterView that now contains no selected item.
336 | */
337 | void onNothingSelected(PLA_AdapterView> parent);
338 | }
339 |
340 |
341 | /**
342 | * Register a callback to be invoked when an item in this AdapterView has
343 | * been selected.
344 | *
345 | * @param listener The callback that will run
346 | */
347 | public void setOnItemSelectedListener(OnItemSelectedListener listener) {
348 | mOnItemSelectedListener = listener;
349 | }
350 |
351 | public final OnItemSelectedListener getOnItemSelectedListener() {
352 | return mOnItemSelectedListener;
353 | }
354 |
355 | /**
356 | * Extra menu information provided to the
357 | * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
358 | * callback when a context menu is brought up for this AdapterView.
359 | *
360 | */
361 | public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
362 |
363 | public AdapterContextMenuInfo(View targetView, int position, long id) {
364 | this.targetView = targetView;
365 | this.position = position;
366 | this.id = id;
367 | }
368 |
369 | /**
370 | * The child view for which the context menu is being displayed. This
371 | * will be one of the children of this AdapterView.
372 | */
373 | public View targetView;
374 |
375 | /**
376 | * The position in the adapter for which the context menu is being
377 | * displayed.
378 | */
379 | public int position;
380 |
381 | /**
382 | * The row id of the item for which the context menu is being displayed.
383 | */
384 | public long id;
385 | }
386 |
387 | /**
388 | * Returns the adapter currently associated with this widget.
389 | *
390 | * @return The adapter used to provide this view's content.
391 | */
392 | public abstract T getAdapter();
393 |
394 | /**
395 | * Sets the adapter that provides the data and the views to represent the data
396 | * in this widget.
397 | *
398 | * @param adapter The adapter to use to create this view's content.
399 | */
400 | public abstract void setAdapter(T adapter);
401 |
402 | /**
403 | * This method is not supported and throws an UnsupportedOperationException when called.
404 | *
405 | * @param child Ignored.
406 | *
407 | * @throws UnsupportedOperationException Every time this method is invoked.
408 | */
409 | @Override
410 | public void addView(View child) {
411 | throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
412 | }
413 |
414 | /**
415 | * This method is not supported and throws an UnsupportedOperationException when called.
416 | *
417 | * @param child Ignored.
418 | * @param index Ignored.
419 | *
420 | * @throws UnsupportedOperationException Every time this method is invoked.
421 | */
422 | @Override
423 | public void addView(View child, int index) {
424 | throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
425 | }
426 |
427 | /**
428 | * This method is not supported and throws an UnsupportedOperationException when called.
429 | *
430 | * @param child Ignored.
431 | * @param params Ignored.
432 | *
433 | * @throws UnsupportedOperationException Every time this method is invoked.
434 | */
435 | @Override
436 | public void addView(View child, LayoutParams params) {
437 | throw new UnsupportedOperationException("addView(View, LayoutParams) "
438 | + "is not supported in AdapterView");
439 | }
440 |
441 | /**
442 | * This method is not supported and throws an UnsupportedOperationException when called.
443 | *
444 | * @param child Ignored.
445 | * @param index Ignored.
446 | * @param params Ignored.
447 | *
448 | * @throws UnsupportedOperationException Every time this method is invoked.
449 | */
450 | @Override
451 | public void addView(View child, int index, LayoutParams params) {
452 | throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
453 | + "is not supported in AdapterView");
454 | }
455 |
456 | /**
457 | * This method is not supported and throws an UnsupportedOperationException when called.
458 | *
459 | * @param child Ignored.
460 | *
461 | * @throws UnsupportedOperationException Every time this method is invoked.
462 | */
463 | @Override
464 | public void removeView(View child) {
465 | throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
466 | }
467 |
468 | /**
469 | * This method is not supported and throws an UnsupportedOperationException when called.
470 | *
471 | * @param index Ignored.
472 | *
473 | * @throws UnsupportedOperationException Every time this method is invoked.
474 | */
475 | @Override
476 | public void removeViewAt(int index) {
477 | throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
478 | }
479 |
480 | /**
481 | * This method is not supported and throws an UnsupportedOperationException when called.
482 | *
483 | * @throws UnsupportedOperationException Every time this method is invoked.
484 | */
485 | @Override
486 | public void removeAllViews() {
487 | throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
488 | }
489 |
490 | @Override
491 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
492 | mLayoutHeight = getHeight();
493 | }
494 |
495 | /**
496 | * Return the position of the currently selected item within the adapter's data set
497 | *
498 | * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
499 | */
500 | @ViewDebug.CapturedViewProperty
501 | public int getSelectedItemPosition() {
502 | return INVALID_POSITION;
503 | }
504 |
505 | /**
506 | * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
507 | * if nothing is selected.
508 | */
509 | @ViewDebug.CapturedViewProperty
510 | public long getSelectedItemId() {
511 | return INVALID_ROW_ID;
512 | }
513 |
514 | /**
515 | * @return The view corresponding to the currently selected item, or null
516 | * if nothing is selected
517 | */
518 | public abstract View getSelectedView();
519 |
520 | /**
521 | * @return The data corresponding to the currently selected item, or
522 | * null if there is nothing selected.
523 | */
524 | public Object getSelectedItem() {
525 | T adapter = getAdapter();
526 | int selection = getSelectedItemPosition();
527 | if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
528 | return adapter.getItem(selection);
529 | } else {
530 | return null;
531 | }
532 | }
533 |
534 | /**
535 | * @return The number of items owned by the Adapter associated with this
536 | * AdapterView. (This is the number of data items, which may be
537 | * larger than the number of visible view.)
538 | */
539 | @ViewDebug.CapturedViewProperty
540 | public int getCount() {
541 | return mItemCount;
542 | }
543 |
544 | /**
545 | * Get the position within the adapter's data set for the view, where view is a an adapter item
546 | * or a descendant of an adapter item.
547 | *
548 | * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
549 | * AdapterView at the time of the call.
550 | * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
551 | * if the view does not correspond to a list item (or it is not currently visible).
552 | */
553 | public int getPositionForView(View view) {
554 | View listItem = view;
555 | try {
556 | View v;
557 | while (!(v = (View) listItem.getParent()).equals(this)) {
558 | listItem = v;
559 | }
560 | } catch (ClassCastException e) {
561 | // We made it up to the window without find this list view
562 | return INVALID_POSITION;
563 | }
564 |
565 | // Search the children for the list item
566 | final int childCount = getChildCount();
567 | for (int i = 0; i < childCount; i++) {
568 | if (getChildAt(i).equals(listItem)) {
569 | return mFirstPosition + i;
570 | }
571 | }
572 |
573 | // Child not found!
574 | return INVALID_POSITION;
575 | }
576 |
577 | /**
578 | * Returns the position within the adapter's data set for the first item
579 | * displayed on screen.
580 | *
581 | * @return The position within the adapter's data set
582 | */
583 | public int getFirstVisiblePosition() {
584 | return mFirstPosition;
585 | }
586 |
587 | /**
588 | * Returns the position within the adapter's data set for the last item
589 | * displayed on screen.
590 | *
591 | * @return The position within the adapter's data set
592 | */
593 | public int getLastVisiblePosition() {
594 | return mFirstPosition + getChildCount() - 1;
595 | }
596 |
597 | /**
598 | * Sets the currently selected item. To support accessibility subclasses that
599 | * override this method must invoke the overriden super method first.
600 | *
601 | * @param position Index (starting at 0) of the data item to be selected.
602 | */
603 | public abstract void setSelection(int position);
604 |
605 | /**
606 | * Sets the view to show if the adapter is empty
607 | */
608 | public void setEmptyView(View emptyView) {
609 | mEmptyView = emptyView;
610 |
611 | final T adapter = getAdapter();
612 | final boolean empty = ((adapter == null) || adapter.isEmpty());
613 | updateEmptyStatus(empty);
614 | }
615 |
616 | /**
617 | * When the current adapter is empty, the AdapterView can display a special view
618 | * call the empty view. The empty view is used to provide feedback to the user
619 | * that no data is available in this AdapterView.
620 | *
621 | * @return The view to show if the adapter is empty.
622 | */
623 | public View getEmptyView() {
624 | return mEmptyView;
625 | }
626 |
627 | /**
628 | * Indicates whether this view is in filter mode. Filter mode can for instance
629 | * be enabled by a user when typing on the keyboard.
630 | *
631 | * @return True if the view is in filter mode, false otherwise.
632 | */
633 | boolean isInFilterMode() {
634 | return false;
635 | }
636 |
637 | @Override
638 | public void setFocusable(boolean focusable) {
639 | final T adapter = getAdapter();
640 | final boolean empty = adapter == null || adapter.getCount() == 0;
641 |
642 | mDesiredFocusableState = focusable;
643 | if (!focusable) {
644 | mDesiredFocusableInTouchModeState = false;
645 | }
646 |
647 | super.setFocusable(focusable && (!empty || isInFilterMode()));
648 | }
649 |
650 | @Override
651 | public void setFocusableInTouchMode(boolean focusable) {
652 | final T adapter = getAdapter();
653 | final boolean empty = adapter == null || adapter.getCount() == 0;
654 |
655 | mDesiredFocusableInTouchModeState = focusable;
656 | if (focusable) {
657 | mDesiredFocusableState = true;
658 | }
659 |
660 | super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
661 | }
662 |
663 | void checkFocus() {
664 | final T adapter = getAdapter();
665 | final boolean empty = adapter == null || adapter.getCount() == 0;
666 | final boolean focusable = !empty || isInFilterMode();
667 | // The order in which we set focusable in touch mode/focusable may matter
668 | // for the client, see View.setFocusableInTouchMode() comments for more
669 | // details
670 | super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
671 | super.setFocusable(focusable && mDesiredFocusableState);
672 | if (mEmptyView != null) {
673 | updateEmptyStatus((adapter == null) || adapter.isEmpty());
674 | }
675 | }
676 |
677 | /**
678 | * Update the status of the list based on the empty parameter. If empty is true and
679 | * we have an empty view, display it. In all the other cases, make sure that the listview
680 | * is VISIBLE and that the empty view is GONE (if it's not null).
681 | */
682 | private void updateEmptyStatus(boolean empty) {
683 | if (isInFilterMode()) {
684 | empty = false;
685 | }
686 |
687 | if (empty) {
688 | if (mEmptyView != null) {
689 | mEmptyView.setVisibility(View.VISIBLE);
690 | setVisibility(View.GONE);
691 | } else {
692 | // If the caller just removed our empty view, make sure the list view is visible
693 | setVisibility(View.VISIBLE);
694 | }
695 |
696 | // We are now GONE, so pending layouts will not be dispatched.
697 | // Force one here to make sure that the state of the list matches
698 | // the state of the adapter.
699 | if (mDataChanged) {
700 | this.onLayout(false, getLeft(), getTop(), getRight(), getBottom());
701 | }
702 | } else {
703 | if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
704 | setVisibility(View.VISIBLE);
705 | }
706 | }
707 |
708 | /**
709 | * Gets the data associated with the specified position in the list.
710 | *
711 | * @param position Which data to get
712 | * @return The data associated with the specified position in the list
713 | */
714 | public Object getItemAtPosition(int position) {
715 | T adapter = getAdapter();
716 | return (adapter == null || position < 0) ? null : adapter.getItem(position);
717 | }
718 |
719 | public long getItemIdAtPosition(int position) {
720 | T adapter = getAdapter();
721 | return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
722 | }
723 |
724 | @Override
725 | public void setOnClickListener(OnClickListener l) {
726 | throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
727 | + "You probably want setOnItemClickListener instead");
728 | }
729 |
730 | /**
731 | * Override to prevent freezing of any views created by the adapter.
732 | */
733 | @Override
734 | protected void dispatchSaveInstanceState(SparseArray container) {
735 | dispatchFreezeSelfOnly(container);
736 | }
737 |
738 | /**
739 | * Override to prevent thawing of any views created by the adapter.
740 | */
741 | @Override
742 | protected void dispatchRestoreInstanceState(SparseArray container) {
743 | dispatchThawSelfOnly(container);
744 | }
745 |
746 | class AdapterDataSetObserver extends DataSetObserver {
747 |
748 | private Parcelable mInstanceState = null;
749 |
750 | @Override
751 | public void onChanged() {
752 | mDataChanged = true;
753 | mOldItemCount = mItemCount;
754 | mItemCount = getAdapter().getCount();
755 |
756 | // Detect the case where a cursor that was previously invalidated has
757 | // been repopulated with new data.
758 | if (PLA_AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
759 | && mOldItemCount == 0 && mItemCount > 0) {
760 | PLA_AdapterView.this.onRestoreInstanceState(mInstanceState);
761 | mInstanceState = null;
762 | } else {
763 | rememberSyncState();
764 | }
765 | checkFocus();
766 | requestLayout();
767 | }
768 |
769 | @Override
770 | public void onInvalidated() {
771 | mDataChanged = true;
772 |
773 | if (PLA_AdapterView.this.getAdapter().hasStableIds()) {
774 | // Remember the current state for the case where our hosting activity is being
775 | // stopped and later restarted
776 | mInstanceState = PLA_AdapterView.this.onSaveInstanceState();
777 | }
778 |
779 | // Data is invalid so we should reset our state
780 | mOldItemCount = mItemCount;
781 | mItemCount = 0;
782 | mNeedSync = false;
783 |
784 | checkFocus();
785 | requestLayout();
786 | }
787 |
788 | public void clearSavedState() {
789 | mInstanceState = null;
790 | }
791 | }
792 |
793 | @Override
794 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
795 | boolean populated = false;
796 | // This is an exceptional case which occurs when a window gets the
797 | // focus and sends a focus event via its focused child to announce
798 | // current focus/selection. AdapterView fires selection but not focus
799 | // events so we change the event type here.
800 | if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
801 | event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
802 | }
803 |
804 | // we send selection events only from AdapterView to avoid
805 | // generation of such event for each child
806 | View selectedView = getSelectedView();
807 | if (selectedView != null) {
808 | populated = selectedView.dispatchPopulateAccessibilityEvent(event);
809 | }
810 |
811 | if (!populated) {
812 | if (selectedView != null) {
813 | event.setEnabled(selectedView.isEnabled());
814 | }
815 | event.setItemCount(getCount());
816 | event.setCurrentItemIndex(getSelectedItemPosition());
817 | }
818 |
819 | return populated;
820 | }
821 |
822 | @Override
823 | protected boolean canAnimate() {
824 | return super.canAnimate() && mItemCount > 0;
825 | }
826 |
827 | void handleDataChanged() {
828 | final int count = mItemCount;
829 |
830 | if (count > 0) {
831 | // Find the row we are supposed to sync to
832 | if (mNeedSync) {
833 | mNeedSync = false;
834 | }
835 | }
836 | }
837 |
838 | /**
839 | * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
840 | * and then alternates between moving up and moving down until 1) we find the right position, or
841 | * 2) we run out of time, or 3) we have looked at every position
842 | *
843 | * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
844 | * be found
845 | */
846 | int findSyncPosition() {
847 | int count = mItemCount;
848 |
849 | if (count == 0) {
850 | return INVALID_POSITION;
851 | }
852 |
853 | long idToMatch = mSyncRowId;
854 | int seed = mSyncPosition;
855 |
856 | // If there isn't a selection don't hunt for it
857 | if (idToMatch == INVALID_ROW_ID) {
858 | return INVALID_POSITION;
859 | }
860 |
861 | // Pin seed to reasonable values
862 | seed = Math.max(0, seed);
863 | seed = Math.min(count - 1, seed);
864 |
865 | long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
866 |
867 | long rowId;
868 |
869 | // first position scanned so far
870 | int first = seed;
871 |
872 | // last position scanned so far
873 | int last = seed;
874 |
875 | // True if we should move down on the next iteration
876 | boolean next = false;
877 |
878 | // True when we have looked at the first item in the data
879 | boolean hitFirst;
880 |
881 | // True when we have looked at the last item in the data
882 | boolean hitLast;
883 |
884 | // Get the item ID locally (instead of getItemIdAtPosition), so
885 | // we need the adapter
886 | T adapter = getAdapter();
887 | if (adapter == null) {
888 | return INVALID_POSITION;
889 | }
890 |
891 | while (SystemClock.uptimeMillis() <= endTime) {
892 | rowId = adapter.getItemId(seed);
893 | if (rowId == idToMatch) {
894 | // Found it!
895 | return seed;
896 | }
897 |
898 | hitLast = last == count - 1;
899 | hitFirst = first == 0;
900 |
901 | if (hitLast && hitFirst) {
902 | // Looked at everything
903 | break;
904 | }
905 |
906 | if (hitFirst || (next && !hitLast)) {
907 | // Either we hit the top, or we are trying to move down
908 | last++;
909 | seed = last;
910 | // Try going up next time
911 | next = false;
912 | } else if (hitLast || (!next && !hitFirst)) {
913 | // Either we hit the bottom, or we are trying to move up
914 | first--;
915 | seed = first;
916 | // Try going down next time
917 | next = true;
918 | }
919 |
920 | }
921 |
922 | return INVALID_POSITION;
923 | }
924 |
925 | /**
926 | * Find a position that can be selected (i.e., is not a separator).
927 | *
928 | * @param position The starting position to look at.
929 | * @param lookDown Whether to look down for other positions.
930 | * @return The next selectable position starting at position and then searching either up or
931 | * down. Returns {@link #INVALID_POSITION} if nothing can be found.
932 | */
933 | int lookForSelectablePosition(int position, boolean lookDown) {
934 | return position;
935 | }
936 |
937 | /**
938 | * Remember enough information to restore the screen state when the data has
939 | * changed.
940 | *
941 | */
942 | void rememberSyncState() {
943 | if (getChildCount() > 0) {
944 | mNeedSync = true;
945 | mSyncHeight = mLayoutHeight;
946 | // Sync the based on the offset of the first view
947 | View v = getChildAt(0);
948 | T adapter = getAdapter();
949 | if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
950 | mSyncRowId = adapter.getItemId(mFirstPosition);
951 | } else {
952 | mSyncRowId = NO_ID;
953 | }
954 | mSyncPosition = mFirstPosition;
955 | if (v != null) {
956 | mSpecificTop = v.getTop();
957 | }
958 | mSyncMode = SYNC_FIRST_POSITION;
959 | }
960 | }
961 | }
962 |
--------------------------------------------------------------------------------
/src/com/huewu/pla/lib/internal/PLA_HeaderViewListAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2006 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.huewu.pla.lib.internal;
18 |
19 | import android.database.DataSetObserver;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.widget.Filter;
23 | import android.widget.Filterable;
24 | import android.widget.ListAdapter;
25 | import android.widget.WrapperListAdapter;
26 |
27 | import java.util.ArrayList;
28 |
29 | /**
30 | * ListAdapter used when a ListView has header views. This ListAdapter
31 | * wraps another one and also keeps track of the header views and their
32 | * associated data objects.
33 | *This is intended as a base class; you will probably not need to
34 | * use this class directly in your own code.
35 | */
36 | public class PLA_HeaderViewListAdapter implements WrapperListAdapter, Filterable {
37 |
38 | private final ListAdapter mAdapter;
39 |
40 | // These two ArrayList are assumed to NOT be null.
41 | // They are indeed created when declared in ListView and then shared.
42 | ArrayList mHeaderViewInfos;
43 | ArrayList mFooterViewInfos;
44 |
45 | // Used as a placeholder in case the provided info views are indeed null.
46 | // Currently only used by some CTS tests, which may be removed.
47 | static final ArrayList EMPTY_INFO_LIST =
48 | new ArrayList();
49 |
50 | boolean mAreAllFixedViewsSelectable;
51 |
52 | private final boolean mIsFilterable;
53 |
54 | public PLA_HeaderViewListAdapter(ArrayList headerViewInfos,
55 | ArrayList footerViewInfos,
56 | ListAdapter adapter) {
57 | mAdapter = adapter;
58 | mIsFilterable = adapter instanceof Filterable;
59 |
60 | if (headerViewInfos == null) {
61 | mHeaderViewInfos = EMPTY_INFO_LIST;
62 | } else {
63 | mHeaderViewInfos = headerViewInfos;
64 | }
65 |
66 | if (footerViewInfos == null) {
67 | mFooterViewInfos = EMPTY_INFO_LIST;
68 | } else {
69 | mFooterViewInfos = footerViewInfos;
70 | }
71 |
72 | mAreAllFixedViewsSelectable =
73 | areAllListInfosSelectable(mHeaderViewInfos)
74 | && areAllListInfosSelectable(mFooterViewInfos);
75 | }
76 |
77 | public int getHeadersCount() {
78 | return mHeaderViewInfos.size();
79 | }
80 |
81 | public int getFootersCount() {
82 | return mFooterViewInfos.size();
83 | }
84 |
85 | public boolean isEmpty() {
86 | return mAdapter == null || mAdapter.isEmpty();
87 | }
88 |
89 | private boolean areAllListInfosSelectable(ArrayList infos) {
90 | if (infos != null) {
91 | for (PLA_ListView.FixedViewInfo info : infos) {
92 | if (!info.isSelectable) {
93 | return false;
94 | }
95 | }
96 | }
97 | return true;
98 | }
99 |
100 | public boolean removeHeader(View v) {
101 | for (int i = 0; i < mHeaderViewInfos.size(); i++) {
102 | PLA_ListView.FixedViewInfo info = mHeaderViewInfos.get(i);
103 | if (info.view == v) {
104 | mHeaderViewInfos.remove(i);
105 |
106 | mAreAllFixedViewsSelectable =
107 | areAllListInfosSelectable(mHeaderViewInfos)
108 | && areAllListInfosSelectable(mFooterViewInfos);
109 |
110 | return true;
111 | }
112 | }
113 |
114 | return false;
115 | }
116 |
117 | public boolean removeFooter(View v) {
118 | for (int i = 0; i < mFooterViewInfos.size(); i++) {
119 | PLA_ListView.FixedViewInfo info = mFooterViewInfos.get(i);
120 | if (info.view == v) {
121 | mFooterViewInfos.remove(i);
122 |
123 | mAreAllFixedViewsSelectable =
124 | areAllListInfosSelectable(mHeaderViewInfos)
125 | && areAllListInfosSelectable(mFooterViewInfos);
126 |
127 | return true;
128 | }
129 | }
130 |
131 | return false;
132 | }
133 |
134 | public int getCount() {
135 | if (mAdapter != null) {
136 | return getFootersCount() + getHeadersCount() + mAdapter.getCount();
137 | } else {
138 | return getFootersCount() + getHeadersCount();
139 | }
140 | }
141 |
142 | public boolean areAllItemsEnabled() {
143 | if (mAdapter != null) {
144 | return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
145 | } else {
146 | return true;
147 | }
148 | }
149 |
150 | public boolean isEnabled(int position) {
151 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
152 | int numHeaders = getHeadersCount();
153 | if (position < numHeaders) {
154 | return mHeaderViewInfos.get(position).isSelectable;
155 | }
156 |
157 | // Adapter
158 | final int adjPosition = position - numHeaders;
159 | int adapterCount = 0;
160 | if (mAdapter != null) {
161 | adapterCount = mAdapter.getCount();
162 | if (adjPosition < adapterCount) {
163 | return mAdapter.isEnabled(adjPosition);
164 | }
165 | }
166 |
167 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
168 | return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
169 | }
170 |
171 | public Object getItem(int position) {
172 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
173 | int numHeaders = getHeadersCount();
174 | if (position < numHeaders) {
175 | return mHeaderViewInfos.get(position).data;
176 | }
177 |
178 | // Adapter
179 | final int adjPosition = position - numHeaders;
180 | int adapterCount = 0;
181 | if (mAdapter != null) {
182 | adapterCount = mAdapter.getCount();
183 | if (adjPosition < adapterCount) {
184 | return mAdapter.getItem(adjPosition);
185 | }
186 | }
187 |
188 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
189 | return mFooterViewInfos.get(adjPosition - adapterCount).data;
190 | }
191 |
192 | public long getItemId(int position) {
193 | int numHeaders = getHeadersCount();
194 | if (mAdapter != null && position >= numHeaders) {
195 | int adjPosition = position - numHeaders;
196 | int adapterCount = mAdapter.getCount();
197 | if (adjPosition < adapterCount) {
198 | return mAdapter.getItemId(adjPosition);
199 | }
200 | }
201 | return -1;
202 | }
203 |
204 | public boolean hasStableIds() {
205 | if (mAdapter != null) {
206 | return mAdapter.hasStableIds();
207 | }
208 | return false;
209 | }
210 |
211 | public View getView(int position, View convertView, ViewGroup parent) {
212 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
213 | int numHeaders = getHeadersCount();
214 | if (position < numHeaders) {
215 | return mHeaderViewInfos.get(position).view;
216 | }
217 |
218 | // Adapter
219 | final int adjPosition = position - numHeaders;
220 | int adapterCount = 0;
221 | if (mAdapter != null) {
222 | adapterCount = mAdapter.getCount();
223 | if (adjPosition < adapterCount) {
224 | return mAdapter.getView(adjPosition, convertView, parent);
225 | }
226 | }
227 |
228 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
229 | return mFooterViewInfos.get(adjPosition - adapterCount).view;
230 | }
231 |
232 | public int getItemViewType(int position) {
233 | int numHeaders = getHeadersCount();
234 | if (mAdapter != null && position >= numHeaders) {
235 | int adjPosition = position - numHeaders;
236 | int adapterCount = mAdapter.getCount();
237 | if (adjPosition < adapterCount) {
238 | return mAdapter.getItemViewType(adjPosition);
239 | }
240 | }
241 |
242 | return PLA_AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
243 | }
244 |
245 | public int getViewTypeCount() {
246 | if (mAdapter != null) {
247 | return mAdapter.getViewTypeCount();
248 | }
249 | return 1;
250 | }
251 |
252 | public void registerDataSetObserver(DataSetObserver observer) {
253 | if (mAdapter != null) {
254 | mAdapter.registerDataSetObserver(observer);
255 | }
256 | }
257 |
258 | public void unregisterDataSetObserver(DataSetObserver observer) {
259 | if (mAdapter != null) {
260 | mAdapter.unregisterDataSetObserver(observer);
261 | }
262 | }
263 |
264 | public Filter getFilter() {
265 | if (mIsFilterable) {
266 | return ((Filterable) mAdapter).getFilter();
267 | }
268 | return null;
269 | }
270 |
271 | public ListAdapter getWrappedAdapter() {
272 | return mAdapter;
273 | }
274 | }
275 |
--------------------------------------------------------------------------------
/src/com/woozzu/android/widget/IndexScroller.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2011 woozzu
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.woozzu.android.widget;
18 |
19 | import android.content.Context;
20 | import android.graphics.Canvas;
21 | import android.graphics.Color;
22 | import android.graphics.Paint;
23 | import android.graphics.RectF;
24 | import android.os.Handler;
25 | import android.os.Message;
26 | import android.os.SystemClock;
27 | import android.view.MotionEvent;
28 | import android.widget.Adapter;
29 | import android.widget.ListView;
30 | import android.widget.SectionIndexer;
31 |
32 | public class IndexScroller {
33 |
34 | private float mIndexbarWidth;
35 | private float mIndexbarMargin;
36 | private float mPreviewPadding;
37 | private float mDensity;
38 | private float mScaledDensity;
39 | private float mAlphaRate;
40 | private int mState = STATE_HIDDEN;
41 | private int mListViewWidth;
42 | private int mListViewHeight;
43 | private int mCurrentSection = -1;
44 | private boolean mIsIndexing = false;
45 | private ListView mListView = null;
46 | private SectionIndexer mIndexer = null;
47 | private String[] mSections = null;
48 | private RectF mIndexbarRect;
49 |
50 | private static final int STATE_HIDDEN = 0;
51 | private static final int STATE_SHOWING = 1;
52 | private static final int STATE_SHOWN = 2;
53 | private static final int STATE_HIDING = 3;
54 |
55 | public IndexScroller(Context context, ListView lv) {
56 | mDensity = context.getResources().getDisplayMetrics().density;
57 | mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
58 | mListView = lv;
59 | setAdapter(mListView.getAdapter());
60 |
61 | mIndexbarWidth = 20 * mDensity;
62 | mIndexbarMargin = 10 * mDensity;
63 | mPreviewPadding = 5 * mDensity;
64 | }
65 |
66 | public void draw(Canvas canvas) {
67 | if (mState == STATE_HIDDEN)
68 | return;
69 |
70 | // mAlphaRate determines the rate of opacity
71 | Paint indexbarPaint = new Paint();
72 | indexbarPaint.setColor(Color.BLACK);
73 | indexbarPaint.setAlpha((int) (64 * mAlphaRate));
74 | indexbarPaint.setAntiAlias(true);
75 | canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity, indexbarPaint);
76 |
77 | if (mSections != null && mSections.length > 0) {
78 | // Preview is shown when mCurrentSection is set
79 | if (mCurrentSection >= 0) {
80 | Paint previewPaint = new Paint();
81 | previewPaint.setColor(Color.BLACK);
82 | previewPaint.setAlpha(96);
83 | previewPaint.setAntiAlias(true);
84 | previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0));
85 |
86 | Paint previewTextPaint = new Paint();
87 | previewTextPaint.setColor(Color.WHITE);
88 | previewTextPaint.setAntiAlias(true);
89 | previewTextPaint.setTextSize(50 * mScaledDensity);
90 |
91 | float previewTextWidth = previewTextPaint.measureText(mSections[mCurrentSection]);
92 | float previewSize = 2 * mPreviewPadding + previewTextPaint.descent() - previewTextPaint.ascent();
93 | RectF previewRect = new RectF((mListViewWidth - previewSize) / 2
94 | , (mListViewHeight - previewSize) / 2
95 | , (mListViewWidth - previewSize) / 2 + previewSize
96 | , (mListViewHeight - previewSize) / 2 + previewSize);
97 |
98 | canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint);
99 | canvas.drawText(mSections[mCurrentSection], previewRect.left + (previewSize - previewTextWidth) / 2 - 1
100 | , previewRect.top + mPreviewPadding - previewTextPaint.ascent() + 1, previewTextPaint);
101 | }
102 |
103 | Paint indexPaint = new Paint();
104 | indexPaint.setColor(Color.WHITE);
105 | indexPaint.setAlpha((int) (255 * mAlphaRate));
106 | indexPaint.setAntiAlias(true);
107 | indexPaint.setTextSize(12 * mScaledDensity);
108 |
109 | float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length;
110 | float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint.ascent())) / 2;
111 | for (int i = 0; i < mSections.length; i++) {
112 | float paddingLeft = (mIndexbarWidth - indexPaint.measureText(mSections[i])) / 2;
113 | canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
114 | , mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - indexPaint.ascent(), indexPaint);
115 | }
116 | }
117 | }
118 |
119 | public boolean onTouchEvent(MotionEvent ev) {
120 | switch (ev.getAction()) {
121 | case MotionEvent.ACTION_DOWN:
122 | // If down event occurs inside index bar region, start indexing
123 | if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) {
124 | setState(STATE_SHOWN);
125 |
126 | // It demonstrates that the motion event started from index bar
127 | mIsIndexing = true;
128 | // Determine which section the point is in, and move the list to that section
129 | mCurrentSection = getSectionByPoint(ev.getY());
130 | mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
131 | return true;
132 | }
133 | break;
134 | case MotionEvent.ACTION_MOVE:
135 | if (mIsIndexing) {
136 | // If this event moves inside index bar
137 | if (contains(ev.getX(), ev.getY())) {
138 | // Determine which section the point is in, and move the list to that section
139 | mCurrentSection = getSectionByPoint(ev.getY());
140 | mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
141 | }
142 | return true;
143 | }
144 | break;
145 | case MotionEvent.ACTION_UP:
146 | if (mIsIndexing) {
147 | mIsIndexing = false;
148 | mCurrentSection = -1;
149 | }
150 | if (mState == STATE_SHOWN)
151 | setState(STATE_HIDING);
152 | break;
153 | }
154 | return false;
155 | }
156 |
157 | public void onSizeChanged(int w, int h, int oldw, int oldh) {
158 | mListViewWidth = w;
159 | mListViewHeight = h;
160 | mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth
161 | , mIndexbarMargin
162 | , w - mIndexbarMargin
163 | , h - mIndexbarMargin);
164 | }
165 |
166 | public void show() {
167 | if (mState == STATE_HIDDEN)
168 | setState(STATE_SHOWING);
169 | else if (mState == STATE_HIDING)
170 | setState(STATE_HIDING);
171 | }
172 |
173 | public void hide() {
174 | if (mState == STATE_SHOWN)
175 | setState(STATE_HIDING);
176 | }
177 |
178 | public void setAdapter(Adapter adapter) {
179 | if (adapter instanceof SectionIndexer) {
180 | mIndexer = (SectionIndexer) adapter;
181 | mSections = (String[]) mIndexer.getSections();
182 | }
183 | }
184 |
185 | private void setState(int state) {
186 | if (state < STATE_HIDDEN || state > STATE_HIDING)
187 | return;
188 |
189 | mState = state;
190 | switch (mState) {
191 | case STATE_HIDDEN:
192 | // Cancel any fade effect
193 | mHandler.removeMessages(0);
194 | break;
195 | case STATE_SHOWING:
196 | // Start to fade in
197 | mAlphaRate = 0;
198 | fade(0);
199 | break;
200 | case STATE_SHOWN:
201 | // Cancel any fade effect
202 | mHandler.removeMessages(0);
203 | break;
204 | case STATE_HIDING:
205 | // Start to fade out after three seconds
206 | mAlphaRate = 1;
207 | fade(3000);
208 | break;
209 | }
210 | }
211 |
212 | private boolean contains(float x, float y) {
213 | // Determine if the point is in index bar region, which includes the right margin of the bar
214 | return (x >= mIndexbarRect.left && y >= mIndexbarRect.top && y <= mIndexbarRect.top + mIndexbarRect.height());
215 | }
216 |
217 | private int getSectionByPoint(float y) {
218 | if (mSections == null || mSections.length == 0)
219 | return 0;
220 | if (y < mIndexbarRect.top + mIndexbarMargin)
221 | return 0;
222 | if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin)
223 | return mSections.length - 1;
224 | return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length));
225 | }
226 |
227 | private void fade(long delay) {
228 | mHandler.removeMessages(0);
229 | mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
230 | }
231 |
232 | private Handler mHandler = new Handler() {
233 |
234 | @Override
235 | public void handleMessage(Message msg) {
236 | super.handleMessage(msg);
237 |
238 | switch (mState) {
239 | case STATE_SHOWING:
240 | // Fade in effect
241 | mAlphaRate += (1 - mAlphaRate) * 0.2;
242 | if (mAlphaRate > 0.9) {
243 | mAlphaRate = 1;
244 | setState(STATE_SHOWN);
245 | }
246 |
247 | mListView.invalidate();
248 | fade(10);
249 | break;
250 | case STATE_SHOWN:
251 | // If no action, hide automatically
252 | setState(STATE_HIDING);
253 | break;
254 | case STATE_HIDING:
255 | // Fade out effect
256 | mAlphaRate -= mAlphaRate * 0.2;
257 | if (mAlphaRate < 0.1) {
258 | mAlphaRate = 0;
259 | setState(STATE_HIDDEN);
260 | }
261 |
262 | mListView.invalidate();
263 | fade(10);
264 | break;
265 | }
266 | }
267 |
268 | };
269 | }
270 |
--------------------------------------------------------------------------------
/src/com/woozzu/android/widget/IndexableListView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2011 woozzu
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.woozzu.android.widget;
18 |
19 | import android.content.Context;
20 | import android.graphics.Canvas;
21 | import android.util.AttributeSet;
22 | import android.view.GestureDetector;
23 | import android.view.MotionEvent;
24 | import android.widget.ListAdapter;
25 | import android.widget.ListView;
26 |
27 | public class IndexableListView extends ListView {
28 |
29 | private boolean mIsFastScrollEnabled = false;
30 | private IndexScroller mScroller = null;
31 | private GestureDetector mGestureDetector = null;
32 |
33 | public IndexableListView(Context context) {
34 | super(context);
35 | }
36 |
37 | public IndexableListView(Context context, AttributeSet attrs) {
38 | super(context, attrs);
39 | }
40 |
41 | public IndexableListView(Context context, AttributeSet attrs, int defStyle) {
42 | super(context, attrs, defStyle);
43 | }
44 |
45 | @Override
46 | public boolean isFastScrollEnabled() {
47 | return mIsFastScrollEnabled;
48 | }
49 |
50 | @Override
51 | public void setFastScrollEnabled(boolean enabled) {
52 | mIsFastScrollEnabled = enabled;
53 | if (mIsFastScrollEnabled) {
54 | if (mScroller == null)
55 | mScroller = new IndexScroller(getContext(), this);
56 | } else {
57 | if (mScroller != null) {
58 | mScroller.hide();
59 | mScroller = null;
60 | }
61 | }
62 | }
63 |
64 | @Override
65 | public void draw(Canvas canvas) {
66 | super.draw(canvas);
67 |
68 | // Overlay index bar
69 | if (mScroller != null)
70 | mScroller.draw(canvas);
71 | }
72 |
73 | @Override
74 | public boolean onTouchEvent(MotionEvent ev) {
75 | // Intercept ListView's touch event
76 | if (mScroller != null && mScroller.onTouchEvent(ev))
77 | return true;
78 |
79 | if (mGestureDetector == null) {
80 | mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
81 |
82 | @Override
83 | public boolean onFling(MotionEvent e1, MotionEvent e2,
84 | float velocityX, float velocityY) {
85 | // If fling happens, index bar shows
86 | mScroller.show();
87 | return super.onFling(e1, e2, velocityX, velocityY);
88 | }
89 |
90 | });
91 | }
92 | mGestureDetector.onTouchEvent(ev);
93 |
94 | return super.onTouchEvent(ev);
95 | }
96 |
97 | @Override
98 | public boolean onInterceptTouchEvent(MotionEvent ev) {
99 | return true;
100 | }
101 |
102 | @Override
103 | public void setAdapter(ListAdapter adapter) {
104 | super.setAdapter(adapter);
105 | if (mScroller != null)
106 | mScroller.setAdapter(adapter);
107 | }
108 |
109 | @Override
110 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
111 | super.onSizeChanged(w, h, oldw, oldh);
112 | if (mScroller != null)
113 | mScroller.onSizeChanged(w, h, oldw, oldh);
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/com/youxiachai/onexlistview/XIndexableView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2011 woozzu
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.youxiachai.onexlistview;
18 |
19 | import me.maxwin.view.XListView;
20 | import android.content.Context;
21 | import android.graphics.Canvas;
22 | import android.util.AttributeSet;
23 | import android.view.GestureDetector;
24 | import android.view.MotionEvent;
25 | import android.widget.ListAdapter;
26 |
27 | import com.woozzu.android.widget.IndexScroller;
28 |
29 | /**
30 | * @author youxiachai
31 | * @date 2013/5/11
32 | */
33 | public class XIndexableView extends XListView {
34 |
35 | private boolean mIsFastScrollEnabled = false;
36 | private IndexScroller mScroller = null;
37 | private GestureDetector mGestureDetector = null;
38 |
39 | public XIndexableView(Context context) {
40 | super(context);
41 | }
42 |
43 | public XIndexableView(Context context, AttributeSet attrs) {
44 | super(context, attrs);
45 | }
46 |
47 | public XIndexableView(Context context, AttributeSet attrs, int defStyle) {
48 | super(context, attrs, defStyle);
49 | }
50 |
51 | @Override
52 | public boolean isFastScrollEnabled() {
53 | return mIsFastScrollEnabled;
54 | }
55 |
56 | @Override
57 | public void setFastScrollEnabled(boolean enabled) {
58 | mIsFastScrollEnabled = enabled;
59 | if (mIsFastScrollEnabled) {
60 | if (mScroller == null)
61 | mScroller = new IndexScroller(getContext(), this);
62 | } else {
63 | if (mScroller != null) {
64 | mScroller.hide();
65 | mScroller = null;
66 | }
67 | }
68 | }
69 |
70 | @Override
71 | public void draw(Canvas canvas) {
72 | super.draw(canvas);
73 |
74 | // Overlay index bar
75 | if (mScroller != null)
76 | mScroller.draw(canvas);
77 | }
78 |
79 | @Override
80 | public boolean onTouchEvent(MotionEvent ev) {
81 | // Intercept ListView's touch event
82 | if (mScroller != null && mScroller.onTouchEvent(ev))
83 | return true;
84 |
85 | if (mGestureDetector == null) {
86 | mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
87 |
88 | @Override
89 | public boolean onFling(MotionEvent e1, MotionEvent e2,
90 | float velocityX, float velocityY) {
91 | // If fling happens, index bar shows
92 | mScroller.show();
93 | return super.onFling(e1, e2, velocityX, velocityY);
94 | }
95 |
96 | });
97 | }
98 | mGestureDetector.onTouchEvent(ev);
99 |
100 | return super.onTouchEvent(ev);
101 | }
102 |
103 | @Override
104 | public boolean onInterceptTouchEvent(MotionEvent ev) {
105 | return true;
106 | }
107 |
108 | @Override
109 | public void setAdapter(ListAdapter adapter) {
110 | super.setAdapter(adapter);
111 | if (mScroller != null)
112 | mScroller.setAdapter(adapter);
113 | }
114 |
115 | @Override
116 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
117 | super.onSizeChanged(w, h, oldw, oldh);
118 | if (mScroller != null)
119 | mScroller.onSizeChanged(w, h, oldw, oldh);
120 | }
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/src/com/youxiachai/onexlistview/XMultiColumnListView.java:
--------------------------------------------------------------------------------
1 | package com.youxiachai.onexlistview;
2 |
3 | import me.maxwin.view.IXListViewLoadMore;
4 | import me.maxwin.view.IXListViewRefreshListener;
5 | import me.maxwin.view.IXScrollListener;
6 | import me.maxwin.view.XListViewFooter;
7 | import me.maxwin.view.XListViewHeader;
8 | import android.content.Context;
9 | import android.os.Handler;
10 | import android.util.AttributeSet;
11 | import android.util.Log;
12 | import android.view.MotionEvent;
13 | import android.view.View;
14 | import android.view.ViewTreeObserver.OnGlobalLayoutListener;
15 | import android.view.animation.DecelerateInterpolator;
16 | import android.widget.ListAdapter;
17 | import android.widget.RelativeLayout;
18 | import android.widget.Scroller;
19 | import android.widget.TextView;
20 |
21 | import com.huewu.pla.lib.MultiColumnListView;
22 | import com.huewu.pla.lib.internal.PLA_AbsListView;
23 | import com.huewu.pla.lib.internal.PLA_AbsListView.OnScrollListener;
24 |
25 | /**
26 | * @author youxiachai
27 | * @date 2013-5-3
28 | */
29 | public class XMultiColumnListView extends MultiColumnListView implements
30 | OnScrollListener {
31 | protected float mLastY = -1; // save event y
32 | protected Scroller mScroller; // used for scroll back
33 | protected OnScrollListener mScrollListener; // user's scroll listener
34 |
35 | // the interface to trigger refresh and load more.
36 | protected IXListViewLoadMore mLoadMore;
37 | protected IXListViewRefreshListener mOnRefresh;
38 | // -- header view
39 | protected XListViewHeader mHeaderView;
40 | // header view content, use it to calculate the Header's height. And hide it
41 | // when disable pull refresh.
42 | protected RelativeLayout mHeaderViewContent;
43 | protected TextView mHeaderTimeView;
44 | protected int mHeaderViewHeight; // header view's height
45 | protected boolean mEnablePullRefresh = true;
46 | protected boolean mPullRefreshing = false; // is refreashing.
47 |
48 | // -- footer view
49 | protected XListViewFooter mFooterView;
50 | protected boolean mEnablePullLoad;
51 | protected boolean mPullLoading;
52 | protected boolean mIsFooterReady = false;
53 |
54 | // total list items, used to detect is at the bottom of listview.
55 | protected int mTotalItemCount;
56 |
57 | // for mScroller, scroll back from header or footer.
58 | protected int mScrollBack;
59 | protected final static int SCROLLBACK_HEADER = 0;
60 | protected final static int SCROLLBACK_FOOTER = 1;
61 |
62 | protected final static int SCROLL_DURATION = 400; // scroll back duration
63 | protected final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >=
64 | // 50px
65 | // at bottom,
66 | // trigger
67 | // load more.
68 | protected final static float OFFSET_RADIO = 1.8f; // support iOS like pull
69 | // feature.
70 |
71 | public XMultiColumnListView(Context context) {
72 | super(context);
73 | initWithContext(context);
74 | }
75 |
76 | public XMultiColumnListView(Context context, AttributeSet attrs) {
77 | super(context, attrs);
78 | initWithContext(context);
79 | }
80 |
81 | public XMultiColumnListView(Context context, AttributeSet attrs,
82 | int defStyle) {
83 | super(context, attrs, defStyle);
84 | initWithContext(context);
85 | }
86 |
87 | protected void initWithContext(Context context) {
88 | mScroller = new Scroller(context, new DecelerateInterpolator());
89 | // XListView need the scroll event, and it will dispatch the event to
90 | // user's listener (as a proxy).
91 | super.setOnScrollListener(this);
92 |
93 | // init header view
94 | mHeaderView = new XListViewHeader(context);
95 | mHeaderViewContent = (RelativeLayout) mHeaderView
96 | .findViewById(R.id.xlistview_header_content);
97 | mHeaderTimeView = (TextView) mHeaderView
98 | .findViewById(R.id.xlistview_header_time);
99 | addHeaderView(mHeaderView);
100 |
101 | // init footer view
102 | mFooterView = new XListViewFooter(context);
103 |
104 | // init header height
105 | mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(
106 | new OnGlobalLayoutListener() {
107 | @Override
108 | public void onGlobalLayout() {
109 | mHeaderViewHeight = mHeaderViewContent.getHeight();
110 | getViewTreeObserver()
111 | .removeGlobalOnLayoutListener(this);
112 | }
113 | });
114 | // 默认关闭所有操作
115 | disablePullLoad();
116 | disablePullRefreash();
117 | // setPullRefreshEnable(mEnablePullRefresh);
118 | // setPullLoadEnable(mEnablePullLoad);
119 | }
120 |
121 | public void updateHeaderHeight(float delta) {
122 | mHeaderView.setVisiableHeight((int) delta
123 | + mHeaderView.getVisiableHeight());
124 | if (mEnablePullRefresh && !mPullRefreshing) { // 未处于刷新状态,更新箭头
125 | if (mHeaderView.getVisiableHeight() > mHeaderViewHeight) {
126 | mHeaderView.setState(XListViewHeader.STATE_READY);
127 | } else {
128 | mHeaderView.setState(XListViewHeader.STATE_NORMAL);
129 | }
130 | }
131 | setSelection(0); // scroll to top each time
132 | }
133 |
134 | protected void invokeOnScrolling() {
135 | if (mScrollListener instanceof IXScrollListener) {
136 | IXScrollListener l = (IXScrollListener) mScrollListener;
137 | l.onXScrolling(this);
138 | }
139 | }
140 |
141 | protected void startLoadMore() {
142 | if (mEnablePullLoad
143 | && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA
144 | && !mPullLoading) {
145 | mPullLoading = true;
146 | mFooterView.setState(XListViewFooter.STATE_LOADING);
147 | if (mLoadMore != null) {
148 | mLoadMore.onLoadMore();
149 | }
150 | }
151 | }
152 |
153 | protected void resetFooterHeight() {
154 | int bottomMargin = mFooterView.getBottomMargin();
155 | if (bottomMargin > 0) {
156 | mScrollBack = SCROLLBACK_FOOTER;
157 | mScroller.startScroll(0, bottomMargin, 0, -bottomMargin,
158 | SCROLL_DURATION);
159 | invalidate();
160 | }
161 | }
162 |
163 | protected void updateFooterHeight(float delta) {
164 | int height = mFooterView.getBottomMargin() + (int) delta;
165 | if (mEnablePullLoad && !mPullLoading) {
166 | if (height > PULL_LOAD_MORE_DELTA) { // height enough to invoke load
167 | // more.
168 | mFooterView.setState(XListViewFooter.STATE_READY);
169 | } else {
170 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
171 | }
172 | }
173 | mFooterView.setBottomMargin(height);
174 |
175 | // setSelection(mTotalItemCount - 1); // scroll to bottom
176 | }
177 |
178 | @Override
179 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
180 | super.onLayout(changed, l, t, r, b);
181 | // make sure XListViewFooter is the last footer view, and only add once.
182 | if (mIsFooterReady == false) {
183 | // if not inflate screen ,footerview not add
184 | if(getAdapter() != null){
185 | if (getLastVisiblePosition() != (getAdapter().getCount() - 1)) {
186 | mIsFooterReady = true;
187 | addFooterView(mFooterView);
188 | }
189 | }
190 |
191 | }
192 | }
193 |
194 | /**
195 | * reset header view's height.
196 | */
197 | public void resetHeaderHeight() {
198 | int height = mHeaderView.getVisiableHeight();
199 | if (height == 0) // not visible.
200 | return;
201 | // refreshing and header isn't shown fully. do nothing.
202 | if (mPullRefreshing && height <= mHeaderViewHeight) {
203 | return;
204 | }
205 | int finalHeight = 0; // default: scroll back to dismiss header.
206 | // is refreshing, just scroll back to show all the header.
207 | if (mPullRefreshing && height > mHeaderViewHeight) {
208 | finalHeight = mHeaderViewHeight;
209 | }
210 | Log.d("xlistview", "resetHeaderHeight-->" + (finalHeight - height));
211 | mScrollBack = SCROLLBACK_HEADER;
212 | mScroller.startScroll(0, height, 0, finalHeight - height,
213 | SCROLL_DURATION);
214 | // trigger computeScroll
215 | invalidate();
216 | }
217 |
218 | /*
219 | * 神奇的bug....
220 | */
221 | @Override
222 | public void setAdapter(ListAdapter adapter) {
223 | super.setAdapter(adapter);
224 |
225 | //莫名其妙的bug....
226 | //updateHeaderHeight(10);
227 | postDelayed(new Runnable() {
228 | @Override
229 | public void run() {
230 | // resetHeaderHeight();
231 | mScroller.startScroll(0, 0, 0, 0,
232 | SCROLL_DURATION);
233 | // // trigger computeScroll
234 | invalidate();
235 | }
236 | }, 100);
237 |
238 | }
239 |
240 | /**
241 | * enable or disable pull down refresh feature.
242 | *
243 | * @param enable
244 | */
245 | public void setPullRefreshEnable(IXListViewRefreshListener refreshListener) {
246 | mEnablePullRefresh = true;
247 | mHeaderViewContent.setVisibility(View.VISIBLE);
248 | this.mOnRefresh = refreshListener;
249 |
250 | }
251 |
252 | public void disablePullRefreash() {
253 | mEnablePullRefresh = false;
254 | // disable, hide the content
255 | mHeaderViewContent.setVisibility(View.INVISIBLE);
256 | }
257 |
258 | /**
259 | * enable or disable pull up load more feature.
260 | *
261 | * @param enable
262 | */
263 | public void setPullLoadEnable(IXListViewLoadMore loadMoreListener) {
264 | mEnablePullLoad = true;
265 | this.mLoadMore = loadMoreListener;
266 | mPullLoading = false;
267 | mFooterView.show();
268 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
269 | // both "pull up" and "click" will invoke load more.
270 | mFooterView.setOnClickListener(new OnClickListener() {
271 | @Override
272 | public void onClick(View v) {
273 | startLoadMore();
274 | }
275 | });
276 |
277 | }
278 |
279 | public void disablePullLoad() {
280 | mEnablePullLoad = false;
281 | mFooterView.hide();
282 | mFooterView.setOnClickListener(null);
283 | }
284 |
285 | /**
286 | * set last refresh time
287 | *
288 | * @param time
289 | */
290 | public void setRefreshTime(String time) {
291 | mHeaderTimeView.setText(time);
292 | }
293 |
294 | /**
295 | * stop refresh, reset header view.
296 | */
297 | public void stopRefresh(String time) {
298 | if (mPullRefreshing == true) {
299 | mPullRefreshing = false;
300 | mHeaderTimeView.setText(time);
301 | resetHeaderHeight();
302 | }
303 | }
304 |
305 | /**
306 | * stop load more, reset footer view.
307 | */
308 | public void stopLoadMore() {
309 | if (mPullLoading == true) {
310 | mPullLoading = false;
311 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
312 | }
313 | }
314 |
315 | @Override
316 | public void computeScroll() {
317 | if (mScroller.computeScrollOffset()) {
318 | if (mScrollBack == SCROLLBACK_HEADER) {
319 | mHeaderView.setVisiableHeight(mScroller.getCurrY());
320 | } else {
321 | mFooterView.setBottomMargin(mScroller.getCurrY());
322 | }
323 | postInvalidate();
324 | invokeOnScrolling();
325 | }
326 | super.computeScroll();
327 | }
328 |
329 | @Override
330 | public boolean onTouchEvent(MotionEvent ev) {
331 | if (mLastY == -1) {
332 | mLastY = ev.getRawY();
333 | }
334 |
335 | switch (ev.getAction()) {
336 | case MotionEvent.ACTION_DOWN:
337 | mLastY = ev.getRawY();
338 | break;
339 | case MotionEvent.ACTION_MOVE:
340 | final float deltaY = ev.getRawY() - mLastY;
341 | mLastY = ev.getRawY();
342 | Log.d("xlistview", "getFirstVisiblePosition()-->"
343 | + getFirstVisiblePosition() + "getVisiableHeight()"
344 | + mHeaderView.getVisiableHeight() + "deltaY->" + deltaY);
345 | if (getFirstVisiblePosition() == 0
346 | && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0) && !mPullRefreshing) {
347 | // the first item is showing, header has shown or pull down.
348 | if(mEnablePullRefresh){
349 | updateHeaderHeight(deltaY / OFFSET_RADIO);
350 | invokeOnScrolling();
351 | }
352 | } else if (getLastVisiblePosition() == mTotalItemCount - 1
353 | && (mFooterView.getBottomMargin() > 0 || deltaY < 0) && !mPullLoading) {
354 | // last item, already pulled up or want to pull up.
355 | if(mEnablePullLoad){
356 | updateFooterHeight(-deltaY / OFFSET_RADIO);
357 | }
358 | }
359 | break;
360 | default:
361 | mLastY = -1; // reset
362 | if (getFirstVisiblePosition() == 0) {
363 | // invoke refresh
364 | startOnRefresh();
365 | resetHeaderHeight();
366 | } else if (getLastVisiblePosition() == mTotalItemCount - 1) {
367 | // invoke load more.
368 |
369 | startLoadMore();
370 | resetFooterHeight();
371 | }
372 | break;
373 | }
374 | return super.onTouchEvent(ev);
375 | }
376 |
377 | protected void startOnRefresh() {
378 | if (mEnablePullRefresh
379 | && mHeaderView.getVisiableHeight() > mHeaderViewHeight
380 | && !mPullRefreshing) {
381 | mPullRefreshing = true;
382 | mHeaderView.setState(XListViewHeader.STATE_REFRESHING);
383 | if (mOnRefresh != null) {
384 | mOnRefresh.onRefresh();
385 | }
386 | }
387 | }
388 |
389 | @Override
390 | public void onScrollStateChanged(PLA_AbsListView view, int scrollState) {
391 | if (mScrollListener != null) {
392 | mScrollListener.onScrollStateChanged(view, scrollState);
393 | }
394 | }
395 |
396 | @Override
397 | public void onScroll(PLA_AbsListView view, int firstVisibleItem,
398 | int visibleItemCount, int totalItemCount) {
399 | // send to user's listener
400 | mTotalItemCount = totalItemCount;
401 | if (mScrollListener != null) {
402 | mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount,
403 | totalItemCount);
404 | }
405 | }
406 |
407 | @Override
408 | public void setOnScrollListener(OnScrollListener l) {
409 | mScrollListener = l;
410 | }
411 |
412 |
413 | }
414 |
--------------------------------------------------------------------------------
/src/com/youxiachai/onexlistview/XStickyListHeadersIndexableView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2011 woozzu
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.youxiachai.onexlistview;
18 |
19 | import android.content.Context;
20 | import android.graphics.Canvas;
21 | import android.util.AttributeSet;
22 | import android.view.GestureDetector;
23 | import android.view.MotionEvent;
24 | import android.widget.ListAdapter;
25 |
26 | import com.woozzu.android.widget.IndexScroller;
27 |
28 | /**
29 | * @author youxiachai
30 | * @date 2013/5/11
31 | */
32 | public class XStickyListHeadersIndexableView extends XStickyListHeadersView {
33 |
34 | private boolean mIsFastScrollEnabled = false;
35 | private IndexScroller mScroller = null;
36 | private GestureDetector mGestureDetector = null;
37 |
38 | public XStickyListHeadersIndexableView(Context context) {
39 | super(context);
40 | }
41 |
42 | public XStickyListHeadersIndexableView(Context context, AttributeSet attrs) {
43 | super(context, attrs);
44 | }
45 |
46 | public XStickyListHeadersIndexableView(Context context, AttributeSet attrs, int defStyle) {
47 | super(context, attrs, defStyle);
48 | }
49 |
50 | @Override
51 | public boolean isFastScrollEnabled() {
52 | return mIsFastScrollEnabled;
53 | }
54 |
55 | @Override
56 | public void setFastScrollEnabled(boolean enabled) {
57 | mIsFastScrollEnabled = enabled;
58 | if (mIsFastScrollEnabled) {
59 | if (mScroller == null)
60 | mScroller = new IndexScroller(getContext(), this);
61 | } else {
62 | if (mScroller != null) {
63 | mScroller.hide();
64 | mScroller = null;
65 | }
66 | }
67 | }
68 |
69 | @Override
70 | public void draw(Canvas canvas) {
71 | super.draw(canvas);
72 |
73 | // Overlay index bar
74 | if (mScroller != null)
75 | mScroller.draw(canvas);
76 | }
77 |
78 | @Override
79 | public boolean onTouchEvent(MotionEvent ev) {
80 | // Intercept ListView's touch event
81 | if (mScroller != null && mScroller.onTouchEvent(ev))
82 | return true;
83 |
84 | if (mGestureDetector == null) {
85 | mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
86 |
87 | @Override
88 | public boolean onFling(MotionEvent e1, MotionEvent e2,
89 | float velocityX, float velocityY) {
90 | // If fling happens, index bar shows
91 | if(mScroller != null){
92 | mScroller.show();
93 | }
94 | return super.onFling(e1, e2, velocityX, velocityY);
95 | }
96 |
97 | });
98 | }
99 | mGestureDetector.onTouchEvent(ev);
100 |
101 | return super.onTouchEvent(ev);
102 | }
103 |
104 | @Override
105 | public boolean onInterceptTouchEvent(MotionEvent ev) {
106 | return true;
107 | }
108 |
109 | @Override
110 | public void setAdapter(ListAdapter adapter) {
111 | super.setAdapter(adapter);
112 | if (mScroller != null)
113 | mScroller.setAdapter(adapter);
114 | }
115 |
116 | @Override
117 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
118 | super.onSizeChanged(w, h, oldw, oldh);
119 | if (mScroller != null)
120 | mScroller.onSizeChanged(w, h, oldw, oldh);
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/com/youxiachai/onexlistview/XStickyListHeadersView.java:
--------------------------------------------------------------------------------
1 | package com.youxiachai.onexlistview;
2 |
3 | import java.lang.reflect.Field;
4 | import java.util.ArrayList;
5 |
6 | import me.maxwin.view.XListView;
7 | import android.annotation.SuppressLint;
8 | import android.content.Context;
9 | import android.database.DataSetObserver;
10 | import android.graphics.Canvas;
11 | import android.graphics.Rect;
12 | import android.graphics.drawable.Drawable;
13 | import android.os.Build;
14 | import android.util.AttributeSet;
15 | import android.view.MotionEvent;
16 | import android.view.View;
17 | import android.view.View.OnClickListener;
18 | import android.view.ViewConfiguration;
19 | import android.view.ViewGroup;
20 | import android.widget.AbsListView;
21 | import android.widget.AbsListView.OnScrollListener;
22 | import android.widget.ListAdapter;
23 | import android.widget.SectionIndexer;
24 |
25 | import com.emilsjolander.components.stickylistheaders.AdapterWrapper;
26 | import com.emilsjolander.components.stickylistheaders.SectionIndexerAdapterWrapper;
27 | import com.emilsjolander.components.stickylistheaders.StickyListHeadersAdapter;
28 | import com.emilsjolander.components.stickylistheaders.WrapperView;
29 |
30 | /**
31 | * @author youxiachai
32 | */
33 | @SuppressLint("NewApi")
34 | public class XStickyListHeadersView extends XListView implements
35 | OnScrollListener, OnClickListener {
36 |
37 | public interface OnHeaderClickListener {
38 | public void onHeaderClick(XStickyListHeadersView l, View header,
39 | int itemPosition, long headerId, boolean currentlySticky);
40 | }
41 |
42 | private OnScrollListener mOnScrollListenerDelegate;
43 | private boolean mAreHeadersSticky = true;
44 | private int mHeaderBottomPosition;
45 | private View mHeader;
46 | private int mDividerHeight;
47 | private Drawable mDivider;
48 | private Boolean mClippingToPadding;
49 | private final Rect mClippingRect = new Rect();
50 | private Long mCurrentHeaderId = null;
51 | private AdapterWrapper mAdapter;
52 | private float mHeaderDownY = -1;
53 | private boolean mHeaderBeingPressed = false;
54 | private OnHeaderClickListener mOnHeaderClickListener;
55 | private int mHeaderPosition;
56 | private ViewConfiguration mViewConfig;
57 | private ArrayList mFooterViews;
58 | private boolean mDrawingListUnderStickyHeader = false;
59 | private Rect mSelectorRect = new Rect();// for if reflection fails
60 | private Field mSelectorPositionField;
61 |
62 | private AdapterWrapper.OnHeaderClickListener mAdapterHeaderClickListener = new AdapterWrapper.OnHeaderClickListener() {
63 |
64 | @Override
65 | public void onHeaderClick(View header, int itemPosition, long headerId) {
66 | if (mOnHeaderClickListener != null) {
67 | mOnHeaderClickListener.onHeaderClick(
68 | XStickyListHeadersView.this, header, itemPosition,
69 | headerId, false);
70 | }
71 | }
72 | };
73 |
74 | private DataSetObserver mDataSetChangedObserver = new DataSetObserver() {
75 | @Override
76 | public void onChanged() {
77 | reset();
78 | }
79 |
80 | @Override
81 | public void onInvalidated() {
82 | reset();
83 | }
84 | };
85 |
86 | private OnScrollListener mOnScrollListener = new OnScrollListener() {
87 |
88 | @Override
89 | public void onScrollStateChanged(AbsListView view, int scrollState) {
90 | if (mOnScrollListenerDelegate != null) {
91 | mOnScrollListenerDelegate.onScrollStateChanged(view,
92 | scrollState);
93 | }
94 | }
95 |
96 | @Override
97 | public void onScroll(AbsListView view, int firstVisibleItem,
98 | int visibleItemCount, int totalItemCount) {
99 | if (mOnScrollListenerDelegate != null) {
100 | mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
101 | visibleItemCount, totalItemCount);
102 | }
103 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
104 | scrollChanged(firstVisibleItem);
105 | }
106 | }
107 | };
108 |
109 | public XStickyListHeadersView(Context context) {
110 | this(context, null);
111 | }
112 |
113 | public XStickyListHeadersView(Context context, AttributeSet attrs) {
114 | this(context, attrs, android.R.attr.listViewStyle);
115 | }
116 |
117 | public XStickyListHeadersView(Context context, AttributeSet attrs,
118 | int defStyle) {
119 | super(context, attrs, defStyle);
120 |
121 | super.setOnScrollListener(mOnScrollListener);
122 | // null out divider, dividers are handled by adapter so they look good
123 | // with headers
124 | super.setDivider(null);
125 | super.setDividerHeight(0);
126 | mViewConfig = ViewConfiguration.get(context);
127 | if (mClippingToPadding == null) {
128 | mClippingToPadding = true;
129 | }
130 |
131 | try {
132 | Field selectorRectField = AbsListView.class
133 | .getDeclaredField("mSelectorRect");
134 | selectorRectField.setAccessible(true);
135 | mSelectorRect = (Rect) selectorRectField.get(this);
136 |
137 | mSelectorPositionField = AbsListView.class
138 | .getDeclaredField("mSelectorPosition");
139 | mSelectorPositionField.setAccessible(true);
140 | } catch (NoSuchFieldException e) {
141 | e.printStackTrace();
142 | } catch (IllegalArgumentException e) {
143 | e.printStackTrace();
144 | } catch (IllegalAccessException e) {
145 | e.printStackTrace();
146 | }
147 | }
148 |
149 | @Override
150 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
151 | super.onLayout(changed, l, t, r, b);
152 | if (changed) {
153 | reset();
154 | scrollChanged(getFirstVisiblePosition());
155 | }
156 | }
157 |
158 | private void reset() {
159 | mHeader = null;
160 | mCurrentHeaderId = null;
161 | mHeaderBottomPosition = -1;
162 | }
163 |
164 | @Override
165 | public boolean performItemClick(View view, int position, long id) {
166 | if (view instanceof WrapperView) {
167 | view = ((WrapperView) view).mItem;
168 | }
169 | return super.performItemClick(view, position, id);
170 | }
171 |
172 | @Override
173 | public void setDivider(Drawable divider) {
174 | this.mDivider = divider;
175 | if (divider != null) {
176 | int dividerDrawableHeight = divider.getIntrinsicHeight();
177 | if (dividerDrawableHeight >= 0) {
178 | setDividerHeight(dividerDrawableHeight);
179 | }
180 | }
181 | if (mAdapter != null) {
182 | mAdapter.setDivider(divider);
183 | requestLayout();
184 | invalidate();
185 | }
186 | }
187 |
188 | @Override
189 | public void setDividerHeight(int height) {
190 | mDividerHeight = height;
191 | if (mAdapter != null) {
192 | mAdapter.setDividerHeight(height);
193 | requestLayout();
194 | invalidate();
195 | }
196 | }
197 |
198 | @Override
199 | public void setOnScrollListener(OnScrollListener l) {
200 | mOnScrollListenerDelegate = l;
201 | }
202 |
203 | public void setAreHeadersSticky(boolean areHeadersSticky) {
204 | if (this.mAreHeadersSticky != areHeadersSticky) {
205 | this.mAreHeadersSticky = areHeadersSticky;
206 | requestLayout();
207 | }
208 | }
209 |
210 | public boolean getAreHeadersSticky() {
211 | return mAreHeadersSticky;
212 | }
213 |
214 | @Override
215 | public void setAdapter(ListAdapter adapter) {
216 | if (this.isInEditMode()) {
217 | super.setAdapter(adapter);
218 | return;
219 | }
220 | if (adapter == null) {
221 | mAdapter = null;
222 | reset();
223 | super.setAdapter(null);
224 | return;
225 | }
226 | if (!(adapter instanceof StickyListHeadersAdapter)) {
227 | throw new IllegalArgumentException(
228 | "Adapter must implement StickyListHeadersAdapter");
229 | }
230 | mAdapter = wrapAdapter(adapter);
231 | reset();
232 | super.setAdapter(this.mAdapter);
233 | }
234 |
235 | private AdapterWrapper wrapAdapter(ListAdapter adapter) {
236 | AdapterWrapper wrapper;
237 | if (adapter instanceof SectionIndexer) {
238 | wrapper = new SectionIndexerAdapterWrapper(getContext(),
239 | (StickyListHeadersAdapter) adapter);
240 | } else {
241 | wrapper = new AdapterWrapper(getContext(),
242 | (StickyListHeadersAdapter) adapter);
243 | }
244 | wrapper.setDivider(mDivider);
245 | wrapper.setDividerHeight(mDividerHeight);
246 | wrapper.registerDataSetObserver(mDataSetChangedObserver);
247 | wrapper.setOnHeaderClickListener(mAdapterHeaderClickListener);
248 | return wrapper;
249 | }
250 |
251 | public StickyListHeadersAdapter getWrappedAdapter() {
252 | return mAdapter == null ? null : mAdapter.mDelegate;
253 | }
254 |
255 | @Override
256 | protected void dispatchDraw(Canvas canvas) {
257 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
258 | scrollChanged(getFirstVisiblePosition());
259 | }
260 | positionSelectorRect();
261 | if (!mAreHeadersSticky || mHeader == null) {
262 | super.dispatchDraw(canvas);
263 | return;
264 | }
265 |
266 | if (!mDrawingListUnderStickyHeader) {
267 | mClippingRect
268 | .set(0, mHeaderBottomPosition, getWidth(), getHeight());
269 | canvas.save();
270 | canvas.clipRect(mClippingRect);
271 | }
272 |
273 | super.dispatchDraw(canvas);
274 |
275 | if (!mDrawingListUnderStickyHeader) {
276 | canvas.restore();
277 | }
278 |
279 | drawStickyHeader(canvas);
280 | }
281 |
282 | private void positionSelectorRect() {
283 | if (!mSelectorRect.isEmpty()) {
284 | int selectorPosition = getSelectorPosition();
285 | if (selectorPosition >= 0) {
286 | int firstVisibleItem = fixedFirstVisibleItem(getFirstVisiblePosition());
287 | View v = getChildAt(selectorPosition - firstVisibleItem);
288 | if (v instanceof WrapperView) {
289 | WrapperView wrapper = ((WrapperView) v);
290 | mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop;
291 | }
292 | }
293 | }
294 | }
295 |
296 | private int getSelectorPosition() {
297 | if (mSelectorPositionField == null) { //not all supported andorid version have this variable
298 | for (int i = 0; i < getChildCount(); i++) {
299 | if (getChildAt(i).getBottom() == mSelectorRect.bottom) {
300 | return i + fixedFirstVisibleItem(getFirstVisiblePosition());
301 | }
302 | }
303 | } else {
304 | try {
305 | return mSelectorPositionField.getInt(this);
306 | } catch (IllegalArgumentException e) {
307 | e.printStackTrace();
308 | } catch (IllegalAccessException e) {
309 | e.printStackTrace();
310 | }
311 | }
312 | return -1;
313 | }
314 |
315 | private void drawStickyHeader(Canvas canvas) {
316 | int headerHeight = getHeaderHeight();
317 | int top = mHeaderBottomPosition - headerHeight;
318 | // clip the headers drawing region
319 | mClippingRect.left = getPaddingLeft();
320 | mClippingRect.right = getWidth() - getPaddingRight();
321 | mClippingRect.bottom = top + headerHeight;
322 | mClippingRect.top = mClippingToPadding ? getPaddingTop() : 0;
323 |
324 | canvas.save();
325 | canvas.clipRect(mClippingRect);
326 | canvas.translate(getPaddingLeft(), top);
327 | mHeader.draw(canvas);
328 | canvas.restore();
329 | }
330 |
331 | private void measureHeader() {
332 | int widthMeasureSpec = MeasureSpec.makeMeasureSpec(getWidth(),
333 | MeasureSpec.EXACTLY);
334 | int heightMeasureSpec = 0;
335 |
336 | ViewGroup.LayoutParams params = mHeader.getLayoutParams();
337 | if (params != null && params.height > 0) {
338 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(params.height,
339 | MeasureSpec.EXACTLY);
340 | } else {
341 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(0,
342 | MeasureSpec.UNSPECIFIED);
343 | }
344 | mHeader.measure(widthMeasureSpec, heightMeasureSpec);
345 | mHeader.layout(getLeft() + getPaddingLeft(), 0, getRight()
346 | - getPaddingRight(), mHeader.getMeasuredHeight());
347 | }
348 |
349 | private int getHeaderHeight() {
350 | return mHeader == null ? 0 : mHeader.getMeasuredHeight();
351 | }
352 |
353 | @Override
354 | public void setClipToPadding(boolean clipToPadding) {
355 | super.setClipToPadding(clipToPadding);
356 | mClippingToPadding = clipToPadding;
357 | }
358 |
359 | private void scrollChanged(int reportedFirstVisibleItem) {
360 |
361 | int adapterCount = mAdapter == null ? 0 : mAdapter.getCount();
362 | if (adapterCount == 0 || !mAreHeadersSticky) {
363 | return;
364 | }
365 |
366 | final int listViewHeaderCount = getHeaderViewsCount();
367 | final int firstVisibleItem = fixedFirstVisibleItem(reportedFirstVisibleItem)
368 | - listViewHeaderCount;
369 |
370 | if (firstVisibleItem < 0 || firstVisibleItem > adapterCount - 1) {
371 | reset();
372 | updateHeaderVisibilities();
373 | invalidate();
374 | return;
375 | }
376 |
377 | long newHeaderId = mAdapter.getHeaderId(firstVisibleItem);
378 | if (mCurrentHeaderId == null || mCurrentHeaderId != newHeaderId) {
379 | mHeaderPosition = firstVisibleItem;
380 | mCurrentHeaderId = newHeaderId;
381 | mHeader = mAdapter.getHeaderView(mHeaderPosition, mHeader, this);
382 | measureHeader();
383 | }
384 |
385 | int childCount = getChildCount();
386 | if (childCount != 0) {
387 | View viewToWatch = null;
388 | int watchingChildDistance = Integer.MAX_VALUE;
389 | boolean viewToWatchIsFooter = false;
390 |
391 | for (int i = 0; i < childCount; i++) {
392 | final View child = super.getChildAt(i);
393 | final boolean childIsFooter = mFooterViews != null
394 | && mFooterViews.contains(child);
395 |
396 | final int childDistance = child.getTop()
397 | - (mClippingToPadding ? getPaddingTop() : 0);
398 | if (childDistance < 0) {
399 | continue;
400 | }
401 |
402 | if (viewToWatch == null
403 | || (!viewToWatchIsFooter && !((WrapperView) viewToWatch)
404 | .hasHeader())
405 | || ((childIsFooter || ((WrapperView) child).hasHeader()) && childDistance < watchingChildDistance)) {
406 | viewToWatch = child;
407 | viewToWatchIsFooter = childIsFooter;
408 | watchingChildDistance = childDistance;
409 | }
410 | }
411 |
412 | final int headerHeight = getHeaderHeight();
413 | if (viewToWatch != null
414 | && (viewToWatchIsFooter || ((WrapperView) viewToWatch)
415 | .hasHeader())) {
416 | if (firstVisibleItem == listViewHeaderCount
417 | && super.getChildAt(0).getTop() > 0
418 | && !mClippingToPadding) {
419 | mHeaderBottomPosition = 0;
420 | } else {
421 | final int paddingTop = mClippingToPadding ? getPaddingTop()
422 | : 0;
423 | mHeaderBottomPosition = Math.min(viewToWatch.getTop(),
424 | headerHeight + paddingTop);
425 | mHeaderBottomPosition = mHeaderBottomPosition < paddingTop ? headerHeight
426 | + paddingTop
427 | : mHeaderBottomPosition;
428 | }
429 | } else {
430 | mHeaderBottomPosition = headerHeight
431 | + (mClippingToPadding ? getPaddingTop() : 0);
432 | }
433 | }
434 | updateHeaderVisibilities();
435 | invalidate();
436 | }
437 |
438 | @Override
439 | public void addFooterView(View v) {
440 | super.addFooterView(v);
441 | if (mFooterViews == null) {
442 | mFooterViews = new ArrayList();
443 | }
444 | mFooterViews.add(v);
445 | }
446 |
447 | @Override
448 | public boolean removeFooterView(View v) {
449 | if (super.removeFooterView(v)) {
450 | mFooterViews.remove(v);
451 | return true;
452 | }
453 | return false;
454 | }
455 |
456 | private void updateHeaderVisibilities() {
457 | int top = mClippingToPadding ? getPaddingTop() : 0;
458 | int childCount = getChildCount();
459 | for (int i = 0; i < childCount; i++) {
460 | View child = super.getChildAt(i);
461 | if (child instanceof WrapperView) {
462 | WrapperView wrapperViewChild = (WrapperView) child;
463 | if (wrapperViewChild.hasHeader()) {
464 | View childHeader = wrapperViewChild.mHeader;
465 | if (wrapperViewChild.getTop() < top) {
466 | childHeader.setVisibility(View.INVISIBLE);
467 | } else {
468 | childHeader.setVisibility(View.VISIBLE);
469 | }
470 | }
471 | }
472 | }
473 | }
474 |
475 | private int fixedFirstVisibleItem(int firstVisibleItem) {
476 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
477 | return firstVisibleItem;
478 | }
479 |
480 | for (int i = 0; i < getChildCount(); i++) {
481 | if (getChildAt(i).getBottom() >= 0) {
482 | firstVisibleItem += i;
483 | break;
484 | }
485 | }
486 |
487 | // work around to fix bug with firstVisibleItem being to high because
488 | // listview does not take clipToPadding=false into account
489 | if (!mClippingToPadding && getPaddingTop() > 0) {
490 | if (super.getChildAt(0).getTop() > 0) {
491 | if (firstVisibleItem > 0) {
492 | firstVisibleItem -= 1;
493 | }
494 | }
495 | }
496 | return firstVisibleItem;
497 | }
498 |
499 | @Override
500 | public void setSelectionFromTop(int position, int y) {
501 | if (mAreHeadersSticky) {
502 | y += getHeaderHeight();
503 | }
504 | super.setSelectionFromTop(position, y);
505 | }
506 |
507 | @SuppressLint("NewApi")
508 | @Override
509 | public void smoothScrollToPositionFromTop(int position, int offset) {
510 | if (mAreHeadersSticky) {
511 | offset += getHeaderHeight();
512 | }
513 | super.smoothScrollToPositionFromTop(position, offset);
514 | }
515 |
516 | @SuppressLint("NewApi")
517 | @Override
518 | public void smoothScrollToPositionFromTop(int position, int offset,
519 | int duration) {
520 | if (mAreHeadersSticky) {
521 | offset += getHeaderHeight();
522 | }
523 | super.smoothScrollToPositionFromTop(position, offset, duration);
524 | }
525 |
526 | public void setOnHeaderClickListener(
527 | OnHeaderClickListener onHeaderClickListener) {
528 | this.mOnHeaderClickListener = onHeaderClickListener;
529 | }
530 |
531 | public void setDrawingListUnderStickyHeader(
532 | boolean drawingListUnderStickyHeader) {
533 | mDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
534 | }
535 |
536 | public boolean isDrawingListUnderStickyHeader() {
537 | return mDrawingListUnderStickyHeader;
538 | }
539 |
540 | // TODO handle touches better, multitouch etc.
541 | @Override
542 | public boolean onTouchEvent(MotionEvent ev) {
543 | int action = ev.getAction();
544 | if (action == MotionEvent.ACTION_DOWN
545 | && ev.getY() <= mHeaderBottomPosition) {
546 | mHeaderDownY = ev.getY();
547 | mHeaderBeingPressed = true;
548 | mHeader.setPressed(true);
549 | mHeader.invalidate();
550 | invalidate(0, 0, getWidth(), mHeaderBottomPosition);
551 | return true;
552 | }
553 | if (mHeaderBeingPressed) {
554 | if (Math.abs(ev.getY() - mHeaderDownY) < mViewConfig
555 | .getScaledTouchSlop()) {
556 | if (action == MotionEvent.ACTION_UP
557 | || action == MotionEvent.ACTION_CANCEL) {
558 | mHeaderDownY = -1;
559 | mHeaderBeingPressed = false;
560 | mHeader.setPressed(false);
561 | mHeader.invalidate();
562 | invalidate(0, 0, getWidth(), mHeaderBottomPosition);
563 | if (mOnHeaderClickListener != null) {
564 | mOnHeaderClickListener.onHeaderClick(this, mHeader,
565 | mHeaderPosition, mCurrentHeaderId, true);
566 | }
567 | }
568 | return true;
569 | } else {
570 | mHeaderDownY = -1;
571 | mHeaderBeingPressed = false;
572 | mHeader.setPressed(false);
573 | mHeader.invalidate();
574 | invalidate(0, 0, getWidth(), mHeaderBottomPosition);
575 | }
576 | }
577 | return super.onTouchEvent(ev);
578 | }
579 |
580 | @Override
581 | public void onClick(View v) {
582 | // TODO Auto-generated method stub
583 |
584 | }
585 |
586 | }
587 |
--------------------------------------------------------------------------------
/src/com/youxiachai/onexlistview/util/StringMatcher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 youxiachai
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.youxiachai.onexlistview.util;
18 |
19 |
20 | public class StringMatcher {
21 |
22 | public static boolean match(String value, String keyword) {
23 | if (value == null || keyword == null)
24 | return false;
25 | if (keyword.length() > value.length())
26 | return false;
27 | int i = 0, j = 0;
28 | do {
29 | if (keyword.charAt(j) == value.charAt(i)) {
30 | i++;
31 | j++;
32 | } else if (j > 0)
33 | break;
34 | else
35 | i++;
36 | } while (i < value.length() && j < keyword.length());
37 |
38 | return (j == keyword.length()) ? true : false;
39 | }
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/IXListViewLoadMore.java:
--------------------------------------------------------------------------------
1 | package me.maxwin.view;
2 |
3 | public interface IXListViewLoadMore {
4 | public void onLoadMore();
5 | }
6 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/IXListViewRefreshListener.java:
--------------------------------------------------------------------------------
1 | package me.maxwin.view;
2 |
3 | public interface IXListViewRefreshListener {
4 | public void onRefresh();
5 | }
6 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/IXScrollListener.java:
--------------------------------------------------------------------------------
1 | package me.maxwin.view;
2 |
3 | import android.view.View;
4 |
5 | /**
6 | * @author youxiachai
7 | * @date 2013-5-3
8 | * you can listen ListView.OnScrollListener or this one. it will invoke
9 | * onXScrolling when header/footer scroll back.
10 | */
11 | public interface IXScrollListener {
12 | /**
13 | * @param view
14 | */
15 | public void onXScrolling(View view);
16 | }
17 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/XListView.java:
--------------------------------------------------------------------------------
1 | /**
2 | * @file XListView.java
3 | * @package me.maxwin.view
4 | * @create 2013/5/13
5 | * @author youxiachai
6 | * @description An ListView support (a) Pull down to refresh, (b) Pull up to load more.
7 | * Implement IXListViewListener, and see stopRefresh() / stopLoadMore().
8 | *
9 | * bugfix: 刷新,加载更多,重复加载
10 | * bugfix: item 数目不满一屏幕的时候不显示更多加载按钮
11 | * improvement: 正在刷新,或者加载更多的时候不应该可以继续拉
12 | */
13 | package me.maxwin.view;
14 |
15 | import com.youxiachai.onexlistview.R;
16 |
17 | import android.content.Context;
18 | import android.util.AttributeSet;
19 | import android.util.Log;
20 | import android.view.MotionEvent;
21 | import android.view.View;
22 | import android.view.ViewTreeObserver.OnGlobalLayoutListener;
23 | import android.view.animation.DecelerateInterpolator;
24 | import android.widget.AbsListView;
25 | import android.widget.AbsListView.OnScrollListener;
26 | import android.widget.ListAdapter;
27 | import android.widget.ListView;
28 | import android.widget.RelativeLayout;
29 | import android.widget.Scroller;
30 | import android.widget.TextView;
31 |
32 | public class XListView extends ListView implements OnScrollListener {
33 |
34 | protected float mLastY = -1; // save event y
35 | protected Scroller mScroller; // used for scroll back
36 | protected OnScrollListener mScrollListener; // user's scroll listener
37 |
38 | // the interface to trigger refresh and load more.
39 |
40 | protected IXListViewLoadMore mLoadMore;
41 | protected IXListViewRefreshListener mOnRefresh;
42 |
43 | // -- header view
44 | protected XListViewHeader mHeaderView;
45 | // header view content, use it to calculate the Header's height. And hide it
46 | // when disable pull refresh.
47 | protected RelativeLayout mHeaderViewContent;
48 | protected TextView mHeaderTimeView;
49 | protected int mHeaderViewHeight; // header view's height
50 | protected boolean mEnablePullRefresh = true;
51 | protected boolean mPullRefreshing = false; // is refreashing.
52 |
53 | // -- footer view
54 | protected XListViewFooter mFooterView;
55 | protected boolean mEnablePullLoad;
56 | protected boolean mPullLoading;
57 | protected boolean mIsFooterReady = false;
58 |
59 | // total list items, used to detect is at the bottom of listview.
60 | protected int mTotalItemCount;
61 |
62 | // for mScroller, scroll back from header or footer.
63 | protected int mScrollBack;
64 | protected final static int SCROLLBACK_HEADER = 0;
65 | protected final static int SCROLLBACK_FOOTER = 1;
66 |
67 | protected final static int SCROLL_DURATION = 400; // scroll back duration
68 | protected final static int PULL_LOAD_MORE_DELTA = 50; // when pull up >=
69 | // 50px
70 | // at bottom,
71 | // trigger
72 | // load more.
73 | protected final static float OFFSET_RADIO = 1.8f; // support iOS like pull
74 | // feature.
75 | //support perload
76 | private int preloadCount = 0;
77 |
78 | /**
79 | * @param context
80 | */
81 | public XListView(Context context) {
82 | super(context);
83 | initWithContext(context);
84 | }
85 |
86 | public XListView(Context context, AttributeSet attrs) {
87 | super(context, attrs);
88 | initWithContext(context);
89 | }
90 |
91 | public XListView(Context context, AttributeSet attrs, int defStyle) {
92 | super(context, attrs, defStyle);
93 | initWithContext(context);
94 | }
95 |
96 | protected void initWithContext(Context context) {
97 | mScroller = new Scroller(context, new DecelerateInterpolator());
98 | // XListView need the scroll event, and it will dispatch the event to
99 | // user's listener (as a proxy).
100 | super.setOnScrollListener(this);
101 |
102 | // init header view
103 | mHeaderView = new XListViewHeader(context);
104 | mHeaderViewContent = (RelativeLayout) mHeaderView
105 | .findViewById(R.id.xlistview_header_content);
106 | mHeaderTimeView = (TextView) mHeaderView
107 | .findViewById(R.id.xlistview_header_time);
108 | addHeaderView(mHeaderView);
109 |
110 | // init footer view
111 | mFooterView = new XListViewFooter(context);
112 |
113 | // init header height
114 | mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(
115 | new OnGlobalLayoutListener() {
116 | @Override
117 | public void onGlobalLayout() {
118 | mHeaderViewHeight = mHeaderViewContent.getHeight();
119 | getViewTreeObserver()
120 | .removeGlobalOnLayoutListener(this);
121 | }
122 | });
123 | // 补充修改
124 | disablePullLoad();
125 | disablePullRefreash();
126 | }
127 |
128 | @Override
129 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
130 | super.onLayout(changed, l, t, r, b);
131 | // make sure XListViewFooter is the last footer view, and only add once.
132 | if (mIsFooterReady == false) {
133 | // if not inflate screen ,footerview not add
134 | if(getAdapter() != null){
135 | if (getLastVisiblePosition() != (getAdapter().getCount() - 1)) {
136 | mIsFooterReady = true;
137 | addFooterView(mFooterView);
138 | }
139 | }
140 |
141 | }
142 | }
143 |
144 | @Override
145 | public void setAdapter(ListAdapter adapter) {
146 | super.setAdapter(adapter);
147 |
148 | }
149 |
150 | public void setPreLoadCount(int count){
151 | this.preloadCount = count;
152 | }
153 |
154 | /**
155 | * enable or disable pull down refresh feature.
156 | *
157 | * @param enable
158 | */
159 | public void setPullRefreshEnable(IXListViewRefreshListener refreshListener) {
160 | mEnablePullRefresh = true;
161 | mHeaderViewContent.setVisibility(View.VISIBLE);
162 | this.mOnRefresh = refreshListener;
163 |
164 | }
165 |
166 | public void disablePullRefreash() {
167 | mEnablePullRefresh = false;
168 | // disable, hide the content
169 | mHeaderViewContent.setVisibility(View.INVISIBLE);
170 | }
171 |
172 | /**
173 | * enable or disable pull up load more feature.
174 | *
175 | * @param enable
176 | */
177 | public void setPullLoadEnable(IXListViewLoadMore loadMoreListener) {
178 | mEnablePullLoad = true;
179 | this.mLoadMore = loadMoreListener;
180 | mPullLoading = false;
181 | mFooterView.show();
182 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
183 | // both "pull up" and "click" will invoke load more.
184 | mFooterView.setOnClickListener(new OnClickListener() {
185 | @Override
186 | public void onClick(View v) {
187 | // startLoadMore();
188 | mPullLoading = true;
189 | mFooterView.setState(XListViewFooter.STATE_LOADING);
190 | if (mLoadMore != null) {
191 | mLoadMore.onLoadMore();
192 | }
193 | }
194 | });
195 |
196 | }
197 |
198 | public void disablePullLoad() {
199 | mEnablePullLoad = false;
200 | mFooterView.hide();
201 | mFooterView.setOnClickListener(null);
202 | }
203 |
204 | public void hidePullLoad () {
205 | mEnablePullLoad = false;
206 | mFooterView.hide();
207 | }
208 |
209 | public void showPullLoad () {
210 | mEnablePullLoad = true;
211 | mFooterView.show();
212 | }
213 |
214 | /**
215 | * stop refresh, reset header view.
216 | */
217 | public void stopRefresh(String time) {
218 | if (mPullRefreshing == true) {
219 | mPullRefreshing = false;
220 | mHeaderTimeView.setText(time);
221 | resetHeaderHeight();
222 | }
223 | }
224 |
225 | /**
226 | * stop load more, reset footer view.
227 | */
228 | public void stopLoadMore() {
229 | if (mPullLoading == true) {
230 | mPullLoading = false;
231 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
232 | }
233 | }
234 |
235 | /**
236 | * set last refresh time
237 | *
238 | * @param time
239 | */
240 | public void setRefreshTime(String time) {
241 |
242 | }
243 |
244 | protected void invokeOnScrolling() {
245 | if (mScrollListener instanceof IXScrollListener) {
246 | IXScrollListener l = (IXScrollListener) mScrollListener;
247 | l.onXScrolling(this);
248 | }
249 | }
250 |
251 | protected void updateHeaderHeight(float delta) {
252 | mHeaderView.setVisiableHeight((int) delta
253 | + mHeaderView.getVisiableHeight());
254 | if (mEnablePullRefresh && !mPullRefreshing) { // 未处于刷新状态,更新箭头
255 | if (mHeaderView.getVisiableHeight() > mHeaderViewHeight) {
256 | mHeaderView.setState(XListViewHeader.STATE_READY);
257 | } else {
258 | mHeaderView.setState(XListViewHeader.STATE_NORMAL);
259 | }
260 | }
261 | setSelection(0); // scroll to top each time
262 |
263 |
264 | }
265 |
266 | /**
267 | * reset header view's height.
268 | */
269 | protected void resetHeaderHeight() {
270 | int height = mHeaderView.getVisiableHeight();
271 | if (height == 0) // not visible.
272 | return;
273 | // refreshing and header isn't shown fully. do nothing.
274 | if (mPullRefreshing && height <= mHeaderViewHeight) {
275 | return;
276 | }
277 | int finalHeight = 0; // default: scroll back to dismiss header.
278 | // is refreshing, just scroll back to show all the header.
279 | if (mPullRefreshing && height > mHeaderViewHeight) {
280 | finalHeight = mHeaderViewHeight;
281 | }
282 | Log.d("xlistview", "resetHeaderHeight-->" + (finalHeight - height));
283 | mScrollBack = SCROLLBACK_HEADER;
284 | mScroller.startScroll(0, height, 0, finalHeight - height,
285 | SCROLL_DURATION);
286 | // trigger computeScroll
287 | invalidate();
288 | }
289 |
290 | protected void resetHeaderHeight(int disy) {
291 | int height = mHeaderView.getVisiableHeight();
292 | if (height == 0) // not visible.
293 | return;
294 | // refreshing and header isn't shown fully. do nothing.
295 | if (mPullRefreshing && height <= mHeaderViewHeight) {
296 | return;
297 | }
298 | int finalHeight = 0; // default: scroll back to dismiss header.
299 | // is refreshing, just scroll back to show all the header.
300 | if (mPullRefreshing && height > mHeaderViewHeight) {
301 | finalHeight = mHeaderViewHeight;
302 | }
303 | mScrollBack = SCROLLBACK_HEADER;
304 | Log.d("xlistview", "resetHeaderHeight-->" + (finalHeight - height));
305 | mScroller.startScroll(0, height, 0, finalHeight - height + 100,
306 | SCROLL_DURATION);
307 | // trigger computeScroll
308 | invalidate();
309 | }
310 |
311 | protected void updateFooterHeight(float delta) {
312 | int height = mFooterView.getBottomMargin() + (int) delta;
313 | if (mEnablePullLoad && !mPullLoading) {
314 | if (height > PULL_LOAD_MORE_DELTA) { // height enough to invoke load
315 | // more.
316 | mFooterView.setState(XListViewFooter.STATE_READY);
317 | } else {
318 | mFooterView.setState(XListViewFooter.STATE_NORMAL);
319 | }
320 | }
321 | mFooterView.setBottomMargin(height);
322 |
323 | // setSelection(mTotalItemCount - 1); // scroll to bottom
324 | }
325 |
326 | protected void resetFooterHeight() {
327 | int bottomMargin = mFooterView.getBottomMargin();
328 | if (bottomMargin > 0) {
329 | mScrollBack = SCROLLBACK_FOOTER;
330 | mScroller.startScroll(0, bottomMargin, 0, -bottomMargin,
331 | SCROLL_DURATION);
332 | invalidate();
333 | }
334 | }
335 |
336 | protected void startLoadMore() {
337 | if (mEnablePullLoad
338 | && mFooterView.getBottomMargin() > PULL_LOAD_MORE_DELTA
339 | && !mPullLoading) {
340 | mPullLoading = true;
341 | mFooterView.setState(XListViewFooter.STATE_LOADING);
342 | if (mLoadMore != null) {
343 | mLoadMore.onLoadMore();
344 | }
345 | }
346 | }
347 |
348 |
349 | @Override
350 | public boolean onTouchEvent(MotionEvent ev) {
351 |
352 | if (mLastY == -1) {
353 | mLastY = ev.getRawY();
354 | }
355 |
356 | switch (ev.getAction()) {
357 | case MotionEvent.ACTION_DOWN:
358 | mLastY = ev.getRawY();
359 | break;
360 | case MotionEvent.ACTION_MOVE:
361 | final float deltaY = ev.getRawY() - mLastY;
362 | mLastY = ev.getRawY();
363 | Log.d("xlistview", "onTouchEvent "+" LastVisiblePosition " + getLastVisiblePosition() + " mTotalItemCount " + mTotalItemCount);
364 |
365 | if (getFirstVisiblePosition() == 0
366 | && (mHeaderView.getVisiableHeight() > 0 || deltaY > 0)
367 | && !mPullRefreshing) {
368 | // the first item is showing, header has shown or pull down.
369 | if(mEnablePullRefresh){
370 | updateHeaderHeight(deltaY / OFFSET_RADIO);
371 | invokeOnScrolling();
372 | }
373 |
374 | } else if (getLastVisiblePosition() == mTotalItemCount - 1
375 | && (mFooterView.getBottomMargin() > 0 || deltaY < 0)
376 | && !mPullLoading) {
377 | // last item, already pulled up or want to pull up.
378 | if(mEnablePullLoad){
379 | updateFooterHeight(-deltaY / OFFSET_RADIO);
380 | }
381 | } else if(preloadCount != 0){
382 |
383 | if(mEnablePullLoad && !mPullLoading && getLastVisiblePosition() >= mTotalItemCount - preloadCount){
384 | mPullLoading = true;
385 | mFooterView.setState(XListViewFooter.STATE_LOADING);
386 | if (mLoadMore != null) {
387 | mLoadMore.onLoadMore();
388 | }
389 | }
390 |
391 | }
392 | break;
393 | default:
394 | mLastY = -1; // reset
395 | if (getFirstVisiblePosition() == 0) {
396 | // invoke refresh
397 | startOnRefresh();
398 | resetHeaderHeight();
399 | } else if (getLastVisiblePosition() == mTotalItemCount - 1) {
400 | // invoke load more.
401 | startLoadMore();
402 | resetFooterHeight();
403 | }
404 | break;
405 | }
406 |
407 | return super.onTouchEvent(ev);
408 | }
409 |
410 | protected void startOnRefresh() {
411 | if (mEnablePullRefresh
412 | && mHeaderView.getVisiableHeight() > mHeaderViewHeight
413 | && !mPullRefreshing) {
414 | mPullRefreshing = true;
415 | mHeaderView.setState(XListViewHeader.STATE_REFRESHING);
416 | if (mOnRefresh != null) {
417 | mOnRefresh.onRefresh();
418 | }
419 | }
420 | }
421 |
422 | @Override
423 | public void computeScroll() {
424 | if (mScroller.computeScrollOffset()) {
425 | if (mScrollBack == SCROLLBACK_HEADER) {
426 | mHeaderView.setVisiableHeight(mScroller.getCurrY());
427 | } else {
428 | mFooterView.setBottomMargin(mScroller.getCurrY());
429 | }
430 | postInvalidate();
431 | invokeOnScrolling();
432 | }
433 | super.computeScroll();
434 | }
435 |
436 | @Override
437 | public void setOnScrollListener(OnScrollListener l) {
438 | mScrollListener = l;
439 | }
440 |
441 | @Override
442 | public void onScrollStateChanged(AbsListView view, int scrollState) {
443 | if (mScrollListener != null) {
444 | mScrollListener.onScrollStateChanged(view, scrollState);
445 | }
446 | }
447 |
448 | @Override
449 | public void onScroll(AbsListView view, int firstVisibleItem,
450 | int visibleItemCount, int totalItemCount) {
451 | // send to user's listener
452 | Log.d("xlistview", "onScroll firstVisibleItem " + firstVisibleItem + " visibleItemCount " + visibleItemCount + " totalItemCount " + totalItemCount);
453 | mTotalItemCount = totalItemCount;
454 | if (mScrollListener != null) {
455 | mScrollListener.onScroll(view, firstVisibleItem, visibleItemCount,
456 | totalItemCount);
457 | }
458 | }
459 |
460 | }
461 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/XListViewFooter.java:
--------------------------------------------------------------------------------
1 | /**
2 | * @file XFooterView.java
3 | * @create Mar 31, 2012 9:33:43 PM
4 | * @author Maxwin
5 | * @description XListView's footer
6 | */
7 | package me.maxwin.view;
8 |
9 | import com.youxiachai.onexlistview.R;
10 | import android.content.Context;
11 | import android.util.AttributeSet;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.widget.LinearLayout;
15 | import android.widget.TextView;
16 |
17 | public class XListViewFooter extends LinearLayout {
18 | public final static int STATE_NORMAL = 0;
19 | public final static int STATE_READY = 1;
20 | public final static int STATE_LOADING = 2;
21 |
22 | private Context mContext;
23 |
24 | private View mContentView;
25 | private View mProgressBar;
26 | private TextView mHintView;
27 |
28 | public XListViewFooter(Context context) {
29 | super(context);
30 | initView(context);
31 | }
32 |
33 | public XListViewFooter(Context context, AttributeSet attrs) {
34 | super(context, attrs);
35 | initView(context);
36 | }
37 |
38 |
39 | public void setState(int state) {
40 | mHintView.setVisibility(View.INVISIBLE);
41 | mProgressBar.setVisibility(View.INVISIBLE);
42 | mHintView.setVisibility(View.INVISIBLE);
43 | if (state == STATE_READY) {
44 | mHintView.setVisibility(View.VISIBLE);
45 | mHintView.setText(R.string.xlistview_footer_hint_ready);
46 | } else if (state == STATE_LOADING) {
47 | mProgressBar.setVisibility(View.VISIBLE);
48 | } else {
49 | mHintView.setVisibility(View.VISIBLE);
50 | mHintView.setText(R.string.xlistview_footer_hint_normal);
51 | }
52 | }
53 |
54 | public void setBottomMargin(int height) {
55 | if (height < 0) return ;
56 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)mContentView.getLayoutParams();
57 | lp.bottomMargin = height;
58 | mContentView.setLayoutParams(lp);
59 | }
60 |
61 | public int getBottomMargin() {
62 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)mContentView.getLayoutParams();
63 | return lp.bottomMargin;
64 | }
65 |
66 |
67 | /**
68 | * normal status
69 | */
70 | public void normal() {
71 | mHintView.setVisibility(View.VISIBLE);
72 | mProgressBar.setVisibility(View.GONE);
73 | }
74 |
75 |
76 | /**
77 | * loading status
78 | */
79 | public void loading() {
80 | mHintView.setVisibility(View.GONE);
81 | mProgressBar.setVisibility(View.VISIBLE);
82 | }
83 |
84 | /**
85 | * hide footer when disable pull load more
86 | */
87 | public void hide() {
88 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)mContentView.getLayoutParams();
89 | lp.height = 0;
90 | mContentView.setLayoutParams(lp);
91 | }
92 |
93 | /**
94 | * show footer
95 | */
96 | public void show() {
97 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)mContentView.getLayoutParams();
98 | lp.height = LayoutParams.WRAP_CONTENT;
99 | mContentView.setLayoutParams(lp);
100 | }
101 |
102 | private void initView(Context context) {
103 | mContext = context;
104 | LinearLayout moreView = (LinearLayout)LayoutInflater.from(mContext).inflate(R.layout.xlistview_footer, null);
105 | addView(moreView);
106 | moreView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
107 |
108 | mContentView = moreView.findViewById(R.id.xlistview_footer_content);
109 | mProgressBar = moreView.findViewById(R.id.xlistview_footer_progressbar);
110 | mHintView = (TextView)moreView.findViewById(R.id.xlistview_footer_hint_textview);
111 | }
112 |
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/me/maxwin/view/XListViewHeader.java:
--------------------------------------------------------------------------------
1 | /**
2 | * @file XListViewHeader.java
3 | * @create Apr 18, 2012 5:22:27 PM
4 | * @author Maxwin
5 | * @description XListView's header
6 | */
7 | package me.maxwin.view;
8 |
9 | import com.youxiachai.onexlistview.R;
10 | import android.content.Context;
11 | import android.util.AttributeSet;
12 | import android.view.Gravity;
13 | import android.view.LayoutInflater;
14 | import android.view.View;
15 | import android.view.animation.Animation;
16 | import android.view.animation.RotateAnimation;
17 | import android.widget.ImageView;
18 | import android.widget.LinearLayout;
19 | import android.widget.ProgressBar;
20 | import android.widget.TextView;
21 |
22 | public class XListViewHeader extends LinearLayout {
23 | private LinearLayout mContainer;
24 | private ImageView mArrowImageView;
25 | private ProgressBar mProgressBar;
26 | private TextView mHintTextView;
27 | private int mState = STATE_NORMAL;
28 |
29 | private Animation mRotateUpAnim;
30 | private Animation mRotateDownAnim;
31 |
32 | private final int ROTATE_ANIM_DURATION = 180;
33 |
34 | public final static int STATE_NORMAL = 0;
35 | public final static int STATE_READY = 1;
36 | public final static int STATE_REFRESHING = 2;
37 |
38 | public XListViewHeader(Context context) {
39 | super(context);
40 | initView(context);
41 | }
42 |
43 | /**
44 | * @param context
45 | * @param attrs
46 | */
47 | public XListViewHeader(Context context, AttributeSet attrs) {
48 | super(context, attrs);
49 | initView(context);
50 | }
51 |
52 | private void initView(Context context) {
53 | // 初始情况,设置下拉刷新view高度为0
54 | LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
55 | LayoutParams.FILL_PARENT, 0);
56 | mContainer = (LinearLayout) LayoutInflater.from(context).inflate(
57 | R.layout.xlistview_header, null);
58 | addView(mContainer, lp);
59 | setGravity(Gravity.BOTTOM);
60 |
61 | mArrowImageView = (ImageView)findViewById(R.id.xlistview_header_arrow);
62 | mHintTextView = (TextView)findViewById(R.id.xlistview_header_hint_textview);
63 | mProgressBar = (ProgressBar)findViewById(R.id.xlistview_header_progressbar);
64 |
65 | mRotateUpAnim = new RotateAnimation(0.0f, -180.0f,
66 | Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
67 | 0.5f);
68 | mRotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
69 | mRotateUpAnim.setFillAfter(true);
70 | mRotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
71 | Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
72 | 0.5f);
73 | mRotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
74 | mRotateDownAnim.setFillAfter(true);
75 | }
76 |
77 | public void setState(int state) {
78 | if (state == mState) return ;
79 |
80 | if (state == STATE_REFRESHING) { // 显示进度
81 | mArrowImageView.clearAnimation();
82 | mArrowImageView.setVisibility(View.INVISIBLE);
83 | mProgressBar.setVisibility(View.VISIBLE);
84 | } else { // 显示箭头图片
85 | mArrowImageView.setVisibility(View.VISIBLE);
86 | mProgressBar.setVisibility(View.INVISIBLE);
87 | }
88 |
89 | switch(state){
90 | case STATE_NORMAL:
91 | if (mState == STATE_READY) {
92 | mArrowImageView.startAnimation(mRotateDownAnim);
93 | }
94 | if (mState == STATE_REFRESHING) {
95 | mArrowImageView.clearAnimation();
96 | }
97 | mHintTextView.setText(R.string.xlistview_header_hint_normal);
98 | break;
99 | case STATE_READY:
100 | if (mState != STATE_READY) {
101 | mArrowImageView.clearAnimation();
102 | mArrowImageView.startAnimation(mRotateUpAnim);
103 | mHintTextView.setText(R.string.xlistview_header_hint_ready);
104 | }
105 | break;
106 | case STATE_REFRESHING:
107 | mHintTextView.setText(R.string.xlistview_header_hint_loading);
108 | break;
109 | default:
110 | }
111 |
112 | mState = state;
113 | }
114 |
115 | public void setVisiableHeight(int height) {
116 | if (height < 0)
117 | height = 0;
118 | LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mContainer
119 | .getLayoutParams();
120 | lp.height = height;
121 | mContainer.setLayoutParams(lp);
122 | }
123 |
124 | public int getVisiableHeight() {
125 | return mContainer.getHeight();
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------