mobile/android/base/widget/TwoWayView.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /*
     2  * Copyright (C) 2013 Lucas Rocha
     3  *
     4  * This code is based on bits and pieces of Android's AbsListView,
     5  * Listview, and StaggeredGridView.
     6  *
     7  * Copyright (C) 2012 The Android Open Source Project
     8  *
     9  * Licensed under the Apache License, Version 2.0 (the "License");
    10  * you may not use this file except in compliance with the License.
    11  * You may obtain a copy of the License at
    12  *
    13  *      http://www.apache.org/licenses/LICENSE-2.0
    14  *
    15  * Unless required by applicable law or agreed to in writing, software
    16  * distributed under the License is distributed on an "AS IS" BASIS,
    17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    18  * See the License for the specific language governing permissions and
    19  * limitations under the License.
    20  */
    22 package org.mozilla.gecko.widget;
    24 import org.mozilla.gecko.R;
    26 import android.annotation.TargetApi;
    27 import android.content.Context;
    28 import android.content.res.TypedArray;
    29 import android.database.DataSetObserver;
    30 import android.graphics.Canvas;
    31 import android.graphics.Rect;
    32 import android.graphics.drawable.Drawable;
    33 import android.graphics.drawable.TransitionDrawable;
    34 import android.os.Build;
    35 import android.os.Bundle;
    36 import android.os.Parcel;
    37 import android.os.Parcelable;
    38 import android.os.SystemClock;
    39 import android.support.v4.util.LongSparseArray;
    40 import android.support.v4.util.SparseArrayCompat;
    41 import android.support.v4.view.AccessibilityDelegateCompat;
    42 import android.support.v4.view.KeyEventCompat;
    43 import android.support.v4.view.MotionEventCompat;
    44 import android.support.v4.view.VelocityTrackerCompat;
    45 import android.support.v4.view.ViewCompat;
    46 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
    47 import android.support.v4.widget.EdgeEffectCompat;
    48 import android.util.AttributeSet;
    49 import android.util.Log;
    50 import android.util.SparseBooleanArray;
    51 import android.view.ContextMenu.ContextMenuInfo;
    52 import android.view.FocusFinder;
    53 import android.view.HapticFeedbackConstants;
    54 import android.view.KeyEvent;
    55 import android.view.MotionEvent;
    56 import android.view.SoundEffectConstants;
    57 import android.view.VelocityTracker;
    58 import android.view.View;
    59 import android.view.ViewConfiguration;
    60 import android.view.ViewGroup;
    61 import android.view.ViewParent;
    62 import android.view.ViewTreeObserver;
    63 import android.view.accessibility.AccessibilityEvent;
    64 import android.view.accessibility.AccessibilityNodeInfo;
    65 import android.widget.AdapterView;
    66 import android.widget.Checkable;
    67 import android.widget.ListAdapter;
    68 import android.widget.Scroller;
    70 import java.util.ArrayList;
    71 import java.util.List;
    73 /*
    74  * Implementation Notes:
    75  *
    76  * Some terminology:
    77  *
    78  *     index    - index of the items that are currently visible
    79  *     position - index of the items in the cursor
    80  *
    81  * Given the bi-directional nature of this view, the source code
    82  * usually names variables with 'start' to mean 'top' or 'left'; and
    83  * 'end' to mean 'bottom' or 'right', depending on the current
    84  * orientation of the widget.
    85  */
    87 /**
    88  * A view that shows items in a vertical or horizontal scrolling list.
    89  * The items come from the {@link ListAdapter} associated with this view.
    90  */
    91 public class TwoWayView extends AdapterView<ListAdapter> implements
    92         ViewTreeObserver.OnTouchModeChangeListener {
    93     private static final String LOGTAG = "TwoWayView";
    95     private static final int NO_POSITION = -1;
    96     private static final int INVALID_POINTER = -1;
    98     public static final int[] STATE_NOTHING = new int[] { 0 };
   100     private static final int TOUCH_MODE_REST = -1;
   101     private static final int TOUCH_MODE_DOWN = 0;
   102     private static final int TOUCH_MODE_TAP = 1;
   103     private static final int TOUCH_MODE_DONE_WAITING = 2;
   104     private static final int TOUCH_MODE_DRAGGING = 3;
   105     private static final int TOUCH_MODE_FLINGING = 4;
   106     private static final int TOUCH_MODE_OVERSCROLL = 5;
   108     private static final int TOUCH_MODE_UNKNOWN = -1;
   109     private static final int TOUCH_MODE_ON = 0;
   110     private static final int TOUCH_MODE_OFF = 1;
   112     private static final int LAYOUT_NORMAL = 0;
   113     private static final int LAYOUT_FORCE_TOP = 1;
   114     private static final int LAYOUT_SET_SELECTION = 2;
   115     private static final int LAYOUT_FORCE_BOTTOM = 3;
   116     private static final int LAYOUT_SPECIFIC = 4;
   117     private static final int LAYOUT_SYNC = 5;
   118     private static final int LAYOUT_MOVE_SELECTION = 6;
   120     private static final int SYNC_SELECTED_POSITION = 0;
   121     private static final int SYNC_FIRST_POSITION = 1;
   123     private static final int SYNC_MAX_DURATION_MILLIS = 100;
   125     private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
   127     private static final float MAX_SCROLL_FACTOR = 0.33f;
   129     private static final int MIN_SCROLL_PREVIEW_PIXELS = 10;
   131     public static enum ChoiceMode {
   132         NONE,
   133         SINGLE,
   134         MULTIPLE
   135     }
   137     public static enum Orientation {
   138         HORIZONTAL,
   139         VERTICAL;
   140     };
   142     private ListAdapter mAdapter;
   144     private boolean mIsVertical;
   146     private int mItemMargin;
   148     private boolean mInLayout;
   149     private boolean mBlockLayoutRequests;
   151     private boolean mIsAttached;
   153     private final RecycleBin mRecycler;
   154     private AdapterDataSetObserver mDataSetObserver;
   156     private boolean mItemsCanFocus;
   158     final boolean[] mIsScrap = new boolean[1];
   160     private boolean mDataChanged;
   161     private int mItemCount;
   162     private int mOldItemCount;
   163     private boolean mHasStableIds;
   164     private boolean mAreAllItemsSelectable;
   166     private int mFirstPosition;
   167     private int mSpecificStart;
   169     private SavedState mPendingSync;
   171     private final int mTouchSlop;
   172     private final int mMaximumVelocity;
   173     private final int mFlingVelocity;
   174     private float mLastTouchPos;
   175     private float mTouchRemainderPos;
   176     private int mActivePointerId;
   178     private final Rect mTempRect;
   180     private final ArrowScrollFocusResult mArrowScrollFocusResult;
   182     private Rect mTouchFrame;
   183     private int mMotionPosition;
   184     private CheckForTap mPendingCheckForTap;
   185     private CheckForLongPress mPendingCheckForLongPress;
   186     private CheckForKeyLongPress mPendingCheckForKeyLongPress;
   187     private PerformClick mPerformClick;
   188     private Runnable mTouchModeReset;
   189     private int mResurrectToPosition;
   191     private boolean mIsChildViewEnabled;
   193     private boolean mDrawSelectorOnTop;
   194     private Drawable mSelector;
   195     private int mSelectorPosition;
   196     private final Rect mSelectorRect;
   198     private int mOverScroll;
   199     private final int mOverscrollDistance;
   201     private boolean mDesiredFocusableState;
   202     private boolean mDesiredFocusableInTouchModeState;
   204     private SelectionNotifier mSelectionNotifier;
   206     private boolean mNeedSync;
   207     private int mSyncMode;
   208     private int mSyncPosition;
   209     private long mSyncRowId;
   210     private long mSyncHeight;
   211     private int mSelectedStart;
   213     private int mNextSelectedPosition;
   214     private long mNextSelectedRowId;
   215     private int mSelectedPosition;
   216     private long mSelectedRowId;
   217     private int mOldSelectedPosition;
   218     private long mOldSelectedRowId;
   220     private ChoiceMode mChoiceMode;
   221     private int mCheckedItemCount;
   222     private SparseBooleanArray mCheckStates;
   223     LongSparseArray<Integer> mCheckedIdStates;
   225     private ContextMenuInfo mContextMenuInfo;
   227     private int mLayoutMode;
   228     private int mTouchMode;
   229     private int mLastTouchMode;
   230     private VelocityTracker mVelocityTracker;
   231     private final Scroller mScroller;
   233     private EdgeEffectCompat mStartEdge;
   234     private EdgeEffectCompat mEndEdge;
   236     private OnScrollListener mOnScrollListener;
   237     private int mLastScrollState;
   239     private View mEmptyView;
   241     private ListItemAccessibilityDelegate mAccessibilityDelegate;
   243     private int mLastAccessibilityScrollEventFromIndex;
   244     private int mLastAccessibilityScrollEventToIndex;
   246     public interface OnScrollListener {
   248         /**
   249          * The view is not scrolling. Note navigating the list using the trackball counts as
   250          * being in the idle state since these transitions are not animated.
   251          */
   252         public static int SCROLL_STATE_IDLE = 0;
   254         /**
   255          * The user is scrolling using touch, and their finger is still on the screen
   256          */
   257         public static int SCROLL_STATE_TOUCH_SCROLL = 1;
   259         /**
   260          * The user had previously been scrolling using touch and had performed a fling. The
   261          * animation is now coasting to a stop
   262          */
   263         public static int SCROLL_STATE_FLING = 2;
   265         /**
   266          * Callback method to be invoked while the list view or grid view is being scrolled. If the
   267          * view is being scrolled, this method will be called before the next frame of the scroll is
   268          * rendered. In particular, it will be called before any calls to
   269          * {@link Adapter#getView(int, View, ViewGroup)}.
   270          *
   271          * @param view The view whose scroll state is being reported
   272          *
   273          * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
   274          * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
   275          */
   276         public void onScrollStateChanged(TwoWayView view, int scrollState);
   278         /**
   279          * Callback method to be invoked when the list or grid has been scrolled. This will be
   280          * called after the scroll has completed
   281          * @param view The view whose scroll state is being reported
   282          * @param firstVisibleItem the index of the first visible cell (ignore if
   283          *        visibleItemCount == 0)
   284          * @param visibleItemCount the number of visible cells
   285          * @param totalItemCount the number of items in the list adaptor
   286          */
   287         public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount,
   288                 int totalItemCount);
   289     }
   291     /**
   292      * A RecyclerListener is used to receive a notification whenever a View is placed
   293      * inside the RecycleBin's scrap heap. This listener is used to free resources
   294      * associated to Views placed in the RecycleBin.
   295      *
   296      * @see TwoWayView.RecycleBin
   297      * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener)
   298      */
   299     public static interface RecyclerListener {
   300         /**
   301          * Indicates that the specified View was moved into the recycler's scrap heap.
   302          * The view is not displayed on screen any more and any expensive resource
   303          * associated with the view should be discarded.
   304          *
   305          * @param view
   306          */
   307         void onMovedToScrapHeap(View view);
   308     }
   310     public TwoWayView(Context context) {
   311         this(context, null);
   312     }
   314     public TwoWayView(Context context, AttributeSet attrs) {
   315         this(context, attrs, 0);
   316     }
   318     public TwoWayView(Context context, AttributeSet attrs, int defStyle) {
   319         super(context, attrs, defStyle);
   321         mNeedSync = false;
   322         mVelocityTracker = null;
   324         mLayoutMode = LAYOUT_NORMAL;
   325         mTouchMode = TOUCH_MODE_REST;
   326         mLastTouchMode = TOUCH_MODE_UNKNOWN;
   328         mIsAttached = false;
   330         mContextMenuInfo = null;
   332         mOnScrollListener = null;
   333         mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
   335         final ViewConfiguration vc = ViewConfiguration.get(context);
   336         mTouchSlop = vc.getScaledTouchSlop();
   337         mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
   338         mFlingVelocity = vc.getScaledMinimumFlingVelocity();
   339         mOverscrollDistance = getScaledOverscrollDistance(vc);
   341         mOverScroll = 0;
   343         mScroller = new Scroller(context);
   345         mIsVertical = true;
   347         mItemsCanFocus = false;
   349         mTempRect = new Rect();
   351         mArrowScrollFocusResult = new ArrowScrollFocusResult();
   353         mSelectorPosition = INVALID_POSITION;
   355         mSelectorRect = new Rect();
   356         mSelectedStart = 0;
   358         mResurrectToPosition = INVALID_POSITION;
   360         mSelectedStart = 0;
   361         mNextSelectedPosition = INVALID_POSITION;
   362         mNextSelectedRowId = INVALID_ROW_ID;
   363         mSelectedPosition = INVALID_POSITION;
   364         mSelectedRowId = INVALID_ROW_ID;
   365         mOldSelectedPosition = INVALID_POSITION;
   366         mOldSelectedRowId = INVALID_ROW_ID;
   368         mChoiceMode = ChoiceMode.NONE;
   369         mCheckedItemCount = 0;
   370         mCheckedIdStates = null;
   371         mCheckStates = null;
   373         mRecycler = new RecycleBin();
   374         mDataSetObserver = null;
   376         mAreAllItemsSelectable = true;
   378         mStartEdge = null;
   379         mEndEdge = null;
   381         setClickable(true);
   382         setFocusableInTouchMode(true);
   383         setWillNotDraw(false);
   384         setAlwaysDrawnWithCacheEnabled(false);
   385         setWillNotDraw(false);
   386         setClipToPadding(false);
   388         ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);
   390         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);
   391         initializeScrollbars(a);
   393         mDrawSelectorOnTop = a.getBoolean(
   394                 R.styleable.TwoWayView_android_drawSelectorOnTop, false);
   396         Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector);
   397         if (d != null) {
   398             setSelector(d);
   399         }
   401         int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1);
   402         if (orientation >= 0) {
   403             setOrientation(Orientation.values()[orientation]);
   404         }
   406         int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1);
   407         if (choiceMode >= 0) {
   408             setChoiceMode(ChoiceMode.values()[choiceMode]);
   409         }
   411         a.recycle();
   413         updateScrollbarsDirection();
   414     }
   416     public void setOrientation(Orientation orientation) {
   417         final boolean isVertical = (orientation.compareTo(Orientation.VERTICAL) == 0);
   418         if (mIsVertical == isVertical) {
   419             return;
   420         }
   422         mIsVertical = isVertical;
   424         updateScrollbarsDirection();
   425         resetState();
   426         mRecycler.clear();
   428         requestLayout();
   429     }
   431     public Orientation getOrientation() {
   432         return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
   433     }
   435     public void setItemMargin(int itemMargin) {
   436         if (mItemMargin == itemMargin) {
   437             return;
   438         }
   440         mItemMargin = itemMargin;
   441         requestLayout();
   442     }
   444     public int getItemMargin() {
   445         return mItemMargin;
   446     }
   448     /**
   449      * Indicates that the views created by the ListAdapter can contain focusable
   450      * items.
   451      *
   452      * @param itemsCanFocus true if items can get focus, false otherwise
   453      */
   454     public void setItemsCanFocus(boolean itemsCanFocus) {
   455         mItemsCanFocus = itemsCanFocus;
   456         if (!itemsCanFocus) {
   457             setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
   458         }
   459     }
   461     /**
   462      * @return Whether the views created by the ListAdapter can contain focusable
   463      * items.
   464      */
   465     public boolean getItemsCanFocus() {
   466         return mItemsCanFocus;
   467     }
   469     /**
   470      * Set the listener that will receive notifications every time the list scrolls.
   471      *
   472      * @param l the scroll listener
   473      */
   474     public void setOnScrollListener(OnScrollListener l) {
   475         mOnScrollListener = l;
   476         invokeOnItemScrollListener();
   477     }
   479     /**
   480      * Sets the recycler listener to be notified whenever a View is set aside in
   481      * the recycler for later reuse. This listener can be used to free resources
   482      * associated to the View.
   483      *
   484      * @param listener The recycler listener to be notified of views set aside
   485      *        in the recycler.
   486      *
   487      * @see TwoWayView.RecycleBin
   488      * @see TwoWayView.RecyclerListener
   489      */
   490     public void setRecyclerListener(RecyclerListener l) {
   491         mRecycler.mRecyclerListener = l;
   492     }
   494     /**
   495      * Controls whether the selection highlight drawable should be drawn on top of the item or
   496      * behind it.
   497      *
   498      * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
   499      *        is false.
   500      *
   501      * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
   502      */
   503     public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
   504         mDrawSelectorOnTop = drawSelectorOnTop;
   505     }
   507     /**
   508      * Set a Drawable that should be used to highlight the currently selected item.
   509      *
   510      * @param resID A Drawable resource to use as the selection highlight.
   511      *
   512      * @attr ref android.R.styleable#AbsListView_listSelector
   513      */
   514     public void setSelector(int resID) {
   515         setSelector(getResources().getDrawable(resID));
   516     }
   518     /**
   519      * Set a Drawable that should be used to highlight the currently selected item.
   520      *
   521      * @param selector A Drawable to use as the selection highlight.
   522      *
   523      * @attr ref android.R.styleable#AbsListView_listSelector
   524      */
   525     public void setSelector(Drawable selector) {
   526         if (mSelector != null) {
   527             mSelector.setCallback(null);
   528             unscheduleDrawable(mSelector);
   529         }
   531         mSelector = selector;
   532         Rect padding = new Rect();
   533         selector.getPadding(padding);
   535         selector.setCallback(this);
   536         updateSelectorState();
   537     }
   539     /**
   540      * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
   541      * selection in the list.
   542      *
   543      * @return the drawable used to display the selector
   544      */
   545     public Drawable getSelector() {
   546         return mSelector;
   547     }
   549     /**
   550      * {@inheritDoc}
   551      */
   552     @Override
   553     public int getSelectedItemPosition() {
   554         return mNextSelectedPosition;
   555     }
   557     /**
   558      * {@inheritDoc}
   559      */
   560     @Override
   561     public long getSelectedItemId() {
   562         return mNextSelectedRowId;
   563     }
   565     /**
   566      * Returns the number of items currently selected. This will only be valid
   567      * if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
   568      *
   569      * <p>To determine the specific items that are currently selected, use one of
   570      * the <code>getChecked*</code> methods.
   571      *
   572      * @return The number of items currently selected
   573      *
   574      * @see #getCheckedItemPosition()
   575      * @see #getCheckedItemPositions()
   576      * @see #getCheckedItemIds()
   577      */
   578     public int getCheckedItemCount() {
   579         return mCheckedItemCount;
   580     }
   582     /**
   583      * Returns the checked state of the specified position. The result is only
   584      * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
   585      * or {@link #CHOICE_MODE_MULTIPLE}.
   586      *
   587      * @param position The item whose checked state to return
   588      * @return The item's checked state or <code>false</code> if choice mode
   589      *         is invalid
   590      *
   591      * @see #setChoiceMode(int)
   592      */
   593     public boolean isItemChecked(int position) {
   594         if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 && mCheckStates != null) {
   595             return mCheckStates.get(position);
   596         }
   598         return false;
   599     }
   601     /**
   602      * Returns the currently checked item. The result is only valid if the choice
   603      * mode has been set to {@link #CHOICE_MODE_SINGLE}.
   604      *
   605      * @return The position of the currently checked item or
   606      *         {@link #INVALID_POSITION} if nothing is selected
   607      *
   608      * @see #setChoiceMode(int)
   609      */
   610     public int getCheckedItemPosition() {
   611         if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0 &&
   612                 mCheckStates != null && mCheckStates.size() == 1) {
   613             return mCheckStates.keyAt(0);
   614         }
   616         return INVALID_POSITION;
   617     }
   619     /**
   620      * Returns the set of checked items in the list. The result is only valid if
   621      * the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
   622      *
   623      * @return  A SparseBooleanArray which will return true for each call to
   624      *          get(int position) where position is a position in the list,
   625      *          or <code>null</code> if the choice mode is set to
   626      *          {@link #CHOICE_MODE_NONE}.
   627      */
   628     public SparseBooleanArray getCheckedItemPositions() {
   629         if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
   630             return mCheckStates;
   631         }
   633         return null;
   634     }
   636     /**
   637      * Returns the set of checked items ids. The result is only valid if the
   638      * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
   639      * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
   640      *
   641      * @return A new array which contains the id of each checked item in the
   642      *         list.
   643      */
   644     public long[] getCheckedItemIds() {
   645         if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 ||
   646                 mCheckedIdStates == null || mAdapter == null) {
   647             return new long[0];
   648         }
   650         final LongSparseArray<Integer> idStates = mCheckedIdStates;
   651         final int count = idStates.size();
   652         final long[] ids = new long[count];
   654         for (int i = 0; i < count; i++) {
   655             ids[i] = idStates.keyAt(i);
   656         }
   658         return ids;
   659     }
   661     /**
   662      * Sets the checked state of the specified position. The is only valid if
   663      * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
   664      * {@link #CHOICE_MODE_MULTIPLE}.
   665      *
   666      * @param position The item whose checked state is to be checked
   667      * @param value The new checked state for the item
   668      */
   669     public void setItemChecked(int position, boolean value) {
   670         if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0) {
   671             return;
   672         }
   674         if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
   675             boolean oldValue = mCheckStates.get(position);
   676             mCheckStates.put(position, value);
   678             if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
   679                 if (value) {
   680                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
   681                 } else {
   682                     mCheckedIdStates.delete(mAdapter.getItemId(position));
   683                 }
   684             }
   686             if (oldValue != value) {
   687                 if (value) {
   688                     mCheckedItemCount++;
   689                 } else {
   690                     mCheckedItemCount--;
   691                 }
   692             }
   693         } else {
   694             boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
   696             // Clear all values if we're checking something, or unchecking the currently
   697             // selected item
   698             if (value || isItemChecked(position)) {
   699                 mCheckStates.clear();
   701                 if (updateIds) {
   702                     mCheckedIdStates.clear();
   703                 }
   704             }
   706             // This may end up selecting the value we just cleared but this way
   707             // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
   708             if (value) {
   709                 mCheckStates.put(position, true);
   711                 if (updateIds) {
   712                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
   713                 }
   715                 mCheckedItemCount = 1;
   716             } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
   717                 mCheckedItemCount = 0;
   718             }
   719         }
   721         // Do not generate a data change while we are in the layout phase
   722         if (!mInLayout && !mBlockLayoutRequests) {
   723             mDataChanged = true;
   724             rememberSyncState();
   725             requestLayout();
   726         }
   727     }
   729     /**
   730      * Clear any choices previously set
   731      */
   732     public void clearChoices() {
   733         if (mCheckStates != null) {
   734             mCheckStates.clear();
   735         }
   737         if (mCheckedIdStates != null) {
   738             mCheckedIdStates.clear();
   739         }
   741         mCheckedItemCount = 0;
   742     }
   744     /**
   745      * @see #setChoiceMode(int)
   746      *
   747      * @return The current choice mode
   748      */
   749     public ChoiceMode getChoiceMode() {
   750         return mChoiceMode;
   751     }
   753     /**
   754      * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
   755      * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
   756      * List allows up to one item to  be in a chosen state. By setting the choiceMode to
   757      * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
   758      *
   759      * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
   760      * {@link #CHOICE_MODE_MULTIPLE}
   761      */
   762     public void setChoiceMode(ChoiceMode choiceMode) {
   763         mChoiceMode = choiceMode;
   765         if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
   766             if (mCheckStates == null) {
   767                 mCheckStates = new SparseBooleanArray();
   768             }
   770             if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
   771                 mCheckedIdStates = new LongSparseArray<Integer>();
   772             }
   773         }
   774     }
   776     @Override
   777     public ListAdapter getAdapter() {
   778         return mAdapter;
   779     }
   781     @Override
   782     public void setAdapter(ListAdapter adapter) {
   783         if (mAdapter != null && mDataSetObserver != null) {
   784             mAdapter.unregisterDataSetObserver(mDataSetObserver);
   785         }
   787         resetState();
   788         mRecycler.clear();
   790         mAdapter = adapter;
   791         mDataChanged = true;
   793         mOldSelectedPosition = INVALID_POSITION;
   794         mOldSelectedRowId = INVALID_ROW_ID;
   796         if (mCheckStates != null) {
   797             mCheckStates.clear();
   798         }
   800         if (mCheckedIdStates != null) {
   801             mCheckedIdStates.clear();
   802         }
   804         if (mAdapter != null) {
   805             mOldItemCount = mItemCount;
   806             mItemCount = adapter.getCount();
   808             mDataSetObserver = new AdapterDataSetObserver();
   809             mAdapter.registerDataSetObserver(mDataSetObserver);
   811             mRecycler.setViewTypeCount(adapter.getViewTypeCount());
   813             mHasStableIds = adapter.hasStableIds();
   814             mAreAllItemsSelectable = adapter.areAllItemsEnabled();
   816             if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mHasStableIds &&
   817                     mCheckedIdStates == null) {
   818                 mCheckedIdStates = new LongSparseArray<Integer>();
   819             }
   821             final int position = lookForSelectablePosition(0);
   822             setSelectedPositionInt(position);
   823             setNextSelectedPositionInt(position);
   825             if (mItemCount == 0) {
   826                 checkSelectionChanged();
   827             }
   828         } else {
   829             mItemCount = 0;
   830             mHasStableIds = false;
   831             mAreAllItemsSelectable = true;
   833             checkSelectionChanged();
   834         }
   836         checkFocus();
   837         requestLayout();
   838     }
   840     @Override
   841     public int getFirstVisiblePosition() {
   842         return mFirstPosition;
   843     }
   845     @Override
   846     public int getLastVisiblePosition() {
   847         return mFirstPosition + getChildCount() - 1;
   848     }
   850     @Override
   851     public int getCount() {
   852         return mItemCount;
   853     }
   855     @Override
   856     public int getPositionForView(View view) {
   857         View child = view;
   858         try {
   859             View v;
   860             while (!(v = (View) child.getParent()).equals(this)) {
   861                 child = v;
   862             }
   863         } catch (ClassCastException e) {
   864             // We made it up to the window without find this list view
   865             return INVALID_POSITION;
   866         }
   868         // Search the children for the list item
   869         final int childCount = getChildCount();
   870         for (int i = 0; i < childCount; i++) {
   871             if (getChildAt(i).equals(child)) {
   872                 return mFirstPosition + i;
   873             }
   874         }
   876         // Child not found!
   877         return INVALID_POSITION;
   878     }
   880     @Override
   881     public void getFocusedRect(Rect r) {
   882         View view = getSelectedView();
   884         if (view != null && view.getParent() == this) {
   885             // The focused rectangle of the selected view offset into the
   886             // coordinate space of this view.
   887             view.getFocusedRect(r);
   888             offsetDescendantRectToMyCoords(view, r);
   889         } else {
   890             super.getFocusedRect(r);
   891         }
   892     }
   894     @Override
   895     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
   896         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
   898         if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
   899             if (!mIsAttached && mAdapter != null) {
   900                 // Data may have changed while we were detached and it's valid
   901                 // to change focus while detached. Refresh so we don't die.
   902                 mDataChanged = true;
   903                 mOldItemCount = mItemCount;
   904                 mItemCount = mAdapter.getCount();
   905             }
   907             resurrectSelection();
   908         }
   910         final ListAdapter adapter = mAdapter;
   911         int closetChildIndex = INVALID_POSITION;
   912         int closestChildStart = 0;
   914         if (adapter != null && gainFocus && previouslyFocusedRect != null) {
   915             previouslyFocusedRect.offset(getScrollX(), getScrollY());
   917             // Don't cache the result of getChildCount or mFirstPosition here,
   918             // it could change in layoutChildren.
   919             if (adapter.getCount() < getChildCount() + mFirstPosition) {
   920                 mLayoutMode = LAYOUT_NORMAL;
   921                 layoutChildren();
   922             }
   924             // Figure out which item should be selected based on previously
   925             // focused rect.
   926             Rect otherRect = mTempRect;
   927             int minDistance = Integer.MAX_VALUE;
   928             final int childCount = getChildCount();
   929             final int firstPosition = mFirstPosition;
   931             for (int i = 0; i < childCount; i++) {
   932                 // Only consider selectable views
   933                 if (!adapter.isEnabled(firstPosition + i)) {
   934                     continue;
   935                 }
   937                 View other = getChildAt(i);
   938                 other.getDrawingRect(otherRect);
   939                 offsetDescendantRectToMyCoords(other, otherRect);
   940                 int distance = getDistance(previouslyFocusedRect, otherRect, direction);
   942                 if (distance < minDistance) {
   943                     minDistance = distance;
   944                     closetChildIndex = i;
   945                     closestChildStart = (mIsVertical ? other.getTop() : other.getLeft());
   946                 }
   947             }
   948         }
   950         if (closetChildIndex >= 0) {
   951             setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
   952         } else {
   953             requestLayout();
   954         }
   955     }
   957     @Override
   958     protected void onAttachedToWindow() {
   959         super.onAttachedToWindow();
   961         final ViewTreeObserver treeObserver = getViewTreeObserver();
   962         treeObserver.addOnTouchModeChangeListener(this);
   964         if (mAdapter != null && mDataSetObserver == null) {
   965             mDataSetObserver = new AdapterDataSetObserver();
   966             mAdapter.registerDataSetObserver(mDataSetObserver);
   968             // Data may have changed while we were detached. Refresh.
   969             mDataChanged = true;
   970             mOldItemCount = mItemCount;
   971             mItemCount = mAdapter.getCount();
   972         }
   974         mIsAttached = true;
   975     }
   977     @Override
   978     protected void onDetachedFromWindow() {
   979         super.onDetachedFromWindow();
   981         // Detach any view left in the scrap heap
   982         mRecycler.clear();
   984         final ViewTreeObserver treeObserver = getViewTreeObserver();
   985         treeObserver.removeOnTouchModeChangeListener(this);
   987         if (mAdapter != null) {
   988             mAdapter.unregisterDataSetObserver(mDataSetObserver);
   989             mDataSetObserver = null;
   990         }
   992         if (mPerformClick != null) {
   993             removeCallbacks(mPerformClick);
   994         }
   996         if (mTouchModeReset != null) {
   997             removeCallbacks(mTouchModeReset);
   998             mTouchModeReset.run();
   999         }
  1001         mIsAttached = false;
  1004     @Override
  1005     public void onWindowFocusChanged(boolean hasWindowFocus) {
  1006         super.onWindowFocusChanged(hasWindowFocus);
  1008         final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
  1010         if (!hasWindowFocus) {
  1011             if (touchMode == TOUCH_MODE_OFF) {
  1012                 // Remember the last selected element
  1013                 mResurrectToPosition = mSelectedPosition;
  1015         } else {
  1016             // If we changed touch mode since the last time we had focus
  1017             if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
  1018                 // If we come back in trackball mode, we bring the selection back
  1019                 if (touchMode == TOUCH_MODE_OFF) {
  1020                     // This will trigger a layout
  1021                     resurrectSelection();
  1023                 // If we come back in touch mode, then we want to hide the selector
  1024                 } else {
  1025                     hideSelector();
  1026                     mLayoutMode = LAYOUT_NORMAL;
  1027                     layoutChildren();
  1032         mLastTouchMode = touchMode;
  1035     @Override
  1036     protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
  1037         boolean needsInvalidate = false;
  1039         if (mIsVertical && mOverScroll != scrollY) {
  1040             onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll);
  1041             mOverScroll = scrollY;
  1042             needsInvalidate = true;
  1043         } else if (!mIsVertical && mOverScroll != scrollX) {
  1044             onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY());
  1045             mOverScroll = scrollX;
  1046             needsInvalidate = true;
  1049         if (needsInvalidate) {
  1050             invalidate();
  1051             awakenScrollbarsInternal();
  1055     @TargetApi(9)
  1056     private boolean overScrollByInternal(int deltaX, int deltaY,
  1057             int scrollX, int scrollY,
  1058             int scrollRangeX, int scrollRangeY,
  1059             int maxOverScrollX, int maxOverScrollY,
  1060             boolean isTouchEvent) {
  1061         if (Build.VERSION.SDK_INT < 9) {
  1062             return false;
  1065         return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
  1066                 scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
  1069     @Override
  1070     @TargetApi(9)
  1071     public void setOverScrollMode(int mode) {
  1072         if (Build.VERSION.SDK_INT < 9) {
  1073             return;
  1076         if (mode != ViewCompat.OVER_SCROLL_NEVER) {
  1077             if (mStartEdge == null) {
  1078                 Context context = getContext();
  1080                 mStartEdge = new EdgeEffectCompat(context);
  1081                 mEndEdge = new EdgeEffectCompat(context);
  1083         } else {
  1084             mStartEdge = null;
  1085             mEndEdge = null;
  1088         super.setOverScrollMode(mode);
  1091     public int pointToPosition(int x, int y) {
  1092         Rect frame = mTouchFrame;
  1093         if (frame == null) {
  1094             mTouchFrame = new Rect();
  1095             frame = mTouchFrame;
  1098         final int count = getChildCount();
  1099         for (int i = count - 1; i >= 0; i--) {
  1100             final View child = getChildAt(i);
  1102             if (child.getVisibility() == View.VISIBLE) {
  1103                 child.getHitRect(frame);
  1105                 if (frame.contains(x, y)) {
  1106                     return mFirstPosition + i;
  1110         return INVALID_POSITION;
  1113     @Override
  1114     protected int computeVerticalScrollExtent() {
  1115         final int count = getChildCount();
  1116         if (count == 0) {
  1117             return 0;
  1120         int extent = count * 100;
  1122         View child = getChildAt(0);
  1123         final int childTop = child.getTop();
  1125         int childHeight = child.getHeight();
  1126         if (childHeight > 0) {
  1127             extent += (childTop * 100) / childHeight;
  1130         child = getChildAt(count - 1);
  1131         final int childBottom = child.getBottom();
  1133         childHeight = child.getHeight();
  1134         if (childHeight > 0) {
  1135             extent -= ((childBottom - getHeight()) * 100) / childHeight;
  1138         return extent;
  1141     @Override
  1142     protected int computeHorizontalScrollExtent() {
  1143         final int count = getChildCount();
  1144         if (count == 0) {
  1145             return 0;
  1148         int extent = count * 100;
  1150         View child = getChildAt(0);
  1151         final int childLeft = child.getLeft();
  1153         int childWidth = child.getWidth();
  1154         if (childWidth > 0) {
  1155             extent += (childLeft * 100) / childWidth;
  1158         child = getChildAt(count - 1);
  1159         final int childRight = child.getRight();
  1161         childWidth = child.getWidth();
  1162         if (childWidth > 0) {
  1163             extent -= ((childRight - getWidth()) * 100) / childWidth;
  1166         return extent;
  1169     @Override
  1170     protected int computeVerticalScrollOffset() {
  1171         final int firstPosition = mFirstPosition;
  1172         final int childCount = getChildCount();
  1174         if (firstPosition < 0 || childCount == 0) {
  1175             return 0;
  1178         final View child = getChildAt(0);
  1179         final int childTop = child.getTop();
  1181         int childHeight = child.getHeight();
  1182         if (childHeight > 0) {
  1183             return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0);
  1186         return 0;
  1189     @Override
  1190     protected int computeHorizontalScrollOffset() {
  1191         final int firstPosition = mFirstPosition;
  1192         final int childCount = getChildCount();
  1194         if (firstPosition < 0 || childCount == 0) {
  1195             return 0;
  1198         final View child = getChildAt(0);
  1199         final int childLeft = child.getLeft();
  1201         int childWidth = child.getWidth();
  1202         if (childWidth > 0) {
  1203             return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0);
  1206         return 0;
  1209     @Override
  1210     protected int computeVerticalScrollRange() {
  1211         int result = Math.max(mItemCount * 100, 0);
  1213         if (mIsVertical && mOverScroll != 0) {
  1214             // Compensate for overscroll
  1215             result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100));
  1218         return result;
  1221     @Override
  1222     protected int computeHorizontalScrollRange() {
  1223         int result = Math.max(mItemCount * 100, 0);
  1225         if (!mIsVertical && mOverScroll != 0) {
  1226             // Compensate for overscroll
  1227             result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100));
  1230         return result;
  1233     @Override
  1234     public boolean showContextMenuForChild(View originalView) {
  1235         final int longPressPosition = getPositionForView(originalView);
  1236         if (longPressPosition >= 0) {
  1237             final long longPressId = mAdapter.getItemId(longPressPosition);
  1238             boolean handled = false;
  1240             OnItemLongClickListener listener = getOnItemLongClickListener();
  1241             if (listener != null) {
  1242                 handled = listener.onItemLongClick(TwoWayView.this, originalView,
  1243                         longPressPosition, longPressId);
  1246             if (!handled) {
  1247                 mContextMenuInfo = createContextMenuInfo(
  1248                         getChildAt(longPressPosition - mFirstPosition),
  1249                         longPressPosition, longPressId);
  1251                 handled = super.showContextMenuForChild(originalView);
  1254             return handled;
  1257         return false;
  1260     @Override
  1261     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
  1262         if (disallowIntercept) {
  1263             recycleVelocityTracker();
  1266         super.requestDisallowInterceptTouchEvent(disallowIntercept);
  1269     @Override
  1270     public boolean onInterceptTouchEvent(MotionEvent ev) {
  1271         if (!mIsAttached) {
  1272             return false;
  1275         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
  1276         switch (action) {
  1277         case MotionEvent.ACTION_DOWN:
  1278             initOrResetVelocityTracker();
  1279             mVelocityTracker.addMovement(ev);
  1281             mScroller.abortAnimation();
  1283             final float x = ev.getX();
  1284             final float y = ev.getY();
  1286             mLastTouchPos = (mIsVertical ? y : x);
  1288             final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
  1290             mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
  1291             mTouchRemainderPos = 0;
  1293             if (mTouchMode == TOUCH_MODE_FLINGING) {
  1294                 return true;
  1295             } else if (motionPosition >= 0) {
  1296                 mMotionPosition = motionPosition;
  1297                 mTouchMode = TOUCH_MODE_DOWN;
  1300             break;
  1302         case MotionEvent.ACTION_MOVE: {
  1303             if (mTouchMode != TOUCH_MODE_DOWN) {
  1304                 break;
  1307             initVelocityTrackerIfNotExists();
  1308             mVelocityTracker.addMovement(ev);
  1310             final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
  1311             if (index < 0) {
  1312                 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
  1313                         mActivePointerId + " - did TwoWayView receive an inconsistent " +
  1314                         "event stream?");
  1315                 return false;
  1318             final float pos;
  1319             if (mIsVertical) {
  1320                 pos = MotionEventCompat.getY(ev, index);
  1321             } else {
  1322                 pos = MotionEventCompat.getX(ev, index);
  1325             final float diff = pos - mLastTouchPos + mTouchRemainderPos;
  1326             final int delta = (int) diff;
  1327             mTouchRemainderPos = diff - delta;
  1329             if (maybeStartScrolling(delta)) {
  1330                 return true;
  1333             break;
  1336         case MotionEvent.ACTION_CANCEL:
  1337         case MotionEvent.ACTION_UP:
  1338             mActivePointerId = INVALID_POINTER;
  1339             mTouchMode = TOUCH_MODE_REST;
  1340             recycleVelocityTracker();
  1341             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  1343             break;
  1346         return false;
  1349     @Override
  1350     public boolean onTouchEvent(MotionEvent ev) {
  1351         if (!isEnabled()) {
  1352             // A disabled view that is clickable still consumes the touch
  1353             // events, it just doesn't respond to them.
  1354             return isClickable() || isLongClickable();
  1357         if (!mIsAttached) {
  1358             return false;
  1361         boolean needsInvalidate = false;
  1363         initVelocityTrackerIfNotExists();
  1364         mVelocityTracker.addMovement(ev);
  1366         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
  1367         switch (action) {
  1368         case MotionEvent.ACTION_DOWN: {
  1369             if (mDataChanged) {
  1370                 break;
  1373             mVelocityTracker.clear();
  1374             mScroller.abortAnimation();
  1376             final float x = ev.getX();
  1377             final float y = ev.getY();
  1379             mLastTouchPos = (mIsVertical ? y : x);
  1381             int motionPosition = pointToPosition((int) x, (int) y);
  1383             mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
  1384             mTouchRemainderPos = 0;
  1386             if (mDataChanged) {
  1387                 break;
  1390             if (mTouchMode == TOUCH_MODE_FLINGING) {
  1391                 mTouchMode = TOUCH_MODE_DRAGGING;
  1392                 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
  1393                 motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
  1394                 return true;
  1395             } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) {
  1396                 mTouchMode = TOUCH_MODE_DOWN;
  1397                 triggerCheckForTap();
  1400             mMotionPosition = motionPosition;
  1402             break;
  1405         case MotionEvent.ACTION_MOVE: {
  1406             final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
  1407             if (index < 0) {
  1408                 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
  1409                         mActivePointerId + " - did TwoWayView receive an inconsistent " +
  1410                         "event stream?");
  1411                 return false;
  1414             final float pos;
  1415             if (mIsVertical) {
  1416                 pos = MotionEventCompat.getY(ev, index);
  1417             } else {
  1418                 pos = MotionEventCompat.getX(ev, index);
  1421             if (mDataChanged) {
  1422                 // Re-sync everything if data has been changed
  1423                 // since the scroll operation can query the adapter.
  1424                 layoutChildren();
  1427             final float diff = pos - mLastTouchPos + mTouchRemainderPos;
  1428             final int delta = (int) diff;
  1429             mTouchRemainderPos = diff - delta;
  1431             switch (mTouchMode) {
  1432             case TOUCH_MODE_DOWN:
  1433             case TOUCH_MODE_TAP:
  1434             case TOUCH_MODE_DONE_WAITING:
  1435                 // Check if we have moved far enough that it looks more like a
  1436                 // scroll than a tap
  1437                 maybeStartScrolling(delta);
  1438                 break;
  1440             case TOUCH_MODE_DRAGGING:
  1441             case TOUCH_MODE_OVERSCROLL:
  1442                 mLastTouchPos = pos;
  1443                 maybeScroll(delta);
  1444                 break;
  1447             break;
  1450         case MotionEvent.ACTION_CANCEL:
  1451             cancelCheckForTap();
  1452             mTouchMode = TOUCH_MODE_REST;
  1453             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  1455             setPressed(false);
  1456             View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
  1457             if (motionView != null) {
  1458                 motionView.setPressed(false);
  1461             if (mStartEdge != null && mEndEdge != null) {
  1462                 needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease();
  1465             recycleVelocityTracker();
  1467             break;
  1469         case MotionEvent.ACTION_UP: {
  1470             switch (mTouchMode) {
  1471             case TOUCH_MODE_DOWN:
  1472             case TOUCH_MODE_TAP:
  1473             case TOUCH_MODE_DONE_WAITING: {
  1474                 final int motionPosition = mMotionPosition;
  1475                 final View child = getChildAt(motionPosition - mFirstPosition);
  1477                 final float x = ev.getX();
  1478                 final float y = ev.getY();
  1480                 boolean inList = false;
  1481                 if (mIsVertical) {
  1482                     inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
  1483                 } else {
  1484                     inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom();
  1487                 if (child != null && !child.hasFocusable() && inList) {
  1488                     if (mTouchMode != TOUCH_MODE_DOWN) {
  1489                         child.setPressed(false);
  1492                     if (mPerformClick == null) {
  1493                         mPerformClick = new PerformClick();
  1496                     final PerformClick performClick = mPerformClick;
  1497                     performClick.mClickMotionPosition = motionPosition;
  1498                     performClick.rememberWindowAttachCount();
  1500                     mResurrectToPosition = motionPosition;
  1502                     if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
  1503                         if (mTouchMode == TOUCH_MODE_DOWN) {
  1504                             cancelCheckForTap();
  1505                         } else {
  1506                             cancelCheckForLongPress();
  1509                         mLayoutMode = LAYOUT_NORMAL;
  1511                         if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  1512                             mTouchMode = TOUCH_MODE_TAP;
  1514                             setPressed(true);
  1515                             positionSelector(mMotionPosition, child);
  1516                             child.setPressed(true);
  1518                             if (mSelector != null) {
  1519                                 Drawable d = mSelector.getCurrent();
  1520                                 if (d != null && d instanceof TransitionDrawable) {
  1521                                     ((TransitionDrawable) d).resetTransition();
  1525                             if (mTouchModeReset != null) {
  1526                                 removeCallbacks(mTouchModeReset);
  1529                             mTouchModeReset = new Runnable() {
  1530                                 @Override
  1531                                 public void run() {
  1532                                     mTouchMode = TOUCH_MODE_REST;
  1534                                     setPressed(false);
  1535                                     child.setPressed(false);
  1537                                     if (!mDataChanged) {
  1538                                         performClick.run();
  1541                                     mTouchModeReset = null;
  1543                             };
  1545                             postDelayed(mTouchModeReset,
  1546                                     ViewConfiguration.getPressedStateDuration());
  1547                         } else {
  1548                             mTouchMode = TOUCH_MODE_REST;
  1549                             updateSelectorState();
  1551                     } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
  1552                         performClick.run();
  1556                 mTouchMode = TOUCH_MODE_REST;
  1557                 updateSelectorState();
  1559                 break;
  1562             case TOUCH_MODE_DRAGGING:
  1563                 if (contentFits()) {
  1564                     mTouchMode = TOUCH_MODE_REST;
  1565                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  1566                     break;
  1569                 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
  1571                 final float velocity;
  1572                 if (mIsVertical) {
  1573                     velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
  1574                             mActivePointerId);
  1575                 } else {
  1576                     velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
  1577                             mActivePointerId);
  1580                 if (Math.abs(velocity) >= mFlingVelocity) {
  1581                     mTouchMode = TOUCH_MODE_FLINGING;
  1582                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
  1584                     mScroller.fling(0, 0,
  1585                                     (int) (mIsVertical ? 0 : velocity),
  1586                                     (int) (mIsVertical ? velocity : 0),
  1587                                     (mIsVertical ? 0 : Integer.MIN_VALUE),
  1588                                     (mIsVertical ? 0 : Integer.MAX_VALUE),
  1589                                     (mIsVertical ? Integer.MIN_VALUE : 0),
  1590                                     (mIsVertical ? Integer.MAX_VALUE : 0));
  1592                     mLastTouchPos = 0;
  1593                     needsInvalidate = true;
  1594                 } else {
  1595                     mTouchMode = TOUCH_MODE_REST;
  1596                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  1599                 break;
  1601             case TOUCH_MODE_OVERSCROLL:
  1602                 mTouchMode = TOUCH_MODE_REST;
  1603                 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  1604                 break;
  1607             cancelCheckForTap();
  1608             cancelCheckForLongPress();
  1609             setPressed(false);
  1611             if (mStartEdge != null && mEndEdge != null) {
  1612                 needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease();
  1615             recycleVelocityTracker();
  1617             break;
  1621         if (needsInvalidate) {
  1622             ViewCompat.postInvalidateOnAnimation(this);
  1625         return true;
  1628     @Override
  1629     public void onTouchModeChanged(boolean isInTouchMode) {
  1630         if (isInTouchMode) {
  1631             // Get rid of the selection when we enter touch mode
  1632             hideSelector();
  1634             // Layout, but only if we already have done so previously.
  1635             // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
  1636             // state.)
  1637             if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) {
  1638                 layoutChildren();
  1641             updateSelectorState();
  1642         } else {
  1643             final int touchMode = mTouchMode;
  1644             if (touchMode == TOUCH_MODE_OVERSCROLL) {
  1645                 if (mOverScroll != 0) {
  1646                     mOverScroll = 0;
  1647                     finishEdgeGlows();
  1648                     invalidate();
  1654     @Override
  1655     public boolean onKeyDown(int keyCode, KeyEvent event) {
  1656         return handleKeyEvent(keyCode, 1, event);
  1659     @Override
  1660     public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
  1661         return handleKeyEvent(keyCode, repeatCount, event);
  1664     @Override
  1665     public boolean onKeyUp(int keyCode, KeyEvent event) {
  1666         return handleKeyEvent(keyCode, 1, event);
  1669     @Override
  1670     public void sendAccessibilityEvent(int eventType) {
  1671         // Since this class calls onScrollChanged even if the mFirstPosition and the
  1672         // child count have not changed we will avoid sending duplicate accessibility
  1673         // events.
  1674         if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
  1675             final int firstVisiblePosition = getFirstVisiblePosition();
  1676             final int lastVisiblePosition = getLastVisiblePosition();
  1678             if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
  1679                     && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
  1680                 return;
  1681             } else {
  1682                 mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
  1683                 mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
  1687         super.sendAccessibilityEvent(eventType);
  1690     @Override
  1691     @TargetApi(14)
  1692     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
  1693         super.onInitializeAccessibilityEvent(event);
  1694         event.setClassName(TwoWayView.class.getName());
  1697     @Override
  1698     @TargetApi(14)
  1699     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
  1700         super.onInitializeAccessibilityNodeInfo(info);
  1701         info.setClassName(TwoWayView.class.getName());
  1703         AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
  1705         if (isEnabled()) {
  1706             if (getFirstVisiblePosition() > 0) {
  1707                 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
  1710             if (getLastVisiblePosition() < getCount() - 1) {
  1711                 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
  1716     @Override
  1717     @TargetApi(16)
  1718     public boolean performAccessibilityAction(int action, Bundle arguments) {
  1719         if (super.performAccessibilityAction(action, arguments)) {
  1720             return true;
  1723         switch (action) {
  1724         case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
  1725             if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
  1726                 final int viewportSize;
  1727                 if (mIsVertical) {
  1728                     viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
  1729                 } else {
  1730                     viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
  1733                 // TODO: Use some form of smooth scroll instead
  1734                 trackMotionScroll(viewportSize);
  1735                 return true;
  1737             return false;
  1739         case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
  1740             if (isEnabled() && mFirstPosition > 0) {
  1741                 final int viewportSize;
  1742                 if (mIsVertical) {
  1743                     viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
  1744                 } else {
  1745                     viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
  1748                 // TODO: Use some form of smooth scroll instead
  1749                 trackMotionScroll(-viewportSize);
  1750                 return true;
  1752             return false;
  1755         return false;
  1758     /**
  1759      * Return true if child is an ancestor of parent, (or equal to the parent).
  1760      */
  1761     private boolean isViewAncestorOf(View child, View parent) {
  1762         if (child == parent) {
  1763             return true;
  1766         final ViewParent theParent = child.getParent();
  1768         return (theParent instanceof ViewGroup) &&
  1769                  isViewAncestorOf((View) theParent, parent);
  1772     private void forceValidFocusDirection(int direction) {
  1773         if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
  1774             throw new IllegalArgumentException("Focus direction must be one of"
  1775                     + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation");
  1776         } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
  1777             throw new IllegalArgumentException("Focus direction must be one of"
  1778                     + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
  1782     private void forceValidInnerFocusDirection(int direction) {
  1783         if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
  1784             throw new IllegalArgumentException("Direction must be one of"
  1785                     + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
  1786         } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
  1787             throw new IllegalArgumentException("direction must be one of"
  1788                     + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation");
  1792     /**
  1793      * Scrolls up or down by the number of items currently present on screen.
  1795      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  1796      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  1797      *        current view orientation.
  1799      * @return whether selection was moved
  1800      */
  1801     boolean pageScroll(int direction) {
  1802         forceValidFocusDirection(direction);
  1804         boolean forward = false;
  1805         int nextPage = -1;
  1807         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
  1808             nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
  1809         } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
  1810             nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
  1811             forward = true;
  1814         if (nextPage < 0) {
  1815             return false;
  1818         final int position = lookForSelectablePosition(nextPage, forward);
  1819         if (position >= 0) {
  1820             mLayoutMode = LAYOUT_SPECIFIC;
  1821             mSpecificStart = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  1823             if (forward && position > mItemCount - getChildCount()) {
  1824                 mLayoutMode = LAYOUT_FORCE_BOTTOM;
  1827             if (!forward && position < getChildCount()) {
  1828                 mLayoutMode = LAYOUT_FORCE_TOP;
  1831             setSelectionInt(position);
  1832             invokeOnItemScrollListener();
  1834             if (!awakenScrollbarsInternal()) {
  1835                 invalidate();
  1838             return true;
  1841         return false;
  1844     /**
  1845      * Go to the last or first item if possible (not worrying about panning across or navigating
  1846      * within the internal focus of the currently selected item.)
  1848      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  1849      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  1850      *        current view orientation.
  1852      * @return whether selection was moved
  1853      */
  1854     boolean fullScroll(int direction) {
  1855         forceValidFocusDirection(direction);
  1857         boolean moved = false;
  1858         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
  1859             if (mSelectedPosition != 0) {
  1860                 int position = lookForSelectablePosition(0, true);
  1861                 if (position >= 0) {
  1862                     mLayoutMode = LAYOUT_FORCE_TOP;
  1863                     setSelectionInt(position);
  1864                     invokeOnItemScrollListener();
  1867                 moved = true;
  1869         } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
  1870             if (mSelectedPosition < mItemCount - 1) {
  1871                 int position = lookForSelectablePosition(mItemCount - 1, true);
  1872                 if (position >= 0) {
  1873                     mLayoutMode = LAYOUT_FORCE_BOTTOM;
  1874                     setSelectionInt(position);
  1875                     invokeOnItemScrollListener();
  1878                 moved = true;
  1882         if (moved && !awakenScrollbarsInternal()) {
  1883             awakenScrollbarsInternal();
  1884             invalidate();
  1887         return moved;
  1890     /**
  1891      * To avoid horizontal/vertical focus searches changing the selected item,
  1892      * we manually focus search within the selected item (as applicable), and
  1893      * prevent focus from jumping to something within another item.
  1895      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  1896      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  1897      *        current view orientation.
  1899      * @return Whether this consumes the key event.
  1900      */
  1901     private boolean handleFocusWithinItem(int direction) {
  1902         forceValidInnerFocusDirection(direction);
  1904         final int numChildren = getChildCount();
  1906         if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
  1907             final View selectedView = getSelectedView();
  1909             if (selectedView != null && selectedView.hasFocus() &&
  1910                     selectedView instanceof ViewGroup) {
  1912                 final View currentFocus = selectedView.findFocus();
  1913                 final View nextFocus = FocusFinder.getInstance().findNextFocus(
  1914                         (ViewGroup) selectedView, currentFocus, direction);
  1916                 if (nextFocus != null) {
  1917                     // Do the math to get interesting rect in next focus' coordinates
  1918                     currentFocus.getFocusedRect(mTempRect);
  1919                     offsetDescendantRectToMyCoords(currentFocus, mTempRect);
  1920                     offsetRectIntoDescendantCoords(nextFocus, mTempRect);
  1922                     if (nextFocus.requestFocus(direction, mTempRect)) {
  1923                         return true;
  1927                 // We are blocking the key from being handled (by returning true)
  1928                 // if the global result is going to be some other view within this
  1929                 // list. This is to achieve the overall goal of having horizontal/vertical
  1930                 // d-pad navigation remain in the current item depending on the current
  1931                 // orientation in this view.
  1932                 final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
  1933                         (ViewGroup) getRootView(), currentFocus, direction);
  1935                 if (globalNextFocus != null) {
  1936                     return isViewAncestorOf(globalNextFocus, this);
  1941         return false;
  1944     /**
  1945      * Scrolls to the next or previous item if possible.
  1947      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  1948      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  1949      *        current view orientation.
  1951      * @return whether selection was moved
  1952      */
  1953     private boolean arrowScroll(int direction) {
  1954         forceValidFocusDirection(direction);
  1956         try {
  1957             mInLayout = true;
  1959             final boolean handled = arrowScrollImpl(direction);
  1960             if (handled) {
  1961                 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
  1964             return handled;
  1965         } finally {
  1966             mInLayout = false;
  1970     /**
  1971      * When selection changes, it is possible that the previously selected or the
  1972      * next selected item will change its size.  If so, we need to offset some folks,
  1973      * and re-layout the items as appropriate.
  1975      * @param selectedView The currently selected view (before changing selection).
  1976      *   should be <code>null</code> if there was no previous selection.
  1977      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  1978      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  1979      *        current view orientation.
  1980      * @param newSelectedPosition The position of the next selection.
  1981      * @param newFocusAssigned whether new focus was assigned.  This matters because
  1982      *        when something has focus, we don't want to show selection (ugh).
  1983      */
  1984     private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
  1985             boolean newFocusAssigned) {
  1986         forceValidFocusDirection(direction);
  1988         if (newSelectedPosition == INVALID_POSITION) {
  1989             throw new IllegalArgumentException("newSelectedPosition needs to be valid");
  1992         // Whether or not we are moving down/right or up/left, we want to preserve the
  1993         // top/left of whatever view is at the start:
  1994         // - moving down/right: the view that had selection
  1995         // - moving up/left: the view that is getting selection
  1996         final int selectedIndex = mSelectedPosition - mFirstPosition;
  1997         final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
  1998         int startViewIndex, endViewIndex;
  1999         boolean topSelected = false;
  2000         View startView;
  2001         View endView;
  2003         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
  2004             startViewIndex = nextSelectedIndex;
  2005             endViewIndex = selectedIndex;
  2006             startView = getChildAt(startViewIndex);
  2007             endView = selectedView;
  2008             topSelected = true;
  2009         } else {
  2010             startViewIndex = selectedIndex;
  2011             endViewIndex = nextSelectedIndex;
  2012             startView = selectedView;
  2013             endView = getChildAt(endViewIndex);
  2016         final int numChildren = getChildCount();
  2018         // start with top view: is it changing size?
  2019         if (startView != null) {
  2020             startView.setSelected(!newFocusAssigned && topSelected);
  2021             measureAndAdjustDown(startView, startViewIndex, numChildren);
  2024         // is the bottom view changing size?
  2025         if (endView != null) {
  2026             endView.setSelected(!newFocusAssigned && !topSelected);
  2027             measureAndAdjustDown(endView, endViewIndex, numChildren);
  2031     /**
  2032      * Re-measure a child, and if its height changes, lay it out preserving its
  2033      * top, and adjust the children below it appropriately.
  2035      * @param child The child
  2036      * @param childIndex The view group index of the child.
  2037      * @param numChildren The number of children in the view group.
  2038      */
  2039     private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
  2040         int oldHeight = child.getHeight();
  2041         measureChild(child);
  2043         if (child.getMeasuredHeight() == oldHeight) {
  2044             return;
  2047         // lay out the view, preserving its top
  2048         relayoutMeasuredChild(child);
  2050         // adjust views below appropriately
  2051         final int heightDelta = child.getMeasuredHeight() - oldHeight;
  2052         for (int i = childIndex + 1; i < numChildren; i++) {
  2053             getChildAt(i).offsetTopAndBottom(heightDelta);
  2057     /**
  2058      * Do an arrow scroll based on focus searching.  If a new view is
  2059      * given focus, return the selection delta and amount to scroll via
  2060      * an {@link ArrowScrollFocusResult}, otherwise, return null.
  2062      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  2063      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  2064      *        current view orientation.
  2066      * @return The result if focus has changed, or <code>null</code>.
  2067      */
  2068     private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
  2069         forceValidFocusDirection(direction);
  2071         final View selectedView = getSelectedView();
  2072         final View newFocus;
  2073         final int searchPoint;
  2075         if (selectedView != null && selectedView.hasFocus()) {
  2076             View oldFocus = selectedView.findFocus();
  2077             newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
  2078         } else {
  2079             if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
  2080                 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  2082                 final int selectedStart;
  2083                 if (selectedView != null) {
  2084                     selectedStart = (mIsVertical ? selectedView.getTop() : selectedView.getLeft());
  2085                 } else {
  2086                     selectedStart = start;
  2089                 searchPoint = Math.max(selectedStart, start);
  2090             } else {
  2091                 final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
  2092                     getWidth() - getPaddingRight());
  2094                 final int selectedEnd;
  2095                 if (selectedView != null) {
  2096                     selectedEnd = (mIsVertical ? selectedView.getBottom() : selectedView.getRight());
  2097                 } else {
  2098                     selectedEnd = end;
  2101                 searchPoint = Math.min(selectedEnd, end);
  2104             final int x = (mIsVertical ? 0 : searchPoint);
  2105             final int y = (mIsVertical ? searchPoint : 0);
  2106             mTempRect.set(x, y, x, y);
  2108             newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
  2111         if (newFocus != null) {
  2112             final int positionOfNewFocus = positionOfNewFocus(newFocus);
  2114             // If the focus change is in a different new position, make sure
  2115             // we aren't jumping over another selectable position.
  2116             if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
  2117                 final int selectablePosition = lookForSelectablePositionOnScreen(direction);
  2119                 final boolean movingForward =
  2120                         (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT);
  2121                 final boolean movingBackward =
  2122                         (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT);
  2124                 if (selectablePosition != INVALID_POSITION &&
  2125                         ((movingForward && selectablePosition < positionOfNewFocus) ||
  2126                          (movingBackward && selectablePosition > positionOfNewFocus))) {
  2127                     return null;
  2131             int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
  2133             final int maxScrollAmount = getMaxScrollAmount();
  2134             if (focusScroll < maxScrollAmount) {
  2135                 // Not moving too far, safe to give next view focus
  2136                 newFocus.requestFocus(direction);
  2137                 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
  2138                 return mArrowScrollFocusResult;
  2139             } else if (distanceToView(newFocus) < maxScrollAmount){
  2140                 // Case to consider:
  2141                 // Too far to get entire next focusable on screen, but by going
  2142                 // max scroll amount, we are getting it at least partially in view,
  2143                 // so give it focus and scroll the max amount.
  2144                 newFocus.requestFocus(direction);
  2145                 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
  2146                 return mArrowScrollFocusResult;
  2150         return null;
  2153     /**
  2154      * @return The maximum amount a list view will scroll in response to
  2155      *   an arrow event.
  2156      */
  2157     public int getMaxScrollAmount() {
  2158         return (int) (MAX_SCROLL_FACTOR * getHeight());
  2161     /**
  2162      * @return The amount to preview next items when arrow scrolling.
  2163      */
  2164     private int getArrowScrollPreviewLength() {
  2165         // FIXME: TwoWayView has no fading edge support just yet but using it
  2166         // makes it convenient for defining the next item's previous length.
  2167         int fadingEdgeLength =
  2168                 (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength());
  2170         return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, fadingEdgeLength);
  2173     /**
  2174      * @param newFocus The view that would have focus.
  2175      * @return the position that contains newFocus
  2176      */
  2177     private int positionOfNewFocus(View newFocus) {
  2178         final int numChildren = getChildCount();
  2180         for (int i = 0; i < numChildren; i++) {
  2181             final View child = getChildAt(i);
  2182             if (isViewAncestorOf(newFocus, child)) {
  2183                 return mFirstPosition + i;
  2187         throw new IllegalArgumentException("newFocus is not a child of any of the"
  2188                 + " children of the list!");
  2191     /**
  2192      * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
  2193      * whether there are focusable items, etc.
  2195      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  2196      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  2197      *        current view orientation.
  2199      * @return Whether any scrolling, selection or focus change occurred.
  2200      */
  2201     private boolean arrowScrollImpl(int direction) {
  2202         forceValidFocusDirection(direction);
  2204         if (getChildCount() <= 0) {
  2205             return false;
  2208         View selectedView = getSelectedView();
  2209         int selectedPos = mSelectedPosition;
  2211         int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
  2212         int amountToScroll = amountToScroll(direction, nextSelectedPosition);
  2214         // If we are moving focus, we may OVERRIDE the default behaviour
  2215         final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null);
  2216         if (focusResult != null) {
  2217             nextSelectedPosition = focusResult.getSelectedPosition();
  2218             amountToScroll = focusResult.getAmountToScroll();
  2221         boolean needToRedraw = (focusResult != null);
  2222         if (nextSelectedPosition != INVALID_POSITION) {
  2223             handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
  2225             setSelectedPositionInt(nextSelectedPosition);
  2226             setNextSelectedPositionInt(nextSelectedPosition);
  2228             selectedView = getSelectedView();
  2229             selectedPos = nextSelectedPosition;
  2231             if (mItemsCanFocus && focusResult == null) {
  2232                 // There was no new view found to take focus, make sure we
  2233                 // don't leave focus with the old selection.
  2234                 final View focused = getFocusedChild();
  2235                 if (focused != null) {
  2236                     focused.clearFocus();
  2240             needToRedraw = true;
  2241             checkSelectionChanged();
  2244         if (amountToScroll > 0) {
  2245             trackMotionScroll(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
  2246                     amountToScroll : -amountToScroll);
  2247             needToRedraw = true;
  2250         // If we didn't find a new focusable, make sure any existing focused
  2251         // item that was panned off screen gives up focus.
  2252         if (mItemsCanFocus && focusResult == null &&
  2253                 selectedView != null && selectedView.hasFocus()) {
  2254             final View focused = selectedView.findFocus();
  2255             if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
  2256                 focused.clearFocus();
  2260         // If the current selection is panned off, we need to remove the selection
  2261         if (nextSelectedPosition == INVALID_POSITION && selectedView != null
  2262                 && !isViewAncestorOf(selectedView, this)) {
  2263             selectedView = null;
  2264             hideSelector();
  2266             // But we don't want to set the ressurect position (that would make subsequent
  2267             // unhandled key events bring back the item we just scrolled off)
  2268             mResurrectToPosition = INVALID_POSITION;
  2271         if (needToRedraw) {
  2272             if (selectedView != null) {
  2273                 positionSelector(selectedPos, selectedView);
  2274                 mSelectedStart = selectedView.getTop();
  2277             if (!awakenScrollbarsInternal()) {
  2278                 invalidate();
  2281             invokeOnItemScrollListener();
  2282             return true;
  2285         return false;
  2288     /**
  2289      * Determine how much we need to scroll in order to get the next selected view
  2290      * visible. The amount is capped at {@link #getMaxScrollAmount()}.
  2292      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  2293      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  2294      *        current view orientation.
  2295      * @param nextSelectedPosition The position of the next selection, or
  2296      *        {@link #INVALID_POSITION} if there is no next selectable position
  2298      * @return The amount to scroll. Note: this is always positive!  Direction
  2299      *         needs to be taken into account when actually scrolling.
  2300      */
  2301     private int amountToScroll(int direction, int nextSelectedPosition) {
  2302         forceValidFocusDirection(direction);
  2304         final int numChildren = getChildCount();
  2306         if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
  2307             final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
  2308                 getWidth() - getPaddingRight());
  2310             int indexToMakeVisible = numChildren - 1;
  2311             if (nextSelectedPosition != INVALID_POSITION) {
  2312                 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
  2315             final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
  2316             final View viewToMakeVisible = getChildAt(indexToMakeVisible);
  2318             int goalEnd = end;
  2319             if (positionToMakeVisible < mItemCount - 1) {
  2320                 goalEnd -= getArrowScrollPreviewLength();
  2323             final int viewToMakeVisibleStart =
  2324                     (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
  2325             final int viewToMakeVisibleEnd =
  2326                     (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
  2328             if (viewToMakeVisibleEnd <= goalEnd) {
  2329                 // Target item is fully visible
  2330                 return 0;
  2333             if (nextSelectedPosition != INVALID_POSITION &&
  2334                     (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) {
  2335                 // Item already has enough of it visible, changing selection is good enough
  2336                 return 0;
  2339             int amountToScroll = (viewToMakeVisibleEnd - goalEnd);
  2341             if (mFirstPosition + numChildren == mItemCount) {
  2342                 final View lastChild = getChildAt(numChildren - 1);
  2343                 final int lastChildEnd = (mIsVertical ? lastChild.getBottom() : lastChild.getRight());
  2345                 // Last is last in list -> Make sure we don't scroll past it
  2346                 final int max = lastChildEnd - end;
  2347                 amountToScroll = Math.min(amountToScroll, max);
  2350             return Math.min(amountToScroll, getMaxScrollAmount());
  2351         } else {
  2352             final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  2354             int indexToMakeVisible = 0;
  2355             if (nextSelectedPosition != INVALID_POSITION) {
  2356                 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
  2359             final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
  2360             final View viewToMakeVisible = getChildAt(indexToMakeVisible);
  2362             int goalStart = start;
  2363             if (positionToMakeVisible > 0) {
  2364                 goalStart += getArrowScrollPreviewLength();
  2367             final int viewToMakeVisibleStart =
  2368                     (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
  2369             final int viewToMakeVisibleEnd =
  2370                     (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
  2372             if (viewToMakeVisibleStart >= goalStart) {
  2373                 // Item is fully visible
  2374                 return 0;
  2377             if (nextSelectedPosition != INVALID_POSITION &&
  2378                     (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) {
  2379                 // Item already has enough of it visible, changing selection is good enough
  2380                 return 0;
  2383             int amountToScroll = (goalStart - viewToMakeVisibleStart);
  2385             if (mFirstPosition == 0) {
  2386                 final View firstChild = getChildAt(0);
  2387                 final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); 
  2389                 // First is first in list -> make sure we don't scroll past it
  2390                 final int max = start - firstChildStart;
  2391                 amountToScroll = Math.min(amountToScroll,  max);
  2394             return Math.min(amountToScroll, getMaxScrollAmount());
  2398     /**
  2399      * Determine how much we need to scroll in order to get newFocus in view.
  2401      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  2402      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  2403      *        current view orientation.
  2404      * @param newFocus The view that would take focus.
  2405      * @param positionOfNewFocus The position of the list item containing newFocus
  2407      * @return The amount to scroll. Note: this is always positive! Direction
  2408      *   needs to be taken into account when actually scrolling.
  2409      */
  2410     private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
  2411         forceValidFocusDirection(direction);
  2413         int amountToScroll = 0;
  2415         newFocus.getDrawingRect(mTempRect);
  2416         offsetDescendantRectToMyCoords(newFocus, mTempRect);
  2418         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
  2419             final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  2420             final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);
  2422             if (newFocusStart < start) {
  2423                 amountToScroll = start - newFocusStart;
  2424                 if (positionOfNewFocus > 0) {
  2425                     amountToScroll += getArrowScrollPreviewLength();
  2428         } else {
  2429             final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
  2430                 getWidth() - getPaddingRight());
  2431             final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
  2433             if (newFocusEnd > end) {
  2434                 amountToScroll = newFocusEnd - end;
  2435                 if (positionOfNewFocus < mItemCount - 1) {
  2436                     amountToScroll += getArrowScrollPreviewLength();
  2441         return amountToScroll;
  2444     /**
  2445      * Determine the distance to the nearest edge of a view in a particular
  2446      * direction.
  2448      * @param descendant A descendant of this list.
  2449      * @return The distance, or 0 if the nearest edge is already on screen.
  2450      */
  2451     private int distanceToView(View descendant) {
  2452         descendant.getDrawingRect(mTempRect);
  2453         offsetDescendantRectToMyCoords(descendant, mTempRect);
  2455         final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  2456         final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
  2457             getWidth() - getPaddingRight());
  2459         final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
  2460         final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
  2462         int distance = 0;
  2463         if (viewEnd < start) {
  2464             distance = start - viewEnd;
  2465         } else if (viewStart > end) {
  2466             distance = viewStart - end;
  2469         return distance;
  2472     private boolean handleKeyScroll(KeyEvent event, int count, int direction) {
  2473         boolean handled = false;
  2475         if (KeyEventCompat.hasNoModifiers(event)) {
  2476             handled = resurrectSelectionIfNeeded();
  2477             if (!handled) {
  2478                 while (count-- > 0) {
  2479                     if (arrowScroll(direction)) {
  2480                         handled = true;
  2481                     } else {
  2482                         break;
  2486         } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
  2487             handled = resurrectSelectionIfNeeded() || fullScroll(direction);
  2490         return handled;
  2493     private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) {
  2494         if (mAdapter == null || !mIsAttached) {
  2495             return false;
  2498         if (mDataChanged) {
  2499             layoutChildren();
  2502         boolean handled = false;
  2503         final int action = event.getAction();
  2505         if (action != KeyEvent.ACTION_UP) {
  2506             switch (keyCode) {
  2507             case KeyEvent.KEYCODE_DPAD_UP:
  2508                 if (mIsVertical) {
  2509                     handled = handleKeyScroll(event, count, View.FOCUS_UP);
  2510                 } else if (KeyEventCompat.hasNoModifiers(event)) {
  2511                     handled = handleFocusWithinItem(View.FOCUS_UP);
  2513                 break;
  2515             case KeyEvent.KEYCODE_DPAD_DOWN: {
  2516                 if (mIsVertical) {
  2517                     handled = handleKeyScroll(event, count, View.FOCUS_DOWN);
  2518                 } else if (KeyEventCompat.hasNoModifiers(event)) {
  2519                     handled = handleFocusWithinItem(View.FOCUS_DOWN);
  2521                 break;
  2524             case KeyEvent.KEYCODE_DPAD_LEFT:
  2525                 if (!mIsVertical) {
  2526                     handled = handleKeyScroll(event, count, View.FOCUS_LEFT);
  2527                 } else if (KeyEventCompat.hasNoModifiers(event)) {
  2528                     handled = handleFocusWithinItem(View.FOCUS_LEFT);
  2530                 break;
  2532             case KeyEvent.KEYCODE_DPAD_RIGHT:
  2533                 if (!mIsVertical) {
  2534                     handled = handleKeyScroll(event, count, View.FOCUS_RIGHT);
  2535                 } else if (KeyEventCompat.hasNoModifiers(event)) {
  2536                     handled = handleFocusWithinItem(View.FOCUS_RIGHT);
  2538                 break;
  2540             case KeyEvent.KEYCODE_DPAD_CENTER:
  2541             case KeyEvent.KEYCODE_ENTER:
  2542                 if (KeyEventCompat.hasNoModifiers(event)) {
  2543                     handled = resurrectSelectionIfNeeded();
  2544                     if (!handled
  2545                             && event.getRepeatCount() == 0 && getChildCount() > 0) {
  2546                         keyPressed();
  2547                         handled = true;
  2550                 break;
  2552             case KeyEvent.KEYCODE_SPACE:
  2553                 if (KeyEventCompat.hasNoModifiers(event)) {
  2554                     handled = resurrectSelectionIfNeeded() ||
  2555                             pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
  2556                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
  2557                     handled = resurrectSelectionIfNeeded() ||
  2558                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
  2561                 handled = true;
  2562                 break;
  2564             case KeyEvent.KEYCODE_PAGE_UP:
  2565                 if (KeyEventCompat.hasNoModifiers(event)) {
  2566                     handled = resurrectSelectionIfNeeded() ||
  2567                             pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
  2568                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
  2569                     handled = resurrectSelectionIfNeeded() ||
  2570                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
  2572                 break;
  2574             case KeyEvent.KEYCODE_PAGE_DOWN:
  2575                 if (KeyEventCompat.hasNoModifiers(event)) {
  2576                     handled = resurrectSelectionIfNeeded() ||
  2577                             pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
  2578                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
  2579                     handled = resurrectSelectionIfNeeded() ||
  2580                             fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
  2582                 break;
  2584             case KeyEvent.KEYCODE_MOVE_HOME:
  2585                 if (KeyEventCompat.hasNoModifiers(event)) {
  2586                     handled = resurrectSelectionIfNeeded() ||
  2587                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
  2589                 break;
  2591             case KeyEvent.KEYCODE_MOVE_END:
  2592                 if (KeyEventCompat.hasNoModifiers(event)) {
  2593                     handled = resurrectSelectionIfNeeded() ||
  2594                             fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
  2596                 break;
  2600         if (handled) {
  2601             return true;
  2604         switch (action) {
  2605         case KeyEvent.ACTION_DOWN:
  2606             return super.onKeyDown(keyCode, event);
  2608         case KeyEvent.ACTION_UP:
  2609             if (!isEnabled()) {
  2610                 return true;
  2613             if (isClickable() && isPressed() &&
  2614                     mSelectedPosition >= 0 && mAdapter != null &&
  2615                     mSelectedPosition < mAdapter.getCount()) {
  2617                 final View child = getChildAt(mSelectedPosition - mFirstPosition);
  2618                 if (child != null) {
  2619                     performItemClick(child, mSelectedPosition, mSelectedRowId);
  2620                     child.setPressed(false);
  2623                 setPressed(false);
  2624                 return true;
  2627             return false;
  2629         case KeyEvent.ACTION_MULTIPLE:
  2630             return super.onKeyMultiple(keyCode, count, event);
  2632         default:
  2633             return false;
  2637     private void initOrResetVelocityTracker() {
  2638         if (mVelocityTracker == null) {
  2639             mVelocityTracker = VelocityTracker.obtain();
  2640         } else {
  2641             mVelocityTracker.clear();
  2645     private void initVelocityTrackerIfNotExists() {
  2646         if (mVelocityTracker == null) {
  2647             mVelocityTracker = VelocityTracker.obtain();
  2651     private void recycleVelocityTracker() {
  2652         if (mVelocityTracker != null) {
  2653             mVelocityTracker.recycle();
  2654             mVelocityTracker = null;
  2658     /**
  2659      * Notify our scroll listener (if there is one) of a change in scroll state
  2660      */
  2661     private void invokeOnItemScrollListener() {
  2662         if (mOnScrollListener != null) {
  2663             mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
  2666         // Dummy values, View's implementation does not use these.
  2667         onScrollChanged(0, 0, 0, 0);
  2670     private void reportScrollStateChange(int newState) {
  2671         if (newState == mLastScrollState) {
  2672             return;
  2675         if (mOnScrollListener != null) {
  2676             mLastScrollState = newState;
  2677             mOnScrollListener.onScrollStateChanged(this, newState);
  2681     private boolean maybeStartScrolling(int delta) {
  2682         final boolean isOverScroll = (mOverScroll != 0);
  2683         if (Math.abs(delta) <= mTouchSlop && !isOverScroll) {
  2684             return false;
  2687         if (isOverScroll) {
  2688             mTouchMode = TOUCH_MODE_OVERSCROLL;
  2689         } else {
  2690             mTouchMode = TOUCH_MODE_DRAGGING;
  2693         // Time to start stealing events! Once we've stolen them, don't
  2694         // let anyone steal from us.
  2695         final ViewParent parent = getParent();
  2696         if (parent != null) {
  2697             parent.requestDisallowInterceptTouchEvent(true);
  2700         cancelCheckForLongPress();
  2702         setPressed(false);
  2703         View motionView = getChildAt(mMotionPosition - mFirstPosition);
  2704         if (motionView != null) {
  2705             motionView.setPressed(false);
  2708         reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
  2710         return true;
  2713     private void maybeScroll(int delta) {
  2714         if (mTouchMode == TOUCH_MODE_DRAGGING) {
  2715             handleDragChange(delta);
  2716         } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
  2717             handleOverScrollChange(delta);
  2721     private void handleDragChange(int delta) {
  2722         // Time to start stealing events! Once we've stolen them, don't
  2723         // let anyone steal from us.
  2724         final ViewParent parent = getParent();
  2725         if (parent != null) {
  2726             parent.requestDisallowInterceptTouchEvent(true);
  2729         final int motionIndex;
  2730         if (mMotionPosition >= 0) {
  2731             motionIndex = mMotionPosition - mFirstPosition;
  2732         } else {
  2733             // If we don't have a motion position that we can reliably track,
  2734             // pick something in the middle to make a best guess at things below.
  2735             motionIndex = getChildCount() / 2;
  2738         int motionViewPrevStart = 0;
  2739         View motionView = this.getChildAt(motionIndex);
  2740         if (motionView != null) {
  2741             motionViewPrevStart = (mIsVertical ? motionView.getTop() : motionView.getLeft());
  2744         boolean atEdge = trackMotionScroll(delta);
  2746         motionView = this.getChildAt(motionIndex);
  2747         if (motionView != null) {
  2748             final int motionViewRealStart =
  2749                     (mIsVertical ? motionView.getTop() : motionView.getLeft());
  2751             if (atEdge) {
  2752                 final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart);
  2753                 updateOverScrollState(delta, overscroll);
  2758     private void updateOverScrollState(int delta, int overscroll) {
  2759         overScrollByInternal((mIsVertical ? 0 : overscroll),
  2760                              (mIsVertical ? overscroll : 0),
  2761                              (mIsVertical ? 0 : mOverScroll),
  2762                              (mIsVertical ? mOverScroll : 0),
  2763                              0, 0,
  2764                              (mIsVertical ? 0 : mOverscrollDistance),
  2765                              (mIsVertical ? mOverscrollDistance : 0),
  2766                              true);
  2768         if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) {
  2769             // Break fling velocity if we impacted an edge
  2770             if (mVelocityTracker != null) {
  2771                 mVelocityTracker.clear();
  2775         final int overscrollMode = ViewCompat.getOverScrollMode(this);
  2776         if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
  2777                 (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
  2778             mTouchMode = TOUCH_MODE_OVERSCROLL;
  2780             float pull = (float) overscroll / (mIsVertical ? getHeight() : getWidth());
  2781             if (delta > 0) {
  2782                 mStartEdge.onPull(pull);
  2784                 if (!mEndEdge.isFinished()) {
  2785                     mEndEdge.onRelease();
  2787             } else if (delta < 0) {
  2788                 mEndEdge.onPull(pull);
  2790                 if (!mStartEdge.isFinished()) {
  2791                     mStartEdge.onRelease();
  2795             if (delta != 0) {
  2796                 ViewCompat.postInvalidateOnAnimation(this);
  2801     private void handleOverScrollChange(int delta) {
  2802         final int oldOverScroll = mOverScroll;
  2803         final int newOverScroll = oldOverScroll - delta;
  2805         int overScrollDistance = -delta;
  2806         if ((newOverScroll < 0 && oldOverScroll >= 0) ||
  2807                 (newOverScroll > 0 && oldOverScroll <= 0)) {
  2808             overScrollDistance = -oldOverScroll;
  2809             delta += overScrollDistance;
  2810         } else {
  2811             delta = 0;
  2814         if (overScrollDistance != 0) {
  2815             updateOverScrollState(delta, overScrollDistance);
  2818         if (delta != 0) {
  2819             if (mOverScroll != 0) {
  2820                 mOverScroll = 0;
  2821                 ViewCompat.postInvalidateOnAnimation(this);
  2824             trackMotionScroll(delta);
  2825             mTouchMode = TOUCH_MODE_DRAGGING;
  2827             // We did not scroll the full amount. Treat this essentially like the
  2828             // start of a new touch scroll
  2829             mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos);
  2830             mTouchRemainderPos = 0;
  2834     /**
  2835      * What is the distance between the source and destination rectangles given the direction of
  2836      * focus navigation between them? The direction basically helps figure out more quickly what is
  2837      * self evident by the relationship between the rects...
  2839      * @param source the source rectangle
  2840      * @param dest the destination rectangle
  2841      * @param direction the direction
  2842      * @return the distance between the rectangles
  2843      */
  2844     private static int getDistance(Rect source, Rect dest, int direction) {
  2845         int sX, sY; // source x, y
  2846         int dX, dY; // dest x, y
  2848         switch (direction) {
  2849         case View.FOCUS_RIGHT:
  2850             sX = source.right;
  2851             sY = source.top + source.height() / 2;
  2852             dX = dest.left;
  2853             dY = dest.top + dest.height() / 2;
  2854             break;
  2856         case View.FOCUS_DOWN:
  2857             sX = source.left + source.width() / 2;
  2858             sY = source.bottom;
  2859             dX = dest.left + dest.width() / 2;
  2860             dY = dest.top;
  2861             break;
  2863         case View.FOCUS_LEFT:
  2864             sX = source.left;
  2865             sY = source.top + source.height() / 2;
  2866             dX = dest.right;
  2867             dY = dest.top + dest.height() / 2;
  2868             break;
  2870         case View.FOCUS_UP:
  2871             sX = source.left + source.width() / 2;
  2872             sY = source.top;
  2873             dX = dest.left + dest.width() / 2;
  2874             dY = dest.bottom;
  2875             break;
  2877         case View.FOCUS_FORWARD:
  2878         case View.FOCUS_BACKWARD:
  2879             sX = source.right + source.width() / 2;
  2880             sY = source.top + source.height() / 2;
  2881             dX = dest.left + dest.width() / 2;
  2882             dY = dest.top + dest.height() / 2;
  2883             break;
  2885         default:
  2886             throw new IllegalArgumentException("direction must be one of "
  2887                     + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
  2888                     + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
  2891         int deltaX = dX - sX;
  2892         int deltaY = dY - sY;
  2894         return deltaY * deltaY + deltaX * deltaX;
  2897     private int findMotionRowOrColumn(int motionPos) {
  2898         int childCount = getChildCount();
  2899         if (childCount == 0) {
  2900             return INVALID_POSITION;
  2903         for (int i = 0; i < childCount; i++) {
  2904             View v = getChildAt(i);
  2906             if ((mIsVertical && motionPos <= v.getBottom()) ||
  2907                     (!mIsVertical && motionPos <= v.getRight())) {
  2908                 return mFirstPosition + i;
  2912         return INVALID_POSITION;
  2915     private int findClosestMotionRowOrColumn(int motionPos) {
  2916         final int childCount = getChildCount();
  2917         if (childCount == 0) {
  2918             return INVALID_POSITION;
  2921         final int motionRow = findMotionRowOrColumn(motionPos);
  2922         if (motionRow != INVALID_POSITION) {
  2923             return motionRow;
  2924         } else {
  2925             return mFirstPosition + childCount - 1;
  2929     @TargetApi(9)
  2930     private int getScaledOverscrollDistance(ViewConfiguration vc) {
  2931         if (Build.VERSION.SDK_INT < 9) {
  2932             return 0;
  2935         return vc.getScaledOverscrollDistance();
  2938     private boolean contentFits() {
  2939         final int childCount = getChildCount();
  2940         if (childCount == 0) {
  2941             return true;
  2944         if (childCount != mItemCount) {
  2945             return false;
  2948         View first = getChildAt(0);
  2949         View last = getChildAt(childCount - 1);
  2951         if (mIsVertical) {
  2952             return first.getTop() >= getPaddingTop() &&
  2953                     last.getBottom() <= getHeight() - getPaddingBottom();
  2954         } else {
  2955             return first.getLeft() >= getPaddingLeft() &&
  2956                     last.getRight() <= getWidth() - getPaddingRight();
  2960     private void updateScrollbarsDirection() {
  2961         setHorizontalScrollBarEnabled(!mIsVertical);
  2962         setVerticalScrollBarEnabled(mIsVertical);
  2965     private void triggerCheckForTap() {
  2966         if (mPendingCheckForTap == null) {
  2967             mPendingCheckForTap = new CheckForTap();
  2970         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
  2973     private void cancelCheckForTap() {
  2974         if (mPendingCheckForTap == null) {
  2975             return;
  2978         removeCallbacks(mPendingCheckForTap);
  2981     private void triggerCheckForLongPress() {
  2982         if (mPendingCheckForLongPress == null) {
  2983             mPendingCheckForLongPress = new CheckForLongPress();
  2986         mPendingCheckForLongPress.rememberWindowAttachCount();
  2988         postDelayed(mPendingCheckForLongPress,
  2989                 ViewConfiguration.getLongPressTimeout());
  2992     private void cancelCheckForLongPress() {
  2993         if (mPendingCheckForLongPress == null) {
  2994             return;
  2997         removeCallbacks(mPendingCheckForLongPress);
  3000     boolean trackMotionScroll(int incrementalDelta) {
  3001         final int childCount = getChildCount();
  3002         if (childCount == 0) {
  3003             return true;
  3006         final View first = getChildAt(0);
  3007         final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
  3009         final View last = getChildAt(childCount - 1);
  3010         final int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
  3012         final int paddingTop = getPaddingTop();
  3013         final int paddingBottom = getPaddingBottom();
  3014         final int paddingLeft = getPaddingLeft();
  3015         final int paddingRight = getPaddingRight();
  3017         final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);
  3019         final int spaceBefore = paddingStart - firstStart;
  3020         final int end = (mIsVertical ? getHeight() - paddingBottom :
  3021             getWidth() - paddingRight);
  3022         final int spaceAfter = lastEnd - end;
  3024         final int size;
  3025         if (mIsVertical) {
  3026             size = getHeight() - paddingBottom - paddingTop;
  3027         } else {
  3028             size = getWidth() - paddingRight - paddingLeft;
  3031         if (incrementalDelta < 0) {
  3032             incrementalDelta = Math.max(-(size - 1), incrementalDelta);
  3033         } else {
  3034             incrementalDelta = Math.min(size - 1, incrementalDelta);
  3037         final int firstPosition = mFirstPosition;
  3039         final boolean cannotScrollDown = (firstPosition == 0 &&
  3040                 firstStart >= paddingStart && incrementalDelta >= 0);
  3041         final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
  3042                 lastEnd <= end && incrementalDelta <= 0);
  3044         if (cannotScrollDown || cannotScrollUp) {
  3045             return incrementalDelta != 0;
  3048         final boolean inTouchMode = isInTouchMode();
  3049         if (inTouchMode) {
  3050             hideSelector();
  3053         int start = 0;
  3054         int count = 0;
  3056         final boolean down = (incrementalDelta < 0);
  3057         if (down) {
  3058             int childrenStart = -incrementalDelta + paddingStart;
  3060             for (int i = 0; i < childCount; i++) {
  3061                 final View child = getChildAt(i);
  3062                 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
  3064                 if (childEnd >= childrenStart) {
  3065                     break;
  3068                 count++;
  3069                 mRecycler.addScrapView(child, firstPosition + i);
  3071         } else {
  3072             int childrenEnd = end - incrementalDelta;
  3074             for (int i = childCount - 1; i >= 0; i--) {
  3075                 final View child = getChildAt(i);
  3076                 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
  3078                 if (childStart <= childrenEnd) {
  3079                     break;
  3082                 start = i;
  3083                 count++;
  3084                 mRecycler.addScrapView(child, firstPosition + i);
  3088         mBlockLayoutRequests = true;
  3090         if (count > 0) {
  3091             detachViewsFromParent(start, count);
  3094         // invalidate before moving the children to avoid unnecessary invalidate
  3095         // calls to bubble up from the children all the way to the top
  3096         if (!awakenScrollbarsInternal()) {
  3097            invalidate();
  3100         offsetChildren(incrementalDelta);
  3102         if (down) {
  3103             mFirstPosition += count;
  3106         final int absIncrementalDelta = Math.abs(incrementalDelta);
  3107         if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) {
  3108             fillGap(down);
  3111         if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
  3112             final int childIndex = mSelectedPosition - mFirstPosition;
  3113             if (childIndex >= 0 && childIndex < getChildCount()) {
  3114                 positionSelector(mSelectedPosition, getChildAt(childIndex));
  3116         } else if (mSelectorPosition != INVALID_POSITION) {
  3117             final int childIndex = mSelectorPosition - mFirstPosition;
  3118             if (childIndex >= 0 && childIndex < getChildCount()) {
  3119                 positionSelector(INVALID_POSITION, getChildAt(childIndex));
  3121         } else {
  3122             mSelectorRect.setEmpty();
  3125         mBlockLayoutRequests = false;
  3127         invokeOnItemScrollListener();
  3129         return false;
  3132     @TargetApi(14)
  3133     private final float getCurrVelocity() {
  3134         if (Build.VERSION.SDK_INT >= 14) {
  3135             return mScroller.getCurrVelocity();
  3138         return 0;
  3141     @TargetApi(5)
  3142     private boolean awakenScrollbarsInternal() {
  3143         if (Build.VERSION.SDK_INT >= 5) {
  3144             return super.awakenScrollBars();
  3145         } else {
  3146             return false;
  3150     @Override
  3151     public void computeScroll() {
  3152         if (!mScroller.computeScrollOffset()) {
  3153             return;
  3156         final int pos;
  3157         if (mIsVertical) {
  3158             pos = mScroller.getCurrY();
  3159         } else {
  3160             pos = mScroller.getCurrX();
  3163         final int diff = (int) (pos - mLastTouchPos);
  3164         mLastTouchPos = pos;
  3166         final boolean stopped = trackMotionScroll(diff);
  3168         if (!stopped && !mScroller.isFinished()) {
  3169             ViewCompat.postInvalidateOnAnimation(this);
  3170         } else {
  3171             if (stopped) {
  3172                 final int overScrollMode = ViewCompat.getOverScrollMode(this);
  3173                 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
  3174                     final EdgeEffectCompat edge =
  3175                             (diff > 0 ? mStartEdge : mEndEdge);
  3177                     boolean needsInvalidate =
  3178                             edge.onAbsorb(Math.abs((int) getCurrVelocity()));
  3180                     if (needsInvalidate) {
  3181                         ViewCompat.postInvalidateOnAnimation(this);
  3185                 mScroller.abortAnimation();
  3188             mTouchMode = TOUCH_MODE_REST;
  3189             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  3193     private void finishEdgeGlows() {
  3194         if (mStartEdge != null) {
  3195             mStartEdge.finish();
  3198         if (mEndEdge != null) {
  3199             mEndEdge.finish();
  3203     private boolean drawStartEdge(Canvas canvas) {
  3204         if (mStartEdge.isFinished()) {
  3205             return false;
  3208         if (mIsVertical) {
  3209             return mStartEdge.draw(canvas);
  3212         final int restoreCount = canvas.save();
  3213         final int height = getHeight() - getPaddingTop() - getPaddingBottom();
  3215         canvas.translate(0, height);
  3216         canvas.rotate(270);
  3218         final boolean needsInvalidate = mStartEdge.draw(canvas);
  3219         canvas.restoreToCount(restoreCount);
  3220         return needsInvalidate;
  3223     private boolean drawEndEdge(Canvas canvas) {
  3224         if (mEndEdge.isFinished()) {
  3225             return false;
  3228         final int restoreCount = canvas.save();
  3229         final int width = getWidth() - getPaddingLeft() - getPaddingRight();
  3230         final int height = getHeight() - getPaddingTop() - getPaddingBottom();
  3232         if (mIsVertical) {
  3233             canvas.translate(-width, height);
  3234             canvas.rotate(180, width, 0);
  3235         } else {
  3236             canvas.translate(width, 0);
  3237             canvas.rotate(90);
  3240         final boolean needsInvalidate = mEndEdge.draw(canvas);
  3241         canvas.restoreToCount(restoreCount);
  3242         return needsInvalidate;
  3245     private void drawSelector(Canvas canvas) {
  3246         if (!mSelectorRect.isEmpty()) {
  3247             final Drawable selector = mSelector;
  3248             selector.setBounds(mSelectorRect);
  3249             selector.draw(canvas);
  3253     private void useDefaultSelector() {
  3254         setSelector(getResources().getDrawable(
  3255                 android.R.drawable.list_selector_background));
  3258     private boolean shouldShowSelector() {
  3259         return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
  3262     private void positionSelector(int position, View selected) {
  3263         if (position != INVALID_POSITION) {
  3264             mSelectorPosition = position;
  3267         mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(),
  3268                 selected.getBottom());
  3270         final boolean isChildViewEnabled = mIsChildViewEnabled;
  3271         if (selected.isEnabled() != isChildViewEnabled) {
  3272             mIsChildViewEnabled = !isChildViewEnabled;
  3274             if (getSelectedItemPosition() != INVALID_POSITION) {
  3275                 refreshDrawableState();
  3280     private void hideSelector() {
  3281         if (mSelectedPosition != INVALID_POSITION) {
  3282             if (mLayoutMode != LAYOUT_SPECIFIC) {
  3283                 mResurrectToPosition = mSelectedPosition;
  3286             if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
  3287                 mResurrectToPosition = mNextSelectedPosition;
  3290             setSelectedPositionInt(INVALID_POSITION);
  3291             setNextSelectedPositionInt(INVALID_POSITION);
  3293             mSelectedStart = 0;
  3297     private void setSelectedPositionInt(int position) {
  3298         mSelectedPosition = position;
  3299         mSelectedRowId = getItemIdAtPosition(position);
  3302     private void setSelectionInt(int position) {
  3303         setNextSelectedPositionInt(position);
  3304         boolean awakeScrollbars = false;
  3306         final int selectedPosition = mSelectedPosition;
  3307         if (selectedPosition >= 0) {
  3308             if (position == selectedPosition - 1) {
  3309                 awakeScrollbars = true;
  3310             } else if (position == selectedPosition + 1) {
  3311                 awakeScrollbars = true;
  3315         layoutChildren();
  3317         if (awakeScrollbars) {
  3318             awakenScrollbarsInternal();
  3322     private void setNextSelectedPositionInt(int position) {
  3323         mNextSelectedPosition = position;
  3324         mNextSelectedRowId = getItemIdAtPosition(position);
  3326         // If we are trying to sync to the selection, update that too
  3327         if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
  3328             mSyncPosition = position;
  3329             mSyncRowId = mNextSelectedRowId;
  3333     private boolean touchModeDrawsInPressedState() {
  3334         switch (mTouchMode) {
  3335         case TOUCH_MODE_TAP:
  3336         case TOUCH_MODE_DONE_WAITING:
  3337             return true;
  3338         default:
  3339             return false;
  3343     /**
  3344      * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
  3345      * this is a long press.
  3346      */
  3347     private void keyPressed() {
  3348         if (!isEnabled() || !isClickable()) {
  3349             return;
  3352         final Drawable selector = mSelector;
  3353         final Rect selectorRect = mSelectorRect;
  3355         if (selector != null && (isFocused() || touchModeDrawsInPressedState())
  3356                 && !selectorRect.isEmpty()) {
  3358             final View child = getChildAt(mSelectedPosition - mFirstPosition);
  3360             if (child != null) {
  3361                 if (child.hasFocusable()) {
  3362                     return;
  3365                 child.setPressed(true);
  3368             setPressed(true);
  3370             final boolean longClickable = isLongClickable();
  3371             final Drawable d = selector.getCurrent();
  3372             if (d != null && d instanceof TransitionDrawable) {
  3373                 if (longClickable) {
  3374                     ((TransitionDrawable) d).startTransition(
  3375                             ViewConfiguration.getLongPressTimeout());
  3376                 } else {
  3377                     ((TransitionDrawable) d).resetTransition();
  3381             if (longClickable && !mDataChanged) {
  3382                 if (mPendingCheckForKeyLongPress == null) {
  3383                     mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
  3386                 mPendingCheckForKeyLongPress.rememberWindowAttachCount();
  3387                 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
  3392     private void updateSelectorState() {
  3393         if (mSelector != null) {
  3394             if (shouldShowSelector()) {
  3395                 mSelector.setState(getDrawableState());
  3396             } else {
  3397                 mSelector.setState(STATE_NOTHING);
  3402     private void checkSelectionChanged() {
  3403         if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
  3404             selectionChanged();
  3405             mOldSelectedPosition = mSelectedPosition;
  3406             mOldSelectedRowId = mSelectedRowId;
  3410     private void selectionChanged() {
  3411         OnItemSelectedListener listener = getOnItemSelectedListener();
  3412         if (listener == null) {
  3413             return;
  3416         if (mInLayout || mBlockLayoutRequests) {
  3417             // If we are in a layout traversal, defer notification
  3418             // by posting. This ensures that the view tree is
  3419             // in a consistent state and is able to accommodate
  3420             // new layout or invalidate requests.
  3421             if (mSelectionNotifier == null) {
  3422                 mSelectionNotifier = new SelectionNotifier();
  3425             post(mSelectionNotifier);
  3426         } else {
  3427             fireOnSelected();
  3428             performAccessibilityActionsOnSelected();
  3432     private void fireOnSelected() {
  3433         OnItemSelectedListener listener = getOnItemSelectedListener();
  3434         if (listener == null) {
  3435             return;
  3438         final int selection = getSelectedItemPosition();
  3439         if (selection >= 0) {
  3440             View v = getSelectedView();
  3441             listener.onItemSelected(this, v, selection,
  3442                     mAdapter.getItemId(selection));
  3443         } else {
  3444             listener.onNothingSelected(this);
  3448     private void performAccessibilityActionsOnSelected() {
  3449         final int position = getSelectedItemPosition();
  3450         if (position >= 0) {
  3451             // We fire selection events here not in View
  3452             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
  3456     private int lookForSelectablePosition(int position) {
  3457         return lookForSelectablePosition(position, true);
  3460     private int lookForSelectablePosition(int position, boolean lookDown) {
  3461         final ListAdapter adapter = mAdapter;
  3462         if (adapter == null || isInTouchMode()) {
  3463             return INVALID_POSITION;
  3466         final int itemCount = mItemCount;
  3467         if (!mAreAllItemsSelectable) {
  3468             if (lookDown) {
  3469                 position = Math.max(0, position);
  3470                 while (position < itemCount && !adapter.isEnabled(position)) {
  3471                     position++;
  3473             } else {
  3474                 position = Math.min(position, itemCount - 1);
  3475                 while (position >= 0 && !adapter.isEnabled(position)) {
  3476                     position--;
  3480             if (position < 0 || position >= itemCount) {
  3481                 return INVALID_POSITION;
  3484             return position;
  3485         } else {
  3486             if (position < 0 || position >= itemCount) {
  3487                 return INVALID_POSITION;
  3490             return position;
  3494     /**
  3495      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
  3496      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
  3497      *        current view orientation.
  3499      * @return The position of the next selectable position of the views that
  3500      *         are currently visible, taking into account the fact that there might
  3501      *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
  3502      *         selectable view on screen in the given direction.
  3503      */
  3504     private int lookForSelectablePositionOnScreen(int direction) {
  3505         forceValidFocusDirection(direction);
  3507         final int firstPosition = mFirstPosition;
  3508         final ListAdapter adapter = getAdapter();
  3510         if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
  3511             int startPos = (mSelectedPosition != INVALID_POSITION ?
  3512                     mSelectedPosition + 1 : firstPosition);
  3514             if (startPos >= adapter.getCount()) {
  3515                 return INVALID_POSITION;
  3518             if (startPos < firstPosition) {
  3519                 startPos = firstPosition;
  3522             final int lastVisiblePos = getLastVisiblePosition();
  3524             for (int pos = startPos; pos <= lastVisiblePos; pos++) {
  3525                 if (adapter.isEnabled(pos)
  3526                         && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
  3527                     return pos;
  3530         } else {
  3531             final int last = firstPosition + getChildCount() - 1;
  3533             int startPos = (mSelectedPosition != INVALID_POSITION) ?
  3534                     mSelectedPosition - 1 : firstPosition + getChildCount() - 1;
  3536             if (startPos < 0 || startPos >= adapter.getCount()) {
  3537                 return INVALID_POSITION;
  3540             if (startPos > last) {
  3541                 startPos = last;
  3544             for (int pos = startPos; pos >= firstPosition; pos--) {
  3545                 if (adapter.isEnabled(pos)
  3546                         && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
  3547                     return pos;
  3552         return INVALID_POSITION;
  3555     @Override
  3556     protected void drawableStateChanged() {
  3557         super.drawableStateChanged();
  3558         updateSelectorState();
  3561     @Override
  3562     protected int[] onCreateDrawableState(int extraSpace) {
  3563         // If the child view is enabled then do the default behavior.
  3564         if (mIsChildViewEnabled) {
  3565             // Common case
  3566             return super.onCreateDrawableState(extraSpace);
  3569         // The selector uses this View's drawable state. The selected child view
  3570         // is disabled, so we need to remove the enabled state from the drawable
  3571         // states.
  3572         final int enabledState = ENABLED_STATE_SET[0];
  3574         // If we don't have any extra space, it will return one of the static state arrays,
  3575         // and clearing the enabled state on those arrays is a bad thing!  If we specify
  3576         // we need extra space, it will create+copy into a new array that safely mutable.
  3577         int[] state = super.onCreateDrawableState(extraSpace + 1);
  3578         int enabledPos = -1;
  3579         for (int i = state.length - 1; i >= 0; i--) {
  3580             if (state[i] == enabledState) {
  3581                 enabledPos = i;
  3582                 break;
  3586         // Remove the enabled state
  3587         if (enabledPos >= 0) {
  3588             System.arraycopy(state, enabledPos + 1, state, enabledPos,
  3589                     state.length - enabledPos - 1);
  3592         return state;
  3595     @Override
  3596     protected boolean canAnimate() {
  3597         return (super.canAnimate() && mItemCount > 0);
  3600     @Override
  3601     protected void dispatchDraw(Canvas canvas) {
  3602         final boolean drawSelectorOnTop = mDrawSelectorOnTop;
  3603         if (!drawSelectorOnTop) {
  3604             drawSelector(canvas);
  3607         super.dispatchDraw(canvas);
  3609         if (drawSelectorOnTop) {
  3610             drawSelector(canvas);
  3614     @Override
  3615     public void draw(Canvas canvas) {
  3616         super.draw(canvas);
  3618         boolean needsInvalidate = false;
  3620         if (mStartEdge != null) {
  3621             needsInvalidate |= drawStartEdge(canvas);
  3624         if (mEndEdge != null) {
  3625             needsInvalidate |= drawEndEdge(canvas);
  3628         if (needsInvalidate) {
  3629             ViewCompat.postInvalidateOnAnimation(this);
  3633     @Override
  3634     public void requestLayout() {
  3635         if (!mInLayout && !mBlockLayoutRequests) {
  3636             super.requestLayout();
  3640     @Override
  3641     public View getSelectedView() {
  3642         if (mItemCount > 0 && mSelectedPosition >= 0) {
  3643             return getChildAt(mSelectedPosition - mFirstPosition);
  3644         } else {
  3645             return null;
  3649     @Override
  3650     public void setSelection(int position) {
  3651         setSelectionFromOffset(position, 0);
  3654     public void setSelectionFromOffset(int position, int offset) {
  3655         if (mAdapter == null) {
  3656             return;
  3659         if (!isInTouchMode()) {
  3660             position = lookForSelectablePosition(position);
  3661             if (position >= 0) {
  3662                 setNextSelectedPositionInt(position);
  3664         } else {
  3665             mResurrectToPosition = position;
  3668         if (position >= 0) {
  3669             mLayoutMode = LAYOUT_SPECIFIC;
  3671             if (mIsVertical) {
  3672                 mSpecificStart = getPaddingTop() + offset;
  3673             } else {
  3674                 mSpecificStart = getPaddingLeft() + offset;
  3677             if (mNeedSync) {
  3678                 mSyncPosition = position;
  3679                 mSyncRowId = mAdapter.getItemId(position);
  3682             requestLayout();
  3686     @Override
  3687     public boolean dispatchKeyEvent(KeyEvent event) {
  3688         // Dispatch in the normal way
  3689         boolean handled = super.dispatchKeyEvent(event);
  3690         if (!handled) {
  3691             // If we didn't handle it...
  3692             final View focused = getFocusedChild();
  3693             if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
  3694                 // ... and our focused child didn't handle it
  3695                 // ... give it to ourselves so we can scroll if necessary
  3696                 handled = onKeyDown(event.getKeyCode(), event);
  3700         return handled;
  3703     @Override
  3704     protected void dispatchSetPressed(boolean pressed) {
  3705         // Don't dispatch setPressed to our children. We call setPressed on ourselves to
  3706         // get the selector in the right state, but we don't want to press each child.
  3709     @Override
  3710     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3711         if (mSelector == null) {
  3712             useDefaultSelector();
  3715         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  3716         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  3717         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  3718         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  3720         int childWidth = 0;
  3721         int childHeight = 0;
  3723         mItemCount = (mAdapter == null ? 0 : mAdapter.getCount());
  3724         if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
  3725                 heightMode == MeasureSpec.UNSPECIFIED)) {
  3726             final View child = obtainView(0, mIsScrap);
  3728             final int secondaryMeasureSpec =
  3729                     (mIsVertical ? widthMeasureSpec : heightMeasureSpec);
  3731             measureScrapChild(child, 0, secondaryMeasureSpec);
  3733             childWidth = child.getMeasuredWidth();
  3734             childHeight = child.getMeasuredHeight();
  3736             if (recycleOnMeasure()) {
  3737                 mRecycler.addScrapView(child, -1);
  3741         if (widthMode == MeasureSpec.UNSPECIFIED) {
  3742             widthSize = getPaddingLeft() + getPaddingRight() + childWidth;
  3743             if (mIsVertical) {
  3744                 widthSize += getVerticalScrollbarWidth();
  3748         if (heightMode == MeasureSpec.UNSPECIFIED) {
  3749             heightSize = getPaddingTop() + getPaddingBottom() + childHeight;
  3750             if (!mIsVertical) {
  3751                 heightSize += getHorizontalScrollbarHeight();
  3755         if (mIsVertical && heightMode == MeasureSpec.AT_MOST) {
  3756             heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
  3759         if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) {
  3760             widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1);
  3763         setMeasuredDimension(widthSize, heightSize);
  3766     @Override
  3767     protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3768         mInLayout = true;
  3770         if (changed) {
  3771             final int childCount = getChildCount();
  3772             for (int i = 0; i < childCount; i++) {
  3773                 getChildAt(i).forceLayout();
  3776             mRecycler.markChildrenDirty();
  3779         layoutChildren();
  3781         mInLayout = false;
  3783         final int width = r - l - getPaddingLeft() - getPaddingRight();
  3784         final int height = b - t - getPaddingTop() - getPaddingBottom();
  3786         if (mStartEdge != null && mEndEdge != null) {
  3787             if (mIsVertical) {
  3788                 mStartEdge.setSize(width, height);
  3789                 mEndEdge.setSize(width, height);
  3790             } else {
  3791                 mStartEdge.setSize(height, width);
  3792                 mEndEdge.setSize(height, width);
  3797     private void layoutChildren() {
  3798         if (getWidth() == 0 || getHeight() == 0) {
  3799             return;
  3802         final boolean blockLayoutRequests = mBlockLayoutRequests;
  3803         if (!blockLayoutRequests) {
  3804             mBlockLayoutRequests = true;
  3805         } else {
  3806             return;
  3809         try {
  3810             invalidate();
  3812             if (mAdapter == null) {
  3813                 resetState();
  3814                 return;
  3817             final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  3818             final int end =
  3819                     (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
  3821             int childCount = getChildCount();
  3822             int index = 0;
  3823             int delta = 0;
  3825             View focusLayoutRestoreView = null;
  3827             View selected = null;
  3828             View oldSelected = null;
  3829             View newSelected = null;
  3830             View oldFirstChild = null;
  3832             switch (mLayoutMode) {
  3833             case LAYOUT_SET_SELECTION:
  3834                 index = mNextSelectedPosition - mFirstPosition;
  3835                 if (index >= 0 && index < childCount) {
  3836                     newSelected = getChildAt(index);
  3839                 break;
  3841             case LAYOUT_FORCE_TOP:
  3842             case LAYOUT_FORCE_BOTTOM:
  3843             case LAYOUT_SPECIFIC:
  3844             case LAYOUT_SYNC:
  3845                 break;
  3847             case LAYOUT_MOVE_SELECTION:
  3848             default:
  3849                 // Remember the previously selected view
  3850                 index = mSelectedPosition - mFirstPosition;
  3851                 if (index >= 0 && index < childCount) {
  3852                     oldSelected = getChildAt(index);
  3855                 // Remember the previous first child
  3856                 oldFirstChild = getChildAt(0);
  3858                 if (mNextSelectedPosition >= 0) {
  3859                     delta = mNextSelectedPosition - mSelectedPosition;
  3862                 // Caution: newSelected might be null
  3863                 newSelected = getChildAt(index + delta);
  3866             final boolean dataChanged = mDataChanged;
  3867             if (dataChanged) {
  3868                 handleDataChanged();
  3871             // Handle the empty set by removing all views that are visible
  3872             // and calling it a day
  3873             if (mItemCount == 0) {
  3874                 resetState();
  3875                 return;
  3876             } else if (mItemCount != mAdapter.getCount()) {
  3877                 throw new IllegalStateException("The content of the adapter has changed but "
  3878                         + "TwoWayView did not receive a notification. Make sure the content of "
  3879                         + "your adapter is not modified from a background thread, but only "
  3880                         + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass()
  3881                         + ") with Adapter(" + mAdapter.getClass() + ")]");
  3884             setSelectedPositionInt(mNextSelectedPosition);
  3886             // Reset the focus restoration
  3887             View focusLayoutRestoreDirectChild = null;
  3889             // Pull all children into the RecycleBin.
  3890             // These views will be reused if possible
  3891             final int firstPosition = mFirstPosition;
  3892             final RecycleBin recycleBin = mRecycler;
  3894             if (dataChanged) {
  3895                 for (int i = 0; i < childCount; i++) {
  3896                     recycleBin.addScrapView(getChildAt(i), firstPosition + i);
  3898             } else {
  3899                 recycleBin.fillActiveViews(childCount, firstPosition);
  3902             // Take focus back to us temporarily to avoid the eventual
  3903             // call to clear focus when removing the focused child below
  3904             // from messing things up when ViewAncestor assigns focus back
  3905             // to someone else.
  3906             final View focusedChild = getFocusedChild();
  3907             if (focusedChild != null) {
  3908                 // We can remember the focused view to restore after relayout if the
  3909                 // data hasn't changed, or if the focused position is a header or footer.
  3910                 if (!dataChanged) {
  3911                     focusLayoutRestoreDirectChild = focusedChild;
  3913                     // Remember the specific view that had focus
  3914                     focusLayoutRestoreView = findFocus();
  3915                     if (focusLayoutRestoreView != null) {
  3916                         // Tell it we are going to mess with it
  3917                         focusLayoutRestoreView.onStartTemporaryDetach();
  3921                 requestFocus();
  3924             // FIXME: We need a way to save current accessibility focus here
  3925             // so that it can be restored after we re-attach the children on each
  3926             // layout round.
  3928             detachAllViewsFromParent();
  3930             switch (mLayoutMode) {
  3931             case LAYOUT_SET_SELECTION:
  3932                 if (newSelected != null) {
  3933                     final int newSelectedStart =
  3934                             (mIsVertical ? newSelected.getTop() : newSelected.getLeft());
  3936                     selected = fillFromSelection(newSelectedStart, start, end);
  3937                 } else {
  3938                     selected = fillFromMiddle(start, end);
  3941                 break;
  3943             case LAYOUT_SYNC:
  3944                 selected = fillSpecific(mSyncPosition, mSpecificStart);
  3945                 break;
  3947             case LAYOUT_FORCE_BOTTOM:
  3948                 selected = fillBefore(mItemCount - 1, end);
  3949                 adjustViewsStartOrEnd();
  3950                 break;
  3952             case LAYOUT_FORCE_TOP:
  3953                 mFirstPosition = 0;
  3954                 selected = fillFromOffset(start);
  3955                 adjustViewsStartOrEnd();
  3956                 break;
  3958             case LAYOUT_SPECIFIC:
  3959                 selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart);
  3960                 break;
  3962             case LAYOUT_MOVE_SELECTION:
  3963                 selected = moveSelection(oldSelected, newSelected, delta, start, end);
  3964                 break;
  3966             default:
  3967                 if (childCount == 0) {
  3968                     final int position = lookForSelectablePosition(0);
  3969                     setSelectedPositionInt(position);
  3970                     selected = fillFromOffset(start);
  3971                 } else {
  3972                     if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
  3973                         int offset = start;
  3974                         if (oldSelected != null) {
  3975                             offset = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft());
  3977                         selected = fillSpecific(mSelectedPosition, offset);
  3978                     } else if (mFirstPosition < mItemCount) {
  3979                         int offset = start;
  3980                         if (oldFirstChild != null) {
  3981                             offset = (mIsVertical ? oldFirstChild.getTop() : oldFirstChild.getLeft());
  3984                         selected = fillSpecific(mFirstPosition, offset);
  3985                     } else {
  3986                         selected = fillSpecific(0, start);
  3990                 break;
  3994             recycleBin.scrapActiveViews();
  3996             if (selected != null) {
  3997                 if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
  3998                     final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
  3999                             focusLayoutRestoreView != null &&
  4000                             focusLayoutRestoreView.requestFocus()) || selected.requestFocus();
  4002                     if (!focusWasTaken) {
  4003                         // Selected item didn't take focus, fine, but still want
  4004                         // to make sure something else outside of the selected view
  4005                         // has focus
  4006                         final View focused = getFocusedChild();
  4007                         if (focused != null) {
  4008                             focused.clearFocus();
  4011                         positionSelector(INVALID_POSITION, selected);
  4012                     } else {
  4013                         selected.setSelected(false);
  4014                         mSelectorRect.setEmpty();
  4016                 } else {
  4017                     positionSelector(INVALID_POSITION, selected);
  4020                 mSelectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  4021             } else {
  4022                 if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
  4023                     View child = getChildAt(mMotionPosition - mFirstPosition);
  4025                     if (child != null) {
  4026                         positionSelector(mMotionPosition, child);
  4028                 } else {
  4029                     mSelectedStart = 0;
  4030                     mSelectorRect.setEmpty();
  4033                 // Even if there is not selected position, we may need to restore
  4034                 // focus (i.e. something focusable in touch mode)
  4035                 if (hasFocus() && focusLayoutRestoreView != null) {
  4036                     focusLayoutRestoreView.requestFocus();
  4040             // Tell focus view we are done mucking with it, if it is still in
  4041             // our view hierarchy.
  4042             if (focusLayoutRestoreView != null
  4043                     && focusLayoutRestoreView.getWindowToken() != null) {
  4044                 focusLayoutRestoreView.onFinishTemporaryDetach();
  4047             mLayoutMode = LAYOUT_NORMAL;
  4048             mDataChanged = false;
  4049             mNeedSync = false;
  4051             setNextSelectedPositionInt(mSelectedPosition);
  4052             if (mItemCount > 0) {
  4053                 checkSelectionChanged();
  4056             invokeOnItemScrollListener();
  4057         } finally {
  4058             if (!blockLayoutRequests) {
  4059                 mBlockLayoutRequests = false;
  4060                 mDataChanged = false;
  4065     protected boolean recycleOnMeasure() {
  4066         return true;
  4069     private void offsetChildren(int offset) {
  4070         final int childCount = getChildCount();
  4072         for (int i = 0; i < childCount; i++) {
  4073             final View child = getChildAt(i);
  4075             if (mIsVertical) {
  4076                 child.offsetTopAndBottom(offset);
  4077             } else {
  4078                 child.offsetLeftAndRight(offset);
  4083     private View moveSelection(View oldSelected, View newSelected, int delta, int start,
  4084             int end) {
  4085         final int selectedPosition = mSelectedPosition;
  4087         final int oldSelectedStart = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft());
  4088         final int oldSelectedEnd = (mIsVertical ? oldSelected.getBottom() : oldSelected.getRight());
  4090         View selected = null;
  4092         if (delta > 0) {
  4093             /*
  4094              * Case 1: Scrolling down.
  4095              */
  4097             /*
  4098              *     Before           After
  4099              *    |       |        |       |
  4100              *    +-------+        +-------+
  4101              *    |   A   |        |   A   |
  4102              *    |   1   |   =>   +-------+
  4103              *    +-------+        |   B   |
  4104              *    |   B   |        |   2   |
  4105              *    +-------+        +-------+
  4106              *    |       |        |       |
  4108              *    Try to keep the top of the previously selected item where it was.
  4109              *    oldSelected = A
  4110              *    selected = B
  4111              */
  4113             // Put oldSelected (A) where it belongs
  4114             oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false);
  4116             final int itemMargin = mItemMargin;
  4118             // Now put the new selection (B) below that
  4119             selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);
  4121             final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  4122             final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
  4124             // Some of the newly selected item extends below the bottom of the list
  4125             if (selectedEnd > end) {
  4126                 // Find space available above the selection into which we can scroll upwards
  4127                 final int spaceBefore = selectedStart - start;
  4129                 // Find space required to bring the bottom of the selected item fully into view
  4130                 final int spaceAfter = selectedEnd - end;
  4132                 // Don't scroll more than half the size of the list
  4133                 final int halfSpace = (end - start) / 2;
  4134                 int offset = Math.min(spaceBefore, spaceAfter);
  4135                 offset = Math.min(offset, halfSpace);
  4137                 if (mIsVertical) {
  4138                     oldSelected.offsetTopAndBottom(-offset);
  4139                     selected.offsetTopAndBottom(-offset);
  4140                 } else {
  4141                     oldSelected.offsetLeftAndRight(-offset);
  4142                     selected.offsetLeftAndRight(-offset);
  4146             // Fill in views before and after
  4147             fillBefore(mSelectedPosition - 2, selectedStart - itemMargin);
  4148             adjustViewsStartOrEnd();
  4149             fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin);
  4150         } else if (delta < 0) {
  4151             /*
  4152              * Case 2: Scrolling up.
  4153              */
  4155             /*
  4156              *     Before           After
  4157              *    |       |        |       |
  4158              *    +-------+        +-------+
  4159              *    |   A   |        |   A   |
  4160              *    +-------+   =>   |   1   |
  4161              *    |   B   |        +-------+
  4162              *    |   2   |        |   B   |
  4163              *    +-------+        +-------+
  4164              *    |       |        |       |
  4166              *    Try to keep the top of the item about to become selected where it was.
  4167              *    newSelected = A
  4168              *    olSelected = B
  4169              */
  4171             if (newSelected != null) {
  4172                 // Try to position the top of newSel (A) where it was before it was selected
  4173                 final int newSelectedStart = (mIsVertical ? newSelected.getTop() : newSelected.getLeft());
  4174                 selected = makeAndAddView(selectedPosition, newSelectedStart, true, true);
  4175             } else {
  4176                 // If (A) was not on screen and so did not have a view, position
  4177                 // it above the oldSelected (B)
  4178                 selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
  4181             final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  4182             final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
  4184             // Some of the newly selected item extends above the top of the list
  4185             if (selectedStart < start) {
  4186                 // Find space required to bring the top of the selected item fully into view
  4187                 final int spaceBefore = start - selectedStart;
  4189                // Find space available below the selection into which we can scroll downwards
  4190                 final int spaceAfter = end - selectedEnd;
  4192                 // Don't scroll more than half the height of the list
  4193                 final int halfSpace = (end - start) / 2;
  4194                 int offset = Math.min(spaceBefore, spaceAfter);
  4195                 offset = Math.min(offset, halfSpace);
  4197                 if (mIsVertical) {
  4198                     selected.offsetTopAndBottom(offset);
  4199                 } else {
  4200                     selected.offsetLeftAndRight(offset);
  4204             // Fill in views above and below
  4205             fillBeforeAndAfter(selected, selectedPosition);
  4206         } else {
  4207             /*
  4208              * Case 3: Staying still
  4209              */
  4211             selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);
  4213             final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  4214             final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
  4216             // We're staying still...
  4217             if (oldSelectedStart < start) {
  4218                 // ... but the top of the old selection was off screen.
  4219                 // (This can happen if the data changes size out from under us)
  4220                 int newEnd = selectedEnd;
  4221                 if (newEnd < start + 20) {
  4222                     // Not enough visible -- bring it onscreen
  4223                     if (mIsVertical) {
  4224                         selected.offsetTopAndBottom(start - selectedStart);
  4225                     } else {
  4226                         selected.offsetLeftAndRight(start - selectedStart);
  4231             // Fill in views above and below
  4232             fillBeforeAndAfter(selected, selectedPosition);
  4235         return selected;
  4238     void confirmCheckedPositionsById() {
  4239         // Clear out the positional check states, we'll rebuild it below from IDs.
  4240         mCheckStates.clear();
  4242         for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
  4243             final long id = mCheckedIdStates.keyAt(checkedIndex);
  4244             final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
  4246             final long lastPosId = mAdapter.getItemId(lastPos);
  4247             if (id != lastPosId) {
  4248                 // Look around to see if the ID is nearby. If not, uncheck it.
  4249                 final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
  4250                 final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
  4251                 boolean found = false;
  4253                 for (int searchPos = start; searchPos < end; searchPos++) {
  4254                     final long searchId = mAdapter.getItemId(searchPos);
  4255                     if (id == searchId) {
  4256                         found = true;
  4257                         mCheckStates.put(searchPos, true);
  4258                         mCheckedIdStates.setValueAt(checkedIndex, searchPos);
  4259                         break;
  4263                 if (!found) {
  4264                     mCheckedIdStates.delete(id);
  4265                     checkedIndex--;
  4266                     mCheckedItemCount--;
  4268             } else {
  4269                 mCheckStates.put(lastPos, true);
  4274     private void handleDataChanged() {
  4275         if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mAdapter != null && mAdapter.hasStableIds()) {
  4276             confirmCheckedPositionsById();
  4279         mRecycler.clearTransientStateViews();
  4281         final int itemCount = mItemCount;
  4282         if (itemCount > 0) {
  4283             int newPos;
  4284             int selectablePos;
  4286             // Find the row we are supposed to sync to
  4287             if (mNeedSync) {
  4288                 // Update this first, since setNextSelectedPositionInt inspects it
  4289                 mNeedSync = false;
  4290                 mPendingSync = null;
  4292                 switch (mSyncMode) {
  4293                 case SYNC_SELECTED_POSITION:
  4294                     if (isInTouchMode()) {
  4295                         // We saved our state when not in touch mode. (We know this because
  4296                         // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
  4297                         // restore in touch mode. Just leave mSyncPosition as it is (possibly
  4298                         // adjusting if the available range changed) and return.
  4299                         mLayoutMode = LAYOUT_SYNC;
  4300                         mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
  4302                         return;
  4303                     } else {
  4304                         // See if we can find a position in the new data with the same
  4305                         // id as the old selection. This will change mSyncPosition.
  4306                         newPos = findSyncPosition();
  4307                         if (newPos >= 0) {
  4308                             // Found it. Now verify that new selection is still selectable
  4309                             selectablePos = lookForSelectablePosition(newPos, true);
  4310                             if (selectablePos == newPos) {
  4311                                 // Same row id is selected
  4312                                 mSyncPosition = newPos;
  4314                                 if (mSyncHeight == getHeight()) {
  4315                                     // If we are at the same height as when we saved state, try
  4316                                     // to restore the scroll position too.
  4317                                     mLayoutMode = LAYOUT_SYNC;
  4318                                 } else {
  4319                                     // We are not the same height as when the selection was saved, so
  4320                                     // don't try to restore the exact position
  4321                                     mLayoutMode = LAYOUT_SET_SELECTION;
  4324                                 // Restore selection
  4325                                 setNextSelectedPositionInt(newPos);
  4326                                 return;
  4330                     break;
  4332                 case SYNC_FIRST_POSITION:
  4333                     // Leave mSyncPosition as it is -- just pin to available range
  4334                     mLayoutMode = LAYOUT_SYNC;
  4335                     mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
  4337                     return;
  4341             if (!isInTouchMode()) {
  4342                 // We couldn't find matching data -- try to use the same position
  4343                 newPos = getSelectedItemPosition();
  4345                 // Pin position to the available range
  4346                 if (newPos >= itemCount) {
  4347                     newPos = itemCount - 1;
  4349                 if (newPos < 0) {
  4350                     newPos = 0;
  4353                 // Make sure we select something selectable -- first look down
  4354                 selectablePos = lookForSelectablePosition(newPos, true);
  4356                 if (selectablePos >= 0) {
  4357                     setNextSelectedPositionInt(selectablePos);
  4358                     return;
  4359                 } else {
  4360                     // Looking down didn't work -- try looking up
  4361                     selectablePos = lookForSelectablePosition(newPos, false);
  4362                     if (selectablePos >= 0) {
  4363                         setNextSelectedPositionInt(selectablePos);
  4364                         return;
  4367             } else {
  4368                 // We already know where we want to resurrect the selection
  4369                 if (mResurrectToPosition >= 0) {
  4370                     return;
  4375         // Nothing is selected. Give up and reset everything.
  4376         mLayoutMode = LAYOUT_FORCE_TOP;
  4377         mSelectedPosition = INVALID_POSITION;
  4378         mSelectedRowId = INVALID_ROW_ID;
  4379         mNextSelectedPosition = INVALID_POSITION;
  4380         mNextSelectedRowId = INVALID_ROW_ID;
  4381         mNeedSync = false;
  4382         mPendingSync = null;
  4383         mSelectorPosition = INVALID_POSITION;
  4385         checkSelectionChanged();
  4388     private int reconcileSelectedPosition() {
  4389         int position = mSelectedPosition;
  4390         if (position < 0) {
  4391             position = mResurrectToPosition;
  4394         position = Math.max(0, position);
  4395         position = Math.min(position, mItemCount - 1);
  4397         return position;
  4400     boolean resurrectSelection() {
  4401         final int childCount = getChildCount();
  4402         if (childCount <= 0) {
  4403             return false;
  4406         int selectedStart = 0;
  4407         int selectedPosition;
  4409         final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  4410         final int end =
  4411                 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
  4413         final int firstPosition = mFirstPosition;
  4414         final int toPosition = mResurrectToPosition;
  4415         boolean down = true;
  4417         if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
  4418             selectedPosition = toPosition;
  4420             final View selected = getChildAt(selectedPosition - mFirstPosition);
  4421             selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  4422         } else if (toPosition < firstPosition) {
  4423             // Default to selecting whatever is first
  4424             selectedPosition = firstPosition;
  4426             for (int i = 0; i < childCount; i++) {
  4427                 final View child = getChildAt(i);
  4428                 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
  4430                 if (i == 0) {
  4431                     // Remember the position of the first item
  4432                     selectedStart = childStart;
  4435                 if (childStart >= start) {
  4436                     // Found a view whose top is fully visible
  4437                     selectedPosition = firstPosition + i;
  4438                     selectedStart = childStart;
  4439                     break;
  4442         } else {
  4443             selectedPosition = firstPosition + childCount - 1;
  4444             down = false;
  4446             for (int i = childCount - 1; i >= 0; i--) {
  4447                 final View child = getChildAt(i);
  4448                 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
  4449                 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
  4451                 if (i == childCount - 1) {
  4452                     selectedStart = childStart;
  4455                 if (childEnd <= end) {
  4456                     selectedPosition = firstPosition + i;
  4457                     selectedStart = childStart;
  4458                     break;
  4463         mResurrectToPosition = INVALID_POSITION;
  4464         mTouchMode = TOUCH_MODE_REST;
  4465         reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
  4467         mSpecificStart = selectedStart;
  4469         selectedPosition = lookForSelectablePosition(selectedPosition, down);
  4470         if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) {
  4471             mLayoutMode = LAYOUT_SPECIFIC;
  4472             updateSelectorState();
  4473             setSelectionInt(selectedPosition);
  4474             invokeOnItemScrollListener();
  4475         } else {
  4476             selectedPosition = INVALID_POSITION;
  4479         return selectedPosition >= 0;
  4482     /**
  4483      * If there is a selection returns false.
  4484      * Otherwise resurrects the selection and returns true if resurrected.
  4485      */
  4486     boolean resurrectSelectionIfNeeded() {
  4487         if (mSelectedPosition < 0 && resurrectSelection()) {
  4488             updateSelectorState();
  4489             return true;
  4492         return false;
  4495     private int getChildWidthMeasureSpec(LayoutParams lp) {
  4496         if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) {
  4497             return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  4498         } else if (mIsVertical) {
  4499             final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight();
  4500             return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
  4501         } else {
  4502             return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
  4506     private int getChildHeightMeasureSpec(LayoutParams lp) {
  4507         if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) {
  4508             return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
  4509         } else if (!mIsVertical) {
  4510             final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom();
  4511             return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
  4512         } else {
  4513             return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
  4517     private void measureChild(View child) {
  4518         measureChild(child, (LayoutParams) child.getLayoutParams());
  4521     private void measureChild(View child, LayoutParams lp) {
  4522         final int widthSpec = getChildWidthMeasureSpec(lp);
  4523         final int heightSpec = getChildHeightMeasureSpec(lp);
  4524         child.measure(widthSpec, heightSpec);
  4527     private void relayoutMeasuredChild(View child) {
  4528         final int w = child.getMeasuredWidth();
  4529         final int h = child.getMeasuredHeight();
  4531         final int childLeft = getPaddingLeft();
  4532         final int childRight = childLeft + w;
  4533         final int childTop = child.getTop();
  4534         final int childBottom = childTop + h;
  4536         child.layout(childLeft, childTop, childRight, childBottom);
  4539     private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) {
  4540         LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams();
  4541         if (lp == null) {
  4542             lp = generateDefaultLayoutParams();
  4543             scrapChild.setLayoutParams(lp);
  4546         lp.viewType = mAdapter.getItemViewType(position);
  4547         lp.forceAdd = true;
  4549         final int widthMeasureSpec;
  4550         final int heightMeasureSpec;
  4551         if (mIsVertical) {
  4552             widthMeasureSpec = secondaryMeasureSpec;
  4553             heightMeasureSpec = getChildHeightMeasureSpec(lp);
  4554         } else {
  4555             widthMeasureSpec = getChildWidthMeasureSpec(lp);
  4556             heightMeasureSpec = secondaryMeasureSpec;
  4559         scrapChild.measure(widthMeasureSpec, heightMeasureSpec);
  4562     /**
  4563      * Measures the height of the given range of children (inclusive) and
  4564      * returns the height with this TwoWayView's padding and item margin heights
  4565      * included. If maxHeight is provided, the measuring will stop when the
  4566      * current height reaches maxHeight.
  4568      * @param widthMeasureSpec The width measure spec to be given to a child's
  4569      *            {@link View#measure(int, int)}.
  4570      * @param startPosition The position of the first child to be shown.
  4571      * @param endPosition The (inclusive) position of the last child to be
  4572      *            shown. Specify {@link #NO_POSITION} if the last child should be
  4573      *            the last available child from the adapter.
  4574      * @param maxHeight The maximum height that will be returned (if all the
  4575      *            children don't fit in this value, this value will be
  4576      *            returned).
  4577      * @param disallowPartialChildPosition In general, whether the returned
  4578      *            height should only contain entire children. This is more
  4579      *            powerful--it is the first inclusive position at which partial
  4580      *            children will not be allowed. Example: it looks nice to have
  4581      *            at least 3 completely visible children, and in portrait this
  4582      *            will most likely fit; but in landscape there could be times
  4583      *            when even 2 children can not be completely shown, so a value
  4584      *            of 2 (remember, inclusive) would be good (assuming
  4585      *            startPosition is 0).
  4586      * @return The height of this TwoWayView with the given children.
  4587      */
  4588     private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
  4589             final int maxHeight, int disallowPartialChildPosition) {
  4591         final int paddingTop = getPaddingTop();
  4592         final int paddingBottom = getPaddingBottom();
  4594         final ListAdapter adapter = mAdapter;
  4595         if (adapter == null) {
  4596             return paddingTop + paddingBottom;
  4599         // Include the padding of the list
  4600         int returnedHeight = paddingTop + paddingBottom;
  4601         final int itemMargin = mItemMargin;
  4603         // The previous height value that was less than maxHeight and contained
  4604         // no partial children
  4605         int prevHeightWithoutPartialChild = 0;
  4606         int i;
  4607         View child;
  4609         // mItemCount - 1 since endPosition parameter is inclusive
  4610         endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
  4611         final RecycleBin recycleBin = mRecycler;
  4612         final boolean shouldRecycle = recycleOnMeasure();
  4613         final boolean[] isScrap = mIsScrap;
  4615         for (i = startPosition; i <= endPosition; ++i) {
  4616             child = obtainView(i, isScrap);
  4618             measureScrapChild(child, i, widthMeasureSpec);
  4620             if (i > 0) {
  4621                 // Count the item margin for all but one child
  4622                 returnedHeight += itemMargin;
  4625             // Recycle the view before we possibly return from the method
  4626             if (shouldRecycle) {
  4627                 recycleBin.addScrapView(child, -1);
  4630             returnedHeight += child.getMeasuredHeight();
  4632             if (returnedHeight >= maxHeight) {
  4633                 // We went over, figure out which height to return.  If returnedHeight > maxHeight,
  4634                 // then the i'th position did not fit completely.
  4635                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
  4636                             && (i > disallowPartialChildPosition) // We've past the min pos
  4637                             && (prevHeightWithoutPartialChild > 0) // We have a prev height
  4638                             && (returnedHeight != maxHeight) // i'th child did not fit completely
  4639                         ? prevHeightWithoutPartialChild
  4640                         : maxHeight;
  4643             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
  4644                 prevHeightWithoutPartialChild = returnedHeight;
  4648         // At this point, we went through the range of children, and they each
  4649         // completely fit, so return the returnedHeight
  4650         return returnedHeight;
  4653     /**
  4654      * Measures the width of the given range of children (inclusive) and
  4655      * returns the width with this TwoWayView's padding and item margin widths
  4656      * included. If maxWidth is provided, the measuring will stop when the
  4657      * current width reaches maxWidth.
  4659      * @param heightMeasureSpec The height measure spec to be given to a child's
  4660      *            {@link View#measure(int, int)}.
  4661      * @param startPosition The position of the first child to be shown.
  4662      * @param endPosition The (inclusive) position of the last child to be
  4663      *            shown. Specify {@link #NO_POSITION} if the last child should be
  4664      *            the last available child from the adapter.
  4665      * @param maxWidth The maximum width that will be returned (if all the
  4666      *            children don't fit in this value, this value will be
  4667      *            returned).
  4668      * @param disallowPartialChildPosition In general, whether the returned
  4669      *            width should only contain entire children. This is more
  4670      *            powerful--it is the first inclusive position at which partial
  4671      *            children will not be allowed. Example: it looks nice to have
  4672      *            at least 3 completely visible children, and in portrait this
  4673      *            will most likely fit; but in landscape there could be times
  4674      *            when even 2 children can not be completely shown, so a value
  4675      *            of 2 (remember, inclusive) would be good (assuming
  4676      *            startPosition is 0).
  4677      * @return The width of this TwoWayView with the given children.
  4678      */
  4679     private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition,
  4680             final int maxWidth, int disallowPartialChildPosition) {
  4682         final int paddingLeft = getPaddingLeft();
  4683         final int paddingRight = getPaddingRight();
  4685         final ListAdapter adapter = mAdapter;
  4686         if (adapter == null) {
  4687             return paddingLeft + paddingRight;
  4690         // Include the padding of the list
  4691         int returnedWidth = paddingLeft + paddingRight;
  4692         final int itemMargin = mItemMargin;
  4694         // The previous height value that was less than maxHeight and contained
  4695         // no partial children
  4696         int prevWidthWithoutPartialChild = 0;
  4697         int i;
  4698         View child;
  4700         // mItemCount - 1 since endPosition parameter is inclusive
  4701         endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
  4702         final RecycleBin recycleBin = mRecycler;
  4703         final boolean shouldRecycle = recycleOnMeasure();
  4704         final boolean[] isScrap = mIsScrap;
  4706         for (i = startPosition; i <= endPosition; ++i) {
  4707             child = obtainView(i, isScrap);
  4709             measureScrapChild(child, i, heightMeasureSpec);
  4711             if (i > 0) {
  4712                 // Count the item margin for all but one child
  4713                 returnedWidth += itemMargin;
  4716             // Recycle the view before we possibly return from the method
  4717             if (shouldRecycle) {
  4718                 recycleBin.addScrapView(child, -1);
  4721             returnedWidth += child.getMeasuredHeight();
  4723             if (returnedWidth >= maxWidth) {
  4724                 // We went over, figure out which width to return.  If returnedWidth > maxWidth,
  4725                 // then the i'th position did not fit completely.
  4726                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
  4727                             && (i > disallowPartialChildPosition) // We've past the min pos
  4728                             && (prevWidthWithoutPartialChild > 0) // We have a prev width
  4729                             && (returnedWidth != maxWidth) // i'th child did not fit completely
  4730                         ? prevWidthWithoutPartialChild
  4731                         : maxWidth;
  4734             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
  4735                 prevWidthWithoutPartialChild = returnedWidth;
  4739         // At this point, we went through the range of children, and they each
  4740         // completely fit, so return the returnedWidth
  4741         return returnedWidth;
  4744     private View makeAndAddView(int position, int offset, boolean flow, boolean selected) {
  4745         final int top;
  4746         final int left;
  4748         if (mIsVertical) {
  4749             top = offset;
  4750             left = getPaddingLeft();
  4751         } else {
  4752             top = getPaddingTop();
  4753             left = offset;
  4756         if (!mDataChanged) {
  4757             // Try to use an existing view for this position
  4758             final View activeChild = mRecycler.getActiveView(position);
  4759             if (activeChild != null) {
  4760                 // Found it -- we're using an existing child
  4761                 // This just needs to be positioned
  4762                 setupChild(activeChild, position, top, left, flow, selected, true);
  4764                 return activeChild;
  4768         // Make a new view for this position, or convert an unused view if possible
  4769         final View child = obtainView(position, mIsScrap);
  4771         // This needs to be positioned and measured
  4772         setupChild(child, position, top, left, flow, selected, mIsScrap[0]);
  4774         return child;
  4777     @TargetApi(11)
  4778     private void setupChild(View child, int position, int top, int left,
  4779             boolean flow, boolean selected, boolean recycled) {
  4780         final boolean isSelected = selected && shouldShowSelector();
  4781         final boolean updateChildSelected = isSelected != child.isSelected();
  4782         final int touchMode = mTouchMode;
  4784         final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING &&
  4785                 mMotionPosition == position;
  4787         final boolean updateChildPressed = isPressed != child.isPressed();
  4788         final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
  4790         // Respect layout params that are already in the view. Otherwise make some up...
  4791         LayoutParams lp = (LayoutParams) child.getLayoutParams();
  4792         if (lp == null) {
  4793             lp = generateDefaultLayoutParams();
  4796         lp.viewType = mAdapter.getItemViewType(position);
  4798         if (recycled && !lp.forceAdd) {
  4799             attachViewToParent(child, (flow ? -1 : 0), lp);
  4800         } else {
  4801             lp.forceAdd = false;
  4802             addViewInLayout(child, (flow ? -1 : 0), lp, true);
  4805         if (updateChildSelected) {
  4806             child.setSelected(isSelected);
  4809         if (updateChildPressed) {
  4810             child.setPressed(isPressed);
  4813         if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mCheckStates != null) {
  4814             if (child instanceof Checkable) {
  4815                 ((Checkable) child).setChecked(mCheckStates.get(position));
  4816             } else if (getContext().getApplicationInfo().targetSdkVersion
  4817                     >= Build.VERSION_CODES.HONEYCOMB) {
  4818                 child.setActivated(mCheckStates.get(position));
  4822         if (needToMeasure) {
  4823             measureChild(child, lp);
  4824         } else {
  4825             cleanupLayoutState(child);
  4828         final int w = child.getMeasuredWidth();
  4829         final int h = child.getMeasuredHeight();
  4831         final int childTop = (mIsVertical && !flow ? top - h : top);
  4832         final int childLeft = (!mIsVertical && !flow ? left - w : left);
  4834         if (needToMeasure) {
  4835             final int childRight = childLeft + w;
  4836             final int childBottom = childTop + h;
  4838             child.layout(childLeft, childTop, childRight, childBottom);
  4839         } else {
  4840             child.offsetLeftAndRight(childLeft - child.getLeft());
  4841             child.offsetTopAndBottom(childTop - child.getTop());
  4845     void fillGap(boolean down) {
  4846         final int childCount = getChildCount();
  4848         if (down) {
  4849             final int paddingStart = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  4851             final int lastEnd;
  4852             if (mIsVertical) {
  4853                 lastEnd = getChildAt(childCount - 1).getBottom();
  4854             } else {
  4855                 lastEnd = getChildAt(childCount - 1).getRight();
  4858             final int offset = (childCount > 0 ? lastEnd + mItemMargin : paddingStart);
  4859             fillAfter(mFirstPosition + childCount, offset);
  4860             correctTooHigh(getChildCount());
  4861         } else {
  4862             final int end;
  4863             final int firstStart;
  4865             if (mIsVertical) {
  4866                 end = getHeight() - getPaddingBottom();
  4867                 firstStart = getChildAt(0).getTop();
  4868             } else {
  4869                 end = getWidth() - getPaddingRight();
  4870                 firstStart = getChildAt(0).getLeft();
  4873             final int offset = (childCount > 0 ? firstStart - mItemMargin : end);
  4874             fillBefore(mFirstPosition - 1, offset);
  4875             correctTooLow(getChildCount());
  4879     private View fillBefore(int pos, int nextOffset) {
  4880         View selectedView = null;
  4882         final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  4884         while (nextOffset > start && pos >= 0) {
  4885             boolean isSelected = (pos == mSelectedPosition);
  4886             View child = makeAndAddView(pos, nextOffset, false, isSelected);
  4888             if (mIsVertical) {
  4889                 nextOffset = child.getTop() - mItemMargin;
  4890             } else {
  4891                 nextOffset = child.getLeft() - mItemMargin;
  4894             if (isSelected) {
  4895                 selectedView = child;
  4898             pos--;
  4901         mFirstPosition = pos + 1;
  4903         return selectedView;
  4906     private View fillAfter(int pos, int nextOffset) {
  4907         View selectedView = null;
  4909         final int end =
  4910                 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
  4912         while (nextOffset < end && pos < mItemCount) {
  4913             boolean selected = (pos == mSelectedPosition);
  4915             View child = makeAndAddView(pos, nextOffset, true, selected);
  4917             if (mIsVertical) {
  4918                 nextOffset = child.getBottom() + mItemMargin;
  4919             } else {
  4920                 nextOffset = child.getRight() + mItemMargin;
  4923             if (selected) {
  4924                 selectedView = child;
  4927             pos++;
  4930         return selectedView;
  4933     private View fillSpecific(int position, int offset) {
  4934         final boolean tempIsSelected = (position == mSelectedPosition);
  4935         View temp = makeAndAddView(position, offset, true, tempIsSelected);
  4937         // Possibly changed again in fillBefore if we add rows above this one.
  4938         mFirstPosition = position;
  4940         final int itemMargin = mItemMargin;
  4942         final int offsetBefore;
  4943         if (mIsVertical) {
  4944             offsetBefore = temp.getTop() - itemMargin;
  4945         } else {
  4946             offsetBefore = temp.getLeft() - itemMargin;
  4948         final View before = fillBefore(position - 1, offsetBefore);
  4950         // This will correct for the top of the first view not touching the top of the list
  4951         adjustViewsStartOrEnd();
  4953         final int offsetAfter;
  4954         if (mIsVertical) {
  4955             offsetAfter = temp.getBottom() + itemMargin;
  4956         } else {
  4957             offsetAfter = temp.getRight() + itemMargin;
  4959         final View after = fillAfter(position + 1, offsetAfter);
  4961         final int childCount = getChildCount();
  4962         if (childCount > 0) {
  4963             correctTooHigh(childCount);
  4966         if (tempIsSelected) {
  4967             return temp;
  4968         } else if (before != null) {
  4969             return before;
  4970         } else {
  4971             return after;
  4975     private View fillFromOffset(int nextOffset) {
  4976         mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
  4977         mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
  4979         if (mFirstPosition < 0) {
  4980             mFirstPosition = 0;
  4983         return fillAfter(mFirstPosition, nextOffset);
  4986     private View fillFromMiddle(int start, int end) {
  4987         final int size = end - start;
  4988         int position = reconcileSelectedPosition();
  4990         View selected = makeAndAddView(position, start, true, true);
  4991         mFirstPosition = position;
  4993         if (mIsVertical) {
  4994             int selectedHeight = selected.getMeasuredHeight();
  4995             if (selectedHeight <= size) {
  4996                 selected.offsetTopAndBottom((size - selectedHeight) / 2);
  4998         } else {
  4999             int selectedWidth = selected.getMeasuredWidth();
  5000             if (selectedWidth <= size) {
  5001                 selected.offsetLeftAndRight((size - selectedWidth) / 2);
  5005         fillBeforeAndAfter(selected, position);
  5006         correctTooHigh(getChildCount());
  5008         return selected;
  5011     private void fillBeforeAndAfter(View selected, int position) {
  5012         final int itemMargin = mItemMargin;
  5014         final int offsetBefore;
  5015         if (mIsVertical) {
  5016             offsetBefore = selected.getTop() - itemMargin;
  5017         } else {
  5018             offsetBefore = selected.getLeft() - itemMargin;
  5021         fillBefore(position - 1, offsetBefore);
  5023         adjustViewsStartOrEnd();
  5025         final int offsetAfter;
  5026         if (mIsVertical) {
  5027             offsetAfter = selected.getBottom() + itemMargin;
  5028         } else {
  5029             offsetAfter = selected.getRight() + itemMargin;
  5032         fillAfter(position + 1, offsetAfter);
  5035     private View fillFromSelection(int selectedTop, int start, int end) {
  5036         final int selectedPosition = mSelectedPosition;
  5037         View selected;
  5039         selected = makeAndAddView(selectedPosition, selectedTop, true, true);
  5041         final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
  5042         final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
  5044         // Some of the newly selected item extends below the bottom of the list
  5045         if (selectedEnd > end) {
  5046             // Find space available above the selection into which we can scroll
  5047             // upwards
  5048             final int spaceAbove = selectedStart - start;
  5050             // Find space required to bring the bottom of the selected item
  5051             // fully into view
  5052             final int spaceBelow = selectedEnd - end;
  5054             final int offset = Math.min(spaceAbove, spaceBelow);
  5056             // Now offset the selected item to get it into view
  5057             selected.offsetTopAndBottom(-offset);
  5058         } else if (selectedStart < start) {
  5059             // Find space required to bring the top of the selected item fully
  5060             // into view
  5061             final int spaceAbove = start - selectedStart;
  5063             // Find space available below the selection into which we can scroll
  5064             // downwards
  5065             final int spaceBelow = end - selectedEnd;
  5067             final int offset = Math.min(spaceAbove, spaceBelow);
  5069             // Offset the selected item to get it into view
  5070             selected.offsetTopAndBottom(offset);
  5073         // Fill in views above and below
  5074         fillBeforeAndAfter(selected, selectedPosition);
  5075         correctTooHigh(getChildCount());
  5077         return selected;
  5080     private void correctTooHigh(int childCount) {
  5081         // First see if the last item is visible. If it is not, it is OK for the
  5082         // top of the list to be pushed up.
  5083         final int lastPosition = mFirstPosition + childCount - 1;
  5084         if (lastPosition != mItemCount - 1 || childCount == 0) {
  5085             return;
  5088         // Get the last child ...
  5089         final View lastChild = getChildAt(childCount - 1);
  5091         // ... and its end edge
  5092         final int lastEnd;
  5093         if (mIsVertical) {
  5094             lastEnd = lastChild.getBottom();
  5095         } else {
  5096             lastEnd = lastChild.getRight();
  5099         // This is bottom of our drawable area
  5100         final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  5101         final int end =
  5102                 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
  5104         // This is how far the end edge of the last view is from the end of the
  5105         // drawable area
  5106         int endOffset = end - lastEnd;
  5108         View firstChild = getChildAt(0);
  5109         int firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
  5111         // Make sure we are 1) Too high, and 2) Either there are more rows above the
  5112         // first row or the first row is scrolled off the top of the drawable area
  5113         if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start))  {
  5114             if (mFirstPosition == 0) {
  5115                 // Don't pull the top too far down
  5116                 endOffset = Math.min(endOffset, start - firstStart);
  5119             // Move everything down
  5120             offsetChildren(endOffset);
  5122             if (mFirstPosition > 0) {
  5123                 firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
  5125                 // Fill the gap that was opened above mFirstPosition with more rows, if
  5126                 // possible
  5127                 fillBefore(mFirstPosition - 1, firstStart - mItemMargin);
  5129                 // Close up the remaining gap
  5130                 adjustViewsStartOrEnd();
  5135     private void correctTooLow(int childCount) {
  5136         // First see if the first item is visible. If it is not, it is OK for the
  5137         // bottom of the list to be pushed down.
  5138         if (mFirstPosition != 0 || childCount == 0) {
  5139             return;
  5142         final View first = getChildAt(0);
  5143         final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
  5145         final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
  5147         final int end;
  5148         if (mIsVertical) {
  5149             end = getHeight() - getPaddingBottom();
  5150         } else {
  5151             end = getWidth() - getPaddingRight();
  5154         // This is how far the start edge of the first view is from the start of the
  5155         // drawable area
  5156         int startOffset = firstStart - start;
  5158         View last = getChildAt(childCount - 1);
  5159         int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
  5161         int lastPosition = mFirstPosition + childCount - 1;
  5163         // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the
  5164         // last column/row or the last column/row is scrolled off the end of the
  5165         // drawable area
  5166         if (startOffset > 0) {
  5167             if (lastPosition < mItemCount - 1 || lastEnd > end)  {
  5168                 if (lastPosition == mItemCount - 1) {
  5169                     // Don't pull the bottom too far up
  5170                     startOffset = Math.min(startOffset, lastEnd - end);
  5173                 // Move everything up
  5174                 offsetChildren(-startOffset);
  5176                 if (lastPosition < mItemCount - 1) {
  5177                     lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
  5179                     // Fill the gap that was opened below the last position with more rows, if
  5180                     // possible
  5181                     fillAfter(lastPosition + 1, lastEnd + mItemMargin);
  5183                     // Close up the remaining gap
  5184                     adjustViewsStartOrEnd();
  5186             } else if (lastPosition == mItemCount - 1) {
  5187                 adjustViewsStartOrEnd();
  5192     private void adjustViewsStartOrEnd() {
  5193         if (getChildCount() == 0) {
  5194             return;
  5197         final View firstChild = getChildAt(0);
  5199         int delta;
  5200         if (mIsVertical) {
  5201             delta = firstChild.getTop() - getPaddingTop() - mItemMargin;
  5202         } else {
  5203             delta = firstChild.getLeft() - getPaddingLeft() - mItemMargin;
  5206         if (delta < 0) {
  5207             // We only are looking to see if we are too low, not too high
  5208             delta = 0;
  5211         if (delta != 0) {
  5212             offsetChildren(-delta);
  5216     @TargetApi(14)
  5217     private SparseBooleanArray cloneCheckStates() {
  5218         if (mCheckStates == null) {
  5219             return null;
  5222         SparseBooleanArray checkedStates;
  5224         if (Build.VERSION.SDK_INT >= 14) {
  5225             checkedStates = mCheckStates.clone();
  5226         } else {
  5227             checkedStates = new SparseBooleanArray();
  5229             for (int i = 0; i < mCheckStates.size(); i++) {
  5230                 checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
  5234         return checkedStates;
  5237     private int findSyncPosition() {
  5238         int itemCount = mItemCount;
  5240         if (itemCount == 0) {
  5241             return INVALID_POSITION;
  5244         final long idToMatch = mSyncRowId;
  5246         // If there isn't a selection don't hunt for it
  5247         if (idToMatch == INVALID_ROW_ID) {
  5248             return INVALID_POSITION;
  5251         // Pin seed to reasonable values
  5252         int seed = mSyncPosition;
  5253         seed = Math.max(0, seed);
  5254         seed = Math.min(itemCount - 1, seed);
  5256         long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
  5258         long rowId;
  5260         // first position scanned so far
  5261         int first = seed;
  5263         // last position scanned so far
  5264         int last = seed;
  5266         // True if we should move down on the next iteration
  5267         boolean next = false;
  5269         // True when we have looked at the first item in the data
  5270         boolean hitFirst;
  5272         // True when we have looked at the last item in the data
  5273         boolean hitLast;
  5275         // Get the item ID locally (instead of getItemIdAtPosition), so
  5276         // we need the adapter
  5277         final ListAdapter adapter = mAdapter;
  5278         if (adapter == null) {
  5279             return INVALID_POSITION;
  5282         while (SystemClock.uptimeMillis() <= endTime) {
  5283             rowId = adapter.getItemId(seed);
  5284             if (rowId == idToMatch) {
  5285                 // Found it!
  5286                 return seed;
  5289             hitLast = (last == itemCount - 1);
  5290             hitFirst = (first == 0);
  5292             if (hitLast && hitFirst) {
  5293                 // Looked at everything
  5294                 break;
  5297             if (hitFirst || (next && !hitLast)) {
  5298                 // Either we hit the top, or we are trying to move down
  5299                 last++;
  5300                 seed = last;
  5302                 // Try going up next time
  5303                 next = false;
  5304             } else if (hitLast || (!next && !hitFirst)) {
  5305                 // Either we hit the bottom, or we are trying to move up
  5306                 first--;
  5307                 seed = first;
  5309                 // Try going down next time
  5310                 next = true;
  5314         return INVALID_POSITION;
  5317     @TargetApi(16)
  5318     private View obtainView(int position, boolean[] isScrap) {
  5319         isScrap[0] = false;
  5321         View scrapView = mRecycler.getTransientStateView(position);
  5322         if (scrapView != null) {
  5323             return scrapView;
  5326         scrapView = mRecycler.getScrapView(position);
  5328         final View child;
  5329         if (scrapView != null) {
  5330             child = mAdapter.getView(position, scrapView, this);
  5332             if (child != scrapView) {
  5333                 mRecycler.addScrapView(scrapView, position);
  5334             } else {
  5335                 isScrap[0] = true;
  5337         } else {
  5338             child = mAdapter.getView(position, null, this);
  5341         if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
  5342             ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
  5345         if (mHasStableIds) {
  5346             LayoutParams lp = (LayoutParams) child.getLayoutParams();
  5348             if (lp == null) {
  5349                 lp = generateDefaultLayoutParams();
  5350             } else if (!checkLayoutParams(lp)) {
  5351                 lp = generateLayoutParams(lp);
  5354             lp.id = mAdapter.getItemId(position);
  5356             child.setLayoutParams(lp);
  5359         if (mAccessibilityDelegate == null) {
  5360             mAccessibilityDelegate = new ListItemAccessibilityDelegate();
  5363         ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);
  5365         return child;
  5368     void resetState() {
  5369         removeAllViewsInLayout();
  5371         mSelectedStart = 0;
  5372         mFirstPosition = 0;
  5373         mDataChanged = false;
  5374         mNeedSync = false;
  5375         mPendingSync = null;
  5376         mOldSelectedPosition = INVALID_POSITION;
  5377         mOldSelectedRowId = INVALID_ROW_ID;
  5379         mOverScroll = 0;
  5381         setSelectedPositionInt(INVALID_POSITION);
  5382         setNextSelectedPositionInt(INVALID_POSITION);
  5384         mSelectorPosition = INVALID_POSITION;
  5385         mSelectorRect.setEmpty();
  5387         invalidate();
  5390     private void rememberSyncState() {
  5391         if (getChildCount() == 0) {
  5392             return;
  5395         mNeedSync = true;
  5397         if (mSelectedPosition >= 0) {
  5398             View child = getChildAt(mSelectedPosition - mFirstPosition);
  5400             mSyncRowId = mNextSelectedRowId;
  5401             mSyncPosition = mNextSelectedPosition;
  5403             if (child != null) {
  5404                 mSpecificStart = (mIsVertical ? child.getTop() : child.getLeft());
  5407             mSyncMode = SYNC_SELECTED_POSITION;
  5408         } else {
  5409             // Sync the based on the offset of the first view
  5410             View child = getChildAt(0);
  5411             ListAdapter adapter = getAdapter();
  5413             if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
  5414                 mSyncRowId = adapter.getItemId(mFirstPosition);
  5415             } else {
  5416                 mSyncRowId = NO_ID;
  5419             mSyncPosition = mFirstPosition;
  5421             if (child != null) {
  5422                 mSpecificStart = child.getTop();
  5425             mSyncMode = SYNC_FIRST_POSITION;
  5429     private ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
  5430         return new AdapterContextMenuInfo(view, position, id);
  5433     @TargetApi(11)
  5434     private void updateOnScreenCheckedViews() {
  5435         final int firstPos = mFirstPosition;
  5436         final int count = getChildCount();
  5438         final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
  5439                 >= Build.VERSION_CODES.HONEYCOMB;
  5441         for (int i = 0; i < count; i++) {
  5442             final View child = getChildAt(i);
  5443             final int position = firstPos + i;
  5445             if (child instanceof Checkable) {
  5446                 ((Checkable) child).setChecked(mCheckStates.get(position));
  5447             } else if (useActivated) {
  5448                 child.setActivated(mCheckStates.get(position));
  5453     @Override
  5454     public boolean performItemClick(View view, int position, long id) {
  5455         boolean checkedStateChanged = false;
  5457         if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
  5458             boolean checked = !mCheckStates.get(position, false);
  5459             mCheckStates.put(position, checked);
  5461             if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
  5462                 if (checked) {
  5463                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
  5464                 } else {
  5465                     mCheckedIdStates.delete(mAdapter.getItemId(position));
  5469             if (checked) {
  5470                 mCheckedItemCount++;
  5471             } else {
  5472                 mCheckedItemCount--;
  5475             checkedStateChanged = true;
  5476         } else if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0) {
  5477             boolean checked = !mCheckStates.get(position, false);
  5478             if (checked) {
  5479                 mCheckStates.clear();
  5480                 mCheckStates.put(position, true);
  5482                 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
  5483                     mCheckedIdStates.clear();
  5484                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
  5487                 mCheckedItemCount = 1;
  5488             } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
  5489                 mCheckedItemCount = 0;
  5492             checkedStateChanged = true;
  5495         if (checkedStateChanged) {
  5496             updateOnScreenCheckedViews();
  5499         return super.performItemClick(view, position, id);
  5502     private boolean performLongPress(final View child,
  5503             final int longPressPosition, final long longPressId) {
  5504         // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
  5505         boolean handled = false;
  5507         OnItemLongClickListener listener = getOnItemLongClickListener();
  5508         if (listener != null) {
  5509             handled = listener.onItemLongClick(TwoWayView.this, child,
  5510                     longPressPosition, longPressId);
  5513         if (!handled) {
  5514             mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
  5515             handled = super.showContextMenuForChild(TwoWayView.this);
  5518         if (handled) {
  5519             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
  5522         return handled;
  5525     @Override
  5526     protected LayoutParams generateDefaultLayoutParams() {
  5527         if (mIsVertical) {
  5528             return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
  5529         } else {
  5530             return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
  5534     @Override
  5535     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
  5536         return new LayoutParams(lp);
  5539     @Override
  5540     protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
  5541         return lp instanceof LayoutParams;
  5544     @Override
  5545     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
  5546         return new LayoutParams(getContext(), attrs);
  5549     @Override
  5550     protected ContextMenuInfo getContextMenuInfo() {
  5551         return mContextMenuInfo;
  5554     @Override
  5555     public Parcelable onSaveInstanceState() {
  5556         Parcelable superState = super.onSaveInstanceState();
  5557         SavedState ss = new SavedState(superState);
  5559         if (mPendingSync != null) {
  5560             ss.selectedId = mPendingSync.selectedId;
  5561             ss.firstId = mPendingSync.firstId;
  5562             ss.viewStart = mPendingSync.viewStart;
  5563             ss.position = mPendingSync.position;
  5564             ss.height = mPendingSync.height;
  5566             return ss;
  5569         boolean haveChildren = (getChildCount() > 0 && mItemCount > 0);
  5570         long selectedId = getSelectedItemId();
  5571         ss.selectedId = selectedId;
  5572         ss.height = getHeight();
  5574         if (selectedId >= 0) {
  5575             ss.viewStart = mSelectedStart;
  5576             ss.position = getSelectedItemPosition();
  5577             ss.firstId = INVALID_POSITION;
  5578         } else if (haveChildren && mFirstPosition > 0) {
  5579             // Remember the position of the first child.
  5580             // We only do this if we are not currently at the top of
  5581             // the list, for two reasons:
  5582             //
  5583             // (1) The list may be in the process of becoming empty, in
  5584             // which case mItemCount may not be 0, but if we try to
  5585             // ask for any information about position 0 we will crash.
  5586             //
  5587             // (2) Being "at the top" seems like a special case, anyway,
  5588             // and the user wouldn't expect to end up somewhere else when
  5589             // they revisit the list even if its content has changed.
  5591             View child = getChildAt(0);
  5592             ss.viewStart = (mIsVertical ? child.getTop() : child.getLeft());
  5594             int firstPos = mFirstPosition;
  5595             if (firstPos >= mItemCount) {
  5596                 firstPos = mItemCount - 1;
  5599             ss.position = firstPos;
  5600             ss.firstId = mAdapter.getItemId(firstPos);
  5601         } else {
  5602             ss.viewStart = 0;
  5603             ss.firstId = INVALID_POSITION;
  5604             ss.position = 0;
  5607         if (mCheckStates != null) {
  5608             ss.checkState = cloneCheckStates();
  5611         if (mCheckedIdStates != null) {
  5612             final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
  5614             final int count = mCheckedIdStates.size();
  5615             for (int i = 0; i < count; i++) {
  5616                 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
  5619             ss.checkIdState = idState;
  5622         ss.checkedItemCount = mCheckedItemCount;
  5624         return ss;
  5627     @Override
  5628     public void onRestoreInstanceState(Parcelable state) {
  5629         SavedState ss = (SavedState) state;
  5630         super.onRestoreInstanceState(ss.getSuperState());
  5632         mDataChanged = true;
  5633         mSyncHeight = ss.height;
  5635         if (ss.selectedId >= 0) {
  5636             mNeedSync = true;
  5637             mPendingSync = ss;
  5638             mSyncRowId = ss.selectedId;
  5639             mSyncPosition = ss.position;
  5640             mSpecificStart = ss.viewStart;
  5641             mSyncMode = SYNC_SELECTED_POSITION;
  5642         } else if (ss.firstId >= 0) {
  5643             setSelectedPositionInt(INVALID_POSITION);
  5645             // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
  5646             setNextSelectedPositionInt(INVALID_POSITION);
  5648             mSelectorPosition = INVALID_POSITION;
  5649             mNeedSync = true;
  5650             mPendingSync = ss;
  5651             mSyncRowId = ss.firstId;
  5652             mSyncPosition = ss.position;
  5653             mSpecificStart = ss.viewStart;
  5654             mSyncMode = SYNC_FIRST_POSITION;
  5657         if (ss.checkState != null) {
  5658             mCheckStates = ss.checkState;
  5661         if (ss.checkIdState != null) {
  5662             mCheckedIdStates = ss.checkIdState;
  5665         mCheckedItemCount = ss.checkedItemCount;
  5667         requestLayout();
  5670     public static class LayoutParams extends ViewGroup.LayoutParams {
  5671         /**
  5672          * Type of this view as reported by the adapter
  5673          */
  5674         int viewType;
  5676         /**
  5677          * The stable ID of the item this view displays
  5678          */
  5679         long id = -1;
  5681         /**
  5682          * The position the view was removed from when pulled out of the
  5683          * scrap heap.
  5684          * @hide
  5685          */
  5686         int scrappedFromPosition;
  5688         /**
  5689          * When a TwoWayView is measured with an AT_MOST measure spec, it needs
  5690          * to obtain children views to measure itself. When doing so, the children
  5691          * are not attached to the window, but put in the recycler which assumes
  5692          * they've been attached before. Setting this flag will force the reused
  5693          * view to be attached to the window rather than just attached to the
  5694          * parent.
  5695          */
  5696         boolean forceAdd;
  5698         public LayoutParams(int width, int height) {
  5699             super(width, height);
  5701             if (this.width == MATCH_PARENT) {
  5702                 Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
  5703                         "does not make much sense as the view might change orientation. " +
  5704                         "Falling back to WRAP_CONTENT");
  5705                 this.width = WRAP_CONTENT;
  5708             if (this.height == MATCH_PARENT) {
  5709                 Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
  5710                         "does not make much sense as the view might change orientation. " +
  5711                         "Falling back to WRAP_CONTENT");
  5712                 this.height = WRAP_CONTENT;
  5716         public LayoutParams(Context c, AttributeSet attrs) {
  5717             super(c, attrs);
  5719             if (this.width == MATCH_PARENT) {
  5720                 Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " +
  5721                         "does not make much sense as the view might change orientation. " +
  5722                         "Falling back to WRAP_CONTENT");
  5723                 this.width = MATCH_PARENT;
  5726             if (this.height == MATCH_PARENT) {
  5727                 Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
  5728                         "does not make much sense as the view might change orientation. " +
  5729                         "Falling back to WRAP_CONTENT");
  5730                 this.height = WRAP_CONTENT;
  5734         public LayoutParams(ViewGroup.LayoutParams other) {
  5735             super(other);
  5737             if (this.width == MATCH_PARENT) {
  5738                 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
  5739                         "does not make much sense as the view might change orientation. " +
  5740                         "Falling back to WRAP_CONTENT");
  5741                 this.width = WRAP_CONTENT;
  5744             if (this.height == MATCH_PARENT) {
  5745                 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
  5746                         "does not make much sense as the view might change orientation. " +
  5747                         "Falling back to WRAP_CONTENT");
  5748                 this.height = WRAP_CONTENT;
  5753     class RecycleBin {
  5754         private RecyclerListener mRecyclerListener;
  5755         private int mFirstActivePosition;
  5756         private View[] mActiveViews = new View[0];
  5757         private ArrayList<View>[] mScrapViews;
  5758         private int mViewTypeCount;
  5759         private ArrayList<View> mCurrentScrap;
  5760         private SparseArrayCompat<View> mTransientStateViews;
  5762         public void setViewTypeCount(int viewTypeCount) {
  5763             if (viewTypeCount < 1) {
  5764                 throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
  5767             @SuppressWarnings({"unchecked", "rawtypes"})
  5768             ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
  5769             for (int i = 0; i < viewTypeCount; i++) {
  5770                 scrapViews[i] = new ArrayList<View>();
  5773             mViewTypeCount = viewTypeCount;
  5774             mCurrentScrap = scrapViews[0];
  5775             mScrapViews = scrapViews;
  5778         public void markChildrenDirty() {
  5779             if (mViewTypeCount == 1) {
  5780                 final ArrayList<View> scrap = mCurrentScrap;
  5781                 final int scrapCount = scrap.size();
  5783                 for (int i = 0; i < scrapCount; i++) {
  5784                     scrap.get(i).forceLayout();
  5786             } else {
  5787                 final int typeCount = mViewTypeCount;
  5788                 for (int i = 0; i < typeCount; i++) {
  5789                     final ArrayList<View> scrap = mScrapViews[i];
  5790                     final int scrapCount = scrap.size();
  5792                     for (int j = 0; j < scrapCount; j++) {
  5793                         scrap.get(j).forceLayout();
  5798             if (mTransientStateViews != null) {
  5799                 final int count = mTransientStateViews.size();
  5800                 for (int i = 0; i < count; i++) {
  5801                     mTransientStateViews.valueAt(i).forceLayout();
  5806         public boolean shouldRecycleViewType(int viewType) {
  5807             return viewType >= 0;
  5810         void clear() {
  5811             if (mViewTypeCount == 1) {
  5812                 final ArrayList<View> scrap = mCurrentScrap;
  5813                 final int scrapCount = scrap.size();
  5815                 for (int i = 0; i < scrapCount; i++) {
  5816                     removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
  5818             } else {
  5819                 final int typeCount = mViewTypeCount;
  5820                 for (int i = 0; i < typeCount; i++) {
  5821                     final ArrayList<View> scrap = mScrapViews[i];
  5822                     final int scrapCount = scrap.size();
  5824                     for (int j = 0; j < scrapCount; j++) {
  5825                         removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
  5830             if (mTransientStateViews != null) {
  5831                 mTransientStateViews.clear();
  5835         void fillActiveViews(int childCount, int firstActivePosition) {
  5836             if (mActiveViews.length < childCount) {
  5837                 mActiveViews = new View[childCount];
  5840             mFirstActivePosition = firstActivePosition;
  5842             final View[] activeViews = mActiveViews;
  5843             for (int i = 0; i < childCount; i++) {
  5844                 View child = getChildAt(i);
  5846                 // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
  5847                 //        However, we will NOT place them into scrap views.
  5848                 activeViews[i] = child;
  5852         View getActiveView(int position) {
  5853             final int index = position - mFirstActivePosition;
  5854             final View[] activeViews = mActiveViews;
  5856             if (index >= 0 && index < activeViews.length) {
  5857                 final View match = activeViews[index];
  5858                 activeViews[index] = null;
  5860                 return match;
  5863             return null;
  5866         View getTransientStateView(int position) {
  5867             if (mTransientStateViews == null) {
  5868                 return null;
  5871             final int index = mTransientStateViews.indexOfKey(position);
  5872             if (index < 0) {
  5873                 return null;
  5876             final View result = mTransientStateViews.valueAt(index);
  5877             mTransientStateViews.removeAt(index);
  5879             return result;
  5882         void clearTransientStateViews() {
  5883             if (mTransientStateViews != null) {
  5884                 mTransientStateViews.clear();
  5888         View getScrapView(int position) {
  5889             if (mViewTypeCount == 1) {
  5890                 return retrieveFromScrap(mCurrentScrap, position);
  5891             } else {
  5892                 int whichScrap = mAdapter.getItemViewType(position);
  5893                 if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
  5894                     return retrieveFromScrap(mScrapViews[whichScrap], position);
  5898             return null;
  5901         @TargetApi(14)
  5902         void addScrapView(View scrap, int position) {
  5903             LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
  5904             if (lp == null) {
  5905                 return;
  5908             lp.scrappedFromPosition = position;
  5910             final int viewType = lp.viewType;
  5911             final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
  5913             // Don't put views that should be ignored into the scrap heap
  5914             if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
  5915                 if (scrapHasTransientState) {
  5916                     if (mTransientStateViews == null) {
  5917                         mTransientStateViews = new SparseArrayCompat<View>();
  5920                     mTransientStateViews.put(position, scrap);
  5923                 return;
  5926             if (mViewTypeCount == 1) {
  5927                 mCurrentScrap.add(scrap);
  5928             } else {
  5929                 mScrapViews[viewType].add(scrap);
  5932             // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
  5933             // null delegates.
  5934             if (Build.VERSION.SDK_INT >= 14) {
  5935                 scrap.setAccessibilityDelegate(null);
  5938             if (mRecyclerListener != null) {
  5939                 mRecyclerListener.onMovedToScrapHeap(scrap);
  5943         @TargetApi(14)
  5944         void scrapActiveViews() {
  5945             final View[] activeViews = mActiveViews;
  5946             final boolean multipleScraps = (mViewTypeCount > 1);
  5948             ArrayList<View> scrapViews = mCurrentScrap;
  5949             final int count = activeViews.length;
  5951             for (int i = count - 1; i >= 0; i--) {
  5952                 final View victim = activeViews[i];
  5953                 if (victim != null) {
  5954                     final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
  5955                     int whichScrap = lp.viewType;
  5957                     activeViews[i] = null;
  5959                     final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
  5960                     if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) {
  5961                         if (scrapHasTransientState) {
  5962                             removeDetachedView(victim, false);
  5964                             if (mTransientStateViews == null) {
  5965                                 mTransientStateViews = new SparseArrayCompat<View>();
  5968                             mTransientStateViews.put(mFirstActivePosition + i, victim);
  5971                         continue;
  5974                     if (multipleScraps) {
  5975                         scrapViews = mScrapViews[whichScrap];
  5978                     lp.scrappedFromPosition = mFirstActivePosition + i;
  5979                     scrapViews.add(victim);
  5981                     // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
  5982                     // null delegates.
  5983                     if (Build.VERSION.SDK_INT >= 14) {
  5984                         victim.setAccessibilityDelegate(null);
  5987                     if (mRecyclerListener != null) {
  5988                         mRecyclerListener.onMovedToScrapHeap(victim);
  5993             pruneScrapViews();
  5996         private void pruneScrapViews() {
  5997             final int maxViews = mActiveViews.length;
  5998             final int viewTypeCount = mViewTypeCount;
  5999             final ArrayList<View>[] scrapViews = mScrapViews;
  6001             for (int i = 0; i < viewTypeCount; ++i) {
  6002                 final ArrayList<View> scrapPile = scrapViews[i];
  6003                 int size = scrapPile.size();
  6004                 final int extras = size - maxViews;
  6006                 size--;
  6008                 for (int j = 0; j < extras; j++) {
  6009                     removeDetachedView(scrapPile.remove(size--), false);
  6013             if (mTransientStateViews != null) {
  6014                 for (int i = 0; i < mTransientStateViews.size(); i++) {
  6015                     final View v = mTransientStateViews.valueAt(i);
  6016                     if (!ViewCompat.hasTransientState(v)) {
  6017                         mTransientStateViews.removeAt(i);
  6018                         i--;
  6024         void reclaimScrapViews(List<View> views) {
  6025             if (mViewTypeCount == 1) {
  6026                 views.addAll(mCurrentScrap);
  6027             } else {
  6028                 final int viewTypeCount = mViewTypeCount;
  6029                 final ArrayList<View>[] scrapViews = mScrapViews;
  6031                 for (int i = 0; i < viewTypeCount; ++i) {
  6032                     final ArrayList<View> scrapPile = scrapViews[i];
  6033                     views.addAll(scrapPile);
  6038         View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
  6039             int size = scrapViews.size();
  6040             if (size <= 0) {
  6041                 return null;
  6044             for (int i = 0; i < size; i++) {
  6045                 final View scrapView = scrapViews.get(i);
  6046                 final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams();
  6048                 if (lp.scrappedFromPosition == position) {
  6049                     scrapViews.remove(i);
  6050                     return scrapView;
  6054             return scrapViews.remove(size - 1);
  6058     @Override
  6059     public void setEmptyView(View emptyView) {
  6060         super.setEmptyView(emptyView);
  6061         mEmptyView = emptyView;
  6062         updateEmptyStatus();
  6065     @Override
  6066     public void setFocusable(boolean focusable) {
  6067         final ListAdapter adapter = getAdapter();
  6068         final boolean empty = (adapter == null || adapter.getCount() == 0);
  6070         mDesiredFocusableState = focusable;
  6071         if (!focusable) {
  6072             mDesiredFocusableInTouchModeState = false;
  6075         super.setFocusable(focusable && !empty);
  6078     @Override
  6079     public void setFocusableInTouchMode(boolean focusable) {
  6080         final ListAdapter adapter = getAdapter();
  6081         final boolean empty = (adapter == null || adapter.getCount() == 0);
  6083         mDesiredFocusableInTouchModeState = focusable;
  6084         if (focusable) {
  6085             mDesiredFocusableState = true;
  6088         super.setFocusableInTouchMode(focusable && !empty);
  6091     private void checkFocus() {
  6092         final ListAdapter adapter = getAdapter();
  6093         final boolean focusable = (adapter != null && adapter.getCount() > 0);
  6095         // The order in which we set focusable in touch mode/focusable may matter
  6096         // for the client, see View.setFocusableInTouchMode() comments for more
  6097         // details
  6098         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
  6099         super.setFocusable(focusable && mDesiredFocusableState);
  6101         if (mEmptyView != null) {
  6102             updateEmptyStatus();
  6106     private void updateEmptyStatus() {
  6107         final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());
  6109         if (isEmpty) {
  6110             if (mEmptyView != null) {
  6111                 mEmptyView.setVisibility(View.VISIBLE);
  6112                 setVisibility(View.GONE);
  6113             } else {
  6114                 // If the caller just removed our empty view, make sure the list
  6115                 // view is visible
  6116                 setVisibility(View.VISIBLE);
  6119             // We are now GONE, so pending layouts will not be dispatched.
  6120             // Force one here to make sure that the state of the list matches
  6121             // the state of the adapter.
  6122             if (mDataChanged) {
  6123                 onLayout(false, getLeft(), getTop(), getRight(), getBottom());
  6125         } else {
  6126             if (mEmptyView != null) {
  6127                 mEmptyView.setVisibility(View.GONE);
  6130             setVisibility(View.VISIBLE);
  6134     private class AdapterDataSetObserver extends DataSetObserver {
  6135         private Parcelable mInstanceState = null;
  6137         @Override
  6138         public void onChanged() {
  6139             mDataChanged = true;
  6140             mOldItemCount = mItemCount;
  6141             mItemCount = getAdapter().getCount();
  6143             // Detect the case where a cursor that was previously invalidated has
  6144             // been re-populated with new data.
  6145             if (TwoWayView.this.mHasStableIds && mInstanceState != null
  6146                     && mOldItemCount == 0 && mItemCount > 0) {
  6147                 TwoWayView.this.onRestoreInstanceState(mInstanceState);
  6148                 mInstanceState = null;
  6149             } else {
  6150                 rememberSyncState();
  6153             checkFocus();
  6154             requestLayout();
  6157         @Override
  6158         public void onInvalidated() {
  6159             mDataChanged = true;
  6161             if (TwoWayView.this.mHasStableIds) {
  6162                 // Remember the current state for the case where our hosting activity is being
  6163                 // stopped and later restarted
  6164                 mInstanceState = TwoWayView.this.onSaveInstanceState();
  6167             // Data is invalid so we should reset our state
  6168             mOldItemCount = mItemCount;
  6169             mItemCount = 0;
  6171             mSelectedPosition = INVALID_POSITION;
  6172             mSelectedRowId = INVALID_ROW_ID;
  6174             mNextSelectedPosition = INVALID_POSITION;
  6175             mNextSelectedRowId = INVALID_ROW_ID;
  6177             mNeedSync = false;
  6179             checkFocus();
  6180             requestLayout();
  6184     static class SavedState extends BaseSavedState {
  6185         long selectedId;
  6186         long firstId;
  6187         int viewStart;
  6188         int position;
  6189         int height;
  6190         int checkedItemCount;
  6191         SparseBooleanArray checkState;
  6192         LongSparseArray<Integer> checkIdState;
  6194         /**
  6195          * Constructor called from {@link TwoWayView#onSaveInstanceState()}
  6196          */
  6197         SavedState(Parcelable superState) {
  6198             super(superState);
  6201         /**
  6202          * Constructor called from {@link #CREATOR}
  6203          */
  6204         private SavedState(Parcel in) {
  6205             super(in);
  6207             selectedId = in.readLong();
  6208             firstId = in.readLong();
  6209             viewStart = in.readInt();
  6210             position = in.readInt();
  6211             height = in.readInt();
  6213             checkedItemCount = in.readInt();
  6214             checkState = in.readSparseBooleanArray();
  6216             final int N = in.readInt();
  6217             if (N > 0) {
  6218                 checkIdState = new LongSparseArray<Integer>();
  6219                 for (int i = 0; i < N; i++) {
  6220                     final long key = in.readLong();
  6221                     final int value = in.readInt();
  6222                     checkIdState.put(key, value);
  6227         @Override
  6228         public void writeToParcel(Parcel out, int flags) {
  6229             super.writeToParcel(out, flags);
  6231             out.writeLong(selectedId);
  6232             out.writeLong(firstId);
  6233             out.writeInt(viewStart);
  6234             out.writeInt(position);
  6235             out.writeInt(height);
  6237             out.writeInt(checkedItemCount);
  6238             out.writeSparseBooleanArray(checkState);
  6240             final int N = checkIdState != null ? checkIdState.size() : 0;
  6241             out.writeInt(N);
  6243             for (int i = 0; i < N; i++) {
  6244                 out.writeLong(checkIdState.keyAt(i));
  6245                 out.writeInt(checkIdState.valueAt(i));
  6249         @Override
  6250         public String toString() {
  6251             return "TwoWayView.SavedState{"
  6252                     + Integer.toHexString(System.identityHashCode(this))
  6253                     + " selectedId=" + selectedId
  6254                     + " firstId=" + firstId
  6255                     + " viewStart=" + viewStart
  6256                     + " height=" + height
  6257                     + " position=" + position
  6258                     + " checkState=" + checkState + "}";
  6261         public static final Parcelable.Creator<SavedState> CREATOR
  6262                 = new Parcelable.Creator<SavedState>() {
  6263             @Override
  6264             public SavedState createFromParcel(Parcel in) {
  6265                 return new SavedState(in);
  6268             @Override
  6269             public SavedState[] newArray(int size) {
  6270                 return new SavedState[size];
  6272         };
  6275     private class SelectionNotifier implements Runnable {
  6276         @Override
  6277         public void run() {
  6278             if (mDataChanged) {
  6279                 // Data has changed between when this SelectionNotifier
  6280                 // was posted and now. We need to wait until the AdapterView
  6281                 // has been synched to the new data.
  6282                 if (mAdapter != null) {
  6283                     post(this);
  6285             } else {
  6286                 fireOnSelected();
  6287                 performAccessibilityActionsOnSelected();
  6292     private class WindowRunnnable {
  6293         private int mOriginalAttachCount;
  6295         public void rememberWindowAttachCount() {
  6296             mOriginalAttachCount = getWindowAttachCount();
  6299         public boolean sameWindow() {
  6300             return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
  6304     private class PerformClick extends WindowRunnnable implements Runnable {
  6305         int mClickMotionPosition;
  6307         @Override
  6308         public void run() {
  6309             if (mDataChanged) {
  6310                 return;
  6313             final ListAdapter adapter = mAdapter;
  6314             final int motionPosition = mClickMotionPosition;
  6316             if (adapter != null && mItemCount > 0 &&
  6317                 motionPosition != INVALID_POSITION &&
  6318                 motionPosition < adapter.getCount() && sameWindow()) {
  6320                 final View child = getChildAt(motionPosition - mFirstPosition);
  6321                 if (child != null) {
  6322                     performItemClick(child, motionPosition, adapter.getItemId(motionPosition));
  6328     private final class CheckForTap implements Runnable {
  6329         @Override
  6330         public void run() {
  6331             if (mTouchMode != TOUCH_MODE_DOWN) {
  6332                 return;
  6335             mTouchMode = TOUCH_MODE_TAP;
  6337             final View child = getChildAt(mMotionPosition - mFirstPosition);
  6338             if (child != null && !child.hasFocusable()) {
  6339                 mLayoutMode = LAYOUT_NORMAL;
  6341                 if (!mDataChanged) {
  6342                     setPressed(true);
  6343                     child.setPressed(true);
  6345                     layoutChildren();
  6346                     positionSelector(mMotionPosition, child);
  6347                     refreshDrawableState();
  6349                     positionSelector(mMotionPosition, child);
  6350                     refreshDrawableState();
  6352                     final boolean longClickable = isLongClickable();
  6354                     if (mSelector != null) {
  6355                         Drawable d = mSelector.getCurrent();
  6357                         if (d != null && d instanceof TransitionDrawable) {
  6358                             if (longClickable) {
  6359                                 final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
  6360                                 ((TransitionDrawable) d).startTransition(longPressTimeout);
  6361                             } else {
  6362                                 ((TransitionDrawable) d).resetTransition();
  6367                     if (longClickable) {
  6368                         triggerCheckForLongPress();
  6369                     } else {
  6370                         mTouchMode = TOUCH_MODE_DONE_WAITING;
  6372                 } else {
  6373                     mTouchMode = TOUCH_MODE_DONE_WAITING;
  6379     private class CheckForLongPress extends WindowRunnnable implements Runnable {
  6380         @Override
  6381         public void run() {
  6382             final int motionPosition = mMotionPosition;
  6383             final View child = getChildAt(motionPosition - mFirstPosition);
  6385             if (child != null) {
  6386                 final long longPressId = mAdapter.getItemId(mMotionPosition);
  6388                 boolean handled = false;
  6389                 if (sameWindow() && !mDataChanged) {
  6390                     handled = performLongPress(child, motionPosition, longPressId);
  6393                 if (handled) {
  6394                     mTouchMode = TOUCH_MODE_REST;
  6395                     setPressed(false);
  6396                     child.setPressed(false);
  6397                 } else {
  6398                     mTouchMode = TOUCH_MODE_DONE_WAITING;
  6404     private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
  6405         public void run() {
  6406             if (!isPressed() || mSelectedPosition < 0) {
  6407                 return;
  6410             final int index = mSelectedPosition - mFirstPosition;
  6411             final View v = getChildAt(index);
  6413             if (!mDataChanged) {
  6414                 boolean handled = false;
  6416                 if (sameWindow()) {
  6417                     handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
  6420                 if (handled) {
  6421                     setPressed(false);
  6422                     v.setPressed(false);
  6424             } else {
  6425                 setPressed(false);
  6427                 if (v != null) {
  6428                     v.setPressed(false);
  6434     private static class ArrowScrollFocusResult {
  6435         private int mSelectedPosition;
  6436         private int mAmountToScroll;
  6438         /**
  6439          * How {@link TwoWayView#arrowScrollFocused} returns its values.
  6440          */
  6441         void populate(int selectedPosition, int amountToScroll) {
  6442             mSelectedPosition = selectedPosition;
  6443             mAmountToScroll = amountToScroll;
  6446         public int getSelectedPosition() {
  6447             return mSelectedPosition;
  6450         public int getAmountToScroll() {
  6451             return mAmountToScroll;
  6455     private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
  6456         @Override
  6457         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
  6458             super.onInitializeAccessibilityNodeInfo(host, info);
  6460             final int position = getPositionForView(host);
  6461             final ListAdapter adapter = getAdapter();
  6463             // Cannot perform actions on invalid items
  6464             if (position == INVALID_POSITION || adapter == null) {
  6465                 return;
  6468             // Cannot perform actions on disabled items
  6469             if (!isEnabled() || !adapter.isEnabled(position)) {
  6470                 return;
  6473             if (position == getSelectedItemPosition()) {
  6474                 info.setSelected(true);
  6475                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
  6476             } else {
  6477                 info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
  6480             if (isClickable()) {
  6481                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
  6482                 info.setClickable(true);
  6485             if (isLongClickable()) {
  6486                 info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
  6487                 info.setLongClickable(true);
  6491         @Override
  6492         public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
  6493             if (super.performAccessibilityAction(host, action, arguments)) {
  6494                 return true;
  6497             final int position = getPositionForView(host);
  6498             final ListAdapter adapter = getAdapter();
  6500             // Cannot perform actions on invalid items
  6501             if (position == INVALID_POSITION || adapter == null) {
  6502                 return false;
  6505             // Cannot perform actions on disabled items
  6506             if (!isEnabled() || !adapter.isEnabled(position)) {
  6507                 return false;
  6510             final long id = getItemIdAtPosition(position);
  6512             switch (action) {
  6513             case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
  6514                 if (getSelectedItemPosition() == position) {
  6515                     setSelection(INVALID_POSITION);
  6516                     return true;
  6518                 return false;
  6520             case AccessibilityNodeInfoCompat.ACTION_SELECT:
  6521                 if (getSelectedItemPosition() != position) {
  6522                     setSelection(position);
  6523                     return true;
  6525                 return false;
  6527             case AccessibilityNodeInfoCompat.ACTION_CLICK:
  6528                 if (isClickable()) {
  6529                     return performItemClick(host, position, id);
  6531                 return false;
  6533             case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
  6534                 if (isLongClickable()) {
  6535                     return performLongPress(host, position, id);
  6537                 return false;
  6540             return false;

mercurial