|
1 /* |
|
2 * Copyright (C) 2013 Lucas Rocha |
|
3 * |
|
4 * This code is based on bits and pieces of Android's AbsListView, |
|
5 * Listview, and StaggeredGridView. |
|
6 * |
|
7 * Copyright (C) 2012 The Android Open Source Project |
|
8 * |
|
9 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
10 * you may not use this file except in compliance with the License. |
|
11 * You may obtain a copy of the License at |
|
12 * |
|
13 * http://www.apache.org/licenses/LICENSE-2.0 |
|
14 * |
|
15 * Unless required by applicable law or agreed to in writing, software |
|
16 * distributed under the License is distributed on an "AS IS" BASIS, |
|
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
18 * See the License for the specific language governing permissions and |
|
19 * limitations under the License. |
|
20 */ |
|
21 |
|
22 package org.mozilla.gecko.widget; |
|
23 |
|
24 import org.mozilla.gecko.R; |
|
25 |
|
26 import android.annotation.TargetApi; |
|
27 import android.content.Context; |
|
28 import android.content.res.TypedArray; |
|
29 import android.database.DataSetObserver; |
|
30 import android.graphics.Canvas; |
|
31 import android.graphics.Rect; |
|
32 import android.graphics.drawable.Drawable; |
|
33 import android.graphics.drawable.TransitionDrawable; |
|
34 import android.os.Build; |
|
35 import android.os.Bundle; |
|
36 import android.os.Parcel; |
|
37 import android.os.Parcelable; |
|
38 import android.os.SystemClock; |
|
39 import android.support.v4.util.LongSparseArray; |
|
40 import android.support.v4.util.SparseArrayCompat; |
|
41 import android.support.v4.view.AccessibilityDelegateCompat; |
|
42 import android.support.v4.view.KeyEventCompat; |
|
43 import android.support.v4.view.MotionEventCompat; |
|
44 import android.support.v4.view.VelocityTrackerCompat; |
|
45 import android.support.v4.view.ViewCompat; |
|
46 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; |
|
47 import android.support.v4.widget.EdgeEffectCompat; |
|
48 import android.util.AttributeSet; |
|
49 import android.util.Log; |
|
50 import android.util.SparseBooleanArray; |
|
51 import android.view.ContextMenu.ContextMenuInfo; |
|
52 import android.view.FocusFinder; |
|
53 import android.view.HapticFeedbackConstants; |
|
54 import android.view.KeyEvent; |
|
55 import android.view.MotionEvent; |
|
56 import android.view.SoundEffectConstants; |
|
57 import android.view.VelocityTracker; |
|
58 import android.view.View; |
|
59 import android.view.ViewConfiguration; |
|
60 import android.view.ViewGroup; |
|
61 import android.view.ViewParent; |
|
62 import android.view.ViewTreeObserver; |
|
63 import android.view.accessibility.AccessibilityEvent; |
|
64 import android.view.accessibility.AccessibilityNodeInfo; |
|
65 import android.widget.AdapterView; |
|
66 import android.widget.Checkable; |
|
67 import android.widget.ListAdapter; |
|
68 import android.widget.Scroller; |
|
69 |
|
70 import java.util.ArrayList; |
|
71 import java.util.List; |
|
72 |
|
73 /* |
|
74 * Implementation Notes: |
|
75 * |
|
76 * Some terminology: |
|
77 * |
|
78 * index - index of the items that are currently visible |
|
79 * position - index of the items in the cursor |
|
80 * |
|
81 * Given the bi-directional nature of this view, the source code |
|
82 * usually names variables with 'start' to mean 'top' or 'left'; and |
|
83 * 'end' to mean 'bottom' or 'right', depending on the current |
|
84 * orientation of the widget. |
|
85 */ |
|
86 |
|
87 /** |
|
88 * A view that shows items in a vertical or horizontal scrolling list. |
|
89 * The items come from the {@link ListAdapter} associated with this view. |
|
90 */ |
|
91 public class TwoWayView extends AdapterView<ListAdapter> implements |
|
92 ViewTreeObserver.OnTouchModeChangeListener { |
|
93 private static final String LOGTAG = "TwoWayView"; |
|
94 |
|
95 private static final int NO_POSITION = -1; |
|
96 private static final int INVALID_POINTER = -1; |
|
97 |
|
98 public static final int[] STATE_NOTHING = new int[] { 0 }; |
|
99 |
|
100 private static final int TOUCH_MODE_REST = -1; |
|
101 private static final int TOUCH_MODE_DOWN = 0; |
|
102 private static final int TOUCH_MODE_TAP = 1; |
|
103 private static final int TOUCH_MODE_DONE_WAITING = 2; |
|
104 private static final int TOUCH_MODE_DRAGGING = 3; |
|
105 private static final int TOUCH_MODE_FLINGING = 4; |
|
106 private static final int TOUCH_MODE_OVERSCROLL = 5; |
|
107 |
|
108 private static final int TOUCH_MODE_UNKNOWN = -1; |
|
109 private static final int TOUCH_MODE_ON = 0; |
|
110 private static final int TOUCH_MODE_OFF = 1; |
|
111 |
|
112 private static final int LAYOUT_NORMAL = 0; |
|
113 private static final int LAYOUT_FORCE_TOP = 1; |
|
114 private static final int LAYOUT_SET_SELECTION = 2; |
|
115 private static final int LAYOUT_FORCE_BOTTOM = 3; |
|
116 private static final int LAYOUT_SPECIFIC = 4; |
|
117 private static final int LAYOUT_SYNC = 5; |
|
118 private static final int LAYOUT_MOVE_SELECTION = 6; |
|
119 |
|
120 private static final int SYNC_SELECTED_POSITION = 0; |
|
121 private static final int SYNC_FIRST_POSITION = 1; |
|
122 |
|
123 private static final int SYNC_MAX_DURATION_MILLIS = 100; |
|
124 |
|
125 private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; |
|
126 |
|
127 private static final float MAX_SCROLL_FACTOR = 0.33f; |
|
128 |
|
129 private static final int MIN_SCROLL_PREVIEW_PIXELS = 10; |
|
130 |
|
131 public static enum ChoiceMode { |
|
132 NONE, |
|
133 SINGLE, |
|
134 MULTIPLE |
|
135 } |
|
136 |
|
137 public static enum Orientation { |
|
138 HORIZONTAL, |
|
139 VERTICAL; |
|
140 }; |
|
141 |
|
142 private ListAdapter mAdapter; |
|
143 |
|
144 private boolean mIsVertical; |
|
145 |
|
146 private int mItemMargin; |
|
147 |
|
148 private boolean mInLayout; |
|
149 private boolean mBlockLayoutRequests; |
|
150 |
|
151 private boolean mIsAttached; |
|
152 |
|
153 private final RecycleBin mRecycler; |
|
154 private AdapterDataSetObserver mDataSetObserver; |
|
155 |
|
156 private boolean mItemsCanFocus; |
|
157 |
|
158 final boolean[] mIsScrap = new boolean[1]; |
|
159 |
|
160 private boolean mDataChanged; |
|
161 private int mItemCount; |
|
162 private int mOldItemCount; |
|
163 private boolean mHasStableIds; |
|
164 private boolean mAreAllItemsSelectable; |
|
165 |
|
166 private int mFirstPosition; |
|
167 private int mSpecificStart; |
|
168 |
|
169 private SavedState mPendingSync; |
|
170 |
|
171 private final int mTouchSlop; |
|
172 private final int mMaximumVelocity; |
|
173 private final int mFlingVelocity; |
|
174 private float mLastTouchPos; |
|
175 private float mTouchRemainderPos; |
|
176 private int mActivePointerId; |
|
177 |
|
178 private final Rect mTempRect; |
|
179 |
|
180 private final ArrowScrollFocusResult mArrowScrollFocusResult; |
|
181 |
|
182 private Rect mTouchFrame; |
|
183 private int mMotionPosition; |
|
184 private CheckForTap mPendingCheckForTap; |
|
185 private CheckForLongPress mPendingCheckForLongPress; |
|
186 private CheckForKeyLongPress mPendingCheckForKeyLongPress; |
|
187 private PerformClick mPerformClick; |
|
188 private Runnable mTouchModeReset; |
|
189 private int mResurrectToPosition; |
|
190 |
|
191 private boolean mIsChildViewEnabled; |
|
192 |
|
193 private boolean mDrawSelectorOnTop; |
|
194 private Drawable mSelector; |
|
195 private int mSelectorPosition; |
|
196 private final Rect mSelectorRect; |
|
197 |
|
198 private int mOverScroll; |
|
199 private final int mOverscrollDistance; |
|
200 |
|
201 private boolean mDesiredFocusableState; |
|
202 private boolean mDesiredFocusableInTouchModeState; |
|
203 |
|
204 private SelectionNotifier mSelectionNotifier; |
|
205 |
|
206 private boolean mNeedSync; |
|
207 private int mSyncMode; |
|
208 private int mSyncPosition; |
|
209 private long mSyncRowId; |
|
210 private long mSyncHeight; |
|
211 private int mSelectedStart; |
|
212 |
|
213 private int mNextSelectedPosition; |
|
214 private long mNextSelectedRowId; |
|
215 private int mSelectedPosition; |
|
216 private long mSelectedRowId; |
|
217 private int mOldSelectedPosition; |
|
218 private long mOldSelectedRowId; |
|
219 |
|
220 private ChoiceMode mChoiceMode; |
|
221 private int mCheckedItemCount; |
|
222 private SparseBooleanArray mCheckStates; |
|
223 LongSparseArray<Integer> mCheckedIdStates; |
|
224 |
|
225 private ContextMenuInfo mContextMenuInfo; |
|
226 |
|
227 private int mLayoutMode; |
|
228 private int mTouchMode; |
|
229 private int mLastTouchMode; |
|
230 private VelocityTracker mVelocityTracker; |
|
231 private final Scroller mScroller; |
|
232 |
|
233 private EdgeEffectCompat mStartEdge; |
|
234 private EdgeEffectCompat mEndEdge; |
|
235 |
|
236 private OnScrollListener mOnScrollListener; |
|
237 private int mLastScrollState; |
|
238 |
|
239 private View mEmptyView; |
|
240 |
|
241 private ListItemAccessibilityDelegate mAccessibilityDelegate; |
|
242 |
|
243 private int mLastAccessibilityScrollEventFromIndex; |
|
244 private int mLastAccessibilityScrollEventToIndex; |
|
245 |
|
246 public interface OnScrollListener { |
|
247 |
|
248 /** |
|
249 * The view is not scrolling. Note navigating the list using the trackball counts as |
|
250 * being in the idle state since these transitions are not animated. |
|
251 */ |
|
252 public static int SCROLL_STATE_IDLE = 0; |
|
253 |
|
254 /** |
|
255 * The user is scrolling using touch, and their finger is still on the screen |
|
256 */ |
|
257 public static int SCROLL_STATE_TOUCH_SCROLL = 1; |
|
258 |
|
259 /** |
|
260 * The user had previously been scrolling using touch and had performed a fling. The |
|
261 * animation is now coasting to a stop |
|
262 */ |
|
263 public static int SCROLL_STATE_FLING = 2; |
|
264 |
|
265 /** |
|
266 * Callback method to be invoked while the list view or grid view is being scrolled. If the |
|
267 * view is being scrolled, this method will be called before the next frame of the scroll is |
|
268 * rendered. In particular, it will be called before any calls to |
|
269 * {@link Adapter#getView(int, View, ViewGroup)}. |
|
270 * |
|
271 * @param view The view whose scroll state is being reported |
|
272 * |
|
273 * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, |
|
274 * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. |
|
275 */ |
|
276 public void onScrollStateChanged(TwoWayView view, int scrollState); |
|
277 |
|
278 /** |
|
279 * Callback method to be invoked when the list or grid has been scrolled. This will be |
|
280 * called after the scroll has completed |
|
281 * @param view The view whose scroll state is being reported |
|
282 * @param firstVisibleItem the index of the first visible cell (ignore if |
|
283 * visibleItemCount == 0) |
|
284 * @param visibleItemCount the number of visible cells |
|
285 * @param totalItemCount the number of items in the list adaptor |
|
286 */ |
|
287 public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount, |
|
288 int totalItemCount); |
|
289 } |
|
290 |
|
291 /** |
|
292 * A RecyclerListener is used to receive a notification whenever a View is placed |
|
293 * inside the RecycleBin's scrap heap. This listener is used to free resources |
|
294 * associated to Views placed in the RecycleBin. |
|
295 * |
|
296 * @see TwoWayView.RecycleBin |
|
297 * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener) |
|
298 */ |
|
299 public static interface RecyclerListener { |
|
300 /** |
|
301 * Indicates that the specified View was moved into the recycler's scrap heap. |
|
302 * The view is not displayed on screen any more and any expensive resource |
|
303 * associated with the view should be discarded. |
|
304 * |
|
305 * @param view |
|
306 */ |
|
307 void onMovedToScrapHeap(View view); |
|
308 } |
|
309 |
|
310 public TwoWayView(Context context) { |
|
311 this(context, null); |
|
312 } |
|
313 |
|
314 public TwoWayView(Context context, AttributeSet attrs) { |
|
315 this(context, attrs, 0); |
|
316 } |
|
317 |
|
318 public TwoWayView(Context context, AttributeSet attrs, int defStyle) { |
|
319 super(context, attrs, defStyle); |
|
320 |
|
321 mNeedSync = false; |
|
322 mVelocityTracker = null; |
|
323 |
|
324 mLayoutMode = LAYOUT_NORMAL; |
|
325 mTouchMode = TOUCH_MODE_REST; |
|
326 mLastTouchMode = TOUCH_MODE_UNKNOWN; |
|
327 |
|
328 mIsAttached = false; |
|
329 |
|
330 mContextMenuInfo = null; |
|
331 |
|
332 mOnScrollListener = null; |
|
333 mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; |
|
334 |
|
335 final ViewConfiguration vc = ViewConfiguration.get(context); |
|
336 mTouchSlop = vc.getScaledTouchSlop(); |
|
337 mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); |
|
338 mFlingVelocity = vc.getScaledMinimumFlingVelocity(); |
|
339 mOverscrollDistance = getScaledOverscrollDistance(vc); |
|
340 |
|
341 mOverScroll = 0; |
|
342 |
|
343 mScroller = new Scroller(context); |
|
344 |
|
345 mIsVertical = true; |
|
346 |
|
347 mItemsCanFocus = false; |
|
348 |
|
349 mTempRect = new Rect(); |
|
350 |
|
351 mArrowScrollFocusResult = new ArrowScrollFocusResult(); |
|
352 |
|
353 mSelectorPosition = INVALID_POSITION; |
|
354 |
|
355 mSelectorRect = new Rect(); |
|
356 mSelectedStart = 0; |
|
357 |
|
358 mResurrectToPosition = INVALID_POSITION; |
|
359 |
|
360 mSelectedStart = 0; |
|
361 mNextSelectedPosition = INVALID_POSITION; |
|
362 mNextSelectedRowId = INVALID_ROW_ID; |
|
363 mSelectedPosition = INVALID_POSITION; |
|
364 mSelectedRowId = INVALID_ROW_ID; |
|
365 mOldSelectedPosition = INVALID_POSITION; |
|
366 mOldSelectedRowId = INVALID_ROW_ID; |
|
367 |
|
368 mChoiceMode = ChoiceMode.NONE; |
|
369 mCheckedItemCount = 0; |
|
370 mCheckedIdStates = null; |
|
371 mCheckStates = null; |
|
372 |
|
373 mRecycler = new RecycleBin(); |
|
374 mDataSetObserver = null; |
|
375 |
|
376 mAreAllItemsSelectable = true; |
|
377 |
|
378 mStartEdge = null; |
|
379 mEndEdge = null; |
|
380 |
|
381 setClickable(true); |
|
382 setFocusableInTouchMode(true); |
|
383 setWillNotDraw(false); |
|
384 setAlwaysDrawnWithCacheEnabled(false); |
|
385 setWillNotDraw(false); |
|
386 setClipToPadding(false); |
|
387 |
|
388 ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS); |
|
389 |
|
390 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0); |
|
391 initializeScrollbars(a); |
|
392 |
|
393 mDrawSelectorOnTop = a.getBoolean( |
|
394 R.styleable.TwoWayView_android_drawSelectorOnTop, false); |
|
395 |
|
396 Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector); |
|
397 if (d != null) { |
|
398 setSelector(d); |
|
399 } |
|
400 |
|
401 int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1); |
|
402 if (orientation >= 0) { |
|
403 setOrientation(Orientation.values()[orientation]); |
|
404 } |
|
405 |
|
406 int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1); |
|
407 if (choiceMode >= 0) { |
|
408 setChoiceMode(ChoiceMode.values()[choiceMode]); |
|
409 } |
|
410 |
|
411 a.recycle(); |
|
412 |
|
413 updateScrollbarsDirection(); |
|
414 } |
|
415 |
|
416 public void setOrientation(Orientation orientation) { |
|
417 final boolean isVertical = (orientation.compareTo(Orientation.VERTICAL) == 0); |
|
418 if (mIsVertical == isVertical) { |
|
419 return; |
|
420 } |
|
421 |
|
422 mIsVertical = isVertical; |
|
423 |
|
424 updateScrollbarsDirection(); |
|
425 resetState(); |
|
426 mRecycler.clear(); |
|
427 |
|
428 requestLayout(); |
|
429 } |
|
430 |
|
431 public Orientation getOrientation() { |
|
432 return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL); |
|
433 } |
|
434 |
|
435 public void setItemMargin(int itemMargin) { |
|
436 if (mItemMargin == itemMargin) { |
|
437 return; |
|
438 } |
|
439 |
|
440 mItemMargin = itemMargin; |
|
441 requestLayout(); |
|
442 } |
|
443 |
|
444 public int getItemMargin() { |
|
445 return mItemMargin; |
|
446 } |
|
447 |
|
448 /** |
|
449 * Indicates that the views created by the ListAdapter can contain focusable |
|
450 * items. |
|
451 * |
|
452 * @param itemsCanFocus true if items can get focus, false otherwise |
|
453 */ |
|
454 public void setItemsCanFocus(boolean itemsCanFocus) { |
|
455 mItemsCanFocus = itemsCanFocus; |
|
456 if (!itemsCanFocus) { |
|
457 setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); |
|
458 } |
|
459 } |
|
460 |
|
461 /** |
|
462 * @return Whether the views created by the ListAdapter can contain focusable |
|
463 * items. |
|
464 */ |
|
465 public boolean getItemsCanFocus() { |
|
466 return mItemsCanFocus; |
|
467 } |
|
468 |
|
469 /** |
|
470 * Set the listener that will receive notifications every time the list scrolls. |
|
471 * |
|
472 * @param l the scroll listener |
|
473 */ |
|
474 public void setOnScrollListener(OnScrollListener l) { |
|
475 mOnScrollListener = l; |
|
476 invokeOnItemScrollListener(); |
|
477 } |
|
478 |
|
479 /** |
|
480 * Sets the recycler listener to be notified whenever a View is set aside in |
|
481 * the recycler for later reuse. This listener can be used to free resources |
|
482 * associated to the View. |
|
483 * |
|
484 * @param listener The recycler listener to be notified of views set aside |
|
485 * in the recycler. |
|
486 * |
|
487 * @see TwoWayView.RecycleBin |
|
488 * @see TwoWayView.RecyclerListener |
|
489 */ |
|
490 public void setRecyclerListener(RecyclerListener l) { |
|
491 mRecycler.mRecyclerListener = l; |
|
492 } |
|
493 |
|
494 /** |
|
495 * Controls whether the selection highlight drawable should be drawn on top of the item or |
|
496 * behind it. |
|
497 * |
|
498 * @param onTop If true, the selector will be drawn on the item it is highlighting. The default |
|
499 * is false. |
|
500 * |
|
501 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop |
|
502 */ |
|
503 public void setDrawSelectorOnTop(boolean drawSelectorOnTop) { |
|
504 mDrawSelectorOnTop = drawSelectorOnTop; |
|
505 } |
|
506 |
|
507 /** |
|
508 * Set a Drawable that should be used to highlight the currently selected item. |
|
509 * |
|
510 * @param resID A Drawable resource to use as the selection highlight. |
|
511 * |
|
512 * @attr ref android.R.styleable#AbsListView_listSelector |
|
513 */ |
|
514 public void setSelector(int resID) { |
|
515 setSelector(getResources().getDrawable(resID)); |
|
516 } |
|
517 |
|
518 /** |
|
519 * Set a Drawable that should be used to highlight the currently selected item. |
|
520 * |
|
521 * @param selector A Drawable to use as the selection highlight. |
|
522 * |
|
523 * @attr ref android.R.styleable#AbsListView_listSelector |
|
524 */ |
|
525 public void setSelector(Drawable selector) { |
|
526 if (mSelector != null) { |
|
527 mSelector.setCallback(null); |
|
528 unscheduleDrawable(mSelector); |
|
529 } |
|
530 |
|
531 mSelector = selector; |
|
532 Rect padding = new Rect(); |
|
533 selector.getPadding(padding); |
|
534 |
|
535 selector.setCallback(this); |
|
536 updateSelectorState(); |
|
537 } |
|
538 |
|
539 /** |
|
540 * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the |
|
541 * selection in the list. |
|
542 * |
|
543 * @return the drawable used to display the selector |
|
544 */ |
|
545 public Drawable getSelector() { |
|
546 return mSelector; |
|
547 } |
|
548 |
|
549 /** |
|
550 * {@inheritDoc} |
|
551 */ |
|
552 @Override |
|
553 public int getSelectedItemPosition() { |
|
554 return mNextSelectedPosition; |
|
555 } |
|
556 |
|
557 /** |
|
558 * {@inheritDoc} |
|
559 */ |
|
560 @Override |
|
561 public long getSelectedItemId() { |
|
562 return mNextSelectedRowId; |
|
563 } |
|
564 |
|
565 /** |
|
566 * Returns the number of items currently selected. This will only be valid |
|
567 * if the choice mode is not {@link #CHOICE_MODE_NONE} (default). |
|
568 * |
|
569 * <p>To determine the specific items that are currently selected, use one of |
|
570 * the <code>getChecked*</code> methods. |
|
571 * |
|
572 * @return The number of items currently selected |
|
573 * |
|
574 * @see #getCheckedItemPosition() |
|
575 * @see #getCheckedItemPositions() |
|
576 * @see #getCheckedItemIds() |
|
577 */ |
|
578 public int getCheckedItemCount() { |
|
579 return mCheckedItemCount; |
|
580 } |
|
581 |
|
582 /** |
|
583 * Returns the checked state of the specified position. The result is only |
|
584 * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} |
|
585 * or {@link #CHOICE_MODE_MULTIPLE}. |
|
586 * |
|
587 * @param position The item whose checked state to return |
|
588 * @return The item's checked state or <code>false</code> if choice mode |
|
589 * is invalid |
|
590 * |
|
591 * @see #setChoiceMode(int) |
|
592 */ |
|
593 public boolean isItemChecked(int position) { |
|
594 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 && mCheckStates != null) { |
|
595 return mCheckStates.get(position); |
|
596 } |
|
597 |
|
598 return false; |
|
599 } |
|
600 |
|
601 /** |
|
602 * Returns the currently checked item. The result is only valid if the choice |
|
603 * mode has been set to {@link #CHOICE_MODE_SINGLE}. |
|
604 * |
|
605 * @return The position of the currently checked item or |
|
606 * {@link #INVALID_POSITION} if nothing is selected |
|
607 * |
|
608 * @see #setChoiceMode(int) |
|
609 */ |
|
610 public int getCheckedItemPosition() { |
|
611 if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0 && |
|
612 mCheckStates != null && mCheckStates.size() == 1) { |
|
613 return mCheckStates.keyAt(0); |
|
614 } |
|
615 |
|
616 return INVALID_POSITION; |
|
617 } |
|
618 |
|
619 /** |
|
620 * Returns the set of checked items in the list. The result is only valid if |
|
621 * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. |
|
622 * |
|
623 * @return A SparseBooleanArray which will return true for each call to |
|
624 * get(int position) where position is a position in the list, |
|
625 * or <code>null</code> if the choice mode is set to |
|
626 * {@link #CHOICE_MODE_NONE}. |
|
627 */ |
|
628 public SparseBooleanArray getCheckedItemPositions() { |
|
629 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) { |
|
630 return mCheckStates; |
|
631 } |
|
632 |
|
633 return null; |
|
634 } |
|
635 |
|
636 /** |
|
637 * Returns the set of checked items ids. The result is only valid if the |
|
638 * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter |
|
639 * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) |
|
640 * |
|
641 * @return A new array which contains the id of each checked item in the |
|
642 * list. |
|
643 */ |
|
644 public long[] getCheckedItemIds() { |
|
645 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 || |
|
646 mCheckedIdStates == null || mAdapter == null) { |
|
647 return new long[0]; |
|
648 } |
|
649 |
|
650 final LongSparseArray<Integer> idStates = mCheckedIdStates; |
|
651 final int count = idStates.size(); |
|
652 final long[] ids = new long[count]; |
|
653 |
|
654 for (int i = 0; i < count; i++) { |
|
655 ids[i] = idStates.keyAt(i); |
|
656 } |
|
657 |
|
658 return ids; |
|
659 } |
|
660 |
|
661 /** |
|
662 * Sets the checked state of the specified position. The is only valid if |
|
663 * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or |
|
664 * {@link #CHOICE_MODE_MULTIPLE}. |
|
665 * |
|
666 * @param position The item whose checked state is to be checked |
|
667 * @param value The new checked state for the item |
|
668 */ |
|
669 public void setItemChecked(int position, boolean value) { |
|
670 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0) { |
|
671 return; |
|
672 } |
|
673 |
|
674 if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) { |
|
675 boolean oldValue = mCheckStates.get(position); |
|
676 mCheckStates.put(position, value); |
|
677 |
|
678 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { |
|
679 if (value) { |
|
680 mCheckedIdStates.put(mAdapter.getItemId(position), position); |
|
681 } else { |
|
682 mCheckedIdStates.delete(mAdapter.getItemId(position)); |
|
683 } |
|
684 } |
|
685 |
|
686 if (oldValue != value) { |
|
687 if (value) { |
|
688 mCheckedItemCount++; |
|
689 } else { |
|
690 mCheckedItemCount--; |
|
691 } |
|
692 } |
|
693 } else { |
|
694 boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); |
|
695 |
|
696 // Clear all values if we're checking something, or unchecking the currently |
|
697 // selected item |
|
698 if (value || isItemChecked(position)) { |
|
699 mCheckStates.clear(); |
|
700 |
|
701 if (updateIds) { |
|
702 mCheckedIdStates.clear(); |
|
703 } |
|
704 } |
|
705 |
|
706 // This may end up selecting the value we just cleared but this way |
|
707 // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on |
|
708 if (value) { |
|
709 mCheckStates.put(position, true); |
|
710 |
|
711 if (updateIds) { |
|
712 mCheckedIdStates.put(mAdapter.getItemId(position), position); |
|
713 } |
|
714 |
|
715 mCheckedItemCount = 1; |
|
716 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { |
|
717 mCheckedItemCount = 0; |
|
718 } |
|
719 } |
|
720 |
|
721 // Do not generate a data change while we are in the layout phase |
|
722 if (!mInLayout && !mBlockLayoutRequests) { |
|
723 mDataChanged = true; |
|
724 rememberSyncState(); |
|
725 requestLayout(); |
|
726 } |
|
727 } |
|
728 |
|
729 /** |
|
730 * Clear any choices previously set |
|
731 */ |
|
732 public void clearChoices() { |
|
733 if (mCheckStates != null) { |
|
734 mCheckStates.clear(); |
|
735 } |
|
736 |
|
737 if (mCheckedIdStates != null) { |
|
738 mCheckedIdStates.clear(); |
|
739 } |
|
740 |
|
741 mCheckedItemCount = 0; |
|
742 } |
|
743 |
|
744 /** |
|
745 * @see #setChoiceMode(int) |
|
746 * |
|
747 * @return The current choice mode |
|
748 */ |
|
749 public ChoiceMode getChoiceMode() { |
|
750 return mChoiceMode; |
|
751 } |
|
752 |
|
753 /** |
|
754 * Defines the choice behavior for the List. By default, Lists do not have any choice behavior |
|
755 * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the |
|
756 * List allows up to one item to be in a chosen state. By setting the choiceMode to |
|
757 * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. |
|
758 * |
|
759 * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or |
|
760 * {@link #CHOICE_MODE_MULTIPLE} |
|
761 */ |
|
762 public void setChoiceMode(ChoiceMode choiceMode) { |
|
763 mChoiceMode = choiceMode; |
|
764 |
|
765 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) { |
|
766 if (mCheckStates == null) { |
|
767 mCheckStates = new SparseBooleanArray(); |
|
768 } |
|
769 |
|
770 if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { |
|
771 mCheckedIdStates = new LongSparseArray<Integer>(); |
|
772 } |
|
773 } |
|
774 } |
|
775 |
|
776 @Override |
|
777 public ListAdapter getAdapter() { |
|
778 return mAdapter; |
|
779 } |
|
780 |
|
781 @Override |
|
782 public void setAdapter(ListAdapter adapter) { |
|
783 if (mAdapter != null && mDataSetObserver != null) { |
|
784 mAdapter.unregisterDataSetObserver(mDataSetObserver); |
|
785 } |
|
786 |
|
787 resetState(); |
|
788 mRecycler.clear(); |
|
789 |
|
790 mAdapter = adapter; |
|
791 mDataChanged = true; |
|
792 |
|
793 mOldSelectedPosition = INVALID_POSITION; |
|
794 mOldSelectedRowId = INVALID_ROW_ID; |
|
795 |
|
796 if (mCheckStates != null) { |
|
797 mCheckStates.clear(); |
|
798 } |
|
799 |
|
800 if (mCheckedIdStates != null) { |
|
801 mCheckedIdStates.clear(); |
|
802 } |
|
803 |
|
804 if (mAdapter != null) { |
|
805 mOldItemCount = mItemCount; |
|
806 mItemCount = adapter.getCount(); |
|
807 |
|
808 mDataSetObserver = new AdapterDataSetObserver(); |
|
809 mAdapter.registerDataSetObserver(mDataSetObserver); |
|
810 |
|
811 mRecycler.setViewTypeCount(adapter.getViewTypeCount()); |
|
812 |
|
813 mHasStableIds = adapter.hasStableIds(); |
|
814 mAreAllItemsSelectable = adapter.areAllItemsEnabled(); |
|
815 |
|
816 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mHasStableIds && |
|
817 mCheckedIdStates == null) { |
|
818 mCheckedIdStates = new LongSparseArray<Integer>(); |
|
819 } |
|
820 |
|
821 final int position = lookForSelectablePosition(0); |
|
822 setSelectedPositionInt(position); |
|
823 setNextSelectedPositionInt(position); |
|
824 |
|
825 if (mItemCount == 0) { |
|
826 checkSelectionChanged(); |
|
827 } |
|
828 } else { |
|
829 mItemCount = 0; |
|
830 mHasStableIds = false; |
|
831 mAreAllItemsSelectable = true; |
|
832 |
|
833 checkSelectionChanged(); |
|
834 } |
|
835 |
|
836 checkFocus(); |
|
837 requestLayout(); |
|
838 } |
|
839 |
|
840 @Override |
|
841 public int getFirstVisiblePosition() { |
|
842 return mFirstPosition; |
|
843 } |
|
844 |
|
845 @Override |
|
846 public int getLastVisiblePosition() { |
|
847 return mFirstPosition + getChildCount() - 1; |
|
848 } |
|
849 |
|
850 @Override |
|
851 public int getCount() { |
|
852 return mItemCount; |
|
853 } |
|
854 |
|
855 @Override |
|
856 public int getPositionForView(View view) { |
|
857 View child = view; |
|
858 try { |
|
859 View v; |
|
860 while (!(v = (View) child.getParent()).equals(this)) { |
|
861 child = v; |
|
862 } |
|
863 } catch (ClassCastException e) { |
|
864 // We made it up to the window without find this list view |
|
865 return INVALID_POSITION; |
|
866 } |
|
867 |
|
868 // Search the children for the list item |
|
869 final int childCount = getChildCount(); |
|
870 for (int i = 0; i < childCount; i++) { |
|
871 if (getChildAt(i).equals(child)) { |
|
872 return mFirstPosition + i; |
|
873 } |
|
874 } |
|
875 |
|
876 // Child not found! |
|
877 return INVALID_POSITION; |
|
878 } |
|
879 |
|
880 @Override |
|
881 public void getFocusedRect(Rect r) { |
|
882 View view = getSelectedView(); |
|
883 |
|
884 if (view != null && view.getParent() == this) { |
|
885 // The focused rectangle of the selected view offset into the |
|
886 // coordinate space of this view. |
|
887 view.getFocusedRect(r); |
|
888 offsetDescendantRectToMyCoords(view, r); |
|
889 } else { |
|
890 super.getFocusedRect(r); |
|
891 } |
|
892 } |
|
893 |
|
894 @Override |
|
895 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { |
|
896 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); |
|
897 |
|
898 if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { |
|
899 if (!mIsAttached && mAdapter != null) { |
|
900 // Data may have changed while we were detached and it's valid |
|
901 // to change focus while detached. Refresh so we don't die. |
|
902 mDataChanged = true; |
|
903 mOldItemCount = mItemCount; |
|
904 mItemCount = mAdapter.getCount(); |
|
905 } |
|
906 |
|
907 resurrectSelection(); |
|
908 } |
|
909 |
|
910 final ListAdapter adapter = mAdapter; |
|
911 int closetChildIndex = INVALID_POSITION; |
|
912 int closestChildStart = 0; |
|
913 |
|
914 if (adapter != null && gainFocus && previouslyFocusedRect != null) { |
|
915 previouslyFocusedRect.offset(getScrollX(), getScrollY()); |
|
916 |
|
917 // Don't cache the result of getChildCount or mFirstPosition here, |
|
918 // it could change in layoutChildren. |
|
919 if (adapter.getCount() < getChildCount() + mFirstPosition) { |
|
920 mLayoutMode = LAYOUT_NORMAL; |
|
921 layoutChildren(); |
|
922 } |
|
923 |
|
924 // Figure out which item should be selected based on previously |
|
925 // focused rect. |
|
926 Rect otherRect = mTempRect; |
|
927 int minDistance = Integer.MAX_VALUE; |
|
928 final int childCount = getChildCount(); |
|
929 final int firstPosition = mFirstPosition; |
|
930 |
|
931 for (int i = 0; i < childCount; i++) { |
|
932 // Only consider selectable views |
|
933 if (!adapter.isEnabled(firstPosition + i)) { |
|
934 continue; |
|
935 } |
|
936 |
|
937 View other = getChildAt(i); |
|
938 other.getDrawingRect(otherRect); |
|
939 offsetDescendantRectToMyCoords(other, otherRect); |
|
940 int distance = getDistance(previouslyFocusedRect, otherRect, direction); |
|
941 |
|
942 if (distance < minDistance) { |
|
943 minDistance = distance; |
|
944 closetChildIndex = i; |
|
945 closestChildStart = (mIsVertical ? other.getTop() : other.getLeft()); |
|
946 } |
|
947 } |
|
948 } |
|
949 |
|
950 if (closetChildIndex >= 0) { |
|
951 setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart); |
|
952 } else { |
|
953 requestLayout(); |
|
954 } |
|
955 } |
|
956 |
|
957 @Override |
|
958 protected void onAttachedToWindow() { |
|
959 super.onAttachedToWindow(); |
|
960 |
|
961 final ViewTreeObserver treeObserver = getViewTreeObserver(); |
|
962 treeObserver.addOnTouchModeChangeListener(this); |
|
963 |
|
964 if (mAdapter != null && mDataSetObserver == null) { |
|
965 mDataSetObserver = new AdapterDataSetObserver(); |
|
966 mAdapter.registerDataSetObserver(mDataSetObserver); |
|
967 |
|
968 // Data may have changed while we were detached. Refresh. |
|
969 mDataChanged = true; |
|
970 mOldItemCount = mItemCount; |
|
971 mItemCount = mAdapter.getCount(); |
|
972 } |
|
973 |
|
974 mIsAttached = true; |
|
975 } |
|
976 |
|
977 @Override |
|
978 protected void onDetachedFromWindow() { |
|
979 super.onDetachedFromWindow(); |
|
980 |
|
981 // Detach any view left in the scrap heap |
|
982 mRecycler.clear(); |
|
983 |
|
984 final ViewTreeObserver treeObserver = getViewTreeObserver(); |
|
985 treeObserver.removeOnTouchModeChangeListener(this); |
|
986 |
|
987 if (mAdapter != null) { |
|
988 mAdapter.unregisterDataSetObserver(mDataSetObserver); |
|
989 mDataSetObserver = null; |
|
990 } |
|
991 |
|
992 if (mPerformClick != null) { |
|
993 removeCallbacks(mPerformClick); |
|
994 } |
|
995 |
|
996 if (mTouchModeReset != null) { |
|
997 removeCallbacks(mTouchModeReset); |
|
998 mTouchModeReset.run(); |
|
999 } |
|
1000 |
|
1001 mIsAttached = false; |
|
1002 } |
|
1003 |
|
1004 @Override |
|
1005 public void onWindowFocusChanged(boolean hasWindowFocus) { |
|
1006 super.onWindowFocusChanged(hasWindowFocus); |
|
1007 |
|
1008 final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; |
|
1009 |
|
1010 if (!hasWindowFocus) { |
|
1011 if (touchMode == TOUCH_MODE_OFF) { |
|
1012 // Remember the last selected element |
|
1013 mResurrectToPosition = mSelectedPosition; |
|
1014 } |
|
1015 } else { |
|
1016 // If we changed touch mode since the last time we had focus |
|
1017 if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { |
|
1018 // If we come back in trackball mode, we bring the selection back |
|
1019 if (touchMode == TOUCH_MODE_OFF) { |
|
1020 // This will trigger a layout |
|
1021 resurrectSelection(); |
|
1022 |
|
1023 // If we come back in touch mode, then we want to hide the selector |
|
1024 } else { |
|
1025 hideSelector(); |
|
1026 mLayoutMode = LAYOUT_NORMAL; |
|
1027 layoutChildren(); |
|
1028 } |
|
1029 } |
|
1030 } |
|
1031 |
|
1032 mLastTouchMode = touchMode; |
|
1033 } |
|
1034 |
|
1035 @Override |
|
1036 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { |
|
1037 boolean needsInvalidate = false; |
|
1038 |
|
1039 if (mIsVertical && mOverScroll != scrollY) { |
|
1040 onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll); |
|
1041 mOverScroll = scrollY; |
|
1042 needsInvalidate = true; |
|
1043 } else if (!mIsVertical && mOverScroll != scrollX) { |
|
1044 onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY()); |
|
1045 mOverScroll = scrollX; |
|
1046 needsInvalidate = true; |
|
1047 } |
|
1048 |
|
1049 if (needsInvalidate) { |
|
1050 invalidate(); |
|
1051 awakenScrollbarsInternal(); |
|
1052 } |
|
1053 } |
|
1054 |
|
1055 @TargetApi(9) |
|
1056 private boolean overScrollByInternal(int deltaX, int deltaY, |
|
1057 int scrollX, int scrollY, |
|
1058 int scrollRangeX, int scrollRangeY, |
|
1059 int maxOverScrollX, int maxOverScrollY, |
|
1060 boolean isTouchEvent) { |
|
1061 if (Build.VERSION.SDK_INT < 9) { |
|
1062 return false; |
|
1063 } |
|
1064 |
|
1065 return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, |
|
1066 scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); |
|
1067 } |
|
1068 |
|
1069 @Override |
|
1070 @TargetApi(9) |
|
1071 public void setOverScrollMode(int mode) { |
|
1072 if (Build.VERSION.SDK_INT < 9) { |
|
1073 return; |
|
1074 } |
|
1075 |
|
1076 if (mode != ViewCompat.OVER_SCROLL_NEVER) { |
|
1077 if (mStartEdge == null) { |
|
1078 Context context = getContext(); |
|
1079 |
|
1080 mStartEdge = new EdgeEffectCompat(context); |
|
1081 mEndEdge = new EdgeEffectCompat(context); |
|
1082 } |
|
1083 } else { |
|
1084 mStartEdge = null; |
|
1085 mEndEdge = null; |
|
1086 } |
|
1087 |
|
1088 super.setOverScrollMode(mode); |
|
1089 } |
|
1090 |
|
1091 public int pointToPosition(int x, int y) { |
|
1092 Rect frame = mTouchFrame; |
|
1093 if (frame == null) { |
|
1094 mTouchFrame = new Rect(); |
|
1095 frame = mTouchFrame; |
|
1096 } |
|
1097 |
|
1098 final int count = getChildCount(); |
|
1099 for (int i = count - 1; i >= 0; i--) { |
|
1100 final View child = getChildAt(i); |
|
1101 |
|
1102 if (child.getVisibility() == View.VISIBLE) { |
|
1103 child.getHitRect(frame); |
|
1104 |
|
1105 if (frame.contains(x, y)) { |
|
1106 return mFirstPosition + i; |
|
1107 } |
|
1108 } |
|
1109 } |
|
1110 return INVALID_POSITION; |
|
1111 } |
|
1112 |
|
1113 @Override |
|
1114 protected int computeVerticalScrollExtent() { |
|
1115 final int count = getChildCount(); |
|
1116 if (count == 0) { |
|
1117 return 0; |
|
1118 } |
|
1119 |
|
1120 int extent = count * 100; |
|
1121 |
|
1122 View child = getChildAt(0); |
|
1123 final int childTop = child.getTop(); |
|
1124 |
|
1125 int childHeight = child.getHeight(); |
|
1126 if (childHeight > 0) { |
|
1127 extent += (childTop * 100) / childHeight; |
|
1128 } |
|
1129 |
|
1130 child = getChildAt(count - 1); |
|
1131 final int childBottom = child.getBottom(); |
|
1132 |
|
1133 childHeight = child.getHeight(); |
|
1134 if (childHeight > 0) { |
|
1135 extent -= ((childBottom - getHeight()) * 100) / childHeight; |
|
1136 } |
|
1137 |
|
1138 return extent; |
|
1139 } |
|
1140 |
|
1141 @Override |
|
1142 protected int computeHorizontalScrollExtent() { |
|
1143 final int count = getChildCount(); |
|
1144 if (count == 0) { |
|
1145 return 0; |
|
1146 } |
|
1147 |
|
1148 int extent = count * 100; |
|
1149 |
|
1150 View child = getChildAt(0); |
|
1151 final int childLeft = child.getLeft(); |
|
1152 |
|
1153 int childWidth = child.getWidth(); |
|
1154 if (childWidth > 0) { |
|
1155 extent += (childLeft * 100) / childWidth; |
|
1156 } |
|
1157 |
|
1158 child = getChildAt(count - 1); |
|
1159 final int childRight = child.getRight(); |
|
1160 |
|
1161 childWidth = child.getWidth(); |
|
1162 if (childWidth > 0) { |
|
1163 extent -= ((childRight - getWidth()) * 100) / childWidth; |
|
1164 } |
|
1165 |
|
1166 return extent; |
|
1167 } |
|
1168 |
|
1169 @Override |
|
1170 protected int computeVerticalScrollOffset() { |
|
1171 final int firstPosition = mFirstPosition; |
|
1172 final int childCount = getChildCount(); |
|
1173 |
|
1174 if (firstPosition < 0 || childCount == 0) { |
|
1175 return 0; |
|
1176 } |
|
1177 |
|
1178 final View child = getChildAt(0); |
|
1179 final int childTop = child.getTop(); |
|
1180 |
|
1181 int childHeight = child.getHeight(); |
|
1182 if (childHeight > 0) { |
|
1183 return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0); |
|
1184 } |
|
1185 |
|
1186 return 0; |
|
1187 } |
|
1188 |
|
1189 @Override |
|
1190 protected int computeHorizontalScrollOffset() { |
|
1191 final int firstPosition = mFirstPosition; |
|
1192 final int childCount = getChildCount(); |
|
1193 |
|
1194 if (firstPosition < 0 || childCount == 0) { |
|
1195 return 0; |
|
1196 } |
|
1197 |
|
1198 final View child = getChildAt(0); |
|
1199 final int childLeft = child.getLeft(); |
|
1200 |
|
1201 int childWidth = child.getWidth(); |
|
1202 if (childWidth > 0) { |
|
1203 return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0); |
|
1204 } |
|
1205 |
|
1206 return 0; |
|
1207 } |
|
1208 |
|
1209 @Override |
|
1210 protected int computeVerticalScrollRange() { |
|
1211 int result = Math.max(mItemCount * 100, 0); |
|
1212 |
|
1213 if (mIsVertical && mOverScroll != 0) { |
|
1214 // Compensate for overscroll |
|
1215 result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100)); |
|
1216 } |
|
1217 |
|
1218 return result; |
|
1219 } |
|
1220 |
|
1221 @Override |
|
1222 protected int computeHorizontalScrollRange() { |
|
1223 int result = Math.max(mItemCount * 100, 0); |
|
1224 |
|
1225 if (!mIsVertical && mOverScroll != 0) { |
|
1226 // Compensate for overscroll |
|
1227 result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100)); |
|
1228 } |
|
1229 |
|
1230 return result; |
|
1231 } |
|
1232 |
|
1233 @Override |
|
1234 public boolean showContextMenuForChild(View originalView) { |
|
1235 final int longPressPosition = getPositionForView(originalView); |
|
1236 if (longPressPosition >= 0) { |
|
1237 final long longPressId = mAdapter.getItemId(longPressPosition); |
|
1238 boolean handled = false; |
|
1239 |
|
1240 OnItemLongClickListener listener = getOnItemLongClickListener(); |
|
1241 if (listener != null) { |
|
1242 handled = listener.onItemLongClick(TwoWayView.this, originalView, |
|
1243 longPressPosition, longPressId); |
|
1244 } |
|
1245 |
|
1246 if (!handled) { |
|
1247 mContextMenuInfo = createContextMenuInfo( |
|
1248 getChildAt(longPressPosition - mFirstPosition), |
|
1249 longPressPosition, longPressId); |
|
1250 |
|
1251 handled = super.showContextMenuForChild(originalView); |
|
1252 } |
|
1253 |
|
1254 return handled; |
|
1255 } |
|
1256 |
|
1257 return false; |
|
1258 } |
|
1259 |
|
1260 @Override |
|
1261 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { |
|
1262 if (disallowIntercept) { |
|
1263 recycleVelocityTracker(); |
|
1264 } |
|
1265 |
|
1266 super.requestDisallowInterceptTouchEvent(disallowIntercept); |
|
1267 } |
|
1268 |
|
1269 @Override |
|
1270 public boolean onInterceptTouchEvent(MotionEvent ev) { |
|
1271 if (!mIsAttached) { |
|
1272 return false; |
|
1273 } |
|
1274 |
|
1275 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; |
|
1276 switch (action) { |
|
1277 case MotionEvent.ACTION_DOWN: |
|
1278 initOrResetVelocityTracker(); |
|
1279 mVelocityTracker.addMovement(ev); |
|
1280 |
|
1281 mScroller.abortAnimation(); |
|
1282 |
|
1283 final float x = ev.getX(); |
|
1284 final float y = ev.getY(); |
|
1285 |
|
1286 mLastTouchPos = (mIsVertical ? y : x); |
|
1287 |
|
1288 final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos); |
|
1289 |
|
1290 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
|
1291 mTouchRemainderPos = 0; |
|
1292 |
|
1293 if (mTouchMode == TOUCH_MODE_FLINGING) { |
|
1294 return true; |
|
1295 } else if (motionPosition >= 0) { |
|
1296 mMotionPosition = motionPosition; |
|
1297 mTouchMode = TOUCH_MODE_DOWN; |
|
1298 } |
|
1299 |
|
1300 break; |
|
1301 |
|
1302 case MotionEvent.ACTION_MOVE: { |
|
1303 if (mTouchMode != TOUCH_MODE_DOWN) { |
|
1304 break; |
|
1305 } |
|
1306 |
|
1307 initVelocityTrackerIfNotExists(); |
|
1308 mVelocityTracker.addMovement(ev); |
|
1309 |
|
1310 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
|
1311 if (index < 0) { |
|
1312 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + |
|
1313 mActivePointerId + " - did TwoWayView receive an inconsistent " + |
|
1314 "event stream?"); |
|
1315 return false; |
|
1316 } |
|
1317 |
|
1318 final float pos; |
|
1319 if (mIsVertical) { |
|
1320 pos = MotionEventCompat.getY(ev, index); |
|
1321 } else { |
|
1322 pos = MotionEventCompat.getX(ev, index); |
|
1323 } |
|
1324 |
|
1325 final float diff = pos - mLastTouchPos + mTouchRemainderPos; |
|
1326 final int delta = (int) diff; |
|
1327 mTouchRemainderPos = diff - delta; |
|
1328 |
|
1329 if (maybeStartScrolling(delta)) { |
|
1330 return true; |
|
1331 } |
|
1332 |
|
1333 break; |
|
1334 } |
|
1335 |
|
1336 case MotionEvent.ACTION_CANCEL: |
|
1337 case MotionEvent.ACTION_UP: |
|
1338 mActivePointerId = INVALID_POINTER; |
|
1339 mTouchMode = TOUCH_MODE_REST; |
|
1340 recycleVelocityTracker(); |
|
1341 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
1342 |
|
1343 break; |
|
1344 } |
|
1345 |
|
1346 return false; |
|
1347 } |
|
1348 |
|
1349 @Override |
|
1350 public boolean onTouchEvent(MotionEvent ev) { |
|
1351 if (!isEnabled()) { |
|
1352 // A disabled view that is clickable still consumes the touch |
|
1353 // events, it just doesn't respond to them. |
|
1354 return isClickable() || isLongClickable(); |
|
1355 } |
|
1356 |
|
1357 if (!mIsAttached) { |
|
1358 return false; |
|
1359 } |
|
1360 |
|
1361 boolean needsInvalidate = false; |
|
1362 |
|
1363 initVelocityTrackerIfNotExists(); |
|
1364 mVelocityTracker.addMovement(ev); |
|
1365 |
|
1366 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; |
|
1367 switch (action) { |
|
1368 case MotionEvent.ACTION_DOWN: { |
|
1369 if (mDataChanged) { |
|
1370 break; |
|
1371 } |
|
1372 |
|
1373 mVelocityTracker.clear(); |
|
1374 mScroller.abortAnimation(); |
|
1375 |
|
1376 final float x = ev.getX(); |
|
1377 final float y = ev.getY(); |
|
1378 |
|
1379 mLastTouchPos = (mIsVertical ? y : x); |
|
1380 |
|
1381 int motionPosition = pointToPosition((int) x, (int) y); |
|
1382 |
|
1383 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
|
1384 mTouchRemainderPos = 0; |
|
1385 |
|
1386 if (mDataChanged) { |
|
1387 break; |
|
1388 } |
|
1389 |
|
1390 if (mTouchMode == TOUCH_MODE_FLINGING) { |
|
1391 mTouchMode = TOUCH_MODE_DRAGGING; |
|
1392 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); |
|
1393 motionPosition = findMotionRowOrColumn((int) mLastTouchPos); |
|
1394 return true; |
|
1395 } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) { |
|
1396 mTouchMode = TOUCH_MODE_DOWN; |
|
1397 triggerCheckForTap(); |
|
1398 } |
|
1399 |
|
1400 mMotionPosition = motionPosition; |
|
1401 |
|
1402 break; |
|
1403 } |
|
1404 |
|
1405 case MotionEvent.ACTION_MOVE: { |
|
1406 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
|
1407 if (index < 0) { |
|
1408 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " + |
|
1409 mActivePointerId + " - did TwoWayView receive an inconsistent " + |
|
1410 "event stream?"); |
|
1411 return false; |
|
1412 } |
|
1413 |
|
1414 final float pos; |
|
1415 if (mIsVertical) { |
|
1416 pos = MotionEventCompat.getY(ev, index); |
|
1417 } else { |
|
1418 pos = MotionEventCompat.getX(ev, index); |
|
1419 } |
|
1420 |
|
1421 if (mDataChanged) { |
|
1422 // Re-sync everything if data has been changed |
|
1423 // since the scroll operation can query the adapter. |
|
1424 layoutChildren(); |
|
1425 } |
|
1426 |
|
1427 final float diff = pos - mLastTouchPos + mTouchRemainderPos; |
|
1428 final int delta = (int) diff; |
|
1429 mTouchRemainderPos = diff - delta; |
|
1430 |
|
1431 switch (mTouchMode) { |
|
1432 case TOUCH_MODE_DOWN: |
|
1433 case TOUCH_MODE_TAP: |
|
1434 case TOUCH_MODE_DONE_WAITING: |
|
1435 // Check if we have moved far enough that it looks more like a |
|
1436 // scroll than a tap |
|
1437 maybeStartScrolling(delta); |
|
1438 break; |
|
1439 |
|
1440 case TOUCH_MODE_DRAGGING: |
|
1441 case TOUCH_MODE_OVERSCROLL: |
|
1442 mLastTouchPos = pos; |
|
1443 maybeScroll(delta); |
|
1444 break; |
|
1445 } |
|
1446 |
|
1447 break; |
|
1448 } |
|
1449 |
|
1450 case MotionEvent.ACTION_CANCEL: |
|
1451 cancelCheckForTap(); |
|
1452 mTouchMode = TOUCH_MODE_REST; |
|
1453 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
1454 |
|
1455 setPressed(false); |
|
1456 View motionView = this.getChildAt(mMotionPosition - mFirstPosition); |
|
1457 if (motionView != null) { |
|
1458 motionView.setPressed(false); |
|
1459 } |
|
1460 |
|
1461 if (mStartEdge != null && mEndEdge != null) { |
|
1462 needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease(); |
|
1463 } |
|
1464 |
|
1465 recycleVelocityTracker(); |
|
1466 |
|
1467 break; |
|
1468 |
|
1469 case MotionEvent.ACTION_UP: { |
|
1470 switch (mTouchMode) { |
|
1471 case TOUCH_MODE_DOWN: |
|
1472 case TOUCH_MODE_TAP: |
|
1473 case TOUCH_MODE_DONE_WAITING: { |
|
1474 final int motionPosition = mMotionPosition; |
|
1475 final View child = getChildAt(motionPosition - mFirstPosition); |
|
1476 |
|
1477 final float x = ev.getX(); |
|
1478 final float y = ev.getY(); |
|
1479 |
|
1480 boolean inList = false; |
|
1481 if (mIsVertical) { |
|
1482 inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight(); |
|
1483 } else { |
|
1484 inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom(); |
|
1485 } |
|
1486 |
|
1487 if (child != null && !child.hasFocusable() && inList) { |
|
1488 if (mTouchMode != TOUCH_MODE_DOWN) { |
|
1489 child.setPressed(false); |
|
1490 } |
|
1491 |
|
1492 if (mPerformClick == null) { |
|
1493 mPerformClick = new PerformClick(); |
|
1494 } |
|
1495 |
|
1496 final PerformClick performClick = mPerformClick; |
|
1497 performClick.mClickMotionPosition = motionPosition; |
|
1498 performClick.rememberWindowAttachCount(); |
|
1499 |
|
1500 mResurrectToPosition = motionPosition; |
|
1501 |
|
1502 if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { |
|
1503 if (mTouchMode == TOUCH_MODE_DOWN) { |
|
1504 cancelCheckForTap(); |
|
1505 } else { |
|
1506 cancelCheckForLongPress(); |
|
1507 } |
|
1508 |
|
1509 mLayoutMode = LAYOUT_NORMAL; |
|
1510 |
|
1511 if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { |
|
1512 mTouchMode = TOUCH_MODE_TAP; |
|
1513 |
|
1514 setPressed(true); |
|
1515 positionSelector(mMotionPosition, child); |
|
1516 child.setPressed(true); |
|
1517 |
|
1518 if (mSelector != null) { |
|
1519 Drawable d = mSelector.getCurrent(); |
|
1520 if (d != null && d instanceof TransitionDrawable) { |
|
1521 ((TransitionDrawable) d).resetTransition(); |
|
1522 } |
|
1523 } |
|
1524 |
|
1525 if (mTouchModeReset != null) { |
|
1526 removeCallbacks(mTouchModeReset); |
|
1527 } |
|
1528 |
|
1529 mTouchModeReset = new Runnable() { |
|
1530 @Override |
|
1531 public void run() { |
|
1532 mTouchMode = TOUCH_MODE_REST; |
|
1533 |
|
1534 setPressed(false); |
|
1535 child.setPressed(false); |
|
1536 |
|
1537 if (!mDataChanged) { |
|
1538 performClick.run(); |
|
1539 } |
|
1540 |
|
1541 mTouchModeReset = null; |
|
1542 } |
|
1543 }; |
|
1544 |
|
1545 postDelayed(mTouchModeReset, |
|
1546 ViewConfiguration.getPressedStateDuration()); |
|
1547 } else { |
|
1548 mTouchMode = TOUCH_MODE_REST; |
|
1549 updateSelectorState(); |
|
1550 } |
|
1551 } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { |
|
1552 performClick.run(); |
|
1553 } |
|
1554 } |
|
1555 |
|
1556 mTouchMode = TOUCH_MODE_REST; |
|
1557 updateSelectorState(); |
|
1558 |
|
1559 break; |
|
1560 } |
|
1561 |
|
1562 case TOUCH_MODE_DRAGGING: |
|
1563 if (contentFits()) { |
|
1564 mTouchMode = TOUCH_MODE_REST; |
|
1565 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
1566 break; |
|
1567 } |
|
1568 |
|
1569 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); |
|
1570 |
|
1571 final float velocity; |
|
1572 if (mIsVertical) { |
|
1573 velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, |
|
1574 mActivePointerId); |
|
1575 } else { |
|
1576 velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker, |
|
1577 mActivePointerId); |
|
1578 } |
|
1579 |
|
1580 if (Math.abs(velocity) >= mFlingVelocity) { |
|
1581 mTouchMode = TOUCH_MODE_FLINGING; |
|
1582 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); |
|
1583 |
|
1584 mScroller.fling(0, 0, |
|
1585 (int) (mIsVertical ? 0 : velocity), |
|
1586 (int) (mIsVertical ? velocity : 0), |
|
1587 (mIsVertical ? 0 : Integer.MIN_VALUE), |
|
1588 (mIsVertical ? 0 : Integer.MAX_VALUE), |
|
1589 (mIsVertical ? Integer.MIN_VALUE : 0), |
|
1590 (mIsVertical ? Integer.MAX_VALUE : 0)); |
|
1591 |
|
1592 mLastTouchPos = 0; |
|
1593 needsInvalidate = true; |
|
1594 } else { |
|
1595 mTouchMode = TOUCH_MODE_REST; |
|
1596 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
1597 } |
|
1598 |
|
1599 break; |
|
1600 |
|
1601 case TOUCH_MODE_OVERSCROLL: |
|
1602 mTouchMode = TOUCH_MODE_REST; |
|
1603 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
1604 break; |
|
1605 } |
|
1606 |
|
1607 cancelCheckForTap(); |
|
1608 cancelCheckForLongPress(); |
|
1609 setPressed(false); |
|
1610 |
|
1611 if (mStartEdge != null && mEndEdge != null) { |
|
1612 needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease(); |
|
1613 } |
|
1614 |
|
1615 recycleVelocityTracker(); |
|
1616 |
|
1617 break; |
|
1618 } |
|
1619 } |
|
1620 |
|
1621 if (needsInvalidate) { |
|
1622 ViewCompat.postInvalidateOnAnimation(this); |
|
1623 } |
|
1624 |
|
1625 return true; |
|
1626 } |
|
1627 |
|
1628 @Override |
|
1629 public void onTouchModeChanged(boolean isInTouchMode) { |
|
1630 if (isInTouchMode) { |
|
1631 // Get rid of the selection when we enter touch mode |
|
1632 hideSelector(); |
|
1633 |
|
1634 // Layout, but only if we already have done so previously. |
|
1635 // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore |
|
1636 // state.) |
|
1637 if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) { |
|
1638 layoutChildren(); |
|
1639 } |
|
1640 |
|
1641 updateSelectorState(); |
|
1642 } else { |
|
1643 final int touchMode = mTouchMode; |
|
1644 if (touchMode == TOUCH_MODE_OVERSCROLL) { |
|
1645 if (mOverScroll != 0) { |
|
1646 mOverScroll = 0; |
|
1647 finishEdgeGlows(); |
|
1648 invalidate(); |
|
1649 } |
|
1650 } |
|
1651 } |
|
1652 } |
|
1653 |
|
1654 @Override |
|
1655 public boolean onKeyDown(int keyCode, KeyEvent event) { |
|
1656 return handleKeyEvent(keyCode, 1, event); |
|
1657 } |
|
1658 |
|
1659 @Override |
|
1660 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { |
|
1661 return handleKeyEvent(keyCode, repeatCount, event); |
|
1662 } |
|
1663 |
|
1664 @Override |
|
1665 public boolean onKeyUp(int keyCode, KeyEvent event) { |
|
1666 return handleKeyEvent(keyCode, 1, event); |
|
1667 } |
|
1668 |
|
1669 @Override |
|
1670 public void sendAccessibilityEvent(int eventType) { |
|
1671 // Since this class calls onScrollChanged even if the mFirstPosition and the |
|
1672 // child count have not changed we will avoid sending duplicate accessibility |
|
1673 // events. |
|
1674 if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) { |
|
1675 final int firstVisiblePosition = getFirstVisiblePosition(); |
|
1676 final int lastVisiblePosition = getLastVisiblePosition(); |
|
1677 |
|
1678 if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition |
|
1679 && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) { |
|
1680 return; |
|
1681 } else { |
|
1682 mLastAccessibilityScrollEventFromIndex = firstVisiblePosition; |
|
1683 mLastAccessibilityScrollEventToIndex = lastVisiblePosition; |
|
1684 } |
|
1685 } |
|
1686 |
|
1687 super.sendAccessibilityEvent(eventType); |
|
1688 } |
|
1689 |
|
1690 @Override |
|
1691 @TargetApi(14) |
|
1692 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { |
|
1693 super.onInitializeAccessibilityEvent(event); |
|
1694 event.setClassName(TwoWayView.class.getName()); |
|
1695 } |
|
1696 |
|
1697 @Override |
|
1698 @TargetApi(14) |
|
1699 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
|
1700 super.onInitializeAccessibilityNodeInfo(info); |
|
1701 info.setClassName(TwoWayView.class.getName()); |
|
1702 |
|
1703 AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info); |
|
1704 |
|
1705 if (isEnabled()) { |
|
1706 if (getFirstVisiblePosition() > 0) { |
|
1707 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); |
|
1708 } |
|
1709 |
|
1710 if (getLastVisiblePosition() < getCount() - 1) { |
|
1711 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); |
|
1712 } |
|
1713 } |
|
1714 } |
|
1715 |
|
1716 @Override |
|
1717 @TargetApi(16) |
|
1718 public boolean performAccessibilityAction(int action, Bundle arguments) { |
|
1719 if (super.performAccessibilityAction(action, arguments)) { |
|
1720 return true; |
|
1721 } |
|
1722 |
|
1723 switch (action) { |
|
1724 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: |
|
1725 if (isEnabled() && getLastVisiblePosition() < getCount() - 1) { |
|
1726 final int viewportSize; |
|
1727 if (mIsVertical) { |
|
1728 viewportSize = getHeight() - getPaddingTop() - getPaddingBottom(); |
|
1729 } else { |
|
1730 viewportSize = getWidth() - getPaddingLeft() - getPaddingRight(); |
|
1731 } |
|
1732 |
|
1733 // TODO: Use some form of smooth scroll instead |
|
1734 trackMotionScroll(viewportSize); |
|
1735 return true; |
|
1736 } |
|
1737 return false; |
|
1738 |
|
1739 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: |
|
1740 if (isEnabled() && mFirstPosition > 0) { |
|
1741 final int viewportSize; |
|
1742 if (mIsVertical) { |
|
1743 viewportSize = getHeight() - getPaddingTop() - getPaddingBottom(); |
|
1744 } else { |
|
1745 viewportSize = getWidth() - getPaddingLeft() - getPaddingRight(); |
|
1746 } |
|
1747 |
|
1748 // TODO: Use some form of smooth scroll instead |
|
1749 trackMotionScroll(-viewportSize); |
|
1750 return true; |
|
1751 } |
|
1752 return false; |
|
1753 } |
|
1754 |
|
1755 return false; |
|
1756 } |
|
1757 |
|
1758 /** |
|
1759 * Return true if child is an ancestor of parent, (or equal to the parent). |
|
1760 */ |
|
1761 private boolean isViewAncestorOf(View child, View parent) { |
|
1762 if (child == parent) { |
|
1763 return true; |
|
1764 } |
|
1765 |
|
1766 final ViewParent theParent = child.getParent(); |
|
1767 |
|
1768 return (theParent instanceof ViewGroup) && |
|
1769 isViewAncestorOf((View) theParent, parent); |
|
1770 } |
|
1771 |
|
1772 private void forceValidFocusDirection(int direction) { |
|
1773 if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { |
|
1774 throw new IllegalArgumentException("Focus direction must be one of" |
|
1775 + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation"); |
|
1776 } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { |
|
1777 throw new IllegalArgumentException("Focus direction must be one of" |
|
1778 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); |
|
1779 } |
|
1780 } |
|
1781 |
|
1782 private void forceValidInnerFocusDirection(int direction) { |
|
1783 if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) { |
|
1784 throw new IllegalArgumentException("Direction must be one of" |
|
1785 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation"); |
|
1786 } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) { |
|
1787 throw new IllegalArgumentException("direction must be one of" |
|
1788 + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation"); |
|
1789 } |
|
1790 } |
|
1791 |
|
1792 /** |
|
1793 * Scrolls up or down by the number of items currently present on screen. |
|
1794 * |
|
1795 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
1796 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
1797 * current view orientation. |
|
1798 * |
|
1799 * @return whether selection was moved |
|
1800 */ |
|
1801 boolean pageScroll(int direction) { |
|
1802 forceValidFocusDirection(direction); |
|
1803 |
|
1804 boolean forward = false; |
|
1805 int nextPage = -1; |
|
1806 |
|
1807 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { |
|
1808 nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1); |
|
1809 } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { |
|
1810 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1); |
|
1811 forward = true; |
|
1812 } |
|
1813 |
|
1814 if (nextPage < 0) { |
|
1815 return false; |
|
1816 } |
|
1817 |
|
1818 final int position = lookForSelectablePosition(nextPage, forward); |
|
1819 if (position >= 0) { |
|
1820 mLayoutMode = LAYOUT_SPECIFIC; |
|
1821 mSpecificStart = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
1822 |
|
1823 if (forward && position > mItemCount - getChildCount()) { |
|
1824 mLayoutMode = LAYOUT_FORCE_BOTTOM; |
|
1825 } |
|
1826 |
|
1827 if (!forward && position < getChildCount()) { |
|
1828 mLayoutMode = LAYOUT_FORCE_TOP; |
|
1829 } |
|
1830 |
|
1831 setSelectionInt(position); |
|
1832 invokeOnItemScrollListener(); |
|
1833 |
|
1834 if (!awakenScrollbarsInternal()) { |
|
1835 invalidate(); |
|
1836 } |
|
1837 |
|
1838 return true; |
|
1839 } |
|
1840 |
|
1841 return false; |
|
1842 } |
|
1843 |
|
1844 /** |
|
1845 * Go to the last or first item if possible (not worrying about panning across or navigating |
|
1846 * within the internal focus of the currently selected item.) |
|
1847 * |
|
1848 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
1849 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
1850 * current view orientation. |
|
1851 * |
|
1852 * @return whether selection was moved |
|
1853 */ |
|
1854 boolean fullScroll(int direction) { |
|
1855 forceValidFocusDirection(direction); |
|
1856 |
|
1857 boolean moved = false; |
|
1858 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { |
|
1859 if (mSelectedPosition != 0) { |
|
1860 int position = lookForSelectablePosition(0, true); |
|
1861 if (position >= 0) { |
|
1862 mLayoutMode = LAYOUT_FORCE_TOP; |
|
1863 setSelectionInt(position); |
|
1864 invokeOnItemScrollListener(); |
|
1865 } |
|
1866 |
|
1867 moved = true; |
|
1868 } |
|
1869 } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { |
|
1870 if (mSelectedPosition < mItemCount - 1) { |
|
1871 int position = lookForSelectablePosition(mItemCount - 1, true); |
|
1872 if (position >= 0) { |
|
1873 mLayoutMode = LAYOUT_FORCE_BOTTOM; |
|
1874 setSelectionInt(position); |
|
1875 invokeOnItemScrollListener(); |
|
1876 } |
|
1877 |
|
1878 moved = true; |
|
1879 } |
|
1880 } |
|
1881 |
|
1882 if (moved && !awakenScrollbarsInternal()) { |
|
1883 awakenScrollbarsInternal(); |
|
1884 invalidate(); |
|
1885 } |
|
1886 |
|
1887 return moved; |
|
1888 } |
|
1889 |
|
1890 /** |
|
1891 * To avoid horizontal/vertical focus searches changing the selected item, |
|
1892 * we manually focus search within the selected item (as applicable), and |
|
1893 * prevent focus from jumping to something within another item. |
|
1894 * |
|
1895 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
1896 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
1897 * current view orientation. |
|
1898 * |
|
1899 * @return Whether this consumes the key event. |
|
1900 */ |
|
1901 private boolean handleFocusWithinItem(int direction) { |
|
1902 forceValidInnerFocusDirection(direction); |
|
1903 |
|
1904 final int numChildren = getChildCount(); |
|
1905 |
|
1906 if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) { |
|
1907 final View selectedView = getSelectedView(); |
|
1908 |
|
1909 if (selectedView != null && selectedView.hasFocus() && |
|
1910 selectedView instanceof ViewGroup) { |
|
1911 |
|
1912 final View currentFocus = selectedView.findFocus(); |
|
1913 final View nextFocus = FocusFinder.getInstance().findNextFocus( |
|
1914 (ViewGroup) selectedView, currentFocus, direction); |
|
1915 |
|
1916 if (nextFocus != null) { |
|
1917 // Do the math to get interesting rect in next focus' coordinates |
|
1918 currentFocus.getFocusedRect(mTempRect); |
|
1919 offsetDescendantRectToMyCoords(currentFocus, mTempRect); |
|
1920 offsetRectIntoDescendantCoords(nextFocus, mTempRect); |
|
1921 |
|
1922 if (nextFocus.requestFocus(direction, mTempRect)) { |
|
1923 return true; |
|
1924 } |
|
1925 } |
|
1926 |
|
1927 // We are blocking the key from being handled (by returning true) |
|
1928 // if the global result is going to be some other view within this |
|
1929 // list. This is to achieve the overall goal of having horizontal/vertical |
|
1930 // d-pad navigation remain in the current item depending on the current |
|
1931 // orientation in this view. |
|
1932 final View globalNextFocus = FocusFinder.getInstance().findNextFocus( |
|
1933 (ViewGroup) getRootView(), currentFocus, direction); |
|
1934 |
|
1935 if (globalNextFocus != null) { |
|
1936 return isViewAncestorOf(globalNextFocus, this); |
|
1937 } |
|
1938 } |
|
1939 } |
|
1940 |
|
1941 return false; |
|
1942 } |
|
1943 |
|
1944 /** |
|
1945 * Scrolls to the next or previous item if possible. |
|
1946 * |
|
1947 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
1948 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
1949 * current view orientation. |
|
1950 * |
|
1951 * @return whether selection was moved |
|
1952 */ |
|
1953 private boolean arrowScroll(int direction) { |
|
1954 forceValidFocusDirection(direction); |
|
1955 |
|
1956 try { |
|
1957 mInLayout = true; |
|
1958 |
|
1959 final boolean handled = arrowScrollImpl(direction); |
|
1960 if (handled) { |
|
1961 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); |
|
1962 } |
|
1963 |
|
1964 return handled; |
|
1965 } finally { |
|
1966 mInLayout = false; |
|
1967 } |
|
1968 } |
|
1969 |
|
1970 /** |
|
1971 * When selection changes, it is possible that the previously selected or the |
|
1972 * next selected item will change its size. If so, we need to offset some folks, |
|
1973 * and re-layout the items as appropriate. |
|
1974 * |
|
1975 * @param selectedView The currently selected view (before changing selection). |
|
1976 * should be <code>null</code> if there was no previous selection. |
|
1977 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
1978 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
1979 * current view orientation. |
|
1980 * @param newSelectedPosition The position of the next selection. |
|
1981 * @param newFocusAssigned whether new focus was assigned. This matters because |
|
1982 * when something has focus, we don't want to show selection (ugh). |
|
1983 */ |
|
1984 private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, |
|
1985 boolean newFocusAssigned) { |
|
1986 forceValidFocusDirection(direction); |
|
1987 |
|
1988 if (newSelectedPosition == INVALID_POSITION) { |
|
1989 throw new IllegalArgumentException("newSelectedPosition needs to be valid"); |
|
1990 } |
|
1991 |
|
1992 // Whether or not we are moving down/right or up/left, we want to preserve the |
|
1993 // top/left of whatever view is at the start: |
|
1994 // - moving down/right: the view that had selection |
|
1995 // - moving up/left: the view that is getting selection |
|
1996 final int selectedIndex = mSelectedPosition - mFirstPosition; |
|
1997 final int nextSelectedIndex = newSelectedPosition - mFirstPosition; |
|
1998 int startViewIndex, endViewIndex; |
|
1999 boolean topSelected = false; |
|
2000 View startView; |
|
2001 View endView; |
|
2002 |
|
2003 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { |
|
2004 startViewIndex = nextSelectedIndex; |
|
2005 endViewIndex = selectedIndex; |
|
2006 startView = getChildAt(startViewIndex); |
|
2007 endView = selectedView; |
|
2008 topSelected = true; |
|
2009 } else { |
|
2010 startViewIndex = selectedIndex; |
|
2011 endViewIndex = nextSelectedIndex; |
|
2012 startView = selectedView; |
|
2013 endView = getChildAt(endViewIndex); |
|
2014 } |
|
2015 |
|
2016 final int numChildren = getChildCount(); |
|
2017 |
|
2018 // start with top view: is it changing size? |
|
2019 if (startView != null) { |
|
2020 startView.setSelected(!newFocusAssigned && topSelected); |
|
2021 measureAndAdjustDown(startView, startViewIndex, numChildren); |
|
2022 } |
|
2023 |
|
2024 // is the bottom view changing size? |
|
2025 if (endView != null) { |
|
2026 endView.setSelected(!newFocusAssigned && !topSelected); |
|
2027 measureAndAdjustDown(endView, endViewIndex, numChildren); |
|
2028 } |
|
2029 } |
|
2030 |
|
2031 /** |
|
2032 * Re-measure a child, and if its height changes, lay it out preserving its |
|
2033 * top, and adjust the children below it appropriately. |
|
2034 * |
|
2035 * @param child The child |
|
2036 * @param childIndex The view group index of the child. |
|
2037 * @param numChildren The number of children in the view group. |
|
2038 */ |
|
2039 private void measureAndAdjustDown(View child, int childIndex, int numChildren) { |
|
2040 int oldHeight = child.getHeight(); |
|
2041 measureChild(child); |
|
2042 |
|
2043 if (child.getMeasuredHeight() == oldHeight) { |
|
2044 return; |
|
2045 } |
|
2046 |
|
2047 // lay out the view, preserving its top |
|
2048 relayoutMeasuredChild(child); |
|
2049 |
|
2050 // adjust views below appropriately |
|
2051 final int heightDelta = child.getMeasuredHeight() - oldHeight; |
|
2052 for (int i = childIndex + 1; i < numChildren; i++) { |
|
2053 getChildAt(i).offsetTopAndBottom(heightDelta); |
|
2054 } |
|
2055 } |
|
2056 |
|
2057 /** |
|
2058 * Do an arrow scroll based on focus searching. If a new view is |
|
2059 * given focus, return the selection delta and amount to scroll via |
|
2060 * an {@link ArrowScrollFocusResult}, otherwise, return null. |
|
2061 * |
|
2062 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
2063 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
2064 * current view orientation. |
|
2065 * |
|
2066 * @return The result if focus has changed, or <code>null</code>. |
|
2067 */ |
|
2068 private ArrowScrollFocusResult arrowScrollFocused(final int direction) { |
|
2069 forceValidFocusDirection(direction); |
|
2070 |
|
2071 final View selectedView = getSelectedView(); |
|
2072 final View newFocus; |
|
2073 final int searchPoint; |
|
2074 |
|
2075 if (selectedView != null && selectedView.hasFocus()) { |
|
2076 View oldFocus = selectedView.findFocus(); |
|
2077 newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction); |
|
2078 } else { |
|
2079 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { |
|
2080 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
2081 |
|
2082 final int selectedStart; |
|
2083 if (selectedView != null) { |
|
2084 selectedStart = (mIsVertical ? selectedView.getTop() : selectedView.getLeft()); |
|
2085 } else { |
|
2086 selectedStart = start; |
|
2087 } |
|
2088 |
|
2089 searchPoint = Math.max(selectedStart, start); |
|
2090 } else { |
|
2091 final int end = (mIsVertical ? getHeight() - getPaddingBottom() : |
|
2092 getWidth() - getPaddingRight()); |
|
2093 |
|
2094 final int selectedEnd; |
|
2095 if (selectedView != null) { |
|
2096 selectedEnd = (mIsVertical ? selectedView.getBottom() : selectedView.getRight()); |
|
2097 } else { |
|
2098 selectedEnd = end; |
|
2099 } |
|
2100 |
|
2101 searchPoint = Math.min(selectedEnd, end); |
|
2102 } |
|
2103 |
|
2104 final int x = (mIsVertical ? 0 : searchPoint); |
|
2105 final int y = (mIsVertical ? searchPoint : 0); |
|
2106 mTempRect.set(x, y, x, y); |
|
2107 |
|
2108 newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction); |
|
2109 } |
|
2110 |
|
2111 if (newFocus != null) { |
|
2112 final int positionOfNewFocus = positionOfNewFocus(newFocus); |
|
2113 |
|
2114 // If the focus change is in a different new position, make sure |
|
2115 // we aren't jumping over another selectable position. |
|
2116 if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) { |
|
2117 final int selectablePosition = lookForSelectablePositionOnScreen(direction); |
|
2118 |
|
2119 final boolean movingForward = |
|
2120 (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT); |
|
2121 final boolean movingBackward = |
|
2122 (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT); |
|
2123 |
|
2124 if (selectablePosition != INVALID_POSITION && |
|
2125 ((movingForward && selectablePosition < positionOfNewFocus) || |
|
2126 (movingBackward && selectablePosition > positionOfNewFocus))) { |
|
2127 return null; |
|
2128 } |
|
2129 } |
|
2130 |
|
2131 int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus); |
|
2132 |
|
2133 final int maxScrollAmount = getMaxScrollAmount(); |
|
2134 if (focusScroll < maxScrollAmount) { |
|
2135 // Not moving too far, safe to give next view focus |
|
2136 newFocus.requestFocus(direction); |
|
2137 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll); |
|
2138 return mArrowScrollFocusResult; |
|
2139 } else if (distanceToView(newFocus) < maxScrollAmount){ |
|
2140 // Case to consider: |
|
2141 // Too far to get entire next focusable on screen, but by going |
|
2142 // max scroll amount, we are getting it at least partially in view, |
|
2143 // so give it focus and scroll the max amount. |
|
2144 newFocus.requestFocus(direction); |
|
2145 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount); |
|
2146 return mArrowScrollFocusResult; |
|
2147 } |
|
2148 } |
|
2149 |
|
2150 return null; |
|
2151 } |
|
2152 |
|
2153 /** |
|
2154 * @return The maximum amount a list view will scroll in response to |
|
2155 * an arrow event. |
|
2156 */ |
|
2157 public int getMaxScrollAmount() { |
|
2158 return (int) (MAX_SCROLL_FACTOR * getHeight()); |
|
2159 } |
|
2160 |
|
2161 /** |
|
2162 * @return The amount to preview next items when arrow scrolling. |
|
2163 */ |
|
2164 private int getArrowScrollPreviewLength() { |
|
2165 // FIXME: TwoWayView has no fading edge support just yet but using it |
|
2166 // makes it convenient for defining the next item's previous length. |
|
2167 int fadingEdgeLength = |
|
2168 (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength()); |
|
2169 |
|
2170 return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, fadingEdgeLength); |
|
2171 } |
|
2172 |
|
2173 /** |
|
2174 * @param newFocus The view that would have focus. |
|
2175 * @return the position that contains newFocus |
|
2176 */ |
|
2177 private int positionOfNewFocus(View newFocus) { |
|
2178 final int numChildren = getChildCount(); |
|
2179 |
|
2180 for (int i = 0; i < numChildren; i++) { |
|
2181 final View child = getChildAt(i); |
|
2182 if (isViewAncestorOf(newFocus, child)) { |
|
2183 return mFirstPosition + i; |
|
2184 } |
|
2185 } |
|
2186 |
|
2187 throw new IllegalArgumentException("newFocus is not a child of any of the" |
|
2188 + " children of the list!"); |
|
2189 } |
|
2190 |
|
2191 /** |
|
2192 * Handle an arrow scroll going up or down. Take into account whether items are selectable, |
|
2193 * whether there are focusable items, etc. |
|
2194 * |
|
2195 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
2196 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
2197 * current view orientation. |
|
2198 * |
|
2199 * @return Whether any scrolling, selection or focus change occurred. |
|
2200 */ |
|
2201 private boolean arrowScrollImpl(int direction) { |
|
2202 forceValidFocusDirection(direction); |
|
2203 |
|
2204 if (getChildCount() <= 0) { |
|
2205 return false; |
|
2206 } |
|
2207 |
|
2208 View selectedView = getSelectedView(); |
|
2209 int selectedPos = mSelectedPosition; |
|
2210 |
|
2211 int nextSelectedPosition = lookForSelectablePositionOnScreen(direction); |
|
2212 int amountToScroll = amountToScroll(direction, nextSelectedPosition); |
|
2213 |
|
2214 // If we are moving focus, we may OVERRIDE the default behaviour |
|
2215 final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null); |
|
2216 if (focusResult != null) { |
|
2217 nextSelectedPosition = focusResult.getSelectedPosition(); |
|
2218 amountToScroll = focusResult.getAmountToScroll(); |
|
2219 } |
|
2220 |
|
2221 boolean needToRedraw = (focusResult != null); |
|
2222 if (nextSelectedPosition != INVALID_POSITION) { |
|
2223 handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null); |
|
2224 |
|
2225 setSelectedPositionInt(nextSelectedPosition); |
|
2226 setNextSelectedPositionInt(nextSelectedPosition); |
|
2227 |
|
2228 selectedView = getSelectedView(); |
|
2229 selectedPos = nextSelectedPosition; |
|
2230 |
|
2231 if (mItemsCanFocus && focusResult == null) { |
|
2232 // There was no new view found to take focus, make sure we |
|
2233 // don't leave focus with the old selection. |
|
2234 final View focused = getFocusedChild(); |
|
2235 if (focused != null) { |
|
2236 focused.clearFocus(); |
|
2237 } |
|
2238 } |
|
2239 |
|
2240 needToRedraw = true; |
|
2241 checkSelectionChanged(); |
|
2242 } |
|
2243 |
|
2244 if (amountToScroll > 0) { |
|
2245 trackMotionScroll(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ? |
|
2246 amountToScroll : -amountToScroll); |
|
2247 needToRedraw = true; |
|
2248 } |
|
2249 |
|
2250 // If we didn't find a new focusable, make sure any existing focused |
|
2251 // item that was panned off screen gives up focus. |
|
2252 if (mItemsCanFocus && focusResult == null && |
|
2253 selectedView != null && selectedView.hasFocus()) { |
|
2254 final View focused = selectedView.findFocus(); |
|
2255 if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) { |
|
2256 focused.clearFocus(); |
|
2257 } |
|
2258 } |
|
2259 |
|
2260 // If the current selection is panned off, we need to remove the selection |
|
2261 if (nextSelectedPosition == INVALID_POSITION && selectedView != null |
|
2262 && !isViewAncestorOf(selectedView, this)) { |
|
2263 selectedView = null; |
|
2264 hideSelector(); |
|
2265 |
|
2266 // But we don't want to set the ressurect position (that would make subsequent |
|
2267 // unhandled key events bring back the item we just scrolled off) |
|
2268 mResurrectToPosition = INVALID_POSITION; |
|
2269 } |
|
2270 |
|
2271 if (needToRedraw) { |
|
2272 if (selectedView != null) { |
|
2273 positionSelector(selectedPos, selectedView); |
|
2274 mSelectedStart = selectedView.getTop(); |
|
2275 } |
|
2276 |
|
2277 if (!awakenScrollbarsInternal()) { |
|
2278 invalidate(); |
|
2279 } |
|
2280 |
|
2281 invokeOnItemScrollListener(); |
|
2282 return true; |
|
2283 } |
|
2284 |
|
2285 return false; |
|
2286 } |
|
2287 |
|
2288 /** |
|
2289 * Determine how much we need to scroll in order to get the next selected view |
|
2290 * visible. The amount is capped at {@link #getMaxScrollAmount()}. |
|
2291 * |
|
2292 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
2293 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
2294 * current view orientation. |
|
2295 * @param nextSelectedPosition The position of the next selection, or |
|
2296 * {@link #INVALID_POSITION} if there is no next selectable position |
|
2297 * |
|
2298 * @return The amount to scroll. Note: this is always positive! Direction |
|
2299 * needs to be taken into account when actually scrolling. |
|
2300 */ |
|
2301 private int amountToScroll(int direction, int nextSelectedPosition) { |
|
2302 forceValidFocusDirection(direction); |
|
2303 |
|
2304 final int numChildren = getChildCount(); |
|
2305 |
|
2306 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { |
|
2307 final int end = (mIsVertical ? getHeight() - getPaddingBottom() : |
|
2308 getWidth() - getPaddingRight()); |
|
2309 |
|
2310 int indexToMakeVisible = numChildren - 1; |
|
2311 if (nextSelectedPosition != INVALID_POSITION) { |
|
2312 indexToMakeVisible = nextSelectedPosition - mFirstPosition; |
|
2313 } |
|
2314 |
|
2315 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; |
|
2316 final View viewToMakeVisible = getChildAt(indexToMakeVisible); |
|
2317 |
|
2318 int goalEnd = end; |
|
2319 if (positionToMakeVisible < mItemCount - 1) { |
|
2320 goalEnd -= getArrowScrollPreviewLength(); |
|
2321 } |
|
2322 |
|
2323 final int viewToMakeVisibleStart = |
|
2324 (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft()); |
|
2325 final int viewToMakeVisibleEnd = |
|
2326 (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight()); |
|
2327 |
|
2328 if (viewToMakeVisibleEnd <= goalEnd) { |
|
2329 // Target item is fully visible |
|
2330 return 0; |
|
2331 } |
|
2332 |
|
2333 if (nextSelectedPosition != INVALID_POSITION && |
|
2334 (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) { |
|
2335 // Item already has enough of it visible, changing selection is good enough |
|
2336 return 0; |
|
2337 } |
|
2338 |
|
2339 int amountToScroll = (viewToMakeVisibleEnd - goalEnd); |
|
2340 |
|
2341 if (mFirstPosition + numChildren == mItemCount) { |
|
2342 final View lastChild = getChildAt(numChildren - 1); |
|
2343 final int lastChildEnd = (mIsVertical ? lastChild.getBottom() : lastChild.getRight()); |
|
2344 |
|
2345 // Last is last in list -> Make sure we don't scroll past it |
|
2346 final int max = lastChildEnd - end; |
|
2347 amountToScroll = Math.min(amountToScroll, max); |
|
2348 } |
|
2349 |
|
2350 return Math.min(amountToScroll, getMaxScrollAmount()); |
|
2351 } else { |
|
2352 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
2353 |
|
2354 int indexToMakeVisible = 0; |
|
2355 if (nextSelectedPosition != INVALID_POSITION) { |
|
2356 indexToMakeVisible = nextSelectedPosition - mFirstPosition; |
|
2357 } |
|
2358 |
|
2359 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible; |
|
2360 final View viewToMakeVisible = getChildAt(indexToMakeVisible); |
|
2361 |
|
2362 int goalStart = start; |
|
2363 if (positionToMakeVisible > 0) { |
|
2364 goalStart += getArrowScrollPreviewLength(); |
|
2365 } |
|
2366 |
|
2367 final int viewToMakeVisibleStart = |
|
2368 (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft()); |
|
2369 final int viewToMakeVisibleEnd = |
|
2370 (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight()); |
|
2371 |
|
2372 if (viewToMakeVisibleStart >= goalStart) { |
|
2373 // Item is fully visible |
|
2374 return 0; |
|
2375 } |
|
2376 |
|
2377 if (nextSelectedPosition != INVALID_POSITION && |
|
2378 (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) { |
|
2379 // Item already has enough of it visible, changing selection is good enough |
|
2380 return 0; |
|
2381 } |
|
2382 |
|
2383 int amountToScroll = (goalStart - viewToMakeVisibleStart); |
|
2384 |
|
2385 if (mFirstPosition == 0) { |
|
2386 final View firstChild = getChildAt(0); |
|
2387 final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); |
|
2388 |
|
2389 // First is first in list -> make sure we don't scroll past it |
|
2390 final int max = start - firstChildStart; |
|
2391 amountToScroll = Math.min(amountToScroll, max); |
|
2392 } |
|
2393 |
|
2394 return Math.min(amountToScroll, getMaxScrollAmount()); |
|
2395 } |
|
2396 } |
|
2397 |
|
2398 /** |
|
2399 * Determine how much we need to scroll in order to get newFocus in view. |
|
2400 * |
|
2401 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
2402 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
2403 * current view orientation. |
|
2404 * @param newFocus The view that would take focus. |
|
2405 * @param positionOfNewFocus The position of the list item containing newFocus |
|
2406 * |
|
2407 * @return The amount to scroll. Note: this is always positive! Direction |
|
2408 * needs to be taken into account when actually scrolling. |
|
2409 */ |
|
2410 private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) { |
|
2411 forceValidFocusDirection(direction); |
|
2412 |
|
2413 int amountToScroll = 0; |
|
2414 |
|
2415 newFocus.getDrawingRect(mTempRect); |
|
2416 offsetDescendantRectToMyCoords(newFocus, mTempRect); |
|
2417 |
|
2418 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) { |
|
2419 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
2420 final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left); |
|
2421 |
|
2422 if (newFocusStart < start) { |
|
2423 amountToScroll = start - newFocusStart; |
|
2424 if (positionOfNewFocus > 0) { |
|
2425 amountToScroll += getArrowScrollPreviewLength(); |
|
2426 } |
|
2427 } |
|
2428 } else { |
|
2429 final int end = (mIsVertical ? getHeight() - getPaddingBottom() : |
|
2430 getWidth() - getPaddingRight()); |
|
2431 final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); |
|
2432 |
|
2433 if (newFocusEnd > end) { |
|
2434 amountToScroll = newFocusEnd - end; |
|
2435 if (positionOfNewFocus < mItemCount - 1) { |
|
2436 amountToScroll += getArrowScrollPreviewLength(); |
|
2437 } |
|
2438 } |
|
2439 } |
|
2440 |
|
2441 return amountToScroll; |
|
2442 } |
|
2443 |
|
2444 /** |
|
2445 * Determine the distance to the nearest edge of a view in a particular |
|
2446 * direction. |
|
2447 * |
|
2448 * @param descendant A descendant of this list. |
|
2449 * @return The distance, or 0 if the nearest edge is already on screen. |
|
2450 */ |
|
2451 private int distanceToView(View descendant) { |
|
2452 descendant.getDrawingRect(mTempRect); |
|
2453 offsetDescendantRectToMyCoords(descendant, mTempRect); |
|
2454 |
|
2455 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
2456 final int end = (mIsVertical ? getHeight() - getPaddingBottom() : |
|
2457 getWidth() - getPaddingRight()); |
|
2458 |
|
2459 final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left); |
|
2460 final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right); |
|
2461 |
|
2462 int distance = 0; |
|
2463 if (viewEnd < start) { |
|
2464 distance = start - viewEnd; |
|
2465 } else if (viewStart > end) { |
|
2466 distance = viewStart - end; |
|
2467 } |
|
2468 |
|
2469 return distance; |
|
2470 } |
|
2471 |
|
2472 private boolean handleKeyScroll(KeyEvent event, int count, int direction) { |
|
2473 boolean handled = false; |
|
2474 |
|
2475 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2476 handled = resurrectSelectionIfNeeded(); |
|
2477 if (!handled) { |
|
2478 while (count-- > 0) { |
|
2479 if (arrowScroll(direction)) { |
|
2480 handled = true; |
|
2481 } else { |
|
2482 break; |
|
2483 } |
|
2484 } |
|
2485 } |
|
2486 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { |
|
2487 handled = resurrectSelectionIfNeeded() || fullScroll(direction); |
|
2488 } |
|
2489 |
|
2490 return handled; |
|
2491 } |
|
2492 |
|
2493 private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) { |
|
2494 if (mAdapter == null || !mIsAttached) { |
|
2495 return false; |
|
2496 } |
|
2497 |
|
2498 if (mDataChanged) { |
|
2499 layoutChildren(); |
|
2500 } |
|
2501 |
|
2502 boolean handled = false; |
|
2503 final int action = event.getAction(); |
|
2504 |
|
2505 if (action != KeyEvent.ACTION_UP) { |
|
2506 switch (keyCode) { |
|
2507 case KeyEvent.KEYCODE_DPAD_UP: |
|
2508 if (mIsVertical) { |
|
2509 handled = handleKeyScroll(event, count, View.FOCUS_UP); |
|
2510 } else if (KeyEventCompat.hasNoModifiers(event)) { |
|
2511 handled = handleFocusWithinItem(View.FOCUS_UP); |
|
2512 } |
|
2513 break; |
|
2514 |
|
2515 case KeyEvent.KEYCODE_DPAD_DOWN: { |
|
2516 if (mIsVertical) { |
|
2517 handled = handleKeyScroll(event, count, View.FOCUS_DOWN); |
|
2518 } else if (KeyEventCompat.hasNoModifiers(event)) { |
|
2519 handled = handleFocusWithinItem(View.FOCUS_DOWN); |
|
2520 } |
|
2521 break; |
|
2522 } |
|
2523 |
|
2524 case KeyEvent.KEYCODE_DPAD_LEFT: |
|
2525 if (!mIsVertical) { |
|
2526 handled = handleKeyScroll(event, count, View.FOCUS_LEFT); |
|
2527 } else if (KeyEventCompat.hasNoModifiers(event)) { |
|
2528 handled = handleFocusWithinItem(View.FOCUS_LEFT); |
|
2529 } |
|
2530 break; |
|
2531 |
|
2532 case KeyEvent.KEYCODE_DPAD_RIGHT: |
|
2533 if (!mIsVertical) { |
|
2534 handled = handleKeyScroll(event, count, View.FOCUS_RIGHT); |
|
2535 } else if (KeyEventCompat.hasNoModifiers(event)) { |
|
2536 handled = handleFocusWithinItem(View.FOCUS_RIGHT); |
|
2537 } |
|
2538 break; |
|
2539 |
|
2540 case KeyEvent.KEYCODE_DPAD_CENTER: |
|
2541 case KeyEvent.KEYCODE_ENTER: |
|
2542 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2543 handled = resurrectSelectionIfNeeded(); |
|
2544 if (!handled |
|
2545 && event.getRepeatCount() == 0 && getChildCount() > 0) { |
|
2546 keyPressed(); |
|
2547 handled = true; |
|
2548 } |
|
2549 } |
|
2550 break; |
|
2551 |
|
2552 case KeyEvent.KEYCODE_SPACE: |
|
2553 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2554 handled = resurrectSelectionIfNeeded() || |
|
2555 pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); |
|
2556 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { |
|
2557 handled = resurrectSelectionIfNeeded() || |
|
2558 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); |
|
2559 } |
|
2560 |
|
2561 handled = true; |
|
2562 break; |
|
2563 |
|
2564 case KeyEvent.KEYCODE_PAGE_UP: |
|
2565 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2566 handled = resurrectSelectionIfNeeded() || |
|
2567 pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); |
|
2568 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { |
|
2569 handled = resurrectSelectionIfNeeded() || |
|
2570 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); |
|
2571 } |
|
2572 break; |
|
2573 |
|
2574 case KeyEvent.KEYCODE_PAGE_DOWN: |
|
2575 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2576 handled = resurrectSelectionIfNeeded() || |
|
2577 pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); |
|
2578 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) { |
|
2579 handled = resurrectSelectionIfNeeded() || |
|
2580 fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); |
|
2581 } |
|
2582 break; |
|
2583 |
|
2584 case KeyEvent.KEYCODE_MOVE_HOME: |
|
2585 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2586 handled = resurrectSelectionIfNeeded() || |
|
2587 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT); |
|
2588 } |
|
2589 break; |
|
2590 |
|
2591 case KeyEvent.KEYCODE_MOVE_END: |
|
2592 if (KeyEventCompat.hasNoModifiers(event)) { |
|
2593 handled = resurrectSelectionIfNeeded() || |
|
2594 fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT); |
|
2595 } |
|
2596 break; |
|
2597 } |
|
2598 } |
|
2599 |
|
2600 if (handled) { |
|
2601 return true; |
|
2602 } |
|
2603 |
|
2604 switch (action) { |
|
2605 case KeyEvent.ACTION_DOWN: |
|
2606 return super.onKeyDown(keyCode, event); |
|
2607 |
|
2608 case KeyEvent.ACTION_UP: |
|
2609 if (!isEnabled()) { |
|
2610 return true; |
|
2611 } |
|
2612 |
|
2613 if (isClickable() && isPressed() && |
|
2614 mSelectedPosition >= 0 && mAdapter != null && |
|
2615 mSelectedPosition < mAdapter.getCount()) { |
|
2616 |
|
2617 final View child = getChildAt(mSelectedPosition - mFirstPosition); |
|
2618 if (child != null) { |
|
2619 performItemClick(child, mSelectedPosition, mSelectedRowId); |
|
2620 child.setPressed(false); |
|
2621 } |
|
2622 |
|
2623 setPressed(false); |
|
2624 return true; |
|
2625 } |
|
2626 |
|
2627 return false; |
|
2628 |
|
2629 case KeyEvent.ACTION_MULTIPLE: |
|
2630 return super.onKeyMultiple(keyCode, count, event); |
|
2631 |
|
2632 default: |
|
2633 return false; |
|
2634 } |
|
2635 } |
|
2636 |
|
2637 private void initOrResetVelocityTracker() { |
|
2638 if (mVelocityTracker == null) { |
|
2639 mVelocityTracker = VelocityTracker.obtain(); |
|
2640 } else { |
|
2641 mVelocityTracker.clear(); |
|
2642 } |
|
2643 } |
|
2644 |
|
2645 private void initVelocityTrackerIfNotExists() { |
|
2646 if (mVelocityTracker == null) { |
|
2647 mVelocityTracker = VelocityTracker.obtain(); |
|
2648 } |
|
2649 } |
|
2650 |
|
2651 private void recycleVelocityTracker() { |
|
2652 if (mVelocityTracker != null) { |
|
2653 mVelocityTracker.recycle(); |
|
2654 mVelocityTracker = null; |
|
2655 } |
|
2656 } |
|
2657 |
|
2658 /** |
|
2659 * Notify our scroll listener (if there is one) of a change in scroll state |
|
2660 */ |
|
2661 private void invokeOnItemScrollListener() { |
|
2662 if (mOnScrollListener != null) { |
|
2663 mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); |
|
2664 } |
|
2665 |
|
2666 // Dummy values, View's implementation does not use these. |
|
2667 onScrollChanged(0, 0, 0, 0); |
|
2668 } |
|
2669 |
|
2670 private void reportScrollStateChange(int newState) { |
|
2671 if (newState == mLastScrollState) { |
|
2672 return; |
|
2673 } |
|
2674 |
|
2675 if (mOnScrollListener != null) { |
|
2676 mLastScrollState = newState; |
|
2677 mOnScrollListener.onScrollStateChanged(this, newState); |
|
2678 } |
|
2679 } |
|
2680 |
|
2681 private boolean maybeStartScrolling(int delta) { |
|
2682 final boolean isOverScroll = (mOverScroll != 0); |
|
2683 if (Math.abs(delta) <= mTouchSlop && !isOverScroll) { |
|
2684 return false; |
|
2685 } |
|
2686 |
|
2687 if (isOverScroll) { |
|
2688 mTouchMode = TOUCH_MODE_OVERSCROLL; |
|
2689 } else { |
|
2690 mTouchMode = TOUCH_MODE_DRAGGING; |
|
2691 } |
|
2692 |
|
2693 // Time to start stealing events! Once we've stolen them, don't |
|
2694 // let anyone steal from us. |
|
2695 final ViewParent parent = getParent(); |
|
2696 if (parent != null) { |
|
2697 parent.requestDisallowInterceptTouchEvent(true); |
|
2698 } |
|
2699 |
|
2700 cancelCheckForLongPress(); |
|
2701 |
|
2702 setPressed(false); |
|
2703 View motionView = getChildAt(mMotionPosition - mFirstPosition); |
|
2704 if (motionView != null) { |
|
2705 motionView.setPressed(false); |
|
2706 } |
|
2707 |
|
2708 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); |
|
2709 |
|
2710 return true; |
|
2711 } |
|
2712 |
|
2713 private void maybeScroll(int delta) { |
|
2714 if (mTouchMode == TOUCH_MODE_DRAGGING) { |
|
2715 handleDragChange(delta); |
|
2716 } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { |
|
2717 handleOverScrollChange(delta); |
|
2718 } |
|
2719 } |
|
2720 |
|
2721 private void handleDragChange(int delta) { |
|
2722 // Time to start stealing events! Once we've stolen them, don't |
|
2723 // let anyone steal from us. |
|
2724 final ViewParent parent = getParent(); |
|
2725 if (parent != null) { |
|
2726 parent.requestDisallowInterceptTouchEvent(true); |
|
2727 } |
|
2728 |
|
2729 final int motionIndex; |
|
2730 if (mMotionPosition >= 0) { |
|
2731 motionIndex = mMotionPosition - mFirstPosition; |
|
2732 } else { |
|
2733 // If we don't have a motion position that we can reliably track, |
|
2734 // pick something in the middle to make a best guess at things below. |
|
2735 motionIndex = getChildCount() / 2; |
|
2736 } |
|
2737 |
|
2738 int motionViewPrevStart = 0; |
|
2739 View motionView = this.getChildAt(motionIndex); |
|
2740 if (motionView != null) { |
|
2741 motionViewPrevStart = (mIsVertical ? motionView.getTop() : motionView.getLeft()); |
|
2742 } |
|
2743 |
|
2744 boolean atEdge = trackMotionScroll(delta); |
|
2745 |
|
2746 motionView = this.getChildAt(motionIndex); |
|
2747 if (motionView != null) { |
|
2748 final int motionViewRealStart = |
|
2749 (mIsVertical ? motionView.getTop() : motionView.getLeft()); |
|
2750 |
|
2751 if (atEdge) { |
|
2752 final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart); |
|
2753 updateOverScrollState(delta, overscroll); |
|
2754 } |
|
2755 } |
|
2756 } |
|
2757 |
|
2758 private void updateOverScrollState(int delta, int overscroll) { |
|
2759 overScrollByInternal((mIsVertical ? 0 : overscroll), |
|
2760 (mIsVertical ? overscroll : 0), |
|
2761 (mIsVertical ? 0 : mOverScroll), |
|
2762 (mIsVertical ? mOverScroll : 0), |
|
2763 0, 0, |
|
2764 (mIsVertical ? 0 : mOverscrollDistance), |
|
2765 (mIsVertical ? mOverscrollDistance : 0), |
|
2766 true); |
|
2767 |
|
2768 if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) { |
|
2769 // Break fling velocity if we impacted an edge |
|
2770 if (mVelocityTracker != null) { |
|
2771 mVelocityTracker.clear(); |
|
2772 } |
|
2773 } |
|
2774 |
|
2775 final int overscrollMode = ViewCompat.getOverScrollMode(this); |
|
2776 if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || |
|
2777 (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { |
|
2778 mTouchMode = TOUCH_MODE_OVERSCROLL; |
|
2779 |
|
2780 float pull = (float) overscroll / (mIsVertical ? getHeight() : getWidth()); |
|
2781 if (delta > 0) { |
|
2782 mStartEdge.onPull(pull); |
|
2783 |
|
2784 if (!mEndEdge.isFinished()) { |
|
2785 mEndEdge.onRelease(); |
|
2786 } |
|
2787 } else if (delta < 0) { |
|
2788 mEndEdge.onPull(pull); |
|
2789 |
|
2790 if (!mStartEdge.isFinished()) { |
|
2791 mStartEdge.onRelease(); |
|
2792 } |
|
2793 } |
|
2794 |
|
2795 if (delta != 0) { |
|
2796 ViewCompat.postInvalidateOnAnimation(this); |
|
2797 } |
|
2798 } |
|
2799 } |
|
2800 |
|
2801 private void handleOverScrollChange(int delta) { |
|
2802 final int oldOverScroll = mOverScroll; |
|
2803 final int newOverScroll = oldOverScroll - delta; |
|
2804 |
|
2805 int overScrollDistance = -delta; |
|
2806 if ((newOverScroll < 0 && oldOverScroll >= 0) || |
|
2807 (newOverScroll > 0 && oldOverScroll <= 0)) { |
|
2808 overScrollDistance = -oldOverScroll; |
|
2809 delta += overScrollDistance; |
|
2810 } else { |
|
2811 delta = 0; |
|
2812 } |
|
2813 |
|
2814 if (overScrollDistance != 0) { |
|
2815 updateOverScrollState(delta, overScrollDistance); |
|
2816 } |
|
2817 |
|
2818 if (delta != 0) { |
|
2819 if (mOverScroll != 0) { |
|
2820 mOverScroll = 0; |
|
2821 ViewCompat.postInvalidateOnAnimation(this); |
|
2822 } |
|
2823 |
|
2824 trackMotionScroll(delta); |
|
2825 mTouchMode = TOUCH_MODE_DRAGGING; |
|
2826 |
|
2827 // We did not scroll the full amount. Treat this essentially like the |
|
2828 // start of a new touch scroll |
|
2829 mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos); |
|
2830 mTouchRemainderPos = 0; |
|
2831 } |
|
2832 } |
|
2833 |
|
2834 /** |
|
2835 * What is the distance between the source and destination rectangles given the direction of |
|
2836 * focus navigation between them? The direction basically helps figure out more quickly what is |
|
2837 * self evident by the relationship between the rects... |
|
2838 * |
|
2839 * @param source the source rectangle |
|
2840 * @param dest the destination rectangle |
|
2841 * @param direction the direction |
|
2842 * @return the distance between the rectangles |
|
2843 */ |
|
2844 private static int getDistance(Rect source, Rect dest, int direction) { |
|
2845 int sX, sY; // source x, y |
|
2846 int dX, dY; // dest x, y |
|
2847 |
|
2848 switch (direction) { |
|
2849 case View.FOCUS_RIGHT: |
|
2850 sX = source.right; |
|
2851 sY = source.top + source.height() / 2; |
|
2852 dX = dest.left; |
|
2853 dY = dest.top + dest.height() / 2; |
|
2854 break; |
|
2855 |
|
2856 case View.FOCUS_DOWN: |
|
2857 sX = source.left + source.width() / 2; |
|
2858 sY = source.bottom; |
|
2859 dX = dest.left + dest.width() / 2; |
|
2860 dY = dest.top; |
|
2861 break; |
|
2862 |
|
2863 case View.FOCUS_LEFT: |
|
2864 sX = source.left; |
|
2865 sY = source.top + source.height() / 2; |
|
2866 dX = dest.right; |
|
2867 dY = dest.top + dest.height() / 2; |
|
2868 break; |
|
2869 |
|
2870 case View.FOCUS_UP: |
|
2871 sX = source.left + source.width() / 2; |
|
2872 sY = source.top; |
|
2873 dX = dest.left + dest.width() / 2; |
|
2874 dY = dest.bottom; |
|
2875 break; |
|
2876 |
|
2877 case View.FOCUS_FORWARD: |
|
2878 case View.FOCUS_BACKWARD: |
|
2879 sX = source.right + source.width() / 2; |
|
2880 sY = source.top + source.height() / 2; |
|
2881 dX = dest.left + dest.width() / 2; |
|
2882 dY = dest.top + dest.height() / 2; |
|
2883 break; |
|
2884 |
|
2885 default: |
|
2886 throw new IllegalArgumentException("direction must be one of " |
|
2887 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " |
|
2888 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); |
|
2889 } |
|
2890 |
|
2891 int deltaX = dX - sX; |
|
2892 int deltaY = dY - sY; |
|
2893 |
|
2894 return deltaY * deltaY + deltaX * deltaX; |
|
2895 } |
|
2896 |
|
2897 private int findMotionRowOrColumn(int motionPos) { |
|
2898 int childCount = getChildCount(); |
|
2899 if (childCount == 0) { |
|
2900 return INVALID_POSITION; |
|
2901 } |
|
2902 |
|
2903 for (int i = 0; i < childCount; i++) { |
|
2904 View v = getChildAt(i); |
|
2905 |
|
2906 if ((mIsVertical && motionPos <= v.getBottom()) || |
|
2907 (!mIsVertical && motionPos <= v.getRight())) { |
|
2908 return mFirstPosition + i; |
|
2909 } |
|
2910 } |
|
2911 |
|
2912 return INVALID_POSITION; |
|
2913 } |
|
2914 |
|
2915 private int findClosestMotionRowOrColumn(int motionPos) { |
|
2916 final int childCount = getChildCount(); |
|
2917 if (childCount == 0) { |
|
2918 return INVALID_POSITION; |
|
2919 } |
|
2920 |
|
2921 final int motionRow = findMotionRowOrColumn(motionPos); |
|
2922 if (motionRow != INVALID_POSITION) { |
|
2923 return motionRow; |
|
2924 } else { |
|
2925 return mFirstPosition + childCount - 1; |
|
2926 } |
|
2927 } |
|
2928 |
|
2929 @TargetApi(9) |
|
2930 private int getScaledOverscrollDistance(ViewConfiguration vc) { |
|
2931 if (Build.VERSION.SDK_INT < 9) { |
|
2932 return 0; |
|
2933 } |
|
2934 |
|
2935 return vc.getScaledOverscrollDistance(); |
|
2936 } |
|
2937 |
|
2938 private boolean contentFits() { |
|
2939 final int childCount = getChildCount(); |
|
2940 if (childCount == 0) { |
|
2941 return true; |
|
2942 } |
|
2943 |
|
2944 if (childCount != mItemCount) { |
|
2945 return false; |
|
2946 } |
|
2947 |
|
2948 View first = getChildAt(0); |
|
2949 View last = getChildAt(childCount - 1); |
|
2950 |
|
2951 if (mIsVertical) { |
|
2952 return first.getTop() >= getPaddingTop() && |
|
2953 last.getBottom() <= getHeight() - getPaddingBottom(); |
|
2954 } else { |
|
2955 return first.getLeft() >= getPaddingLeft() && |
|
2956 last.getRight() <= getWidth() - getPaddingRight(); |
|
2957 } |
|
2958 } |
|
2959 |
|
2960 private void updateScrollbarsDirection() { |
|
2961 setHorizontalScrollBarEnabled(!mIsVertical); |
|
2962 setVerticalScrollBarEnabled(mIsVertical); |
|
2963 } |
|
2964 |
|
2965 private void triggerCheckForTap() { |
|
2966 if (mPendingCheckForTap == null) { |
|
2967 mPendingCheckForTap = new CheckForTap(); |
|
2968 } |
|
2969 |
|
2970 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); |
|
2971 } |
|
2972 |
|
2973 private void cancelCheckForTap() { |
|
2974 if (mPendingCheckForTap == null) { |
|
2975 return; |
|
2976 } |
|
2977 |
|
2978 removeCallbacks(mPendingCheckForTap); |
|
2979 } |
|
2980 |
|
2981 private void triggerCheckForLongPress() { |
|
2982 if (mPendingCheckForLongPress == null) { |
|
2983 mPendingCheckForLongPress = new CheckForLongPress(); |
|
2984 } |
|
2985 |
|
2986 mPendingCheckForLongPress.rememberWindowAttachCount(); |
|
2987 |
|
2988 postDelayed(mPendingCheckForLongPress, |
|
2989 ViewConfiguration.getLongPressTimeout()); |
|
2990 } |
|
2991 |
|
2992 private void cancelCheckForLongPress() { |
|
2993 if (mPendingCheckForLongPress == null) { |
|
2994 return; |
|
2995 } |
|
2996 |
|
2997 removeCallbacks(mPendingCheckForLongPress); |
|
2998 } |
|
2999 |
|
3000 boolean trackMotionScroll(int incrementalDelta) { |
|
3001 final int childCount = getChildCount(); |
|
3002 if (childCount == 0) { |
|
3003 return true; |
|
3004 } |
|
3005 |
|
3006 final View first = getChildAt(0); |
|
3007 final int firstStart = (mIsVertical ? first.getTop() : first.getLeft()); |
|
3008 |
|
3009 final View last = getChildAt(childCount - 1); |
|
3010 final int lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); |
|
3011 |
|
3012 final int paddingTop = getPaddingTop(); |
|
3013 final int paddingBottom = getPaddingBottom(); |
|
3014 final int paddingLeft = getPaddingLeft(); |
|
3015 final int paddingRight = getPaddingRight(); |
|
3016 |
|
3017 final int paddingStart = (mIsVertical ? paddingTop : paddingLeft); |
|
3018 |
|
3019 final int spaceBefore = paddingStart - firstStart; |
|
3020 final int end = (mIsVertical ? getHeight() - paddingBottom : |
|
3021 getWidth() - paddingRight); |
|
3022 final int spaceAfter = lastEnd - end; |
|
3023 |
|
3024 final int size; |
|
3025 if (mIsVertical) { |
|
3026 size = getHeight() - paddingBottom - paddingTop; |
|
3027 } else { |
|
3028 size = getWidth() - paddingRight - paddingLeft; |
|
3029 } |
|
3030 |
|
3031 if (incrementalDelta < 0) { |
|
3032 incrementalDelta = Math.max(-(size - 1), incrementalDelta); |
|
3033 } else { |
|
3034 incrementalDelta = Math.min(size - 1, incrementalDelta); |
|
3035 } |
|
3036 |
|
3037 final int firstPosition = mFirstPosition; |
|
3038 |
|
3039 final boolean cannotScrollDown = (firstPosition == 0 && |
|
3040 firstStart >= paddingStart && incrementalDelta >= 0); |
|
3041 final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && |
|
3042 lastEnd <= end && incrementalDelta <= 0); |
|
3043 |
|
3044 if (cannotScrollDown || cannotScrollUp) { |
|
3045 return incrementalDelta != 0; |
|
3046 } |
|
3047 |
|
3048 final boolean inTouchMode = isInTouchMode(); |
|
3049 if (inTouchMode) { |
|
3050 hideSelector(); |
|
3051 } |
|
3052 |
|
3053 int start = 0; |
|
3054 int count = 0; |
|
3055 |
|
3056 final boolean down = (incrementalDelta < 0); |
|
3057 if (down) { |
|
3058 int childrenStart = -incrementalDelta + paddingStart; |
|
3059 |
|
3060 for (int i = 0; i < childCount; i++) { |
|
3061 final View child = getChildAt(i); |
|
3062 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight()); |
|
3063 |
|
3064 if (childEnd >= childrenStart) { |
|
3065 break; |
|
3066 } |
|
3067 |
|
3068 count++; |
|
3069 mRecycler.addScrapView(child, firstPosition + i); |
|
3070 } |
|
3071 } else { |
|
3072 int childrenEnd = end - incrementalDelta; |
|
3073 |
|
3074 for (int i = childCount - 1; i >= 0; i--) { |
|
3075 final View child = getChildAt(i); |
|
3076 final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); |
|
3077 |
|
3078 if (childStart <= childrenEnd) { |
|
3079 break; |
|
3080 } |
|
3081 |
|
3082 start = i; |
|
3083 count++; |
|
3084 mRecycler.addScrapView(child, firstPosition + i); |
|
3085 } |
|
3086 } |
|
3087 |
|
3088 mBlockLayoutRequests = true; |
|
3089 |
|
3090 if (count > 0) { |
|
3091 detachViewsFromParent(start, count); |
|
3092 } |
|
3093 |
|
3094 // invalidate before moving the children to avoid unnecessary invalidate |
|
3095 // calls to bubble up from the children all the way to the top |
|
3096 if (!awakenScrollbarsInternal()) { |
|
3097 invalidate(); |
|
3098 } |
|
3099 |
|
3100 offsetChildren(incrementalDelta); |
|
3101 |
|
3102 if (down) { |
|
3103 mFirstPosition += count; |
|
3104 } |
|
3105 |
|
3106 final int absIncrementalDelta = Math.abs(incrementalDelta); |
|
3107 if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) { |
|
3108 fillGap(down); |
|
3109 } |
|
3110 |
|
3111 if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { |
|
3112 final int childIndex = mSelectedPosition - mFirstPosition; |
|
3113 if (childIndex >= 0 && childIndex < getChildCount()) { |
|
3114 positionSelector(mSelectedPosition, getChildAt(childIndex)); |
|
3115 } |
|
3116 } else if (mSelectorPosition != INVALID_POSITION) { |
|
3117 final int childIndex = mSelectorPosition - mFirstPosition; |
|
3118 if (childIndex >= 0 && childIndex < getChildCount()) { |
|
3119 positionSelector(INVALID_POSITION, getChildAt(childIndex)); |
|
3120 } |
|
3121 } else { |
|
3122 mSelectorRect.setEmpty(); |
|
3123 } |
|
3124 |
|
3125 mBlockLayoutRequests = false; |
|
3126 |
|
3127 invokeOnItemScrollListener(); |
|
3128 |
|
3129 return false; |
|
3130 } |
|
3131 |
|
3132 @TargetApi(14) |
|
3133 private final float getCurrVelocity() { |
|
3134 if (Build.VERSION.SDK_INT >= 14) { |
|
3135 return mScroller.getCurrVelocity(); |
|
3136 } |
|
3137 |
|
3138 return 0; |
|
3139 } |
|
3140 |
|
3141 @TargetApi(5) |
|
3142 private boolean awakenScrollbarsInternal() { |
|
3143 if (Build.VERSION.SDK_INT >= 5) { |
|
3144 return super.awakenScrollBars(); |
|
3145 } else { |
|
3146 return false; |
|
3147 } |
|
3148 } |
|
3149 |
|
3150 @Override |
|
3151 public void computeScroll() { |
|
3152 if (!mScroller.computeScrollOffset()) { |
|
3153 return; |
|
3154 } |
|
3155 |
|
3156 final int pos; |
|
3157 if (mIsVertical) { |
|
3158 pos = mScroller.getCurrY(); |
|
3159 } else { |
|
3160 pos = mScroller.getCurrX(); |
|
3161 } |
|
3162 |
|
3163 final int diff = (int) (pos - mLastTouchPos); |
|
3164 mLastTouchPos = pos; |
|
3165 |
|
3166 final boolean stopped = trackMotionScroll(diff); |
|
3167 |
|
3168 if (!stopped && !mScroller.isFinished()) { |
|
3169 ViewCompat.postInvalidateOnAnimation(this); |
|
3170 } else { |
|
3171 if (stopped) { |
|
3172 final int overScrollMode = ViewCompat.getOverScrollMode(this); |
|
3173 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { |
|
3174 final EdgeEffectCompat edge = |
|
3175 (diff > 0 ? mStartEdge : mEndEdge); |
|
3176 |
|
3177 boolean needsInvalidate = |
|
3178 edge.onAbsorb(Math.abs((int) getCurrVelocity())); |
|
3179 |
|
3180 if (needsInvalidate) { |
|
3181 ViewCompat.postInvalidateOnAnimation(this); |
|
3182 } |
|
3183 } |
|
3184 |
|
3185 mScroller.abortAnimation(); |
|
3186 } |
|
3187 |
|
3188 mTouchMode = TOUCH_MODE_REST; |
|
3189 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
3190 } |
|
3191 } |
|
3192 |
|
3193 private void finishEdgeGlows() { |
|
3194 if (mStartEdge != null) { |
|
3195 mStartEdge.finish(); |
|
3196 } |
|
3197 |
|
3198 if (mEndEdge != null) { |
|
3199 mEndEdge.finish(); |
|
3200 } |
|
3201 } |
|
3202 |
|
3203 private boolean drawStartEdge(Canvas canvas) { |
|
3204 if (mStartEdge.isFinished()) { |
|
3205 return false; |
|
3206 } |
|
3207 |
|
3208 if (mIsVertical) { |
|
3209 return mStartEdge.draw(canvas); |
|
3210 } |
|
3211 |
|
3212 final int restoreCount = canvas.save(); |
|
3213 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); |
|
3214 |
|
3215 canvas.translate(0, height); |
|
3216 canvas.rotate(270); |
|
3217 |
|
3218 final boolean needsInvalidate = mStartEdge.draw(canvas); |
|
3219 canvas.restoreToCount(restoreCount); |
|
3220 return needsInvalidate; |
|
3221 } |
|
3222 |
|
3223 private boolean drawEndEdge(Canvas canvas) { |
|
3224 if (mEndEdge.isFinished()) { |
|
3225 return false; |
|
3226 } |
|
3227 |
|
3228 final int restoreCount = canvas.save(); |
|
3229 final int width = getWidth() - getPaddingLeft() - getPaddingRight(); |
|
3230 final int height = getHeight() - getPaddingTop() - getPaddingBottom(); |
|
3231 |
|
3232 if (mIsVertical) { |
|
3233 canvas.translate(-width, height); |
|
3234 canvas.rotate(180, width, 0); |
|
3235 } else { |
|
3236 canvas.translate(width, 0); |
|
3237 canvas.rotate(90); |
|
3238 } |
|
3239 |
|
3240 final boolean needsInvalidate = mEndEdge.draw(canvas); |
|
3241 canvas.restoreToCount(restoreCount); |
|
3242 return needsInvalidate; |
|
3243 } |
|
3244 |
|
3245 private void drawSelector(Canvas canvas) { |
|
3246 if (!mSelectorRect.isEmpty()) { |
|
3247 final Drawable selector = mSelector; |
|
3248 selector.setBounds(mSelectorRect); |
|
3249 selector.draw(canvas); |
|
3250 } |
|
3251 } |
|
3252 |
|
3253 private void useDefaultSelector() { |
|
3254 setSelector(getResources().getDrawable( |
|
3255 android.R.drawable.list_selector_background)); |
|
3256 } |
|
3257 |
|
3258 private boolean shouldShowSelector() { |
|
3259 return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); |
|
3260 } |
|
3261 |
|
3262 private void positionSelector(int position, View selected) { |
|
3263 if (position != INVALID_POSITION) { |
|
3264 mSelectorPosition = position; |
|
3265 } |
|
3266 |
|
3267 mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(), |
|
3268 selected.getBottom()); |
|
3269 |
|
3270 final boolean isChildViewEnabled = mIsChildViewEnabled; |
|
3271 if (selected.isEnabled() != isChildViewEnabled) { |
|
3272 mIsChildViewEnabled = !isChildViewEnabled; |
|
3273 |
|
3274 if (getSelectedItemPosition() != INVALID_POSITION) { |
|
3275 refreshDrawableState(); |
|
3276 } |
|
3277 } |
|
3278 } |
|
3279 |
|
3280 private void hideSelector() { |
|
3281 if (mSelectedPosition != INVALID_POSITION) { |
|
3282 if (mLayoutMode != LAYOUT_SPECIFIC) { |
|
3283 mResurrectToPosition = mSelectedPosition; |
|
3284 } |
|
3285 |
|
3286 if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { |
|
3287 mResurrectToPosition = mNextSelectedPosition; |
|
3288 } |
|
3289 |
|
3290 setSelectedPositionInt(INVALID_POSITION); |
|
3291 setNextSelectedPositionInt(INVALID_POSITION); |
|
3292 |
|
3293 mSelectedStart = 0; |
|
3294 } |
|
3295 } |
|
3296 |
|
3297 private void setSelectedPositionInt(int position) { |
|
3298 mSelectedPosition = position; |
|
3299 mSelectedRowId = getItemIdAtPosition(position); |
|
3300 } |
|
3301 |
|
3302 private void setSelectionInt(int position) { |
|
3303 setNextSelectedPositionInt(position); |
|
3304 boolean awakeScrollbars = false; |
|
3305 |
|
3306 final int selectedPosition = mSelectedPosition; |
|
3307 if (selectedPosition >= 0) { |
|
3308 if (position == selectedPosition - 1) { |
|
3309 awakeScrollbars = true; |
|
3310 } else if (position == selectedPosition + 1) { |
|
3311 awakeScrollbars = true; |
|
3312 } |
|
3313 } |
|
3314 |
|
3315 layoutChildren(); |
|
3316 |
|
3317 if (awakeScrollbars) { |
|
3318 awakenScrollbarsInternal(); |
|
3319 } |
|
3320 } |
|
3321 |
|
3322 private void setNextSelectedPositionInt(int position) { |
|
3323 mNextSelectedPosition = position; |
|
3324 mNextSelectedRowId = getItemIdAtPosition(position); |
|
3325 |
|
3326 // If we are trying to sync to the selection, update that too |
|
3327 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { |
|
3328 mSyncPosition = position; |
|
3329 mSyncRowId = mNextSelectedRowId; |
|
3330 } |
|
3331 } |
|
3332 |
|
3333 private boolean touchModeDrawsInPressedState() { |
|
3334 switch (mTouchMode) { |
|
3335 case TOUCH_MODE_TAP: |
|
3336 case TOUCH_MODE_DONE_WAITING: |
|
3337 return true; |
|
3338 default: |
|
3339 return false; |
|
3340 } |
|
3341 } |
|
3342 |
|
3343 /** |
|
3344 * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if |
|
3345 * this is a long press. |
|
3346 */ |
|
3347 private void keyPressed() { |
|
3348 if (!isEnabled() || !isClickable()) { |
|
3349 return; |
|
3350 } |
|
3351 |
|
3352 final Drawable selector = mSelector; |
|
3353 final Rect selectorRect = mSelectorRect; |
|
3354 |
|
3355 if (selector != null && (isFocused() || touchModeDrawsInPressedState()) |
|
3356 && !selectorRect.isEmpty()) { |
|
3357 |
|
3358 final View child = getChildAt(mSelectedPosition - mFirstPosition); |
|
3359 |
|
3360 if (child != null) { |
|
3361 if (child.hasFocusable()) { |
|
3362 return; |
|
3363 } |
|
3364 |
|
3365 child.setPressed(true); |
|
3366 } |
|
3367 |
|
3368 setPressed(true); |
|
3369 |
|
3370 final boolean longClickable = isLongClickable(); |
|
3371 final Drawable d = selector.getCurrent(); |
|
3372 if (d != null && d instanceof TransitionDrawable) { |
|
3373 if (longClickable) { |
|
3374 ((TransitionDrawable) d).startTransition( |
|
3375 ViewConfiguration.getLongPressTimeout()); |
|
3376 } else { |
|
3377 ((TransitionDrawable) d).resetTransition(); |
|
3378 } |
|
3379 } |
|
3380 |
|
3381 if (longClickable && !mDataChanged) { |
|
3382 if (mPendingCheckForKeyLongPress == null) { |
|
3383 mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); |
|
3384 } |
|
3385 |
|
3386 mPendingCheckForKeyLongPress.rememberWindowAttachCount(); |
|
3387 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); |
|
3388 } |
|
3389 } |
|
3390 } |
|
3391 |
|
3392 private void updateSelectorState() { |
|
3393 if (mSelector != null) { |
|
3394 if (shouldShowSelector()) { |
|
3395 mSelector.setState(getDrawableState()); |
|
3396 } else { |
|
3397 mSelector.setState(STATE_NOTHING); |
|
3398 } |
|
3399 } |
|
3400 } |
|
3401 |
|
3402 private void checkSelectionChanged() { |
|
3403 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { |
|
3404 selectionChanged(); |
|
3405 mOldSelectedPosition = mSelectedPosition; |
|
3406 mOldSelectedRowId = mSelectedRowId; |
|
3407 } |
|
3408 } |
|
3409 |
|
3410 private void selectionChanged() { |
|
3411 OnItemSelectedListener listener = getOnItemSelectedListener(); |
|
3412 if (listener == null) { |
|
3413 return; |
|
3414 } |
|
3415 |
|
3416 if (mInLayout || mBlockLayoutRequests) { |
|
3417 // If we are in a layout traversal, defer notification |
|
3418 // by posting. This ensures that the view tree is |
|
3419 // in a consistent state and is able to accommodate |
|
3420 // new layout or invalidate requests. |
|
3421 if (mSelectionNotifier == null) { |
|
3422 mSelectionNotifier = new SelectionNotifier(); |
|
3423 } |
|
3424 |
|
3425 post(mSelectionNotifier); |
|
3426 } else { |
|
3427 fireOnSelected(); |
|
3428 performAccessibilityActionsOnSelected(); |
|
3429 } |
|
3430 } |
|
3431 |
|
3432 private void fireOnSelected() { |
|
3433 OnItemSelectedListener listener = getOnItemSelectedListener(); |
|
3434 if (listener == null) { |
|
3435 return; |
|
3436 } |
|
3437 |
|
3438 final int selection = getSelectedItemPosition(); |
|
3439 if (selection >= 0) { |
|
3440 View v = getSelectedView(); |
|
3441 listener.onItemSelected(this, v, selection, |
|
3442 mAdapter.getItemId(selection)); |
|
3443 } else { |
|
3444 listener.onNothingSelected(this); |
|
3445 } |
|
3446 } |
|
3447 |
|
3448 private void performAccessibilityActionsOnSelected() { |
|
3449 final int position = getSelectedItemPosition(); |
|
3450 if (position >= 0) { |
|
3451 // We fire selection events here not in View |
|
3452 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); |
|
3453 } |
|
3454 } |
|
3455 |
|
3456 private int lookForSelectablePosition(int position) { |
|
3457 return lookForSelectablePosition(position, true); |
|
3458 } |
|
3459 |
|
3460 private int lookForSelectablePosition(int position, boolean lookDown) { |
|
3461 final ListAdapter adapter = mAdapter; |
|
3462 if (adapter == null || isInTouchMode()) { |
|
3463 return INVALID_POSITION; |
|
3464 } |
|
3465 |
|
3466 final int itemCount = mItemCount; |
|
3467 if (!mAreAllItemsSelectable) { |
|
3468 if (lookDown) { |
|
3469 position = Math.max(0, position); |
|
3470 while (position < itemCount && !adapter.isEnabled(position)) { |
|
3471 position++; |
|
3472 } |
|
3473 } else { |
|
3474 position = Math.min(position, itemCount - 1); |
|
3475 while (position >= 0 && !adapter.isEnabled(position)) { |
|
3476 position--; |
|
3477 } |
|
3478 } |
|
3479 |
|
3480 if (position < 0 || position >= itemCount) { |
|
3481 return INVALID_POSITION; |
|
3482 } |
|
3483 |
|
3484 return position; |
|
3485 } else { |
|
3486 if (position < 0 || position >= itemCount) { |
|
3487 return INVALID_POSITION; |
|
3488 } |
|
3489 |
|
3490 return position; |
|
3491 } |
|
3492 } |
|
3493 |
|
3494 /** |
|
3495 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or |
|
3496 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the |
|
3497 * current view orientation. |
|
3498 * |
|
3499 * @return The position of the next selectable position of the views that |
|
3500 * are currently visible, taking into account the fact that there might |
|
3501 * be no selection. Returns {@link #INVALID_POSITION} if there is no |
|
3502 * selectable view on screen in the given direction. |
|
3503 */ |
|
3504 private int lookForSelectablePositionOnScreen(int direction) { |
|
3505 forceValidFocusDirection(direction); |
|
3506 |
|
3507 final int firstPosition = mFirstPosition; |
|
3508 final ListAdapter adapter = getAdapter(); |
|
3509 |
|
3510 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) { |
|
3511 int startPos = (mSelectedPosition != INVALID_POSITION ? |
|
3512 mSelectedPosition + 1 : firstPosition); |
|
3513 |
|
3514 if (startPos >= adapter.getCount()) { |
|
3515 return INVALID_POSITION; |
|
3516 } |
|
3517 |
|
3518 if (startPos < firstPosition) { |
|
3519 startPos = firstPosition; |
|
3520 } |
|
3521 |
|
3522 final int lastVisiblePos = getLastVisiblePosition(); |
|
3523 |
|
3524 for (int pos = startPos; pos <= lastVisiblePos; pos++) { |
|
3525 if (adapter.isEnabled(pos) |
|
3526 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { |
|
3527 return pos; |
|
3528 } |
|
3529 } |
|
3530 } else { |
|
3531 final int last = firstPosition + getChildCount() - 1; |
|
3532 |
|
3533 int startPos = (mSelectedPosition != INVALID_POSITION) ? |
|
3534 mSelectedPosition - 1 : firstPosition + getChildCount() - 1; |
|
3535 |
|
3536 if (startPos < 0 || startPos >= adapter.getCount()) { |
|
3537 return INVALID_POSITION; |
|
3538 } |
|
3539 |
|
3540 if (startPos > last) { |
|
3541 startPos = last; |
|
3542 } |
|
3543 |
|
3544 for (int pos = startPos; pos >= firstPosition; pos--) { |
|
3545 if (adapter.isEnabled(pos) |
|
3546 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) { |
|
3547 return pos; |
|
3548 } |
|
3549 } |
|
3550 } |
|
3551 |
|
3552 return INVALID_POSITION; |
|
3553 } |
|
3554 |
|
3555 @Override |
|
3556 protected void drawableStateChanged() { |
|
3557 super.drawableStateChanged(); |
|
3558 updateSelectorState(); |
|
3559 } |
|
3560 |
|
3561 @Override |
|
3562 protected int[] onCreateDrawableState(int extraSpace) { |
|
3563 // If the child view is enabled then do the default behavior. |
|
3564 if (mIsChildViewEnabled) { |
|
3565 // Common case |
|
3566 return super.onCreateDrawableState(extraSpace); |
|
3567 } |
|
3568 |
|
3569 // The selector uses this View's drawable state. The selected child view |
|
3570 // is disabled, so we need to remove the enabled state from the drawable |
|
3571 // states. |
|
3572 final int enabledState = ENABLED_STATE_SET[0]; |
|
3573 |
|
3574 // If we don't have any extra space, it will return one of the static state arrays, |
|
3575 // and clearing the enabled state on those arrays is a bad thing! If we specify |
|
3576 // we need extra space, it will create+copy into a new array that safely mutable. |
|
3577 int[] state = super.onCreateDrawableState(extraSpace + 1); |
|
3578 int enabledPos = -1; |
|
3579 for (int i = state.length - 1; i >= 0; i--) { |
|
3580 if (state[i] == enabledState) { |
|
3581 enabledPos = i; |
|
3582 break; |
|
3583 } |
|
3584 } |
|
3585 |
|
3586 // Remove the enabled state |
|
3587 if (enabledPos >= 0) { |
|
3588 System.arraycopy(state, enabledPos + 1, state, enabledPos, |
|
3589 state.length - enabledPos - 1); |
|
3590 } |
|
3591 |
|
3592 return state; |
|
3593 } |
|
3594 |
|
3595 @Override |
|
3596 protected boolean canAnimate() { |
|
3597 return (super.canAnimate() && mItemCount > 0); |
|
3598 } |
|
3599 |
|
3600 @Override |
|
3601 protected void dispatchDraw(Canvas canvas) { |
|
3602 final boolean drawSelectorOnTop = mDrawSelectorOnTop; |
|
3603 if (!drawSelectorOnTop) { |
|
3604 drawSelector(canvas); |
|
3605 } |
|
3606 |
|
3607 super.dispatchDraw(canvas); |
|
3608 |
|
3609 if (drawSelectorOnTop) { |
|
3610 drawSelector(canvas); |
|
3611 } |
|
3612 } |
|
3613 |
|
3614 @Override |
|
3615 public void draw(Canvas canvas) { |
|
3616 super.draw(canvas); |
|
3617 |
|
3618 boolean needsInvalidate = false; |
|
3619 |
|
3620 if (mStartEdge != null) { |
|
3621 needsInvalidate |= drawStartEdge(canvas); |
|
3622 } |
|
3623 |
|
3624 if (mEndEdge != null) { |
|
3625 needsInvalidate |= drawEndEdge(canvas); |
|
3626 } |
|
3627 |
|
3628 if (needsInvalidate) { |
|
3629 ViewCompat.postInvalidateOnAnimation(this); |
|
3630 } |
|
3631 } |
|
3632 |
|
3633 @Override |
|
3634 public void requestLayout() { |
|
3635 if (!mInLayout && !mBlockLayoutRequests) { |
|
3636 super.requestLayout(); |
|
3637 } |
|
3638 } |
|
3639 |
|
3640 @Override |
|
3641 public View getSelectedView() { |
|
3642 if (mItemCount > 0 && mSelectedPosition >= 0) { |
|
3643 return getChildAt(mSelectedPosition - mFirstPosition); |
|
3644 } else { |
|
3645 return null; |
|
3646 } |
|
3647 } |
|
3648 |
|
3649 @Override |
|
3650 public void setSelection(int position) { |
|
3651 setSelectionFromOffset(position, 0); |
|
3652 } |
|
3653 |
|
3654 public void setSelectionFromOffset(int position, int offset) { |
|
3655 if (mAdapter == null) { |
|
3656 return; |
|
3657 } |
|
3658 |
|
3659 if (!isInTouchMode()) { |
|
3660 position = lookForSelectablePosition(position); |
|
3661 if (position >= 0) { |
|
3662 setNextSelectedPositionInt(position); |
|
3663 } |
|
3664 } else { |
|
3665 mResurrectToPosition = position; |
|
3666 } |
|
3667 |
|
3668 if (position >= 0) { |
|
3669 mLayoutMode = LAYOUT_SPECIFIC; |
|
3670 |
|
3671 if (mIsVertical) { |
|
3672 mSpecificStart = getPaddingTop() + offset; |
|
3673 } else { |
|
3674 mSpecificStart = getPaddingLeft() + offset; |
|
3675 } |
|
3676 |
|
3677 if (mNeedSync) { |
|
3678 mSyncPosition = position; |
|
3679 mSyncRowId = mAdapter.getItemId(position); |
|
3680 } |
|
3681 |
|
3682 requestLayout(); |
|
3683 } |
|
3684 } |
|
3685 |
|
3686 @Override |
|
3687 public boolean dispatchKeyEvent(KeyEvent event) { |
|
3688 // Dispatch in the normal way |
|
3689 boolean handled = super.dispatchKeyEvent(event); |
|
3690 if (!handled) { |
|
3691 // If we didn't handle it... |
|
3692 final View focused = getFocusedChild(); |
|
3693 if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) { |
|
3694 // ... and our focused child didn't handle it |
|
3695 // ... give it to ourselves so we can scroll if necessary |
|
3696 handled = onKeyDown(event.getKeyCode(), event); |
|
3697 } |
|
3698 } |
|
3699 |
|
3700 return handled; |
|
3701 } |
|
3702 |
|
3703 @Override |
|
3704 protected void dispatchSetPressed(boolean pressed) { |
|
3705 // Don't dispatch setPressed to our children. We call setPressed on ourselves to |
|
3706 // get the selector in the right state, but we don't want to press each child. |
|
3707 } |
|
3708 |
|
3709 @Override |
|
3710 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
|
3711 if (mSelector == null) { |
|
3712 useDefaultSelector(); |
|
3713 } |
|
3714 |
|
3715 int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
|
3716 int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
|
3717 int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
|
3718 int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
|
3719 |
|
3720 int childWidth = 0; |
|
3721 int childHeight = 0; |
|
3722 |
|
3723 mItemCount = (mAdapter == null ? 0 : mAdapter.getCount()); |
|
3724 if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || |
|
3725 heightMode == MeasureSpec.UNSPECIFIED)) { |
|
3726 final View child = obtainView(0, mIsScrap); |
|
3727 |
|
3728 final int secondaryMeasureSpec = |
|
3729 (mIsVertical ? widthMeasureSpec : heightMeasureSpec); |
|
3730 |
|
3731 measureScrapChild(child, 0, secondaryMeasureSpec); |
|
3732 |
|
3733 childWidth = child.getMeasuredWidth(); |
|
3734 childHeight = child.getMeasuredHeight(); |
|
3735 |
|
3736 if (recycleOnMeasure()) { |
|
3737 mRecycler.addScrapView(child, -1); |
|
3738 } |
|
3739 } |
|
3740 |
|
3741 if (widthMode == MeasureSpec.UNSPECIFIED) { |
|
3742 widthSize = getPaddingLeft() + getPaddingRight() + childWidth; |
|
3743 if (mIsVertical) { |
|
3744 widthSize += getVerticalScrollbarWidth(); |
|
3745 } |
|
3746 } |
|
3747 |
|
3748 if (heightMode == MeasureSpec.UNSPECIFIED) { |
|
3749 heightSize = getPaddingTop() + getPaddingBottom() + childHeight; |
|
3750 if (!mIsVertical) { |
|
3751 heightSize += getHorizontalScrollbarHeight(); |
|
3752 } |
|
3753 } |
|
3754 |
|
3755 if (mIsVertical && heightMode == MeasureSpec.AT_MOST) { |
|
3756 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); |
|
3757 } |
|
3758 |
|
3759 if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) { |
|
3760 widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1); |
|
3761 } |
|
3762 |
|
3763 setMeasuredDimension(widthSize, heightSize); |
|
3764 } |
|
3765 |
|
3766 @Override |
|
3767 protected void onLayout(boolean changed, int l, int t, int r, int b) { |
|
3768 mInLayout = true; |
|
3769 |
|
3770 if (changed) { |
|
3771 final int childCount = getChildCount(); |
|
3772 for (int i = 0; i < childCount; i++) { |
|
3773 getChildAt(i).forceLayout(); |
|
3774 } |
|
3775 |
|
3776 mRecycler.markChildrenDirty(); |
|
3777 } |
|
3778 |
|
3779 layoutChildren(); |
|
3780 |
|
3781 mInLayout = false; |
|
3782 |
|
3783 final int width = r - l - getPaddingLeft() - getPaddingRight(); |
|
3784 final int height = b - t - getPaddingTop() - getPaddingBottom(); |
|
3785 |
|
3786 if (mStartEdge != null && mEndEdge != null) { |
|
3787 if (mIsVertical) { |
|
3788 mStartEdge.setSize(width, height); |
|
3789 mEndEdge.setSize(width, height); |
|
3790 } else { |
|
3791 mStartEdge.setSize(height, width); |
|
3792 mEndEdge.setSize(height, width); |
|
3793 } |
|
3794 } |
|
3795 } |
|
3796 |
|
3797 private void layoutChildren() { |
|
3798 if (getWidth() == 0 || getHeight() == 0) { |
|
3799 return; |
|
3800 } |
|
3801 |
|
3802 final boolean blockLayoutRequests = mBlockLayoutRequests; |
|
3803 if (!blockLayoutRequests) { |
|
3804 mBlockLayoutRequests = true; |
|
3805 } else { |
|
3806 return; |
|
3807 } |
|
3808 |
|
3809 try { |
|
3810 invalidate(); |
|
3811 |
|
3812 if (mAdapter == null) { |
|
3813 resetState(); |
|
3814 return; |
|
3815 } |
|
3816 |
|
3817 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
3818 final int end = |
|
3819 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); |
|
3820 |
|
3821 int childCount = getChildCount(); |
|
3822 int index = 0; |
|
3823 int delta = 0; |
|
3824 |
|
3825 View focusLayoutRestoreView = null; |
|
3826 |
|
3827 View selected = null; |
|
3828 View oldSelected = null; |
|
3829 View newSelected = null; |
|
3830 View oldFirstChild = null; |
|
3831 |
|
3832 switch (mLayoutMode) { |
|
3833 case LAYOUT_SET_SELECTION: |
|
3834 index = mNextSelectedPosition - mFirstPosition; |
|
3835 if (index >= 0 && index < childCount) { |
|
3836 newSelected = getChildAt(index); |
|
3837 } |
|
3838 |
|
3839 break; |
|
3840 |
|
3841 case LAYOUT_FORCE_TOP: |
|
3842 case LAYOUT_FORCE_BOTTOM: |
|
3843 case LAYOUT_SPECIFIC: |
|
3844 case LAYOUT_SYNC: |
|
3845 break; |
|
3846 |
|
3847 case LAYOUT_MOVE_SELECTION: |
|
3848 default: |
|
3849 // Remember the previously selected view |
|
3850 index = mSelectedPosition - mFirstPosition; |
|
3851 if (index >= 0 && index < childCount) { |
|
3852 oldSelected = getChildAt(index); |
|
3853 } |
|
3854 |
|
3855 // Remember the previous first child |
|
3856 oldFirstChild = getChildAt(0); |
|
3857 |
|
3858 if (mNextSelectedPosition >= 0) { |
|
3859 delta = mNextSelectedPosition - mSelectedPosition; |
|
3860 } |
|
3861 |
|
3862 // Caution: newSelected might be null |
|
3863 newSelected = getChildAt(index + delta); |
|
3864 } |
|
3865 |
|
3866 final boolean dataChanged = mDataChanged; |
|
3867 if (dataChanged) { |
|
3868 handleDataChanged(); |
|
3869 } |
|
3870 |
|
3871 // Handle the empty set by removing all views that are visible |
|
3872 // and calling it a day |
|
3873 if (mItemCount == 0) { |
|
3874 resetState(); |
|
3875 return; |
|
3876 } else if (mItemCount != mAdapter.getCount()) { |
|
3877 throw new IllegalStateException("The content of the adapter has changed but " |
|
3878 + "TwoWayView did not receive a notification. Make sure the content of " |
|
3879 + "your adapter is not modified from a background thread, but only " |
|
3880 + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass() |
|
3881 + ") with Adapter(" + mAdapter.getClass() + ")]"); |
|
3882 } |
|
3883 |
|
3884 setSelectedPositionInt(mNextSelectedPosition); |
|
3885 |
|
3886 // Reset the focus restoration |
|
3887 View focusLayoutRestoreDirectChild = null; |
|
3888 |
|
3889 // Pull all children into the RecycleBin. |
|
3890 // These views will be reused if possible |
|
3891 final int firstPosition = mFirstPosition; |
|
3892 final RecycleBin recycleBin = mRecycler; |
|
3893 |
|
3894 if (dataChanged) { |
|
3895 for (int i = 0; i < childCount; i++) { |
|
3896 recycleBin.addScrapView(getChildAt(i), firstPosition + i); |
|
3897 } |
|
3898 } else { |
|
3899 recycleBin.fillActiveViews(childCount, firstPosition); |
|
3900 } |
|
3901 |
|
3902 // Take focus back to us temporarily to avoid the eventual |
|
3903 // call to clear focus when removing the focused child below |
|
3904 // from messing things up when ViewAncestor assigns focus back |
|
3905 // to someone else. |
|
3906 final View focusedChild = getFocusedChild(); |
|
3907 if (focusedChild != null) { |
|
3908 // We can remember the focused view to restore after relayout if the |
|
3909 // data hasn't changed, or if the focused position is a header or footer. |
|
3910 if (!dataChanged) { |
|
3911 focusLayoutRestoreDirectChild = focusedChild; |
|
3912 |
|
3913 // Remember the specific view that had focus |
|
3914 focusLayoutRestoreView = findFocus(); |
|
3915 if (focusLayoutRestoreView != null) { |
|
3916 // Tell it we are going to mess with it |
|
3917 focusLayoutRestoreView.onStartTemporaryDetach(); |
|
3918 } |
|
3919 } |
|
3920 |
|
3921 requestFocus(); |
|
3922 } |
|
3923 |
|
3924 // FIXME: We need a way to save current accessibility focus here |
|
3925 // so that it can be restored after we re-attach the children on each |
|
3926 // layout round. |
|
3927 |
|
3928 detachAllViewsFromParent(); |
|
3929 |
|
3930 switch (mLayoutMode) { |
|
3931 case LAYOUT_SET_SELECTION: |
|
3932 if (newSelected != null) { |
|
3933 final int newSelectedStart = |
|
3934 (mIsVertical ? newSelected.getTop() : newSelected.getLeft()); |
|
3935 |
|
3936 selected = fillFromSelection(newSelectedStart, start, end); |
|
3937 } else { |
|
3938 selected = fillFromMiddle(start, end); |
|
3939 } |
|
3940 |
|
3941 break; |
|
3942 |
|
3943 case LAYOUT_SYNC: |
|
3944 selected = fillSpecific(mSyncPosition, mSpecificStart); |
|
3945 break; |
|
3946 |
|
3947 case LAYOUT_FORCE_BOTTOM: |
|
3948 selected = fillBefore(mItemCount - 1, end); |
|
3949 adjustViewsStartOrEnd(); |
|
3950 break; |
|
3951 |
|
3952 case LAYOUT_FORCE_TOP: |
|
3953 mFirstPosition = 0; |
|
3954 selected = fillFromOffset(start); |
|
3955 adjustViewsStartOrEnd(); |
|
3956 break; |
|
3957 |
|
3958 case LAYOUT_SPECIFIC: |
|
3959 selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart); |
|
3960 break; |
|
3961 |
|
3962 case LAYOUT_MOVE_SELECTION: |
|
3963 selected = moveSelection(oldSelected, newSelected, delta, start, end); |
|
3964 break; |
|
3965 |
|
3966 default: |
|
3967 if (childCount == 0) { |
|
3968 final int position = lookForSelectablePosition(0); |
|
3969 setSelectedPositionInt(position); |
|
3970 selected = fillFromOffset(start); |
|
3971 } else { |
|
3972 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { |
|
3973 int offset = start; |
|
3974 if (oldSelected != null) { |
|
3975 offset = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft()); |
|
3976 } |
|
3977 selected = fillSpecific(mSelectedPosition, offset); |
|
3978 } else if (mFirstPosition < mItemCount) { |
|
3979 int offset = start; |
|
3980 if (oldFirstChild != null) { |
|
3981 offset = (mIsVertical ? oldFirstChild.getTop() : oldFirstChild.getLeft()); |
|
3982 } |
|
3983 |
|
3984 selected = fillSpecific(mFirstPosition, offset); |
|
3985 } else { |
|
3986 selected = fillSpecific(0, start); |
|
3987 } |
|
3988 } |
|
3989 |
|
3990 break; |
|
3991 |
|
3992 } |
|
3993 |
|
3994 recycleBin.scrapActiveViews(); |
|
3995 |
|
3996 if (selected != null) { |
|
3997 if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) { |
|
3998 final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild && |
|
3999 focusLayoutRestoreView != null && |
|
4000 focusLayoutRestoreView.requestFocus()) || selected.requestFocus(); |
|
4001 |
|
4002 if (!focusWasTaken) { |
|
4003 // Selected item didn't take focus, fine, but still want |
|
4004 // to make sure something else outside of the selected view |
|
4005 // has focus |
|
4006 final View focused = getFocusedChild(); |
|
4007 if (focused != null) { |
|
4008 focused.clearFocus(); |
|
4009 } |
|
4010 |
|
4011 positionSelector(INVALID_POSITION, selected); |
|
4012 } else { |
|
4013 selected.setSelected(false); |
|
4014 mSelectorRect.setEmpty(); |
|
4015 } |
|
4016 } else { |
|
4017 positionSelector(INVALID_POSITION, selected); |
|
4018 } |
|
4019 |
|
4020 mSelectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
4021 } else { |
|
4022 if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) { |
|
4023 View child = getChildAt(mMotionPosition - mFirstPosition); |
|
4024 |
|
4025 if (child != null) { |
|
4026 positionSelector(mMotionPosition, child); |
|
4027 } |
|
4028 } else { |
|
4029 mSelectedStart = 0; |
|
4030 mSelectorRect.setEmpty(); |
|
4031 } |
|
4032 |
|
4033 // Even if there is not selected position, we may need to restore |
|
4034 // focus (i.e. something focusable in touch mode) |
|
4035 if (hasFocus() && focusLayoutRestoreView != null) { |
|
4036 focusLayoutRestoreView.requestFocus(); |
|
4037 } |
|
4038 } |
|
4039 |
|
4040 // Tell focus view we are done mucking with it, if it is still in |
|
4041 // our view hierarchy. |
|
4042 if (focusLayoutRestoreView != null |
|
4043 && focusLayoutRestoreView.getWindowToken() != null) { |
|
4044 focusLayoutRestoreView.onFinishTemporaryDetach(); |
|
4045 } |
|
4046 |
|
4047 mLayoutMode = LAYOUT_NORMAL; |
|
4048 mDataChanged = false; |
|
4049 mNeedSync = false; |
|
4050 |
|
4051 setNextSelectedPositionInt(mSelectedPosition); |
|
4052 if (mItemCount > 0) { |
|
4053 checkSelectionChanged(); |
|
4054 } |
|
4055 |
|
4056 invokeOnItemScrollListener(); |
|
4057 } finally { |
|
4058 if (!blockLayoutRequests) { |
|
4059 mBlockLayoutRequests = false; |
|
4060 mDataChanged = false; |
|
4061 } |
|
4062 } |
|
4063 } |
|
4064 |
|
4065 protected boolean recycleOnMeasure() { |
|
4066 return true; |
|
4067 } |
|
4068 |
|
4069 private void offsetChildren(int offset) { |
|
4070 final int childCount = getChildCount(); |
|
4071 |
|
4072 for (int i = 0; i < childCount; i++) { |
|
4073 final View child = getChildAt(i); |
|
4074 |
|
4075 if (mIsVertical) { |
|
4076 child.offsetTopAndBottom(offset); |
|
4077 } else { |
|
4078 child.offsetLeftAndRight(offset); |
|
4079 } |
|
4080 } |
|
4081 } |
|
4082 |
|
4083 private View moveSelection(View oldSelected, View newSelected, int delta, int start, |
|
4084 int end) { |
|
4085 final int selectedPosition = mSelectedPosition; |
|
4086 |
|
4087 final int oldSelectedStart = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft()); |
|
4088 final int oldSelectedEnd = (mIsVertical ? oldSelected.getBottom() : oldSelected.getRight()); |
|
4089 |
|
4090 View selected = null; |
|
4091 |
|
4092 if (delta > 0) { |
|
4093 /* |
|
4094 * Case 1: Scrolling down. |
|
4095 */ |
|
4096 |
|
4097 /* |
|
4098 * Before After |
|
4099 * | | | | |
|
4100 * +-------+ +-------+ |
|
4101 * | A | | A | |
|
4102 * | 1 | => +-------+ |
|
4103 * +-------+ | B | |
|
4104 * | B | | 2 | |
|
4105 * +-------+ +-------+ |
|
4106 * | | | | |
|
4107 * |
|
4108 * Try to keep the top of the previously selected item where it was. |
|
4109 * oldSelected = A |
|
4110 * selected = B |
|
4111 */ |
|
4112 |
|
4113 // Put oldSelected (A) where it belongs |
|
4114 oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false); |
|
4115 |
|
4116 final int itemMargin = mItemMargin; |
|
4117 |
|
4118 // Now put the new selection (B) below that |
|
4119 selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true); |
|
4120 |
|
4121 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
4122 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); |
|
4123 |
|
4124 // Some of the newly selected item extends below the bottom of the list |
|
4125 if (selectedEnd > end) { |
|
4126 // Find space available above the selection into which we can scroll upwards |
|
4127 final int spaceBefore = selectedStart - start; |
|
4128 |
|
4129 // Find space required to bring the bottom of the selected item fully into view |
|
4130 final int spaceAfter = selectedEnd - end; |
|
4131 |
|
4132 // Don't scroll more than half the size of the list |
|
4133 final int halfSpace = (end - start) / 2; |
|
4134 int offset = Math.min(spaceBefore, spaceAfter); |
|
4135 offset = Math.min(offset, halfSpace); |
|
4136 |
|
4137 if (mIsVertical) { |
|
4138 oldSelected.offsetTopAndBottom(-offset); |
|
4139 selected.offsetTopAndBottom(-offset); |
|
4140 } else { |
|
4141 oldSelected.offsetLeftAndRight(-offset); |
|
4142 selected.offsetLeftAndRight(-offset); |
|
4143 } |
|
4144 } |
|
4145 |
|
4146 // Fill in views before and after |
|
4147 fillBefore(mSelectedPosition - 2, selectedStart - itemMargin); |
|
4148 adjustViewsStartOrEnd(); |
|
4149 fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin); |
|
4150 } else if (delta < 0) { |
|
4151 /* |
|
4152 * Case 2: Scrolling up. |
|
4153 */ |
|
4154 |
|
4155 /* |
|
4156 * Before After |
|
4157 * | | | | |
|
4158 * +-------+ +-------+ |
|
4159 * | A | | A | |
|
4160 * +-------+ => | 1 | |
|
4161 * | B | +-------+ |
|
4162 * | 2 | | B | |
|
4163 * +-------+ +-------+ |
|
4164 * | | | | |
|
4165 * |
|
4166 * Try to keep the top of the item about to become selected where it was. |
|
4167 * newSelected = A |
|
4168 * olSelected = B |
|
4169 */ |
|
4170 |
|
4171 if (newSelected != null) { |
|
4172 // Try to position the top of newSel (A) where it was before it was selected |
|
4173 final int newSelectedStart = (mIsVertical ? newSelected.getTop() : newSelected.getLeft()); |
|
4174 selected = makeAndAddView(selectedPosition, newSelectedStart, true, true); |
|
4175 } else { |
|
4176 // If (A) was not on screen and so did not have a view, position |
|
4177 // it above the oldSelected (B) |
|
4178 selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true); |
|
4179 } |
|
4180 |
|
4181 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
4182 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); |
|
4183 |
|
4184 // Some of the newly selected item extends above the top of the list |
|
4185 if (selectedStart < start) { |
|
4186 // Find space required to bring the top of the selected item fully into view |
|
4187 final int spaceBefore = start - selectedStart; |
|
4188 |
|
4189 // Find space available below the selection into which we can scroll downwards |
|
4190 final int spaceAfter = end - selectedEnd; |
|
4191 |
|
4192 // Don't scroll more than half the height of the list |
|
4193 final int halfSpace = (end - start) / 2; |
|
4194 int offset = Math.min(spaceBefore, spaceAfter); |
|
4195 offset = Math.min(offset, halfSpace); |
|
4196 |
|
4197 if (mIsVertical) { |
|
4198 selected.offsetTopAndBottom(offset); |
|
4199 } else { |
|
4200 selected.offsetLeftAndRight(offset); |
|
4201 } |
|
4202 } |
|
4203 |
|
4204 // Fill in views above and below |
|
4205 fillBeforeAndAfter(selected, selectedPosition); |
|
4206 } else { |
|
4207 /* |
|
4208 * Case 3: Staying still |
|
4209 */ |
|
4210 |
|
4211 selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true); |
|
4212 |
|
4213 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
4214 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); |
|
4215 |
|
4216 // We're staying still... |
|
4217 if (oldSelectedStart < start) { |
|
4218 // ... but the top of the old selection was off screen. |
|
4219 // (This can happen if the data changes size out from under us) |
|
4220 int newEnd = selectedEnd; |
|
4221 if (newEnd < start + 20) { |
|
4222 // Not enough visible -- bring it onscreen |
|
4223 if (mIsVertical) { |
|
4224 selected.offsetTopAndBottom(start - selectedStart); |
|
4225 } else { |
|
4226 selected.offsetLeftAndRight(start - selectedStart); |
|
4227 } |
|
4228 } |
|
4229 } |
|
4230 |
|
4231 // Fill in views above and below |
|
4232 fillBeforeAndAfter(selected, selectedPosition); |
|
4233 } |
|
4234 |
|
4235 return selected; |
|
4236 } |
|
4237 |
|
4238 void confirmCheckedPositionsById() { |
|
4239 // Clear out the positional check states, we'll rebuild it below from IDs. |
|
4240 mCheckStates.clear(); |
|
4241 |
|
4242 for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { |
|
4243 final long id = mCheckedIdStates.keyAt(checkedIndex); |
|
4244 final int lastPos = mCheckedIdStates.valueAt(checkedIndex); |
|
4245 |
|
4246 final long lastPosId = mAdapter.getItemId(lastPos); |
|
4247 if (id != lastPosId) { |
|
4248 // Look around to see if the ID is nearby. If not, uncheck it. |
|
4249 final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); |
|
4250 final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount); |
|
4251 boolean found = false; |
|
4252 |
|
4253 for (int searchPos = start; searchPos < end; searchPos++) { |
|
4254 final long searchId = mAdapter.getItemId(searchPos); |
|
4255 if (id == searchId) { |
|
4256 found = true; |
|
4257 mCheckStates.put(searchPos, true); |
|
4258 mCheckedIdStates.setValueAt(checkedIndex, searchPos); |
|
4259 break; |
|
4260 } |
|
4261 } |
|
4262 |
|
4263 if (!found) { |
|
4264 mCheckedIdStates.delete(id); |
|
4265 checkedIndex--; |
|
4266 mCheckedItemCount--; |
|
4267 } |
|
4268 } else { |
|
4269 mCheckStates.put(lastPos, true); |
|
4270 } |
|
4271 } |
|
4272 } |
|
4273 |
|
4274 private void handleDataChanged() { |
|
4275 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mAdapter != null && mAdapter.hasStableIds()) { |
|
4276 confirmCheckedPositionsById(); |
|
4277 } |
|
4278 |
|
4279 mRecycler.clearTransientStateViews(); |
|
4280 |
|
4281 final int itemCount = mItemCount; |
|
4282 if (itemCount > 0) { |
|
4283 int newPos; |
|
4284 int selectablePos; |
|
4285 |
|
4286 // Find the row we are supposed to sync to |
|
4287 if (mNeedSync) { |
|
4288 // Update this first, since setNextSelectedPositionInt inspects it |
|
4289 mNeedSync = false; |
|
4290 mPendingSync = null; |
|
4291 |
|
4292 switch (mSyncMode) { |
|
4293 case SYNC_SELECTED_POSITION: |
|
4294 if (isInTouchMode()) { |
|
4295 // We saved our state when not in touch mode. (We know this because |
|
4296 // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to |
|
4297 // restore in touch mode. Just leave mSyncPosition as it is (possibly |
|
4298 // adjusting if the available range changed) and return. |
|
4299 mLayoutMode = LAYOUT_SYNC; |
|
4300 mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); |
|
4301 |
|
4302 return; |
|
4303 } else { |
|
4304 // See if we can find a position in the new data with the same |
|
4305 // id as the old selection. This will change mSyncPosition. |
|
4306 newPos = findSyncPosition(); |
|
4307 if (newPos >= 0) { |
|
4308 // Found it. Now verify that new selection is still selectable |
|
4309 selectablePos = lookForSelectablePosition(newPos, true); |
|
4310 if (selectablePos == newPos) { |
|
4311 // Same row id is selected |
|
4312 mSyncPosition = newPos; |
|
4313 |
|
4314 if (mSyncHeight == getHeight()) { |
|
4315 // If we are at the same height as when we saved state, try |
|
4316 // to restore the scroll position too. |
|
4317 mLayoutMode = LAYOUT_SYNC; |
|
4318 } else { |
|
4319 // We are not the same height as when the selection was saved, so |
|
4320 // don't try to restore the exact position |
|
4321 mLayoutMode = LAYOUT_SET_SELECTION; |
|
4322 } |
|
4323 |
|
4324 // Restore selection |
|
4325 setNextSelectedPositionInt(newPos); |
|
4326 return; |
|
4327 } |
|
4328 } |
|
4329 } |
|
4330 break; |
|
4331 |
|
4332 case SYNC_FIRST_POSITION: |
|
4333 // Leave mSyncPosition as it is -- just pin to available range |
|
4334 mLayoutMode = LAYOUT_SYNC; |
|
4335 mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1); |
|
4336 |
|
4337 return; |
|
4338 } |
|
4339 } |
|
4340 |
|
4341 if (!isInTouchMode()) { |
|
4342 // We couldn't find matching data -- try to use the same position |
|
4343 newPos = getSelectedItemPosition(); |
|
4344 |
|
4345 // Pin position to the available range |
|
4346 if (newPos >= itemCount) { |
|
4347 newPos = itemCount - 1; |
|
4348 } |
|
4349 if (newPos < 0) { |
|
4350 newPos = 0; |
|
4351 } |
|
4352 |
|
4353 // Make sure we select something selectable -- first look down |
|
4354 selectablePos = lookForSelectablePosition(newPos, true); |
|
4355 |
|
4356 if (selectablePos >= 0) { |
|
4357 setNextSelectedPositionInt(selectablePos); |
|
4358 return; |
|
4359 } else { |
|
4360 // Looking down didn't work -- try looking up |
|
4361 selectablePos = lookForSelectablePosition(newPos, false); |
|
4362 if (selectablePos >= 0) { |
|
4363 setNextSelectedPositionInt(selectablePos); |
|
4364 return; |
|
4365 } |
|
4366 } |
|
4367 } else { |
|
4368 // We already know where we want to resurrect the selection |
|
4369 if (mResurrectToPosition >= 0) { |
|
4370 return; |
|
4371 } |
|
4372 } |
|
4373 } |
|
4374 |
|
4375 // Nothing is selected. Give up and reset everything. |
|
4376 mLayoutMode = LAYOUT_FORCE_TOP; |
|
4377 mSelectedPosition = INVALID_POSITION; |
|
4378 mSelectedRowId = INVALID_ROW_ID; |
|
4379 mNextSelectedPosition = INVALID_POSITION; |
|
4380 mNextSelectedRowId = INVALID_ROW_ID; |
|
4381 mNeedSync = false; |
|
4382 mPendingSync = null; |
|
4383 mSelectorPosition = INVALID_POSITION; |
|
4384 |
|
4385 checkSelectionChanged(); |
|
4386 } |
|
4387 |
|
4388 private int reconcileSelectedPosition() { |
|
4389 int position = mSelectedPosition; |
|
4390 if (position < 0) { |
|
4391 position = mResurrectToPosition; |
|
4392 } |
|
4393 |
|
4394 position = Math.max(0, position); |
|
4395 position = Math.min(position, mItemCount - 1); |
|
4396 |
|
4397 return position; |
|
4398 } |
|
4399 |
|
4400 boolean resurrectSelection() { |
|
4401 final int childCount = getChildCount(); |
|
4402 if (childCount <= 0) { |
|
4403 return false; |
|
4404 } |
|
4405 |
|
4406 int selectedStart = 0; |
|
4407 int selectedPosition; |
|
4408 |
|
4409 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
4410 final int end = |
|
4411 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); |
|
4412 |
|
4413 final int firstPosition = mFirstPosition; |
|
4414 final int toPosition = mResurrectToPosition; |
|
4415 boolean down = true; |
|
4416 |
|
4417 if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { |
|
4418 selectedPosition = toPosition; |
|
4419 |
|
4420 final View selected = getChildAt(selectedPosition - mFirstPosition); |
|
4421 selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
4422 } else if (toPosition < firstPosition) { |
|
4423 // Default to selecting whatever is first |
|
4424 selectedPosition = firstPosition; |
|
4425 |
|
4426 for (int i = 0; i < childCount; i++) { |
|
4427 final View child = getChildAt(i); |
|
4428 final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); |
|
4429 |
|
4430 if (i == 0) { |
|
4431 // Remember the position of the first item |
|
4432 selectedStart = childStart; |
|
4433 } |
|
4434 |
|
4435 if (childStart >= start) { |
|
4436 // Found a view whose top is fully visible |
|
4437 selectedPosition = firstPosition + i; |
|
4438 selectedStart = childStart; |
|
4439 break; |
|
4440 } |
|
4441 } |
|
4442 } else { |
|
4443 selectedPosition = firstPosition + childCount - 1; |
|
4444 down = false; |
|
4445 |
|
4446 for (int i = childCount - 1; i >= 0; i--) { |
|
4447 final View child = getChildAt(i); |
|
4448 final int childStart = (mIsVertical ? child.getTop() : child.getLeft()); |
|
4449 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight()); |
|
4450 |
|
4451 if (i == childCount - 1) { |
|
4452 selectedStart = childStart; |
|
4453 } |
|
4454 |
|
4455 if (childEnd <= end) { |
|
4456 selectedPosition = firstPosition + i; |
|
4457 selectedStart = childStart; |
|
4458 break; |
|
4459 } |
|
4460 } |
|
4461 } |
|
4462 |
|
4463 mResurrectToPosition = INVALID_POSITION; |
|
4464 mTouchMode = TOUCH_MODE_REST; |
|
4465 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
|
4466 |
|
4467 mSpecificStart = selectedStart; |
|
4468 |
|
4469 selectedPosition = lookForSelectablePosition(selectedPosition, down); |
|
4470 if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) { |
|
4471 mLayoutMode = LAYOUT_SPECIFIC; |
|
4472 updateSelectorState(); |
|
4473 setSelectionInt(selectedPosition); |
|
4474 invokeOnItemScrollListener(); |
|
4475 } else { |
|
4476 selectedPosition = INVALID_POSITION; |
|
4477 } |
|
4478 |
|
4479 return selectedPosition >= 0; |
|
4480 } |
|
4481 |
|
4482 /** |
|
4483 * If there is a selection returns false. |
|
4484 * Otherwise resurrects the selection and returns true if resurrected. |
|
4485 */ |
|
4486 boolean resurrectSelectionIfNeeded() { |
|
4487 if (mSelectedPosition < 0 && resurrectSelection()) { |
|
4488 updateSelectorState(); |
|
4489 return true; |
|
4490 } |
|
4491 |
|
4492 return false; |
|
4493 } |
|
4494 |
|
4495 private int getChildWidthMeasureSpec(LayoutParams lp) { |
|
4496 if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) { |
|
4497 return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
|
4498 } else if (mIsVertical) { |
|
4499 final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight(); |
|
4500 return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY); |
|
4501 } else { |
|
4502 return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); |
|
4503 } |
|
4504 } |
|
4505 |
|
4506 private int getChildHeightMeasureSpec(LayoutParams lp) { |
|
4507 if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) { |
|
4508 return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
|
4509 } else if (!mIsVertical) { |
|
4510 final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom(); |
|
4511 return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY); |
|
4512 } else { |
|
4513 return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); |
|
4514 } |
|
4515 } |
|
4516 |
|
4517 private void measureChild(View child) { |
|
4518 measureChild(child, (LayoutParams) child.getLayoutParams()); |
|
4519 } |
|
4520 |
|
4521 private void measureChild(View child, LayoutParams lp) { |
|
4522 final int widthSpec = getChildWidthMeasureSpec(lp); |
|
4523 final int heightSpec = getChildHeightMeasureSpec(lp); |
|
4524 child.measure(widthSpec, heightSpec); |
|
4525 } |
|
4526 |
|
4527 private void relayoutMeasuredChild(View child) { |
|
4528 final int w = child.getMeasuredWidth(); |
|
4529 final int h = child.getMeasuredHeight(); |
|
4530 |
|
4531 final int childLeft = getPaddingLeft(); |
|
4532 final int childRight = childLeft + w; |
|
4533 final int childTop = child.getTop(); |
|
4534 final int childBottom = childTop + h; |
|
4535 |
|
4536 child.layout(childLeft, childTop, childRight, childBottom); |
|
4537 } |
|
4538 |
|
4539 private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) { |
|
4540 LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams(); |
|
4541 if (lp == null) { |
|
4542 lp = generateDefaultLayoutParams(); |
|
4543 scrapChild.setLayoutParams(lp); |
|
4544 } |
|
4545 |
|
4546 lp.viewType = mAdapter.getItemViewType(position); |
|
4547 lp.forceAdd = true; |
|
4548 |
|
4549 final int widthMeasureSpec; |
|
4550 final int heightMeasureSpec; |
|
4551 if (mIsVertical) { |
|
4552 widthMeasureSpec = secondaryMeasureSpec; |
|
4553 heightMeasureSpec = getChildHeightMeasureSpec(lp); |
|
4554 } else { |
|
4555 widthMeasureSpec = getChildWidthMeasureSpec(lp); |
|
4556 heightMeasureSpec = secondaryMeasureSpec; |
|
4557 } |
|
4558 |
|
4559 scrapChild.measure(widthMeasureSpec, heightMeasureSpec); |
|
4560 } |
|
4561 |
|
4562 /** |
|
4563 * Measures the height of the given range of children (inclusive) and |
|
4564 * returns the height with this TwoWayView's padding and item margin heights |
|
4565 * included. If maxHeight is provided, the measuring will stop when the |
|
4566 * current height reaches maxHeight. |
|
4567 * |
|
4568 * @param widthMeasureSpec The width measure spec to be given to a child's |
|
4569 * {@link View#measure(int, int)}. |
|
4570 * @param startPosition The position of the first child to be shown. |
|
4571 * @param endPosition The (inclusive) position of the last child to be |
|
4572 * shown. Specify {@link #NO_POSITION} if the last child should be |
|
4573 * the last available child from the adapter. |
|
4574 * @param maxHeight The maximum height that will be returned (if all the |
|
4575 * children don't fit in this value, this value will be |
|
4576 * returned). |
|
4577 * @param disallowPartialChildPosition In general, whether the returned |
|
4578 * height should only contain entire children. This is more |
|
4579 * powerful--it is the first inclusive position at which partial |
|
4580 * children will not be allowed. Example: it looks nice to have |
|
4581 * at least 3 completely visible children, and in portrait this |
|
4582 * will most likely fit; but in landscape there could be times |
|
4583 * when even 2 children can not be completely shown, so a value |
|
4584 * of 2 (remember, inclusive) would be good (assuming |
|
4585 * startPosition is 0). |
|
4586 * @return The height of this TwoWayView with the given children. |
|
4587 */ |
|
4588 private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, |
|
4589 final int maxHeight, int disallowPartialChildPosition) { |
|
4590 |
|
4591 final int paddingTop = getPaddingTop(); |
|
4592 final int paddingBottom = getPaddingBottom(); |
|
4593 |
|
4594 final ListAdapter adapter = mAdapter; |
|
4595 if (adapter == null) { |
|
4596 return paddingTop + paddingBottom; |
|
4597 } |
|
4598 |
|
4599 // Include the padding of the list |
|
4600 int returnedHeight = paddingTop + paddingBottom; |
|
4601 final int itemMargin = mItemMargin; |
|
4602 |
|
4603 // The previous height value that was less than maxHeight and contained |
|
4604 // no partial children |
|
4605 int prevHeightWithoutPartialChild = 0; |
|
4606 int i; |
|
4607 View child; |
|
4608 |
|
4609 // mItemCount - 1 since endPosition parameter is inclusive |
|
4610 endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; |
|
4611 final RecycleBin recycleBin = mRecycler; |
|
4612 final boolean shouldRecycle = recycleOnMeasure(); |
|
4613 final boolean[] isScrap = mIsScrap; |
|
4614 |
|
4615 for (i = startPosition; i <= endPosition; ++i) { |
|
4616 child = obtainView(i, isScrap); |
|
4617 |
|
4618 measureScrapChild(child, i, widthMeasureSpec); |
|
4619 |
|
4620 if (i > 0) { |
|
4621 // Count the item margin for all but one child |
|
4622 returnedHeight += itemMargin; |
|
4623 } |
|
4624 |
|
4625 // Recycle the view before we possibly return from the method |
|
4626 if (shouldRecycle) { |
|
4627 recycleBin.addScrapView(child, -1); |
|
4628 } |
|
4629 |
|
4630 returnedHeight += child.getMeasuredHeight(); |
|
4631 |
|
4632 if (returnedHeight >= maxHeight) { |
|
4633 // We went over, figure out which height to return. If returnedHeight > maxHeight, |
|
4634 // then the i'th position did not fit completely. |
|
4635 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) |
|
4636 && (i > disallowPartialChildPosition) // We've past the min pos |
|
4637 && (prevHeightWithoutPartialChild > 0) // We have a prev height |
|
4638 && (returnedHeight != maxHeight) // i'th child did not fit completely |
|
4639 ? prevHeightWithoutPartialChild |
|
4640 : maxHeight; |
|
4641 } |
|
4642 |
|
4643 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { |
|
4644 prevHeightWithoutPartialChild = returnedHeight; |
|
4645 } |
|
4646 } |
|
4647 |
|
4648 // At this point, we went through the range of children, and they each |
|
4649 // completely fit, so return the returnedHeight |
|
4650 return returnedHeight; |
|
4651 } |
|
4652 |
|
4653 /** |
|
4654 * Measures the width of the given range of children (inclusive) and |
|
4655 * returns the width with this TwoWayView's padding and item margin widths |
|
4656 * included. If maxWidth is provided, the measuring will stop when the |
|
4657 * current width reaches maxWidth. |
|
4658 * |
|
4659 * @param heightMeasureSpec The height measure spec to be given to a child's |
|
4660 * {@link View#measure(int, int)}. |
|
4661 * @param startPosition The position of the first child to be shown. |
|
4662 * @param endPosition The (inclusive) position of the last child to be |
|
4663 * shown. Specify {@link #NO_POSITION} if the last child should be |
|
4664 * the last available child from the adapter. |
|
4665 * @param maxWidth The maximum width that will be returned (if all the |
|
4666 * children don't fit in this value, this value will be |
|
4667 * returned). |
|
4668 * @param disallowPartialChildPosition In general, whether the returned |
|
4669 * width should only contain entire children. This is more |
|
4670 * powerful--it is the first inclusive position at which partial |
|
4671 * children will not be allowed. Example: it looks nice to have |
|
4672 * at least 3 completely visible children, and in portrait this |
|
4673 * will most likely fit; but in landscape there could be times |
|
4674 * when even 2 children can not be completely shown, so a value |
|
4675 * of 2 (remember, inclusive) would be good (assuming |
|
4676 * startPosition is 0). |
|
4677 * @return The width of this TwoWayView with the given children. |
|
4678 */ |
|
4679 private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition, |
|
4680 final int maxWidth, int disallowPartialChildPosition) { |
|
4681 |
|
4682 final int paddingLeft = getPaddingLeft(); |
|
4683 final int paddingRight = getPaddingRight(); |
|
4684 |
|
4685 final ListAdapter adapter = mAdapter; |
|
4686 if (adapter == null) { |
|
4687 return paddingLeft + paddingRight; |
|
4688 } |
|
4689 |
|
4690 // Include the padding of the list |
|
4691 int returnedWidth = paddingLeft + paddingRight; |
|
4692 final int itemMargin = mItemMargin; |
|
4693 |
|
4694 // The previous height value that was less than maxHeight and contained |
|
4695 // no partial children |
|
4696 int prevWidthWithoutPartialChild = 0; |
|
4697 int i; |
|
4698 View child; |
|
4699 |
|
4700 // mItemCount - 1 since endPosition parameter is inclusive |
|
4701 endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; |
|
4702 final RecycleBin recycleBin = mRecycler; |
|
4703 final boolean shouldRecycle = recycleOnMeasure(); |
|
4704 final boolean[] isScrap = mIsScrap; |
|
4705 |
|
4706 for (i = startPosition; i <= endPosition; ++i) { |
|
4707 child = obtainView(i, isScrap); |
|
4708 |
|
4709 measureScrapChild(child, i, heightMeasureSpec); |
|
4710 |
|
4711 if (i > 0) { |
|
4712 // Count the item margin for all but one child |
|
4713 returnedWidth += itemMargin; |
|
4714 } |
|
4715 |
|
4716 // Recycle the view before we possibly return from the method |
|
4717 if (shouldRecycle) { |
|
4718 recycleBin.addScrapView(child, -1); |
|
4719 } |
|
4720 |
|
4721 returnedWidth += child.getMeasuredHeight(); |
|
4722 |
|
4723 if (returnedWidth >= maxWidth) { |
|
4724 // We went over, figure out which width to return. If returnedWidth > maxWidth, |
|
4725 // then the i'th position did not fit completely. |
|
4726 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1) |
|
4727 && (i > disallowPartialChildPosition) // We've past the min pos |
|
4728 && (prevWidthWithoutPartialChild > 0) // We have a prev width |
|
4729 && (returnedWidth != maxWidth) // i'th child did not fit completely |
|
4730 ? prevWidthWithoutPartialChild |
|
4731 : maxWidth; |
|
4732 } |
|
4733 |
|
4734 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { |
|
4735 prevWidthWithoutPartialChild = returnedWidth; |
|
4736 } |
|
4737 } |
|
4738 |
|
4739 // At this point, we went through the range of children, and they each |
|
4740 // completely fit, so return the returnedWidth |
|
4741 return returnedWidth; |
|
4742 } |
|
4743 |
|
4744 private View makeAndAddView(int position, int offset, boolean flow, boolean selected) { |
|
4745 final int top; |
|
4746 final int left; |
|
4747 |
|
4748 if (mIsVertical) { |
|
4749 top = offset; |
|
4750 left = getPaddingLeft(); |
|
4751 } else { |
|
4752 top = getPaddingTop(); |
|
4753 left = offset; |
|
4754 } |
|
4755 |
|
4756 if (!mDataChanged) { |
|
4757 // Try to use an existing view for this position |
|
4758 final View activeChild = mRecycler.getActiveView(position); |
|
4759 if (activeChild != null) { |
|
4760 // Found it -- we're using an existing child |
|
4761 // This just needs to be positioned |
|
4762 setupChild(activeChild, position, top, left, flow, selected, true); |
|
4763 |
|
4764 return activeChild; |
|
4765 } |
|
4766 } |
|
4767 |
|
4768 // Make a new view for this position, or convert an unused view if possible |
|
4769 final View child = obtainView(position, mIsScrap); |
|
4770 |
|
4771 // This needs to be positioned and measured |
|
4772 setupChild(child, position, top, left, flow, selected, mIsScrap[0]); |
|
4773 |
|
4774 return child; |
|
4775 } |
|
4776 |
|
4777 @TargetApi(11) |
|
4778 private void setupChild(View child, int position, int top, int left, |
|
4779 boolean flow, boolean selected, boolean recycled) { |
|
4780 final boolean isSelected = selected && shouldShowSelector(); |
|
4781 final boolean updateChildSelected = isSelected != child.isSelected(); |
|
4782 final int touchMode = mTouchMode; |
|
4783 |
|
4784 final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING && |
|
4785 mMotionPosition == position; |
|
4786 |
|
4787 final boolean updateChildPressed = isPressed != child.isPressed(); |
|
4788 final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested(); |
|
4789 |
|
4790 // Respect layout params that are already in the view. Otherwise make some up... |
|
4791 LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
|
4792 if (lp == null) { |
|
4793 lp = generateDefaultLayoutParams(); |
|
4794 } |
|
4795 |
|
4796 lp.viewType = mAdapter.getItemViewType(position); |
|
4797 |
|
4798 if (recycled && !lp.forceAdd) { |
|
4799 attachViewToParent(child, (flow ? -1 : 0), lp); |
|
4800 } else { |
|
4801 lp.forceAdd = false; |
|
4802 addViewInLayout(child, (flow ? -1 : 0), lp, true); |
|
4803 } |
|
4804 |
|
4805 if (updateChildSelected) { |
|
4806 child.setSelected(isSelected); |
|
4807 } |
|
4808 |
|
4809 if (updateChildPressed) { |
|
4810 child.setPressed(isPressed); |
|
4811 } |
|
4812 |
|
4813 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mCheckStates != null) { |
|
4814 if (child instanceof Checkable) { |
|
4815 ((Checkable) child).setChecked(mCheckStates.get(position)); |
|
4816 } else if (getContext().getApplicationInfo().targetSdkVersion |
|
4817 >= Build.VERSION_CODES.HONEYCOMB) { |
|
4818 child.setActivated(mCheckStates.get(position)); |
|
4819 } |
|
4820 } |
|
4821 |
|
4822 if (needToMeasure) { |
|
4823 measureChild(child, lp); |
|
4824 } else { |
|
4825 cleanupLayoutState(child); |
|
4826 } |
|
4827 |
|
4828 final int w = child.getMeasuredWidth(); |
|
4829 final int h = child.getMeasuredHeight(); |
|
4830 |
|
4831 final int childTop = (mIsVertical && !flow ? top - h : top); |
|
4832 final int childLeft = (!mIsVertical && !flow ? left - w : left); |
|
4833 |
|
4834 if (needToMeasure) { |
|
4835 final int childRight = childLeft + w; |
|
4836 final int childBottom = childTop + h; |
|
4837 |
|
4838 child.layout(childLeft, childTop, childRight, childBottom); |
|
4839 } else { |
|
4840 child.offsetLeftAndRight(childLeft - child.getLeft()); |
|
4841 child.offsetTopAndBottom(childTop - child.getTop()); |
|
4842 } |
|
4843 } |
|
4844 |
|
4845 void fillGap(boolean down) { |
|
4846 final int childCount = getChildCount(); |
|
4847 |
|
4848 if (down) { |
|
4849 final int paddingStart = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
4850 |
|
4851 final int lastEnd; |
|
4852 if (mIsVertical) { |
|
4853 lastEnd = getChildAt(childCount - 1).getBottom(); |
|
4854 } else { |
|
4855 lastEnd = getChildAt(childCount - 1).getRight(); |
|
4856 } |
|
4857 |
|
4858 final int offset = (childCount > 0 ? lastEnd + mItemMargin : paddingStart); |
|
4859 fillAfter(mFirstPosition + childCount, offset); |
|
4860 correctTooHigh(getChildCount()); |
|
4861 } else { |
|
4862 final int end; |
|
4863 final int firstStart; |
|
4864 |
|
4865 if (mIsVertical) { |
|
4866 end = getHeight() - getPaddingBottom(); |
|
4867 firstStart = getChildAt(0).getTop(); |
|
4868 } else { |
|
4869 end = getWidth() - getPaddingRight(); |
|
4870 firstStart = getChildAt(0).getLeft(); |
|
4871 } |
|
4872 |
|
4873 final int offset = (childCount > 0 ? firstStart - mItemMargin : end); |
|
4874 fillBefore(mFirstPosition - 1, offset); |
|
4875 correctTooLow(getChildCount()); |
|
4876 } |
|
4877 } |
|
4878 |
|
4879 private View fillBefore(int pos, int nextOffset) { |
|
4880 View selectedView = null; |
|
4881 |
|
4882 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
4883 |
|
4884 while (nextOffset > start && pos >= 0) { |
|
4885 boolean isSelected = (pos == mSelectedPosition); |
|
4886 View child = makeAndAddView(pos, nextOffset, false, isSelected); |
|
4887 |
|
4888 if (mIsVertical) { |
|
4889 nextOffset = child.getTop() - mItemMargin; |
|
4890 } else { |
|
4891 nextOffset = child.getLeft() - mItemMargin; |
|
4892 } |
|
4893 |
|
4894 if (isSelected) { |
|
4895 selectedView = child; |
|
4896 } |
|
4897 |
|
4898 pos--; |
|
4899 } |
|
4900 |
|
4901 mFirstPosition = pos + 1; |
|
4902 |
|
4903 return selectedView; |
|
4904 } |
|
4905 |
|
4906 private View fillAfter(int pos, int nextOffset) { |
|
4907 View selectedView = null; |
|
4908 |
|
4909 final int end = |
|
4910 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); |
|
4911 |
|
4912 while (nextOffset < end && pos < mItemCount) { |
|
4913 boolean selected = (pos == mSelectedPosition); |
|
4914 |
|
4915 View child = makeAndAddView(pos, nextOffset, true, selected); |
|
4916 |
|
4917 if (mIsVertical) { |
|
4918 nextOffset = child.getBottom() + mItemMargin; |
|
4919 } else { |
|
4920 nextOffset = child.getRight() + mItemMargin; |
|
4921 } |
|
4922 |
|
4923 if (selected) { |
|
4924 selectedView = child; |
|
4925 } |
|
4926 |
|
4927 pos++; |
|
4928 } |
|
4929 |
|
4930 return selectedView; |
|
4931 } |
|
4932 |
|
4933 private View fillSpecific(int position, int offset) { |
|
4934 final boolean tempIsSelected = (position == mSelectedPosition); |
|
4935 View temp = makeAndAddView(position, offset, true, tempIsSelected); |
|
4936 |
|
4937 // Possibly changed again in fillBefore if we add rows above this one. |
|
4938 mFirstPosition = position; |
|
4939 |
|
4940 final int itemMargin = mItemMargin; |
|
4941 |
|
4942 final int offsetBefore; |
|
4943 if (mIsVertical) { |
|
4944 offsetBefore = temp.getTop() - itemMargin; |
|
4945 } else { |
|
4946 offsetBefore = temp.getLeft() - itemMargin; |
|
4947 } |
|
4948 final View before = fillBefore(position - 1, offsetBefore); |
|
4949 |
|
4950 // This will correct for the top of the first view not touching the top of the list |
|
4951 adjustViewsStartOrEnd(); |
|
4952 |
|
4953 final int offsetAfter; |
|
4954 if (mIsVertical) { |
|
4955 offsetAfter = temp.getBottom() + itemMargin; |
|
4956 } else { |
|
4957 offsetAfter = temp.getRight() + itemMargin; |
|
4958 } |
|
4959 final View after = fillAfter(position + 1, offsetAfter); |
|
4960 |
|
4961 final int childCount = getChildCount(); |
|
4962 if (childCount > 0) { |
|
4963 correctTooHigh(childCount); |
|
4964 } |
|
4965 |
|
4966 if (tempIsSelected) { |
|
4967 return temp; |
|
4968 } else if (before != null) { |
|
4969 return before; |
|
4970 } else { |
|
4971 return after; |
|
4972 } |
|
4973 } |
|
4974 |
|
4975 private View fillFromOffset(int nextOffset) { |
|
4976 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition); |
|
4977 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); |
|
4978 |
|
4979 if (mFirstPosition < 0) { |
|
4980 mFirstPosition = 0; |
|
4981 } |
|
4982 |
|
4983 return fillAfter(mFirstPosition, nextOffset); |
|
4984 } |
|
4985 |
|
4986 private View fillFromMiddle(int start, int end) { |
|
4987 final int size = end - start; |
|
4988 int position = reconcileSelectedPosition(); |
|
4989 |
|
4990 View selected = makeAndAddView(position, start, true, true); |
|
4991 mFirstPosition = position; |
|
4992 |
|
4993 if (mIsVertical) { |
|
4994 int selectedHeight = selected.getMeasuredHeight(); |
|
4995 if (selectedHeight <= size) { |
|
4996 selected.offsetTopAndBottom((size - selectedHeight) / 2); |
|
4997 } |
|
4998 } else { |
|
4999 int selectedWidth = selected.getMeasuredWidth(); |
|
5000 if (selectedWidth <= size) { |
|
5001 selected.offsetLeftAndRight((size - selectedWidth) / 2); |
|
5002 } |
|
5003 } |
|
5004 |
|
5005 fillBeforeAndAfter(selected, position); |
|
5006 correctTooHigh(getChildCount()); |
|
5007 |
|
5008 return selected; |
|
5009 } |
|
5010 |
|
5011 private void fillBeforeAndAfter(View selected, int position) { |
|
5012 final int itemMargin = mItemMargin; |
|
5013 |
|
5014 final int offsetBefore; |
|
5015 if (mIsVertical) { |
|
5016 offsetBefore = selected.getTop() - itemMargin; |
|
5017 } else { |
|
5018 offsetBefore = selected.getLeft() - itemMargin; |
|
5019 } |
|
5020 |
|
5021 fillBefore(position - 1, offsetBefore); |
|
5022 |
|
5023 adjustViewsStartOrEnd(); |
|
5024 |
|
5025 final int offsetAfter; |
|
5026 if (mIsVertical) { |
|
5027 offsetAfter = selected.getBottom() + itemMargin; |
|
5028 } else { |
|
5029 offsetAfter = selected.getRight() + itemMargin; |
|
5030 } |
|
5031 |
|
5032 fillAfter(position + 1, offsetAfter); |
|
5033 } |
|
5034 |
|
5035 private View fillFromSelection(int selectedTop, int start, int end) { |
|
5036 final int selectedPosition = mSelectedPosition; |
|
5037 View selected; |
|
5038 |
|
5039 selected = makeAndAddView(selectedPosition, selectedTop, true, true); |
|
5040 |
|
5041 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft()); |
|
5042 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight()); |
|
5043 |
|
5044 // Some of the newly selected item extends below the bottom of the list |
|
5045 if (selectedEnd > end) { |
|
5046 // Find space available above the selection into which we can scroll |
|
5047 // upwards |
|
5048 final int spaceAbove = selectedStart - start; |
|
5049 |
|
5050 // Find space required to bring the bottom of the selected item |
|
5051 // fully into view |
|
5052 final int spaceBelow = selectedEnd - end; |
|
5053 |
|
5054 final int offset = Math.min(spaceAbove, spaceBelow); |
|
5055 |
|
5056 // Now offset the selected item to get it into view |
|
5057 selected.offsetTopAndBottom(-offset); |
|
5058 } else if (selectedStart < start) { |
|
5059 // Find space required to bring the top of the selected item fully |
|
5060 // into view |
|
5061 final int spaceAbove = start - selectedStart; |
|
5062 |
|
5063 // Find space available below the selection into which we can scroll |
|
5064 // downwards |
|
5065 final int spaceBelow = end - selectedEnd; |
|
5066 |
|
5067 final int offset = Math.min(spaceAbove, spaceBelow); |
|
5068 |
|
5069 // Offset the selected item to get it into view |
|
5070 selected.offsetTopAndBottom(offset); |
|
5071 } |
|
5072 |
|
5073 // Fill in views above and below |
|
5074 fillBeforeAndAfter(selected, selectedPosition); |
|
5075 correctTooHigh(getChildCount()); |
|
5076 |
|
5077 return selected; |
|
5078 } |
|
5079 |
|
5080 private void correctTooHigh(int childCount) { |
|
5081 // First see if the last item is visible. If it is not, it is OK for the |
|
5082 // top of the list to be pushed up. |
|
5083 final int lastPosition = mFirstPosition + childCount - 1; |
|
5084 if (lastPosition != mItemCount - 1 || childCount == 0) { |
|
5085 return; |
|
5086 } |
|
5087 |
|
5088 // Get the last child ... |
|
5089 final View lastChild = getChildAt(childCount - 1); |
|
5090 |
|
5091 // ... and its end edge |
|
5092 final int lastEnd; |
|
5093 if (mIsVertical) { |
|
5094 lastEnd = lastChild.getBottom(); |
|
5095 } else { |
|
5096 lastEnd = lastChild.getRight(); |
|
5097 } |
|
5098 |
|
5099 // This is bottom of our drawable area |
|
5100 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
5101 final int end = |
|
5102 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight()); |
|
5103 |
|
5104 // This is how far the end edge of the last view is from the end of the |
|
5105 // drawable area |
|
5106 int endOffset = end - lastEnd; |
|
5107 |
|
5108 View firstChild = getChildAt(0); |
|
5109 int firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); |
|
5110 |
|
5111 // Make sure we are 1) Too high, and 2) Either there are more rows above the |
|
5112 // first row or the first row is scrolled off the top of the drawable area |
|
5113 if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start)) { |
|
5114 if (mFirstPosition == 0) { |
|
5115 // Don't pull the top too far down |
|
5116 endOffset = Math.min(endOffset, start - firstStart); |
|
5117 } |
|
5118 |
|
5119 // Move everything down |
|
5120 offsetChildren(endOffset); |
|
5121 |
|
5122 if (mFirstPosition > 0) { |
|
5123 firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); |
|
5124 |
|
5125 // Fill the gap that was opened above mFirstPosition with more rows, if |
|
5126 // possible |
|
5127 fillBefore(mFirstPosition - 1, firstStart - mItemMargin); |
|
5128 |
|
5129 // Close up the remaining gap |
|
5130 adjustViewsStartOrEnd(); |
|
5131 } |
|
5132 } |
|
5133 } |
|
5134 |
|
5135 private void correctTooLow(int childCount) { |
|
5136 // First see if the first item is visible. If it is not, it is OK for the |
|
5137 // bottom of the list to be pushed down. |
|
5138 if (mFirstPosition != 0 || childCount == 0) { |
|
5139 return; |
|
5140 } |
|
5141 |
|
5142 final View first = getChildAt(0); |
|
5143 final int firstStart = (mIsVertical ? first.getTop() : first.getLeft()); |
|
5144 |
|
5145 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft()); |
|
5146 |
|
5147 final int end; |
|
5148 if (mIsVertical) { |
|
5149 end = getHeight() - getPaddingBottom(); |
|
5150 } else { |
|
5151 end = getWidth() - getPaddingRight(); |
|
5152 } |
|
5153 |
|
5154 // This is how far the start edge of the first view is from the start of the |
|
5155 // drawable area |
|
5156 int startOffset = firstStart - start; |
|
5157 |
|
5158 View last = getChildAt(childCount - 1); |
|
5159 int lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); |
|
5160 |
|
5161 int lastPosition = mFirstPosition + childCount - 1; |
|
5162 |
|
5163 // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the |
|
5164 // last column/row or the last column/row is scrolled off the end of the |
|
5165 // drawable area |
|
5166 if (startOffset > 0) { |
|
5167 if (lastPosition < mItemCount - 1 || lastEnd > end) { |
|
5168 if (lastPosition == mItemCount - 1) { |
|
5169 // Don't pull the bottom too far up |
|
5170 startOffset = Math.min(startOffset, lastEnd - end); |
|
5171 } |
|
5172 |
|
5173 // Move everything up |
|
5174 offsetChildren(-startOffset); |
|
5175 |
|
5176 if (lastPosition < mItemCount - 1) { |
|
5177 lastEnd = (mIsVertical ? last.getBottom() : last.getRight()); |
|
5178 |
|
5179 // Fill the gap that was opened below the last position with more rows, if |
|
5180 // possible |
|
5181 fillAfter(lastPosition + 1, lastEnd + mItemMargin); |
|
5182 |
|
5183 // Close up the remaining gap |
|
5184 adjustViewsStartOrEnd(); |
|
5185 } |
|
5186 } else if (lastPosition == mItemCount - 1) { |
|
5187 adjustViewsStartOrEnd(); |
|
5188 } |
|
5189 } |
|
5190 } |
|
5191 |
|
5192 private void adjustViewsStartOrEnd() { |
|
5193 if (getChildCount() == 0) { |
|
5194 return; |
|
5195 } |
|
5196 |
|
5197 final View firstChild = getChildAt(0); |
|
5198 |
|
5199 int delta; |
|
5200 if (mIsVertical) { |
|
5201 delta = firstChild.getTop() - getPaddingTop() - mItemMargin; |
|
5202 } else { |
|
5203 delta = firstChild.getLeft() - getPaddingLeft() - mItemMargin; |
|
5204 } |
|
5205 |
|
5206 if (delta < 0) { |
|
5207 // We only are looking to see if we are too low, not too high |
|
5208 delta = 0; |
|
5209 } |
|
5210 |
|
5211 if (delta != 0) { |
|
5212 offsetChildren(-delta); |
|
5213 } |
|
5214 } |
|
5215 |
|
5216 @TargetApi(14) |
|
5217 private SparseBooleanArray cloneCheckStates() { |
|
5218 if (mCheckStates == null) { |
|
5219 return null; |
|
5220 } |
|
5221 |
|
5222 SparseBooleanArray checkedStates; |
|
5223 |
|
5224 if (Build.VERSION.SDK_INT >= 14) { |
|
5225 checkedStates = mCheckStates.clone(); |
|
5226 } else { |
|
5227 checkedStates = new SparseBooleanArray(); |
|
5228 |
|
5229 for (int i = 0; i < mCheckStates.size(); i++) { |
|
5230 checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i)); |
|
5231 } |
|
5232 } |
|
5233 |
|
5234 return checkedStates; |
|
5235 } |
|
5236 |
|
5237 private int findSyncPosition() { |
|
5238 int itemCount = mItemCount; |
|
5239 |
|
5240 if (itemCount == 0) { |
|
5241 return INVALID_POSITION; |
|
5242 } |
|
5243 |
|
5244 final long idToMatch = mSyncRowId; |
|
5245 |
|
5246 // If there isn't a selection don't hunt for it |
|
5247 if (idToMatch == INVALID_ROW_ID) { |
|
5248 return INVALID_POSITION; |
|
5249 } |
|
5250 |
|
5251 // Pin seed to reasonable values |
|
5252 int seed = mSyncPosition; |
|
5253 seed = Math.max(0, seed); |
|
5254 seed = Math.min(itemCount - 1, seed); |
|
5255 |
|
5256 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; |
|
5257 |
|
5258 long rowId; |
|
5259 |
|
5260 // first position scanned so far |
|
5261 int first = seed; |
|
5262 |
|
5263 // last position scanned so far |
|
5264 int last = seed; |
|
5265 |
|
5266 // True if we should move down on the next iteration |
|
5267 boolean next = false; |
|
5268 |
|
5269 // True when we have looked at the first item in the data |
|
5270 boolean hitFirst; |
|
5271 |
|
5272 // True when we have looked at the last item in the data |
|
5273 boolean hitLast; |
|
5274 |
|
5275 // Get the item ID locally (instead of getItemIdAtPosition), so |
|
5276 // we need the adapter |
|
5277 final ListAdapter adapter = mAdapter; |
|
5278 if (adapter == null) { |
|
5279 return INVALID_POSITION; |
|
5280 } |
|
5281 |
|
5282 while (SystemClock.uptimeMillis() <= endTime) { |
|
5283 rowId = adapter.getItemId(seed); |
|
5284 if (rowId == idToMatch) { |
|
5285 // Found it! |
|
5286 return seed; |
|
5287 } |
|
5288 |
|
5289 hitLast = (last == itemCount - 1); |
|
5290 hitFirst = (first == 0); |
|
5291 |
|
5292 if (hitLast && hitFirst) { |
|
5293 // Looked at everything |
|
5294 break; |
|
5295 } |
|
5296 |
|
5297 if (hitFirst || (next && !hitLast)) { |
|
5298 // Either we hit the top, or we are trying to move down |
|
5299 last++; |
|
5300 seed = last; |
|
5301 |
|
5302 // Try going up next time |
|
5303 next = false; |
|
5304 } else if (hitLast || (!next && !hitFirst)) { |
|
5305 // Either we hit the bottom, or we are trying to move up |
|
5306 first--; |
|
5307 seed = first; |
|
5308 |
|
5309 // Try going down next time |
|
5310 next = true; |
|
5311 } |
|
5312 } |
|
5313 |
|
5314 return INVALID_POSITION; |
|
5315 } |
|
5316 |
|
5317 @TargetApi(16) |
|
5318 private View obtainView(int position, boolean[] isScrap) { |
|
5319 isScrap[0] = false; |
|
5320 |
|
5321 View scrapView = mRecycler.getTransientStateView(position); |
|
5322 if (scrapView != null) { |
|
5323 return scrapView; |
|
5324 } |
|
5325 |
|
5326 scrapView = mRecycler.getScrapView(position); |
|
5327 |
|
5328 final View child; |
|
5329 if (scrapView != null) { |
|
5330 child = mAdapter.getView(position, scrapView, this); |
|
5331 |
|
5332 if (child != scrapView) { |
|
5333 mRecycler.addScrapView(scrapView, position); |
|
5334 } else { |
|
5335 isScrap[0] = true; |
|
5336 } |
|
5337 } else { |
|
5338 child = mAdapter.getView(position, null, this); |
|
5339 } |
|
5340 |
|
5341 if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { |
|
5342 ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); |
|
5343 } |
|
5344 |
|
5345 if (mHasStableIds) { |
|
5346 LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
|
5347 |
|
5348 if (lp == null) { |
|
5349 lp = generateDefaultLayoutParams(); |
|
5350 } else if (!checkLayoutParams(lp)) { |
|
5351 lp = generateLayoutParams(lp); |
|
5352 } |
|
5353 |
|
5354 lp.id = mAdapter.getItemId(position); |
|
5355 |
|
5356 child.setLayoutParams(lp); |
|
5357 } |
|
5358 |
|
5359 if (mAccessibilityDelegate == null) { |
|
5360 mAccessibilityDelegate = new ListItemAccessibilityDelegate(); |
|
5361 } |
|
5362 |
|
5363 ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate); |
|
5364 |
|
5365 return child; |
|
5366 } |
|
5367 |
|
5368 void resetState() { |
|
5369 removeAllViewsInLayout(); |
|
5370 |
|
5371 mSelectedStart = 0; |
|
5372 mFirstPosition = 0; |
|
5373 mDataChanged = false; |
|
5374 mNeedSync = false; |
|
5375 mPendingSync = null; |
|
5376 mOldSelectedPosition = INVALID_POSITION; |
|
5377 mOldSelectedRowId = INVALID_ROW_ID; |
|
5378 |
|
5379 mOverScroll = 0; |
|
5380 |
|
5381 setSelectedPositionInt(INVALID_POSITION); |
|
5382 setNextSelectedPositionInt(INVALID_POSITION); |
|
5383 |
|
5384 mSelectorPosition = INVALID_POSITION; |
|
5385 mSelectorRect.setEmpty(); |
|
5386 |
|
5387 invalidate(); |
|
5388 } |
|
5389 |
|
5390 private void rememberSyncState() { |
|
5391 if (getChildCount() == 0) { |
|
5392 return; |
|
5393 } |
|
5394 |
|
5395 mNeedSync = true; |
|
5396 |
|
5397 if (mSelectedPosition >= 0) { |
|
5398 View child = getChildAt(mSelectedPosition - mFirstPosition); |
|
5399 |
|
5400 mSyncRowId = mNextSelectedRowId; |
|
5401 mSyncPosition = mNextSelectedPosition; |
|
5402 |
|
5403 if (child != null) { |
|
5404 mSpecificStart = (mIsVertical ? child.getTop() : child.getLeft()); |
|
5405 } |
|
5406 |
|
5407 mSyncMode = SYNC_SELECTED_POSITION; |
|
5408 } else { |
|
5409 // Sync the based on the offset of the first view |
|
5410 View child = getChildAt(0); |
|
5411 ListAdapter adapter = getAdapter(); |
|
5412 |
|
5413 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { |
|
5414 mSyncRowId = adapter.getItemId(mFirstPosition); |
|
5415 } else { |
|
5416 mSyncRowId = NO_ID; |
|
5417 } |
|
5418 |
|
5419 mSyncPosition = mFirstPosition; |
|
5420 |
|
5421 if (child != null) { |
|
5422 mSpecificStart = child.getTop(); |
|
5423 } |
|
5424 |
|
5425 mSyncMode = SYNC_FIRST_POSITION; |
|
5426 } |
|
5427 } |
|
5428 |
|
5429 private ContextMenuInfo createContextMenuInfo(View view, int position, long id) { |
|
5430 return new AdapterContextMenuInfo(view, position, id); |
|
5431 } |
|
5432 |
|
5433 @TargetApi(11) |
|
5434 private void updateOnScreenCheckedViews() { |
|
5435 final int firstPos = mFirstPosition; |
|
5436 final int count = getChildCount(); |
|
5437 |
|
5438 final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion |
|
5439 >= Build.VERSION_CODES.HONEYCOMB; |
|
5440 |
|
5441 for (int i = 0; i < count; i++) { |
|
5442 final View child = getChildAt(i); |
|
5443 final int position = firstPos + i; |
|
5444 |
|
5445 if (child instanceof Checkable) { |
|
5446 ((Checkable) child).setChecked(mCheckStates.get(position)); |
|
5447 } else if (useActivated) { |
|
5448 child.setActivated(mCheckStates.get(position)); |
|
5449 } |
|
5450 } |
|
5451 } |
|
5452 |
|
5453 @Override |
|
5454 public boolean performItemClick(View view, int position, long id) { |
|
5455 boolean checkedStateChanged = false; |
|
5456 |
|
5457 if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) { |
|
5458 boolean checked = !mCheckStates.get(position, false); |
|
5459 mCheckStates.put(position, checked); |
|
5460 |
|
5461 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { |
|
5462 if (checked) { |
|
5463 mCheckedIdStates.put(mAdapter.getItemId(position), position); |
|
5464 } else { |
|
5465 mCheckedIdStates.delete(mAdapter.getItemId(position)); |
|
5466 } |
|
5467 } |
|
5468 |
|
5469 if (checked) { |
|
5470 mCheckedItemCount++; |
|
5471 } else { |
|
5472 mCheckedItemCount--; |
|
5473 } |
|
5474 |
|
5475 checkedStateChanged = true; |
|
5476 } else if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0) { |
|
5477 boolean checked = !mCheckStates.get(position, false); |
|
5478 if (checked) { |
|
5479 mCheckStates.clear(); |
|
5480 mCheckStates.put(position, true); |
|
5481 |
|
5482 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { |
|
5483 mCheckedIdStates.clear(); |
|
5484 mCheckedIdStates.put(mAdapter.getItemId(position), position); |
|
5485 } |
|
5486 |
|
5487 mCheckedItemCount = 1; |
|
5488 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { |
|
5489 mCheckedItemCount = 0; |
|
5490 } |
|
5491 |
|
5492 checkedStateChanged = true; |
|
5493 } |
|
5494 |
|
5495 if (checkedStateChanged) { |
|
5496 updateOnScreenCheckedViews(); |
|
5497 } |
|
5498 |
|
5499 return super.performItemClick(view, position, id); |
|
5500 } |
|
5501 |
|
5502 private boolean performLongPress(final View child, |
|
5503 final int longPressPosition, final long longPressId) { |
|
5504 // CHOICE_MODE_MULTIPLE_MODAL takes over long press. |
|
5505 boolean handled = false; |
|
5506 |
|
5507 OnItemLongClickListener listener = getOnItemLongClickListener(); |
|
5508 if (listener != null) { |
|
5509 handled = listener.onItemLongClick(TwoWayView.this, child, |
|
5510 longPressPosition, longPressId); |
|
5511 } |
|
5512 |
|
5513 if (!handled) { |
|
5514 mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); |
|
5515 handled = super.showContextMenuForChild(TwoWayView.this); |
|
5516 } |
|
5517 |
|
5518 if (handled) { |
|
5519 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); |
|
5520 } |
|
5521 |
|
5522 return handled; |
|
5523 } |
|
5524 |
|
5525 @Override |
|
5526 protected LayoutParams generateDefaultLayoutParams() { |
|
5527 if (mIsVertical) { |
|
5528 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); |
|
5529 } else { |
|
5530 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); |
|
5531 } |
|
5532 } |
|
5533 |
|
5534 @Override |
|
5535 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { |
|
5536 return new LayoutParams(lp); |
|
5537 } |
|
5538 |
|
5539 @Override |
|
5540 protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { |
|
5541 return lp instanceof LayoutParams; |
|
5542 } |
|
5543 |
|
5544 @Override |
|
5545 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { |
|
5546 return new LayoutParams(getContext(), attrs); |
|
5547 } |
|
5548 |
|
5549 @Override |
|
5550 protected ContextMenuInfo getContextMenuInfo() { |
|
5551 return mContextMenuInfo; |
|
5552 } |
|
5553 |
|
5554 @Override |
|
5555 public Parcelable onSaveInstanceState() { |
|
5556 Parcelable superState = super.onSaveInstanceState(); |
|
5557 SavedState ss = new SavedState(superState); |
|
5558 |
|
5559 if (mPendingSync != null) { |
|
5560 ss.selectedId = mPendingSync.selectedId; |
|
5561 ss.firstId = mPendingSync.firstId; |
|
5562 ss.viewStart = mPendingSync.viewStart; |
|
5563 ss.position = mPendingSync.position; |
|
5564 ss.height = mPendingSync.height; |
|
5565 |
|
5566 return ss; |
|
5567 } |
|
5568 |
|
5569 boolean haveChildren = (getChildCount() > 0 && mItemCount > 0); |
|
5570 long selectedId = getSelectedItemId(); |
|
5571 ss.selectedId = selectedId; |
|
5572 ss.height = getHeight(); |
|
5573 |
|
5574 if (selectedId >= 0) { |
|
5575 ss.viewStart = mSelectedStart; |
|
5576 ss.position = getSelectedItemPosition(); |
|
5577 ss.firstId = INVALID_POSITION; |
|
5578 } else if (haveChildren && mFirstPosition > 0) { |
|
5579 // Remember the position of the first child. |
|
5580 // We only do this if we are not currently at the top of |
|
5581 // the list, for two reasons: |
|
5582 // |
|
5583 // (1) The list may be in the process of becoming empty, in |
|
5584 // which case mItemCount may not be 0, but if we try to |
|
5585 // ask for any information about position 0 we will crash. |
|
5586 // |
|
5587 // (2) Being "at the top" seems like a special case, anyway, |
|
5588 // and the user wouldn't expect to end up somewhere else when |
|
5589 // they revisit the list even if its content has changed. |
|
5590 |
|
5591 View child = getChildAt(0); |
|
5592 ss.viewStart = (mIsVertical ? child.getTop() : child.getLeft()); |
|
5593 |
|
5594 int firstPos = mFirstPosition; |
|
5595 if (firstPos >= mItemCount) { |
|
5596 firstPos = mItemCount - 1; |
|
5597 } |
|
5598 |
|
5599 ss.position = firstPos; |
|
5600 ss.firstId = mAdapter.getItemId(firstPos); |
|
5601 } else { |
|
5602 ss.viewStart = 0; |
|
5603 ss.firstId = INVALID_POSITION; |
|
5604 ss.position = 0; |
|
5605 } |
|
5606 |
|
5607 if (mCheckStates != null) { |
|
5608 ss.checkState = cloneCheckStates(); |
|
5609 } |
|
5610 |
|
5611 if (mCheckedIdStates != null) { |
|
5612 final LongSparseArray<Integer> idState = new LongSparseArray<Integer>(); |
|
5613 |
|
5614 final int count = mCheckedIdStates.size(); |
|
5615 for (int i = 0; i < count; i++) { |
|
5616 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i)); |
|
5617 } |
|
5618 |
|
5619 ss.checkIdState = idState; |
|
5620 } |
|
5621 |
|
5622 ss.checkedItemCount = mCheckedItemCount; |
|
5623 |
|
5624 return ss; |
|
5625 } |
|
5626 |
|
5627 @Override |
|
5628 public void onRestoreInstanceState(Parcelable state) { |
|
5629 SavedState ss = (SavedState) state; |
|
5630 super.onRestoreInstanceState(ss.getSuperState()); |
|
5631 |
|
5632 mDataChanged = true; |
|
5633 mSyncHeight = ss.height; |
|
5634 |
|
5635 if (ss.selectedId >= 0) { |
|
5636 mNeedSync = true; |
|
5637 mPendingSync = ss; |
|
5638 mSyncRowId = ss.selectedId; |
|
5639 mSyncPosition = ss.position; |
|
5640 mSpecificStart = ss.viewStart; |
|
5641 mSyncMode = SYNC_SELECTED_POSITION; |
|
5642 } else if (ss.firstId >= 0) { |
|
5643 setSelectedPositionInt(INVALID_POSITION); |
|
5644 |
|
5645 // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync |
|
5646 setNextSelectedPositionInt(INVALID_POSITION); |
|
5647 |
|
5648 mSelectorPosition = INVALID_POSITION; |
|
5649 mNeedSync = true; |
|
5650 mPendingSync = ss; |
|
5651 mSyncRowId = ss.firstId; |
|
5652 mSyncPosition = ss.position; |
|
5653 mSpecificStart = ss.viewStart; |
|
5654 mSyncMode = SYNC_FIRST_POSITION; |
|
5655 } |
|
5656 |
|
5657 if (ss.checkState != null) { |
|
5658 mCheckStates = ss.checkState; |
|
5659 } |
|
5660 |
|
5661 if (ss.checkIdState != null) { |
|
5662 mCheckedIdStates = ss.checkIdState; |
|
5663 } |
|
5664 |
|
5665 mCheckedItemCount = ss.checkedItemCount; |
|
5666 |
|
5667 requestLayout(); |
|
5668 } |
|
5669 |
|
5670 public static class LayoutParams extends ViewGroup.LayoutParams { |
|
5671 /** |
|
5672 * Type of this view as reported by the adapter |
|
5673 */ |
|
5674 int viewType; |
|
5675 |
|
5676 /** |
|
5677 * The stable ID of the item this view displays |
|
5678 */ |
|
5679 long id = -1; |
|
5680 |
|
5681 /** |
|
5682 * The position the view was removed from when pulled out of the |
|
5683 * scrap heap. |
|
5684 * @hide |
|
5685 */ |
|
5686 int scrappedFromPosition; |
|
5687 |
|
5688 /** |
|
5689 * When a TwoWayView is measured with an AT_MOST measure spec, it needs |
|
5690 * to obtain children views to measure itself. When doing so, the children |
|
5691 * are not attached to the window, but put in the recycler which assumes |
|
5692 * they've been attached before. Setting this flag will force the reused |
|
5693 * view to be attached to the window rather than just attached to the |
|
5694 * parent. |
|
5695 */ |
|
5696 boolean forceAdd; |
|
5697 |
|
5698 public LayoutParams(int width, int height) { |
|
5699 super(width, height); |
|
5700 |
|
5701 if (this.width == MATCH_PARENT) { |
|
5702 Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " + |
|
5703 "does not make much sense as the view might change orientation. " + |
|
5704 "Falling back to WRAP_CONTENT"); |
|
5705 this.width = WRAP_CONTENT; |
|
5706 } |
|
5707 |
|
5708 if (this.height == MATCH_PARENT) { |
|
5709 Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " + |
|
5710 "does not make much sense as the view might change orientation. " + |
|
5711 "Falling back to WRAP_CONTENT"); |
|
5712 this.height = WRAP_CONTENT; |
|
5713 } |
|
5714 } |
|
5715 |
|
5716 public LayoutParams(Context c, AttributeSet attrs) { |
|
5717 super(c, attrs); |
|
5718 |
|
5719 if (this.width == MATCH_PARENT) { |
|
5720 Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " + |
|
5721 "does not make much sense as the view might change orientation. " + |
|
5722 "Falling back to WRAP_CONTENT"); |
|
5723 this.width = MATCH_PARENT; |
|
5724 } |
|
5725 |
|
5726 if (this.height == MATCH_PARENT) { |
|
5727 Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + |
|
5728 "does not make much sense as the view might change orientation. " + |
|
5729 "Falling back to WRAP_CONTENT"); |
|
5730 this.height = WRAP_CONTENT; |
|
5731 } |
|
5732 } |
|
5733 |
|
5734 public LayoutParams(ViewGroup.LayoutParams other) { |
|
5735 super(other); |
|
5736 |
|
5737 if (this.width == MATCH_PARENT) { |
|
5738 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " + |
|
5739 "does not make much sense as the view might change orientation. " + |
|
5740 "Falling back to WRAP_CONTENT"); |
|
5741 this.width = WRAP_CONTENT; |
|
5742 } |
|
5743 |
|
5744 if (this.height == MATCH_PARENT) { |
|
5745 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " + |
|
5746 "does not make much sense as the view might change orientation. " + |
|
5747 "Falling back to WRAP_CONTENT"); |
|
5748 this.height = WRAP_CONTENT; |
|
5749 } |
|
5750 } |
|
5751 } |
|
5752 |
|
5753 class RecycleBin { |
|
5754 private RecyclerListener mRecyclerListener; |
|
5755 private int mFirstActivePosition; |
|
5756 private View[] mActiveViews = new View[0]; |
|
5757 private ArrayList<View>[] mScrapViews; |
|
5758 private int mViewTypeCount; |
|
5759 private ArrayList<View> mCurrentScrap; |
|
5760 private SparseArrayCompat<View> mTransientStateViews; |
|
5761 |
|
5762 public void setViewTypeCount(int viewTypeCount) { |
|
5763 if (viewTypeCount < 1) { |
|
5764 throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); |
|
5765 } |
|
5766 |
|
5767 @SuppressWarnings({"unchecked", "rawtypes"}) |
|
5768 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; |
|
5769 for (int i = 0; i < viewTypeCount; i++) { |
|
5770 scrapViews[i] = new ArrayList<View>(); |
|
5771 } |
|
5772 |
|
5773 mViewTypeCount = viewTypeCount; |
|
5774 mCurrentScrap = scrapViews[0]; |
|
5775 mScrapViews = scrapViews; |
|
5776 } |
|
5777 |
|
5778 public void markChildrenDirty() { |
|
5779 if (mViewTypeCount == 1) { |
|
5780 final ArrayList<View> scrap = mCurrentScrap; |
|
5781 final int scrapCount = scrap.size(); |
|
5782 |
|
5783 for (int i = 0; i < scrapCount; i++) { |
|
5784 scrap.get(i).forceLayout(); |
|
5785 } |
|
5786 } else { |
|
5787 final int typeCount = mViewTypeCount; |
|
5788 for (int i = 0; i < typeCount; i++) { |
|
5789 final ArrayList<View> scrap = mScrapViews[i]; |
|
5790 final int scrapCount = scrap.size(); |
|
5791 |
|
5792 for (int j = 0; j < scrapCount; j++) { |
|
5793 scrap.get(j).forceLayout(); |
|
5794 } |
|
5795 } |
|
5796 } |
|
5797 |
|
5798 if (mTransientStateViews != null) { |
|
5799 final int count = mTransientStateViews.size(); |
|
5800 for (int i = 0; i < count; i++) { |
|
5801 mTransientStateViews.valueAt(i).forceLayout(); |
|
5802 } |
|
5803 } |
|
5804 } |
|
5805 |
|
5806 public boolean shouldRecycleViewType(int viewType) { |
|
5807 return viewType >= 0; |
|
5808 } |
|
5809 |
|
5810 void clear() { |
|
5811 if (mViewTypeCount == 1) { |
|
5812 final ArrayList<View> scrap = mCurrentScrap; |
|
5813 final int scrapCount = scrap.size(); |
|
5814 |
|
5815 for (int i = 0; i < scrapCount; i++) { |
|
5816 removeDetachedView(scrap.remove(scrapCount - 1 - i), false); |
|
5817 } |
|
5818 } else { |
|
5819 final int typeCount = mViewTypeCount; |
|
5820 for (int i = 0; i < typeCount; i++) { |
|
5821 final ArrayList<View> scrap = mScrapViews[i]; |
|
5822 final int scrapCount = scrap.size(); |
|
5823 |
|
5824 for (int j = 0; j < scrapCount; j++) { |
|
5825 removeDetachedView(scrap.remove(scrapCount - 1 - j), false); |
|
5826 } |
|
5827 } |
|
5828 } |
|
5829 |
|
5830 if (mTransientStateViews != null) { |
|
5831 mTransientStateViews.clear(); |
|
5832 } |
|
5833 } |
|
5834 |
|
5835 void fillActiveViews(int childCount, int firstActivePosition) { |
|
5836 if (mActiveViews.length < childCount) { |
|
5837 mActiveViews = new View[childCount]; |
|
5838 } |
|
5839 |
|
5840 mFirstActivePosition = firstActivePosition; |
|
5841 |
|
5842 final View[] activeViews = mActiveViews; |
|
5843 for (int i = 0; i < childCount; i++) { |
|
5844 View child = getChildAt(i); |
|
5845 |
|
5846 // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. |
|
5847 // However, we will NOT place them into scrap views. |
|
5848 activeViews[i] = child; |
|
5849 } |
|
5850 } |
|
5851 |
|
5852 View getActiveView(int position) { |
|
5853 final int index = position - mFirstActivePosition; |
|
5854 final View[] activeViews = mActiveViews; |
|
5855 |
|
5856 if (index >= 0 && index < activeViews.length) { |
|
5857 final View match = activeViews[index]; |
|
5858 activeViews[index] = null; |
|
5859 |
|
5860 return match; |
|
5861 } |
|
5862 |
|
5863 return null; |
|
5864 } |
|
5865 |
|
5866 View getTransientStateView(int position) { |
|
5867 if (mTransientStateViews == null) { |
|
5868 return null; |
|
5869 } |
|
5870 |
|
5871 final int index = mTransientStateViews.indexOfKey(position); |
|
5872 if (index < 0) { |
|
5873 return null; |
|
5874 } |
|
5875 |
|
5876 final View result = mTransientStateViews.valueAt(index); |
|
5877 mTransientStateViews.removeAt(index); |
|
5878 |
|
5879 return result; |
|
5880 } |
|
5881 |
|
5882 void clearTransientStateViews() { |
|
5883 if (mTransientStateViews != null) { |
|
5884 mTransientStateViews.clear(); |
|
5885 } |
|
5886 } |
|
5887 |
|
5888 View getScrapView(int position) { |
|
5889 if (mViewTypeCount == 1) { |
|
5890 return retrieveFromScrap(mCurrentScrap, position); |
|
5891 } else { |
|
5892 int whichScrap = mAdapter.getItemViewType(position); |
|
5893 if (whichScrap >= 0 && whichScrap < mScrapViews.length) { |
|
5894 return retrieveFromScrap(mScrapViews[whichScrap], position); |
|
5895 } |
|
5896 } |
|
5897 |
|
5898 return null; |
|
5899 } |
|
5900 |
|
5901 @TargetApi(14) |
|
5902 void addScrapView(View scrap, int position) { |
|
5903 LayoutParams lp = (LayoutParams) scrap.getLayoutParams(); |
|
5904 if (lp == null) { |
|
5905 return; |
|
5906 } |
|
5907 |
|
5908 lp.scrappedFromPosition = position; |
|
5909 |
|
5910 final int viewType = lp.viewType; |
|
5911 final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap); |
|
5912 |
|
5913 // Don't put views that should be ignored into the scrap heap |
|
5914 if (!shouldRecycleViewType(viewType) || scrapHasTransientState) { |
|
5915 if (scrapHasTransientState) { |
|
5916 if (mTransientStateViews == null) { |
|
5917 mTransientStateViews = new SparseArrayCompat<View>(); |
|
5918 } |
|
5919 |
|
5920 mTransientStateViews.put(position, scrap); |
|
5921 } |
|
5922 |
|
5923 return; |
|
5924 } |
|
5925 |
|
5926 if (mViewTypeCount == 1) { |
|
5927 mCurrentScrap.add(scrap); |
|
5928 } else { |
|
5929 mScrapViews[viewType].add(scrap); |
|
5930 } |
|
5931 |
|
5932 // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept |
|
5933 // null delegates. |
|
5934 if (Build.VERSION.SDK_INT >= 14) { |
|
5935 scrap.setAccessibilityDelegate(null); |
|
5936 } |
|
5937 |
|
5938 if (mRecyclerListener != null) { |
|
5939 mRecyclerListener.onMovedToScrapHeap(scrap); |
|
5940 } |
|
5941 } |
|
5942 |
|
5943 @TargetApi(14) |
|
5944 void scrapActiveViews() { |
|
5945 final View[] activeViews = mActiveViews; |
|
5946 final boolean multipleScraps = (mViewTypeCount > 1); |
|
5947 |
|
5948 ArrayList<View> scrapViews = mCurrentScrap; |
|
5949 final int count = activeViews.length; |
|
5950 |
|
5951 for (int i = count - 1; i >= 0; i--) { |
|
5952 final View victim = activeViews[i]; |
|
5953 if (victim != null) { |
|
5954 final LayoutParams lp = (LayoutParams) victim.getLayoutParams(); |
|
5955 int whichScrap = lp.viewType; |
|
5956 |
|
5957 activeViews[i] = null; |
|
5958 |
|
5959 final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim); |
|
5960 if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) { |
|
5961 if (scrapHasTransientState) { |
|
5962 removeDetachedView(victim, false); |
|
5963 |
|
5964 if (mTransientStateViews == null) { |
|
5965 mTransientStateViews = new SparseArrayCompat<View>(); |
|
5966 } |
|
5967 |
|
5968 mTransientStateViews.put(mFirstActivePosition + i, victim); |
|
5969 } |
|
5970 |
|
5971 continue; |
|
5972 } |
|
5973 |
|
5974 if (multipleScraps) { |
|
5975 scrapViews = mScrapViews[whichScrap]; |
|
5976 } |
|
5977 |
|
5978 lp.scrappedFromPosition = mFirstActivePosition + i; |
|
5979 scrapViews.add(victim); |
|
5980 |
|
5981 // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept |
|
5982 // null delegates. |
|
5983 if (Build.VERSION.SDK_INT >= 14) { |
|
5984 victim.setAccessibilityDelegate(null); |
|
5985 } |
|
5986 |
|
5987 if (mRecyclerListener != null) { |
|
5988 mRecyclerListener.onMovedToScrapHeap(victim); |
|
5989 } |
|
5990 } |
|
5991 } |
|
5992 |
|
5993 pruneScrapViews(); |
|
5994 } |
|
5995 |
|
5996 private void pruneScrapViews() { |
|
5997 final int maxViews = mActiveViews.length; |
|
5998 final int viewTypeCount = mViewTypeCount; |
|
5999 final ArrayList<View>[] scrapViews = mScrapViews; |
|
6000 |
|
6001 for (int i = 0; i < viewTypeCount; ++i) { |
|
6002 final ArrayList<View> scrapPile = scrapViews[i]; |
|
6003 int size = scrapPile.size(); |
|
6004 final int extras = size - maxViews; |
|
6005 |
|
6006 size--; |
|
6007 |
|
6008 for (int j = 0; j < extras; j++) { |
|
6009 removeDetachedView(scrapPile.remove(size--), false); |
|
6010 } |
|
6011 } |
|
6012 |
|
6013 if (mTransientStateViews != null) { |
|
6014 for (int i = 0; i < mTransientStateViews.size(); i++) { |
|
6015 final View v = mTransientStateViews.valueAt(i); |
|
6016 if (!ViewCompat.hasTransientState(v)) { |
|
6017 mTransientStateViews.removeAt(i); |
|
6018 i--; |
|
6019 } |
|
6020 } |
|
6021 } |
|
6022 } |
|
6023 |
|
6024 void reclaimScrapViews(List<View> views) { |
|
6025 if (mViewTypeCount == 1) { |
|
6026 views.addAll(mCurrentScrap); |
|
6027 } else { |
|
6028 final int viewTypeCount = mViewTypeCount; |
|
6029 final ArrayList<View>[] scrapViews = mScrapViews; |
|
6030 |
|
6031 for (int i = 0; i < viewTypeCount; ++i) { |
|
6032 final ArrayList<View> scrapPile = scrapViews[i]; |
|
6033 views.addAll(scrapPile); |
|
6034 } |
|
6035 } |
|
6036 } |
|
6037 |
|
6038 View retrieveFromScrap(ArrayList<View> scrapViews, int position) { |
|
6039 int size = scrapViews.size(); |
|
6040 if (size <= 0) { |
|
6041 return null; |
|
6042 } |
|
6043 |
|
6044 for (int i = 0; i < size; i++) { |
|
6045 final View scrapView = scrapViews.get(i); |
|
6046 final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams(); |
|
6047 |
|
6048 if (lp.scrappedFromPosition == position) { |
|
6049 scrapViews.remove(i); |
|
6050 return scrapView; |
|
6051 } |
|
6052 } |
|
6053 |
|
6054 return scrapViews.remove(size - 1); |
|
6055 } |
|
6056 } |
|
6057 |
|
6058 @Override |
|
6059 public void setEmptyView(View emptyView) { |
|
6060 super.setEmptyView(emptyView); |
|
6061 mEmptyView = emptyView; |
|
6062 updateEmptyStatus(); |
|
6063 } |
|
6064 |
|
6065 @Override |
|
6066 public void setFocusable(boolean focusable) { |
|
6067 final ListAdapter adapter = getAdapter(); |
|
6068 final boolean empty = (adapter == null || adapter.getCount() == 0); |
|
6069 |
|
6070 mDesiredFocusableState = focusable; |
|
6071 if (!focusable) { |
|
6072 mDesiredFocusableInTouchModeState = false; |
|
6073 } |
|
6074 |
|
6075 super.setFocusable(focusable && !empty); |
|
6076 } |
|
6077 |
|
6078 @Override |
|
6079 public void setFocusableInTouchMode(boolean focusable) { |
|
6080 final ListAdapter adapter = getAdapter(); |
|
6081 final boolean empty = (adapter == null || adapter.getCount() == 0); |
|
6082 |
|
6083 mDesiredFocusableInTouchModeState = focusable; |
|
6084 if (focusable) { |
|
6085 mDesiredFocusableState = true; |
|
6086 } |
|
6087 |
|
6088 super.setFocusableInTouchMode(focusable && !empty); |
|
6089 } |
|
6090 |
|
6091 private void checkFocus() { |
|
6092 final ListAdapter adapter = getAdapter(); |
|
6093 final boolean focusable = (adapter != null && adapter.getCount() > 0); |
|
6094 |
|
6095 // The order in which we set focusable in touch mode/focusable may matter |
|
6096 // for the client, see View.setFocusableInTouchMode() comments for more |
|
6097 // details |
|
6098 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); |
|
6099 super.setFocusable(focusable && mDesiredFocusableState); |
|
6100 |
|
6101 if (mEmptyView != null) { |
|
6102 updateEmptyStatus(); |
|
6103 } |
|
6104 } |
|
6105 |
|
6106 private void updateEmptyStatus() { |
|
6107 final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty()); |
|
6108 |
|
6109 if (isEmpty) { |
|
6110 if (mEmptyView != null) { |
|
6111 mEmptyView.setVisibility(View.VISIBLE); |
|
6112 setVisibility(View.GONE); |
|
6113 } else { |
|
6114 // If the caller just removed our empty view, make sure the list |
|
6115 // view is visible |
|
6116 setVisibility(View.VISIBLE); |
|
6117 } |
|
6118 |
|
6119 // We are now GONE, so pending layouts will not be dispatched. |
|
6120 // Force one here to make sure that the state of the list matches |
|
6121 // the state of the adapter. |
|
6122 if (mDataChanged) { |
|
6123 onLayout(false, getLeft(), getTop(), getRight(), getBottom()); |
|
6124 } |
|
6125 } else { |
|
6126 if (mEmptyView != null) { |
|
6127 mEmptyView.setVisibility(View.GONE); |
|
6128 } |
|
6129 |
|
6130 setVisibility(View.VISIBLE); |
|
6131 } |
|
6132 } |
|
6133 |
|
6134 private class AdapterDataSetObserver extends DataSetObserver { |
|
6135 private Parcelable mInstanceState = null; |
|
6136 |
|
6137 @Override |
|
6138 public void onChanged() { |
|
6139 mDataChanged = true; |
|
6140 mOldItemCount = mItemCount; |
|
6141 mItemCount = getAdapter().getCount(); |
|
6142 |
|
6143 // Detect the case where a cursor that was previously invalidated has |
|
6144 // been re-populated with new data. |
|
6145 if (TwoWayView.this.mHasStableIds && mInstanceState != null |
|
6146 && mOldItemCount == 0 && mItemCount > 0) { |
|
6147 TwoWayView.this.onRestoreInstanceState(mInstanceState); |
|
6148 mInstanceState = null; |
|
6149 } else { |
|
6150 rememberSyncState(); |
|
6151 } |
|
6152 |
|
6153 checkFocus(); |
|
6154 requestLayout(); |
|
6155 } |
|
6156 |
|
6157 @Override |
|
6158 public void onInvalidated() { |
|
6159 mDataChanged = true; |
|
6160 |
|
6161 if (TwoWayView.this.mHasStableIds) { |
|
6162 // Remember the current state for the case where our hosting activity is being |
|
6163 // stopped and later restarted |
|
6164 mInstanceState = TwoWayView.this.onSaveInstanceState(); |
|
6165 } |
|
6166 |
|
6167 // Data is invalid so we should reset our state |
|
6168 mOldItemCount = mItemCount; |
|
6169 mItemCount = 0; |
|
6170 |
|
6171 mSelectedPosition = INVALID_POSITION; |
|
6172 mSelectedRowId = INVALID_ROW_ID; |
|
6173 |
|
6174 mNextSelectedPosition = INVALID_POSITION; |
|
6175 mNextSelectedRowId = INVALID_ROW_ID; |
|
6176 |
|
6177 mNeedSync = false; |
|
6178 |
|
6179 checkFocus(); |
|
6180 requestLayout(); |
|
6181 } |
|
6182 } |
|
6183 |
|
6184 static class SavedState extends BaseSavedState { |
|
6185 long selectedId; |
|
6186 long firstId; |
|
6187 int viewStart; |
|
6188 int position; |
|
6189 int height; |
|
6190 int checkedItemCount; |
|
6191 SparseBooleanArray checkState; |
|
6192 LongSparseArray<Integer> checkIdState; |
|
6193 |
|
6194 /** |
|
6195 * Constructor called from {@link TwoWayView#onSaveInstanceState()} |
|
6196 */ |
|
6197 SavedState(Parcelable superState) { |
|
6198 super(superState); |
|
6199 } |
|
6200 |
|
6201 /** |
|
6202 * Constructor called from {@link #CREATOR} |
|
6203 */ |
|
6204 private SavedState(Parcel in) { |
|
6205 super(in); |
|
6206 |
|
6207 selectedId = in.readLong(); |
|
6208 firstId = in.readLong(); |
|
6209 viewStart = in.readInt(); |
|
6210 position = in.readInt(); |
|
6211 height = in.readInt(); |
|
6212 |
|
6213 checkedItemCount = in.readInt(); |
|
6214 checkState = in.readSparseBooleanArray(); |
|
6215 |
|
6216 final int N = in.readInt(); |
|
6217 if (N > 0) { |
|
6218 checkIdState = new LongSparseArray<Integer>(); |
|
6219 for (int i = 0; i < N; i++) { |
|
6220 final long key = in.readLong(); |
|
6221 final int value = in.readInt(); |
|
6222 checkIdState.put(key, value); |
|
6223 } |
|
6224 } |
|
6225 } |
|
6226 |
|
6227 @Override |
|
6228 public void writeToParcel(Parcel out, int flags) { |
|
6229 super.writeToParcel(out, flags); |
|
6230 |
|
6231 out.writeLong(selectedId); |
|
6232 out.writeLong(firstId); |
|
6233 out.writeInt(viewStart); |
|
6234 out.writeInt(position); |
|
6235 out.writeInt(height); |
|
6236 |
|
6237 out.writeInt(checkedItemCount); |
|
6238 out.writeSparseBooleanArray(checkState); |
|
6239 |
|
6240 final int N = checkIdState != null ? checkIdState.size() : 0; |
|
6241 out.writeInt(N); |
|
6242 |
|
6243 for (int i = 0; i < N; i++) { |
|
6244 out.writeLong(checkIdState.keyAt(i)); |
|
6245 out.writeInt(checkIdState.valueAt(i)); |
|
6246 } |
|
6247 } |
|
6248 |
|
6249 @Override |
|
6250 public String toString() { |
|
6251 return "TwoWayView.SavedState{" |
|
6252 + Integer.toHexString(System.identityHashCode(this)) |
|
6253 + " selectedId=" + selectedId |
|
6254 + " firstId=" + firstId |
|
6255 + " viewStart=" + viewStart |
|
6256 + " height=" + height |
|
6257 + " position=" + position |
|
6258 + " checkState=" + checkState + "}"; |
|
6259 } |
|
6260 |
|
6261 public static final Parcelable.Creator<SavedState> CREATOR |
|
6262 = new Parcelable.Creator<SavedState>() { |
|
6263 @Override |
|
6264 public SavedState createFromParcel(Parcel in) { |
|
6265 return new SavedState(in); |
|
6266 } |
|
6267 |
|
6268 @Override |
|
6269 public SavedState[] newArray(int size) { |
|
6270 return new SavedState[size]; |
|
6271 } |
|
6272 }; |
|
6273 } |
|
6274 |
|
6275 private class SelectionNotifier implements Runnable { |
|
6276 @Override |
|
6277 public void run() { |
|
6278 if (mDataChanged) { |
|
6279 // Data has changed between when this SelectionNotifier |
|
6280 // was posted and now. We need to wait until the AdapterView |
|
6281 // has been synched to the new data. |
|
6282 if (mAdapter != null) { |
|
6283 post(this); |
|
6284 } |
|
6285 } else { |
|
6286 fireOnSelected(); |
|
6287 performAccessibilityActionsOnSelected(); |
|
6288 } |
|
6289 } |
|
6290 } |
|
6291 |
|
6292 private class WindowRunnnable { |
|
6293 private int mOriginalAttachCount; |
|
6294 |
|
6295 public void rememberWindowAttachCount() { |
|
6296 mOriginalAttachCount = getWindowAttachCount(); |
|
6297 } |
|
6298 |
|
6299 public boolean sameWindow() { |
|
6300 return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; |
|
6301 } |
|
6302 } |
|
6303 |
|
6304 private class PerformClick extends WindowRunnnable implements Runnable { |
|
6305 int mClickMotionPosition; |
|
6306 |
|
6307 @Override |
|
6308 public void run() { |
|
6309 if (mDataChanged) { |
|
6310 return; |
|
6311 } |
|
6312 |
|
6313 final ListAdapter adapter = mAdapter; |
|
6314 final int motionPosition = mClickMotionPosition; |
|
6315 |
|
6316 if (adapter != null && mItemCount > 0 && |
|
6317 motionPosition != INVALID_POSITION && |
|
6318 motionPosition < adapter.getCount() && sameWindow()) { |
|
6319 |
|
6320 final View child = getChildAt(motionPosition - mFirstPosition); |
|
6321 if (child != null) { |
|
6322 performItemClick(child, motionPosition, adapter.getItemId(motionPosition)); |
|
6323 } |
|
6324 } |
|
6325 } |
|
6326 } |
|
6327 |
|
6328 private final class CheckForTap implements Runnable { |
|
6329 @Override |
|
6330 public void run() { |
|
6331 if (mTouchMode != TOUCH_MODE_DOWN) { |
|
6332 return; |
|
6333 } |
|
6334 |
|
6335 mTouchMode = TOUCH_MODE_TAP; |
|
6336 |
|
6337 final View child = getChildAt(mMotionPosition - mFirstPosition); |
|
6338 if (child != null && !child.hasFocusable()) { |
|
6339 mLayoutMode = LAYOUT_NORMAL; |
|
6340 |
|
6341 if (!mDataChanged) { |
|
6342 setPressed(true); |
|
6343 child.setPressed(true); |
|
6344 |
|
6345 layoutChildren(); |
|
6346 positionSelector(mMotionPosition, child); |
|
6347 refreshDrawableState(); |
|
6348 |
|
6349 positionSelector(mMotionPosition, child); |
|
6350 refreshDrawableState(); |
|
6351 |
|
6352 final boolean longClickable = isLongClickable(); |
|
6353 |
|
6354 if (mSelector != null) { |
|
6355 Drawable d = mSelector.getCurrent(); |
|
6356 |
|
6357 if (d != null && d instanceof TransitionDrawable) { |
|
6358 if (longClickable) { |
|
6359 final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); |
|
6360 ((TransitionDrawable) d).startTransition(longPressTimeout); |
|
6361 } else { |
|
6362 ((TransitionDrawable) d).resetTransition(); |
|
6363 } |
|
6364 } |
|
6365 } |
|
6366 |
|
6367 if (longClickable) { |
|
6368 triggerCheckForLongPress(); |
|
6369 } else { |
|
6370 mTouchMode = TOUCH_MODE_DONE_WAITING; |
|
6371 } |
|
6372 } else { |
|
6373 mTouchMode = TOUCH_MODE_DONE_WAITING; |
|
6374 } |
|
6375 } |
|
6376 } |
|
6377 } |
|
6378 |
|
6379 private class CheckForLongPress extends WindowRunnnable implements Runnable { |
|
6380 @Override |
|
6381 public void run() { |
|
6382 final int motionPosition = mMotionPosition; |
|
6383 final View child = getChildAt(motionPosition - mFirstPosition); |
|
6384 |
|
6385 if (child != null) { |
|
6386 final long longPressId = mAdapter.getItemId(mMotionPosition); |
|
6387 |
|
6388 boolean handled = false; |
|
6389 if (sameWindow() && !mDataChanged) { |
|
6390 handled = performLongPress(child, motionPosition, longPressId); |
|
6391 } |
|
6392 |
|
6393 if (handled) { |
|
6394 mTouchMode = TOUCH_MODE_REST; |
|
6395 setPressed(false); |
|
6396 child.setPressed(false); |
|
6397 } else { |
|
6398 mTouchMode = TOUCH_MODE_DONE_WAITING; |
|
6399 } |
|
6400 } |
|
6401 } |
|
6402 } |
|
6403 |
|
6404 private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { |
|
6405 public void run() { |
|
6406 if (!isPressed() || mSelectedPosition < 0) { |
|
6407 return; |
|
6408 } |
|
6409 |
|
6410 final int index = mSelectedPosition - mFirstPosition; |
|
6411 final View v = getChildAt(index); |
|
6412 |
|
6413 if (!mDataChanged) { |
|
6414 boolean handled = false; |
|
6415 |
|
6416 if (sameWindow()) { |
|
6417 handled = performLongPress(v, mSelectedPosition, mSelectedRowId); |
|
6418 } |
|
6419 |
|
6420 if (handled) { |
|
6421 setPressed(false); |
|
6422 v.setPressed(false); |
|
6423 } |
|
6424 } else { |
|
6425 setPressed(false); |
|
6426 |
|
6427 if (v != null) { |
|
6428 v.setPressed(false); |
|
6429 } |
|
6430 } |
|
6431 } |
|
6432 } |
|
6433 |
|
6434 private static class ArrowScrollFocusResult { |
|
6435 private int mSelectedPosition; |
|
6436 private int mAmountToScroll; |
|
6437 |
|
6438 /** |
|
6439 * How {@link TwoWayView#arrowScrollFocused} returns its values. |
|
6440 */ |
|
6441 void populate(int selectedPosition, int amountToScroll) { |
|
6442 mSelectedPosition = selectedPosition; |
|
6443 mAmountToScroll = amountToScroll; |
|
6444 } |
|
6445 |
|
6446 public int getSelectedPosition() { |
|
6447 return mSelectedPosition; |
|
6448 } |
|
6449 |
|
6450 public int getAmountToScroll() { |
|
6451 return mAmountToScroll; |
|
6452 } |
|
6453 } |
|
6454 |
|
6455 private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat { |
|
6456 @Override |
|
6457 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { |
|
6458 super.onInitializeAccessibilityNodeInfo(host, info); |
|
6459 |
|
6460 final int position = getPositionForView(host); |
|
6461 final ListAdapter adapter = getAdapter(); |
|
6462 |
|
6463 // Cannot perform actions on invalid items |
|
6464 if (position == INVALID_POSITION || adapter == null) { |
|
6465 return; |
|
6466 } |
|
6467 |
|
6468 // Cannot perform actions on disabled items |
|
6469 if (!isEnabled() || !adapter.isEnabled(position)) { |
|
6470 return; |
|
6471 } |
|
6472 |
|
6473 if (position == getSelectedItemPosition()) { |
|
6474 info.setSelected(true); |
|
6475 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION); |
|
6476 } else { |
|
6477 info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT); |
|
6478 } |
|
6479 |
|
6480 if (isClickable()) { |
|
6481 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); |
|
6482 info.setClickable(true); |
|
6483 } |
|
6484 |
|
6485 if (isLongClickable()) { |
|
6486 info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); |
|
6487 info.setLongClickable(true); |
|
6488 } |
|
6489 } |
|
6490 |
|
6491 @Override |
|
6492 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { |
|
6493 if (super.performAccessibilityAction(host, action, arguments)) { |
|
6494 return true; |
|
6495 } |
|
6496 |
|
6497 final int position = getPositionForView(host); |
|
6498 final ListAdapter adapter = getAdapter(); |
|
6499 |
|
6500 // Cannot perform actions on invalid items |
|
6501 if (position == INVALID_POSITION || adapter == null) { |
|
6502 return false; |
|
6503 } |
|
6504 |
|
6505 // Cannot perform actions on disabled items |
|
6506 if (!isEnabled() || !adapter.isEnabled(position)) { |
|
6507 return false; |
|
6508 } |
|
6509 |
|
6510 final long id = getItemIdAtPosition(position); |
|
6511 |
|
6512 switch (action) { |
|
6513 case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION: |
|
6514 if (getSelectedItemPosition() == position) { |
|
6515 setSelection(INVALID_POSITION); |
|
6516 return true; |
|
6517 } |
|
6518 return false; |
|
6519 |
|
6520 case AccessibilityNodeInfoCompat.ACTION_SELECT: |
|
6521 if (getSelectedItemPosition() != position) { |
|
6522 setSelection(position); |
|
6523 return true; |
|
6524 } |
|
6525 return false; |
|
6526 |
|
6527 case AccessibilityNodeInfoCompat.ACTION_CLICK: |
|
6528 if (isClickable()) { |
|
6529 return performItemClick(host, position, id); |
|
6530 } |
|
6531 return false; |
|
6532 |
|
6533 case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK: |
|
6534 if (isLongClickable()) { |
|
6535 return performLongPress(host, position, id); |
|
6536 } |
|
6537 return false; |
|
6538 } |
|
6539 |
|
6540 return false; |
|
6541 } |
|
6542 } |
|
6543 } |