mobile/android/base/widget/TwoWayView.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial