/*
 * Copyright (C) 2013 Lucas Rocha
 *
 * This code is based on bits and pieces of Android's AbsListView,
 * Listview, and StaggeredGridView.
 *
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mozilla.gecko.widget;

import org.mozilla.gecko.R;

import java.util.ArrayList;
import java.util.List;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v4.util.LongSparseArray;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.KeyEventCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.FocusFinder;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SoundEffectConstants;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Checkable;
import android.widget.ListAdapter;
import android.widget.Scroller;

import static android.os.Build.VERSION_CODES.HONEYCOMB;

/*
 * Implementation Notes:
 *
 * Some terminology:
 *
 *     index    - index of the items that are currently visible
 *     position - index of the items in the cursor
 *
 * Given the bi-directional nature of this view, the source code
 * usually names variables with 'start' to mean 'top' or 'left'; and
 * 'end' to mean 'bottom' or 'right', depending on the current
 * orientation of the widget.
 */

/**
 * A view that shows items in a vertical or horizontal scrolling list.
 * The items come from the {@link ListAdapter} associated with this view.
 */
public class TwoWayView extends AdapterView<ListAdapter> implements
        ViewTreeObserver.OnTouchModeChangeListener {
    private static final String LOGTAG = "TwoWayView";

    private static final int NO_POSITION = -1;
    private static final int INVALID_POINTER = -1;

    public static final int[] STATE_NOTHING = new int[] { 0 };

    private static final int TOUCH_MODE_REST = -1;
    private static final int TOUCH_MODE_DOWN = 0;
    private static final int TOUCH_MODE_TAP = 1;
    private static final int TOUCH_MODE_DONE_WAITING = 2;
    private static final int TOUCH_MODE_DRAGGING = 3;
    private static final int TOUCH_MODE_FLINGING = 4;
    private static final int TOUCH_MODE_OVERSCROLL = 5;

    private static final int TOUCH_MODE_UNKNOWN = -1;
    private static final int TOUCH_MODE_ON = 0;
    private static final int TOUCH_MODE_OFF = 1;

    private static final int LAYOUT_NORMAL = 0;
    private static final int LAYOUT_FORCE_TOP = 1;
    private static final int LAYOUT_SET_SELECTION = 2;
    private static final int LAYOUT_FORCE_BOTTOM = 3;
    private static final int LAYOUT_SPECIFIC = 4;
    private static final int LAYOUT_SYNC = 5;
    private static final int LAYOUT_MOVE_SELECTION = 6;

    private static final int SYNC_SELECTED_POSITION = 0;
    private static final int SYNC_FIRST_POSITION = 1;

    private static final int SYNC_MAX_DURATION_MILLIS = 100;

    private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;

    private static final float MAX_SCROLL_FACTOR = 0.33f;

    private static final int MIN_SCROLL_PREVIEW_PIXELS = 10;

    public static enum ChoiceMode {
        NONE,
        SINGLE,
        MULTIPLE
    }

    public static enum Orientation {
        HORIZONTAL,
        VERTICAL
    }

    private final Context mContext;

    private ListAdapter mAdapter;

    private boolean mIsVertical;

    private int mItemMargin;

    private boolean mInLayout;
    private boolean mBlockLayoutRequests;

    private boolean mIsAttached;

    private final RecycleBin mRecycler;
    private AdapterDataSetObserver mDataSetObserver;

    private boolean mItemsCanFocus;

    final boolean[] mIsScrap = new boolean[1];

    private boolean mDataChanged;
    private int mItemCount;
    private int mOldItemCount;
    private boolean mHasStableIds;
    private boolean mAreAllItemsSelectable;

    private int mFirstPosition;
    private int mSpecificStart;

    private SavedState mPendingSync;

    private PositionScroller mPositionScroller;
    private Runnable mPositionScrollAfterLayout;

    private final int mTouchSlop;
    private final int mMaximumVelocity;
    private final int mFlingVelocity;
    private float mLastTouchPos;
    private float mTouchRemainderPos;
    private int mActivePointerId;

    private final Rect mTempRect;

    private final ArrowScrollFocusResult mArrowScrollFocusResult;

    private Rect mTouchFrame;
    private int mMotionPosition;
    private CheckForTap mPendingCheckForTap;
    private CheckForLongPress mPendingCheckForLongPress;
    private CheckForKeyLongPress mPendingCheckForKeyLongPress;
    private PerformClick mPerformClick;
    private Runnable mTouchModeReset;
    private int mResurrectToPosition;

    private boolean mIsChildViewEnabled;

    private boolean mDrawSelectorOnTop;
    private Drawable mSelector;
    private int mSelectorPosition;
    private final Rect mSelectorRect;

    private int mOverScroll;
    private final int mOverscrollDistance;

    private boolean mDesiredFocusableState;
    private boolean mDesiredFocusableInTouchModeState;

    private SelectionNotifier mSelectionNotifier;

    private boolean mNeedSync;
    private int mSyncMode;
    private int mSyncPosition;
    private long mSyncRowId;
    private long mSyncSize;
    private int mSelectedStart;

    private int mNextSelectedPosition;
    private long mNextSelectedRowId;
    private int mSelectedPosition;
    private long mSelectedRowId;
    private int mOldSelectedPosition;
    private long mOldSelectedRowId;

    private ChoiceMode mChoiceMode;
    private int mCheckedItemCount;
    private SparseBooleanArray mCheckStates;
    LongSparseArray<Integer> mCheckedIdStates;

    private ContextMenuInfo mContextMenuInfo;

    private int mLayoutMode;
    private int mTouchMode;
    private int mLastTouchMode;
    private VelocityTracker mVelocityTracker;
    private final Scroller mScroller;

    private EdgeEffectCompat mStartEdge;
    private EdgeEffectCompat mEndEdge;

    private OnScrollListener mOnScrollListener;
    private int mLastScrollState;

    private View mEmptyView;

    private ListItemAccessibilityDelegate mAccessibilityDelegate;

    private int mLastAccessibilityScrollEventFromIndex;
    private int mLastAccessibilityScrollEventToIndex;

    public interface OnScrollListener {

        /**
         * The view is not scrolling. Note navigating the list using the trackball counts as
         * being in the idle state since these transitions are not animated.
         */
        public static int SCROLL_STATE_IDLE = 0;

        /**
         * The user is scrolling using touch, and their finger is still on the screen
         */
        public static int SCROLL_STATE_TOUCH_SCROLL = 1;

        /**
         * The user had previously been scrolling using touch and had performed a fling. The
         * animation is now coasting to a stop
         */
        public static int SCROLL_STATE_FLING = 2;

        /**
         * Callback method to be invoked while the list view or grid view is being scrolled. If the
         * view is being scrolled, this method will be called before the next frame of the scroll is
         * rendered. In particular, it will be called before any calls to
         * {@link android.widget.Adapter#getView(int, View, ViewGroup)}.
         *
         * @param view The view whose scroll state is being reported
         *
         * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
         * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
         */
        public void onScrollStateChanged(TwoWayView view, int scrollState);

        /**
         * Callback method to be invoked when the list or grid has been scrolled. This will be
         * called after the scroll has completed
         * @param view The view whose scroll state is being reported
         * @param firstVisibleItem the index of the first visible cell (ignore if
         *        visibleItemCount == 0)
         * @param visibleItemCount the number of visible cells
         * @param totalItemCount the number of items in the list adaptor
         */
        public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount,
                int totalItemCount);
    }

    /**
     * A RecyclerListener is used to receive a notification whenever a View is placed
     * inside the RecycleBin's scrap heap. This listener is used to free resources
     * associated to Views placed in the RecycleBin.
     *
     * @see TwoWayView.RecycleBin
     * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener)
     */
    public static interface RecyclerListener {
        /**
         * Indicates that the specified View was moved into the recycler's scrap heap.
         * The view is not displayed on screen any more and any expensive resource
         * associated with the view should be discarded.
         *
         * @param view
         */
        void onMovedToScrapHeap(View view);
    }

    public TwoWayView(Context context) {
        this(context, null);
    }

    public TwoWayView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TwoWayView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mContext = context;

        mLayoutMode = LAYOUT_NORMAL;
        mTouchMode = TOUCH_MODE_REST;
        mLastTouchMode = TOUCH_MODE_UNKNOWN;

        mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;

        final ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
        mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
        mFlingVelocity = vc.getScaledMinimumFlingVelocity();
        mOverscrollDistance = getScaledOverscrollDistance(vc);

        mScroller = new Scroller(context);

        mIsVertical = true;

        mTempRect = new Rect();

        mArrowScrollFocusResult = new ArrowScrollFocusResult();

        mSelectorPosition = INVALID_POSITION;

        mSelectorRect = new Rect();

        mResurrectToPosition = INVALID_POSITION;

        mNextSelectedPosition = INVALID_POSITION;
        mNextSelectedRowId = INVALID_ROW_ID;
        mSelectedPosition = INVALID_POSITION;
        mSelectedRowId = INVALID_ROW_ID;
        mOldSelectedPosition = INVALID_POSITION;
        mOldSelectedRowId = INVALID_ROW_ID;

        mChoiceMode = ChoiceMode.NONE;

        mRecycler = new RecycleBin();

        mAreAllItemsSelectable = true;

        setClickable(true);
        setFocusableInTouchMode(true);
        setWillNotDraw(false);
        setAlwaysDrawnWithCacheEnabled(false);
        setWillNotDraw(false);
        setClipToPadding(false);

        ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);

        mDrawSelectorOnTop = a.getBoolean(
                R.styleable.TwoWayView_android_drawSelectorOnTop, false);

        Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector);
        if (d != null) {
            setSelector(d);
        }

        int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1);
        if (orientation >= 0) {
            setOrientation(Orientation.values()[orientation]);
        }

        int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1);
        if (choiceMode >= 0) {
            setChoiceMode(ChoiceMode.values()[choiceMode]);
        }

        a.recycle();
    }

    public void setOrientation(Orientation orientation) {
        final boolean isVertical = (orientation == Orientation.VERTICAL);
        if (mIsVertical == isVertical) {
            return;
        }

        mIsVertical = isVertical;

        resetState();
        mRecycler.clear();

        requestLayout();
    }

    public Orientation getOrientation() {
        return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
    }

    public void setItemMargin(int itemMargin) {
        if (mItemMargin == itemMargin) {
            return;
        }

        mItemMargin = itemMargin;
        requestLayout();
    }

    @SuppressWarnings("unused")
    public int getItemMargin() {
        return mItemMargin;
    }

    /**
     * Indicates that the views created by the ListAdapter can contain focusable
     * items.
     *
     * @param itemsCanFocus true if items can get focus, false otherwise
     */
    @SuppressWarnings("unused")
    public void setItemsCanFocus(boolean itemsCanFocus) {
        mItemsCanFocus = itemsCanFocus;
        if (!itemsCanFocus) {
            setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        }
    }

    /**
     * @return Whether the views created by the ListAdapter can contain focusable
     * items.
     */
    @SuppressWarnings("unused")
    public boolean getItemsCanFocus() {
        return mItemsCanFocus;
    }

    /**
     * Set the listener that will receive notifications every time the list scrolls.
     *
     * @param l the scroll listener
     */
    public void setOnScrollListener(OnScrollListener l) {
        mOnScrollListener = l;
        invokeOnItemScrollListener();
    }

    /**
     * Sets the recycler listener to be notified whenever a View is set aside in
     * the recycler for later reuse. This listener can be used to free resources
     * associated to the View.
     *
     * @param l The recycler listener to be notified of views set aside
     *        in the recycler.
     *
     * @see TwoWayView.RecycleBin
     * @see TwoWayView.RecyclerListener
     */
    public void setRecyclerListener(RecyclerListener l) {
        mRecycler.mRecyclerListener = l;
    }

    /**
     * Controls whether the selection highlight drawable should be drawn on top of the item or
     * behind it.
     *
     * @param drawSelectorOnTop If true, the selector will be drawn on the item it is highlighting.
     *                          The default is false.
     *
     * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
     */
    @SuppressWarnings("unused")
    public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
        mDrawSelectorOnTop = drawSelectorOnTop;
    }

    /**
     * Set a Drawable that should be used to highlight the currently selected item.
     *
     * @param resID A Drawable resource to use as the selection highlight.
     *
     * @attr ref android.R.styleable#AbsListView_listSelector
     */
    @SuppressWarnings("unused")
    public void setSelector(int resID) {
        setSelector(getResources().getDrawable(resID));
    }

    /**
     * Set a Drawable that should be used to highlight the currently selected item.
     *
     * @param selector A Drawable to use as the selection highlight.
     *
     * @attr ref android.R.styleable#AbsListView_listSelector
     */
    public void setSelector(Drawable selector) {
        if (mSelector != null) {
            mSelector.setCallback(null);
            unscheduleDrawable(mSelector);
        }

        mSelector = selector;
        Rect padding = new Rect();
        selector.getPadding(padding);

        selector.setCallback(this);
        updateSelectorState();
    }

    /**
     * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
     * selection in the list.
     *
     * @return the drawable used to display the selector
     */
    @SuppressWarnings("unused")
    public Drawable getSelector() {
        return mSelector;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getSelectedItemPosition() {
        return mNextSelectedPosition;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getSelectedItemId() {
        return mNextSelectedRowId;
    }

    /**
     * Returns the number of items currently selected. This will only be valid
     * if the choice mode is not {@link ChoiceMode#NONE} (default).
     *
     * <p>To determine the specific items that are currently selected, use one of
     * the <code>getChecked*</code> methods.
     *
     * @return The number of items currently selected
     *
     * @see #getCheckedItemPosition()
     * @see #getCheckedItemPositions()
     * @see #getCheckedItemIds()
     */
    @SuppressWarnings("unused")
    public int getCheckedItemCount() {
        return mCheckedItemCount;
    }

    /**
     * Returns the checked state of the specified position. The result is only
     * valid if the choice mode has been set to {@link ChoiceMode#SINGLE}
     * or {@link ChoiceMode#MULTIPLE}.
     *
     * @param position The item whose checked state to return
     * @return The item's checked state or <code>false</code> if choice mode
     *         is invalid
     *
     * @see #setChoiceMode(ChoiceMode)
     */
    public boolean isItemChecked(int position) {
        if (mChoiceMode == ChoiceMode.NONE && mCheckStates != null) {
            return mCheckStates.get(position);
        }

        return false;
    }

    /**
     * Returns the currently checked item. The result is only valid if the choice
     * mode has been set to {@link ChoiceMode#SINGLE}.
     *
     * @return The position of the currently checked item or
     *         {@link #INVALID_POSITION} if nothing is selected
     *
     * @see #setChoiceMode(ChoiceMode)
     */
    public int getCheckedItemPosition() {
        if (mChoiceMode == ChoiceMode.SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
            return mCheckStates.keyAt(0);
        }

        return INVALID_POSITION;
    }

    /**
     * Returns the set of checked items in the list. The result is only valid if
     * the choice mode has not been set to {@link ChoiceMode#NONE}.
     *
     * @return  A SparseBooleanArray which will return true for each call to
     *          get(int position) where position is a position in the list,
     *          or <code>null</code> if the choice mode is set to
     *          {@link ChoiceMode#NONE}.
     */
    public SparseBooleanArray getCheckedItemPositions() {
        if (mChoiceMode != ChoiceMode.NONE) {
            return mCheckStates;
        }

        return null;
    }

    /**
     * Returns the set of checked items ids. The result is only valid if the
     * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter
     * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
     *
     * @return A new array which contains the id of each checked item in the
     *         list.
     */
    public long[] getCheckedItemIds() {
        if (mChoiceMode == ChoiceMode.NONE || mCheckedIdStates == null || mAdapter == null) {
            return new long[0];
        }

        final LongSparseArray<Integer> idStates = mCheckedIdStates;
        final int count = idStates.size();
        final long[] ids = new long[count];

        for (int i = 0; i < count; i++) {
            ids[i] = idStates.keyAt(i);
        }

        return ids;
    }

    /**
     * Sets the checked state of the specified position. The is only valid if
     * the choice mode has been set to {@link ChoiceMode#SINGLE} or
     * {@link ChoiceMode#MULTIPLE}.
     *
     * @param position The item whose checked state is to be checked
     * @param value The new checked state for the item
     */
    @SuppressWarnings("unused")
    public void setItemChecked(int position, boolean value) {
        if (mChoiceMode == ChoiceMode.NONE) {
            return;
        }

        if (mChoiceMode == ChoiceMode.MULTIPLE) {
            boolean oldValue = mCheckStates.get(position);
            mCheckStates.put(position, value);

            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                if (value) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                } else {
                    mCheckedIdStates.delete(mAdapter.getItemId(position));
                }
            }

            if (oldValue != value) {
                if (value) {
                    mCheckedItemCount++;
                } else {
                    mCheckedItemCount--;
                }
            }
        } else {
            boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();

            // Clear all values if we're checking something, or unchecking the currently
            // selected item
            if (value || isItemChecked(position)) {
                mCheckStates.clear();

                if (updateIds) {
                    mCheckedIdStates.clear();
                }
            }

            // This may end up selecting the value we just cleared but this way
            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
            if (value) {
                mCheckStates.put(position, true);

                if (updateIds) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                }

                mCheckedItemCount = 1;
            } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
                mCheckedItemCount = 0;
            }
        }

        // Do not generate a data change while we are in the layout phase
        if (!mInLayout && !mBlockLayoutRequests) {
            mDataChanged = true;
            rememberSyncState();
            requestLayout();
        }
    }

    /**
     * Clear any choices previously set
     */
    @SuppressWarnings("unused")
    public void clearChoices() {
        if (mCheckStates != null) {
            mCheckStates.clear();
        }

        if (mCheckedIdStates != null) {
            mCheckedIdStates.clear();
        }

        mCheckedItemCount = 0;
    }

    /**
     * @see #setChoiceMode(ChoiceMode)
     *
     * @return The current choice mode
     */
    @SuppressWarnings("unused")
    public ChoiceMode getChoiceMode() {
        return mChoiceMode;
    }

    /**
     * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
     * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the
     * List allows up to one item to  be in a chosen state. By setting the choiceMode to
     * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen.
     *
     * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or
     * {@link ChoiceMode#MULTIPLE}
     */
    public void setChoiceMode(ChoiceMode choiceMode) {
        mChoiceMode = choiceMode;

        if (mChoiceMode != ChoiceMode.NONE) {
            if (mCheckStates == null) {
                mCheckStates = new SparseBooleanArray();
            }

            if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
                mCheckedIdStates = new LongSparseArray<Integer>();
            }
        }
    }

    @Override
    public ListAdapter getAdapter() {
        return mAdapter;
    }

    @Override
    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetState();
        mRecycler.clear();

        mAdapter = adapter;
        mDataChanged = true;

        mOldSelectedPosition = INVALID_POSITION;
        mOldSelectedRowId = INVALID_ROW_ID;

        if (mCheckStates != null) {
            mCheckStates.clear();
        }

        if (mCheckedIdStates != null) {
            mCheckedIdStates.clear();
        }

        if (mAdapter != null) {
            mOldItemCount = mItemCount;
            mItemCount = adapter.getCount();

            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            mRecycler.setViewTypeCount(adapter.getViewTypeCount());

            mHasStableIds = adapter.hasStableIds();
            mAreAllItemsSelectable = adapter.areAllItemsEnabled();

            if (mChoiceMode != ChoiceMode.NONE && mHasStableIds && mCheckedIdStates == null) {
                mCheckedIdStates = new LongSparseArray<Integer>();
            }

            final int position = lookForSelectablePosition(0);
            setSelectedPositionInt(position);
            setNextSelectedPositionInt(position);

            if (mItemCount == 0) {
                checkSelectionChanged();
            }
        } else {
            mItemCount = 0;
            mHasStableIds = false;
            mAreAllItemsSelectable = true;

            checkSelectionChanged();
        }

        checkFocus();
        requestLayout();
    }

    @Override
    public int getFirstVisiblePosition() {
        return mFirstPosition;
    }

    @Override
    public int getLastVisiblePosition() {
        return mFirstPosition + getChildCount() - 1;
    }

    @Override
    public int getCount() {
        return mItemCount;
    }

    @Override
    public int getPositionForView(View view) {
        View child = view;
        try {
            View v;
            while (!(v = (View) child.getParent()).equals(this)) {
                child = v;
            }
        } catch (ClassCastException e) {
            // We made it up to the window without find this list view
            return INVALID_POSITION;
        }

        // Search the children for the list item
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            if (getChildAt(i).equals(child)) {
                return mFirstPosition + i;
            }
        }

        // Child not found!
        return INVALID_POSITION;
    }

    @Override
    public void getFocusedRect(Rect r) {
        View view = getSelectedView();

        if (view != null && view.getParent() == this) {
            // The focused rectangle of the selected view offset into the
            // coordinate space of this view.
            view.getFocusedRect(r);
            offsetDescendantRectToMyCoords(view, r);
        } else {
            super.getFocusedRect(r);
        }
    }

    @Override
    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);

        if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
            if (!mIsAttached && mAdapter != null) {
                // Data may have changed while we were detached and it's valid
                // to change focus while detached. Refresh so we don't die.
                mDataChanged = true;
                mOldItemCount = mItemCount;
                mItemCount = mAdapter.getCount();
            }

            resurrectSelection();
        }

        final ListAdapter adapter = mAdapter;
        int closetChildIndex = INVALID_POSITION;
        int closestChildStart = 0;

        if (adapter != null && gainFocus && previouslyFocusedRect != null) {
            previouslyFocusedRect.offset(getScrollX(), getScrollY());

            // Don't cache the result of getChildCount or mFirstPosition here,
            // it could change in layoutChildren.
            if (adapter.getCount() < getChildCount() + mFirstPosition) {
                mLayoutMode = LAYOUT_NORMAL;
                layoutChildren();
            }

            // Figure out which item should be selected based on previously
            // focused rect.
            Rect otherRect = mTempRect;
            int minDistance = Integer.MAX_VALUE;
            final int childCount = getChildCount();
            final int firstPosition = mFirstPosition;

            for (int i = 0; i < childCount; i++) {
                // Only consider selectable views
                if (!adapter.isEnabled(firstPosition + i)) {
                    continue;
                }

                View other = getChildAt(i);
                other.getDrawingRect(otherRect);
                offsetDescendantRectToMyCoords(other, otherRect);
                int distance = getDistance(previouslyFocusedRect, otherRect, direction);

                if (distance < minDistance) {
                    minDistance = distance;
                    closetChildIndex = i;
                    closestChildStart = getChildStartEdge(other);
                }
            }
        }

        if (closetChildIndex >= 0) {
            setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
        } else {
            requestLayout();
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        final ViewTreeObserver treeObserver = getViewTreeObserver();
        treeObserver.addOnTouchModeChangeListener(this);

        if (mAdapter != null && mDataSetObserver == null) {
            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver);

            // Data may have changed while we were detached. Refresh.
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount();
        }

        mIsAttached = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        // Detach any view left in the scrap heap
        mRecycler.clear();

        final ViewTreeObserver treeObserver = getViewTreeObserver();
        treeObserver.removeOnTouchModeChangeListener(this);

        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
            mDataSetObserver = null;
        }

        if (mPerformClick != null) {
            removeCallbacks(mPerformClick);
        }

        if (mTouchModeReset != null) {
            removeCallbacks(mTouchModeReset);
            mTouchModeReset.run();
        }

        finishSmoothScrolling();

        mIsAttached = false;
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);

        final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;

        if (!hasWindowFocus) {
            if (!mScroller.isFinished()) {
                finishSmoothScrolling();
                if (mOverScroll != 0) {
                    mOverScroll = 0;
                    finishEdgeGlows();
                    invalidate();
                }
            }

            if (touchMode == TOUCH_MODE_OFF) {
                // Remember the last selected element
                mResurrectToPosition = mSelectedPosition;
            }
        } else {
            // If we changed touch mode since the last time we had focus
            if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
                // If we come back in trackball mode, we bring the selection back
                if (touchMode == TOUCH_MODE_OFF) {
                    // This will trigger a layout
                    resurrectSelection();

                // If we come back in touch mode, then we want to hide the selector
                } else {
                    hideSelector();
                    mLayoutMode = LAYOUT_NORMAL;
                    layoutChildren();
                }
            }
        }

        mLastTouchMode = touchMode;
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        boolean needsInvalidate = false;

        if (mIsVertical && mOverScroll != scrollY) {
            onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll);
            mOverScroll = scrollY;
            needsInvalidate = true;
        } else if (!mIsVertical && mOverScroll != scrollX) {
            onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY());
            mOverScroll = scrollX;
            needsInvalidate = true;
        }

        if (needsInvalidate) {
            invalidate();
            awakenScrollbarsInternal();
        }
    }

    @TargetApi(9)
    private boolean overScrollByInternal(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent) {
        if (Build.VERSION.SDK_INT < 9) {
            return false;
        }

        return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
                scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
    }

    @Override
    @TargetApi(9)
    public void setOverScrollMode(int mode) {
        if (Build.VERSION.SDK_INT < 9) {
            return;
        }

        if (mode != ViewCompat.OVER_SCROLL_NEVER) {
            if (mStartEdge == null) {
                Context context = getContext();

                mStartEdge = new EdgeEffectCompat(context);
                mEndEdge = new EdgeEffectCompat(context);
            }
        } else {
            mStartEdge = null;
            mEndEdge = null;
        }

        super.setOverScrollMode(mode);
    }

    public int pointToPosition(int x, int y) {
        Rect frame = mTouchFrame;
        if (frame == null) {
            mTouchFrame = new Rect();
            frame = mTouchFrame;
        }

        final int count = getChildCount();
        for (int i = count - 1; i >= 0; i--) {
            final View child = getChildAt(i);

            if (child.getVisibility() == View.VISIBLE) {
                child.getHitRect(frame);

                if (frame.contains(x, y)) {
                    return mFirstPosition + i;
                }
            }
        }
        return INVALID_POSITION;
    }

    @Override
    protected float getTopFadingEdgeStrength() {
        if (!mIsVertical) {
            return 0f;
        }

        final float fadingEdge = super.getTopFadingEdgeStrength();

        final int childCount = getChildCount();
        if (childCount == 0) {
            return fadingEdge;
        } else {
            if (mFirstPosition > 0) {
                return 1.0f;
            }

            final int top = getChildAt(0).getTop();
            final int paddingTop = getPaddingTop();

            final float length = (float) getVerticalFadingEdgeLength();

            return (top < paddingTop ? (float) -(top - paddingTop) / length : fadingEdge);
        }
    }

    @Override
    protected float getBottomFadingEdgeStrength() {
        if (!mIsVertical) {
            return 0f;
        }

        final float fadingEdge = super.getBottomFadingEdgeStrength();

        final int childCount = getChildCount();
        if (childCount == 0) {
            return fadingEdge;
        } else {
            if (mFirstPosition + childCount - 1 < mItemCount - 1) {
                return 1.0f;
            }

            final int bottom = getChildAt(childCount - 1).getBottom();
            final int paddingBottom = getPaddingBottom();

            final int height = getHeight();
            final float length = (float) getVerticalFadingEdgeLength();

            return (bottom > height - paddingBottom ?
                    (float) (bottom - height + paddingBottom) / length : fadingEdge);
        }
    }

    @Override
    protected float getLeftFadingEdgeStrength() {
        if (mIsVertical) {
            return 0f;
        }

        final float fadingEdge = super.getLeftFadingEdgeStrength();

        final int childCount = getChildCount();
        if (childCount == 0) {
            return fadingEdge;
        } else {
            if (mFirstPosition > 0) {
                return 1.0f;
            }

            final int left = getChildAt(0).getLeft();
            final int paddingLeft = getPaddingLeft();

            final float length = (float) getHorizontalFadingEdgeLength();

            return (left < paddingLeft ? (float) -(left - paddingLeft) / length : fadingEdge);
        }
    }

    @Override
    protected float getRightFadingEdgeStrength() {
        if (mIsVertical) {
            return 0f;
        }

        final float fadingEdge = super.getRightFadingEdgeStrength();

        final int childCount = getChildCount();
        if (childCount == 0) {
            return fadingEdge;
        } else {
            if (mFirstPosition + childCount - 1 < mItemCount - 1) {
                return 1.0f;
            }

            final int right = getChildAt(childCount - 1).getRight();
            final int paddingRight = getPaddingRight();

            final int width = getWidth();
            final float length = (float) getHorizontalFadingEdgeLength();

            return (right > width - paddingRight ?
                    (float) (right - width + paddingRight) / length : fadingEdge);
        }
    }

    @Override
    protected int computeVerticalScrollExtent() {
        final int count = getChildCount();
        if (count == 0) {
            return 0;
        }

        int extent = count * 100;

        View child = getChildAt(0);
        final int childTop = child.getTop();

        int childHeight = child.getHeight();
        if (childHeight > 0) {
            extent += (childTop * 100) / childHeight;
        }

        child = getChildAt(count - 1);
        final int childBottom = child.getBottom();

        childHeight = child.getHeight();
        if (childHeight > 0) {
            extent -= ((childBottom - getHeight()) * 100) / childHeight;
        }

        return extent;
    }

    @Override
    protected int computeHorizontalScrollExtent() {
        final int count = getChildCount();
        if (count == 0) {
            return 0;
        }

        int extent = count * 100;

        View child = getChildAt(0);
        final int childLeft = child.getLeft();

        int childWidth = child.getWidth();
        if (childWidth > 0) {
            extent += (childLeft * 100) / childWidth;
        }

        child = getChildAt(count - 1);
        final int childRight = child.getRight();

        childWidth = child.getWidth();
        if (childWidth > 0) {
            extent -= ((childRight - getWidth()) * 100) / childWidth;
        }

        return extent;
    }

    @Override
    protected int computeVerticalScrollOffset() {
        final int firstPosition = mFirstPosition;
        final int childCount = getChildCount();

        if (firstPosition < 0 || childCount == 0) {
            return 0;
        }

        final View child = getChildAt(0);
        final int childTop = child.getTop();

        int childHeight = child.getHeight();
        if (childHeight > 0) {
            return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0);
        }

        return 0;
    }

    @Override
    protected int computeHorizontalScrollOffset() {
        final int firstPosition = mFirstPosition;
        final int childCount = getChildCount();

        if (firstPosition < 0 || childCount == 0) {
            return 0;
        }

        final View child = getChildAt(0);
        final int childLeft = child.getLeft();

        int childWidth = child.getWidth();
        if (childWidth > 0) {
            return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0);
        }

        return 0;
    }

    @Override
    protected int computeVerticalScrollRange() {
        int result = Math.max(mItemCount * 100, 0);

        if (mIsVertical && mOverScroll != 0) {
            // Compensate for overscroll
            result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100));
        }

        return result;
    }

    @Override
    protected int computeHorizontalScrollRange() {
        int result = Math.max(mItemCount * 100, 0);

        if (!mIsVertical && mOverScroll != 0) {
            // Compensate for overscroll
            result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100));
        }

        return result;
    }

    @Override
    public boolean showContextMenuForChild(View originalView) {
        final int longPressPosition = getPositionForView(originalView);
        if (longPressPosition >= 0) {
            final long longPressId = mAdapter.getItemId(longPressPosition);
            boolean handled = false;

            OnItemLongClickListener listener = getOnItemLongClickListener();
            if (listener != null) {
                handled = listener.onItemLongClick(TwoWayView.this, originalView,
                        longPressPosition, longPressId);
            }

            if (!handled) {
                mContextMenuInfo = createContextMenuInfo(
                        getChildAt(longPressPosition - mFirstPosition),
                        longPressPosition, longPressId);

                handled = super.showContextMenuForChild(originalView);
            }

            return handled;
        }

        return false;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept) {
            recycleVelocityTracker();
        }

        super.requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!mIsAttached || mAdapter == null) {
            return false;
        }

        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            initOrResetVelocityTracker();
            mVelocityTracker.addMovement(ev);

            mScroller.abortAnimation();
            if (mPositionScroller != null) {
                mPositionScroller.stop();
            }

            final float x = ev.getX();
            final float y = ev.getY();

            mLastTouchPos = (mIsVertical ? y : x);

            final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos);

            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderPos = 0;

            if (mTouchMode == TOUCH_MODE_FLINGING) {
                return true;
            } else if (motionPosition >= 0) {
                mMotionPosition = motionPosition;
                mTouchMode = TOUCH_MODE_DOWN;
            }

            break;

        case MotionEvent.ACTION_MOVE: {
            if (mTouchMode != TOUCH_MODE_DOWN) {
                break;
            }

            initVelocityTrackerIfNotExists();
            mVelocityTracker.addMovement(ev);

            final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (index < 0) {
                Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
                        mActivePointerId + " - did TwoWayView receive an inconsistent " +
                        "event stream?");
                return false;
            }

            final float pos;
            if (mIsVertical) {
                pos = MotionEventCompat.getY(ev, index);
            } else {
                pos = MotionEventCompat.getX(ev, index);
            }

            final float diff = pos - mLastTouchPos + mTouchRemainderPos;
            final int delta = (int) diff;
            mTouchRemainderPos = diff - delta;

            if (maybeStartScrolling(delta)) {
                return true;
            }
            
            break;
        }

        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mActivePointerId = INVALID_POINTER;
            mTouchMode = TOUCH_MODE_REST;
            recycleVelocityTracker();
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);

            break;
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!isEnabled()) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return isClickable() || isLongClickable();
        }

        if (!mIsAttached || mAdapter == null) {
            return false;
        }

        boolean needsInvalidate = false;

        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);

        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch (action) {
        case MotionEvent.ACTION_DOWN: {
            if (mDataChanged) {
                break;
            }

            mVelocityTracker.clear();
            mScroller.abortAnimation();
            if (mPositionScroller != null) {
                mPositionScroller.stop();
            }

            final float x = ev.getX();
            final float y = ev.getY();

            mLastTouchPos = (mIsVertical ? y : x);

            int motionPosition = pointToPosition((int) x, (int) y);

            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderPos = 0;

            if (mDataChanged) {
                break;
            }

            if (mTouchMode == TOUCH_MODE_FLINGING) {
                mTouchMode = TOUCH_MODE_DRAGGING;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
            } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) {
                mTouchMode = TOUCH_MODE_DOWN;
                triggerCheckForTap();
            }

            mMotionPosition = motionPosition;

            break;
        }

        case MotionEvent.ACTION_MOVE: {
            final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (index < 0) {
                Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
                        mActivePointerId + " - did TwoWayView receive an inconsistent " +
                        "event stream?");
                return false;
            }

            final float pos;
            if (mIsVertical) {
                pos = MotionEventCompat.getY(ev, index);
            } else {
                pos = MotionEventCompat.getX(ev, index);
            }

            if (mDataChanged) {
                // Re-sync everything if data has been changed
                // since the scroll operation can query the adapter.
                layoutChildren();
            }

            final float diff = pos - mLastTouchPos + mTouchRemainderPos;
            final int delta = (int) diff;
            mTouchRemainderPos = diff - delta;

            switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                // Check if we have moved far enough that it looks more like a
                // scroll than a tap
                maybeStartScrolling(delta);
                break;

            case TOUCH_MODE_DRAGGING:
            case TOUCH_MODE_OVERSCROLL:
                mLastTouchPos = pos;
                maybeScroll(delta);
                break;
            }

            break;
        }

        case MotionEvent.ACTION_CANCEL:
            cancelCheckForTap();
            mTouchMode = TOUCH_MODE_REST;
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);

            setPressed(false);
            View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
            if (motionView != null) {
                motionView.setPressed(false);
            }

            if (mStartEdge != null && mEndEdge != null) {
                needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease();
            }

            recycleVelocityTracker();

            break;

        case MotionEvent.ACTION_UP: {
            switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING: {
                final int motionPosition = mMotionPosition;
                final View child = getChildAt(motionPosition - mFirstPosition);

                final float x = ev.getX();
                final float y = ev.getY();

                final boolean inList;
                if (mIsVertical) {
                    inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
                } else {
                    inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom();
                }

                if (child != null && !child.hasFocusable() && inList) {
                    if (mTouchMode != TOUCH_MODE_DOWN) {
                        child.setPressed(false);
                    }

                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }

                    final PerformClick performClick = mPerformClick;
                    performClick.mClickMotionPosition = motionPosition;
                    performClick.rememberWindowAttachCount();

                    mResurrectToPosition = motionPosition;

                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                        if (mTouchMode == TOUCH_MODE_DOWN) {
                            cancelCheckForTap();
                        } else {
                            cancelCheckForLongPress();
                        }

                        mLayoutMode = LAYOUT_NORMAL;

                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            mTouchMode = TOUCH_MODE_TAP;

                            setPressed(true);
                            positionSelector(mMotionPosition, child);
                            child.setPressed(true);

                            if (mSelector != null) {
                                Drawable d = mSelector.getCurrent();
                                if (d != null && d instanceof TransitionDrawable) {
                                    ((TransitionDrawable) d).resetTransition();
                                }
                            }

                            if (mTouchModeReset != null) {
                                removeCallbacks(mTouchModeReset);
                            }

                            mTouchModeReset = new Runnable() {
                                @Override
                                public void run() {
                                    mTouchMode = TOUCH_MODE_REST;

                                    setPressed(false);
                                    child.setPressed(false);

                                    if (!mDataChanged) {
                                        performClick.run();
                                    }

                                    mTouchModeReset = null;
                                }
                            };

                            postDelayed(mTouchModeReset,
                                    ViewConfiguration.getPressedStateDuration());
                        } else {
                            mTouchMode = TOUCH_MODE_REST;
                            updateSelectorState();
                        }
                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                        performClick.run();
                    }
                }

                mTouchMode = TOUCH_MODE_REST;

                finishSmoothScrolling();
                updateSelectorState();

                break;
            }

            case TOUCH_MODE_DRAGGING:
                if (contentFits()) {
                    mTouchMode = TOUCH_MODE_REST;
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                    break;
                }

                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

                final float velocity;
                if (mIsVertical) {
                    velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
                            mActivePointerId);
                } else {
                    velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
                            mActivePointerId);
                }

                if (Math.abs(velocity) >= mFlingVelocity) {
                    mTouchMode = TOUCH_MODE_FLINGING;
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);

                    mScroller.fling(0, 0,
                                    (int) (mIsVertical ? 0 : velocity),
                                    (int) (mIsVertical ? velocity : 0),
                                    (mIsVertical ? 0 : Integer.MIN_VALUE),
                                    (mIsVertical ? 0 : Integer.MAX_VALUE),
                                    (mIsVertical ? Integer.MIN_VALUE : 0),
                                    (mIsVertical ? Integer.MAX_VALUE : 0));

                    mLastTouchPos = 0;
                    needsInvalidate = true;
                } else {
                    mTouchMode = TOUCH_MODE_REST;
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                }

                break;

            case TOUCH_MODE_OVERSCROLL:
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
                break;
            }

            cancelCheckForTap();
            cancelCheckForLongPress();
            setPressed(false);

            if (mStartEdge != null && mEndEdge != null) {
                needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease();
            }

            recycleVelocityTracker();

            break;
        }
        }

        if (needsInvalidate) {
            ViewCompat.postInvalidateOnAnimation(this);
        }

        return true;
    }

    @Override
    public void onTouchModeChanged(boolean isInTouchMode) {
        if (isInTouchMode) {
            // Get rid of the selection when we enter touch mode
            hideSelector();

            // Layout, but only if we already have done so previously.
            // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
            // state.)
            if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) {
                layoutChildren();
            }

            updateSelectorState();
        } else {
            final int touchMode = mTouchMode;
            if (touchMode == TOUCH_MODE_OVERSCROLL) {
                finishSmoothScrolling();
                if (mOverScroll != 0) {
                    mOverScroll = 0;
                    finishEdgeGlows();
                    invalidate();
                }
            }
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return handleKeyEvent(keyCode, 1, event);
    }

    @Override
    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
        return handleKeyEvent(keyCode, repeatCount, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return handleKeyEvent(keyCode, 1, event);
    }

    @Override
    public void sendAccessibilityEvent(int eventType) {
        // Since this class calls onScrollChanged even if the mFirstPosition and the
        // child count have not changed we will avoid sending duplicate accessibility
        // events.
        if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
            final int firstVisiblePosition = getFirstVisiblePosition();
            final int lastVisiblePosition = getLastVisiblePosition();

            if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
                    && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
                return;
            } else {
                mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
                mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
            }
        }

        super.sendAccessibilityEvent(eventType);
    }

    @Override
    @TargetApi(14)
    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
        super.onInitializeAccessibilityEvent(event);
        event.setClassName(TwoWayView.class.getName());
    }

    @Override
    @TargetApi(14)
    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
        super.onInitializeAccessibilityNodeInfo(info);
        info.setClassName(TwoWayView.class.getName());

        AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);

        if (isEnabled()) {
            if (getFirstVisiblePosition() > 0) {
                infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
            }

            if (getLastVisiblePosition() < getCount() - 1) {
                infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
            }
        }
    }

    @Override
    @TargetApi(16)
    public boolean performAccessibilityAction(int action, Bundle arguments) {
        if (super.performAccessibilityAction(action, arguments)) {
            return true;
        }

        switch (action) {
        case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
            if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
                // TODO: Use some form of smooth scroll instead
                scrollListItemsBy(getAvailableSize());
                return true;
            }
            return false;

        case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
            if (isEnabled() && mFirstPosition > 0) {
                // TODO: Use some form of smooth scroll instead
                scrollListItemsBy(-getAvailableSize());
                return true;
            }
            return false;
        }

        return false;
    }

    /**
     * Return true if child is an ancestor of parent, (or equal to the parent).
     */
    private boolean isViewAncestorOf(View child, View parent) {
        if (child == parent) {
            return true;
        }

        final ViewParent theParent = child.getParent();

        return (theParent instanceof ViewGroup) &&
                 isViewAncestorOf((View) theParent, parent);
    }

    private void forceValidFocusDirection(int direction) {
        if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
            throw new IllegalArgumentException("Focus direction must be one of"
                    + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation");
        } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
            throw new IllegalArgumentException("Focus direction must be one of"
                    + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
        }
    }

    private void forceValidInnerFocusDirection(int direction) {
        if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
            throw new IllegalArgumentException("Direction must be one of"
                    + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
        } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
            throw new IllegalArgumentException("direction must be one of"
                    + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation");
        }
    }

    /**
     * Scrolls up or down by the number of items currently present on screen.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return whether selection was moved
     */
    boolean pageScroll(int direction) {
        forceValidFocusDirection(direction);

        boolean forward = false;
        int nextPage = -1;

        if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
            nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
        } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
            forward = true;
        }

        if (nextPage < 0) {
            return false;
        }

        final int position = lookForSelectablePosition(nextPage, forward);
        if (position >= 0) {
            mLayoutMode = LAYOUT_SPECIFIC;
            mSpecificStart = getStartEdge() + getFadingEdgeLength();

            if (forward && position > mItemCount - getChildCount()) {
                mLayoutMode = LAYOUT_FORCE_BOTTOM;
            }

            if (!forward && position < getChildCount()) {
                mLayoutMode = LAYOUT_FORCE_TOP;
            }

            setSelectionInt(position);
            invokeOnItemScrollListener();

            if (!awakenScrollbarsInternal()) {
                invalidate();
            }

            return true;
        }

        return false;
    }

    /**
     * Go to the last or first item if possible (not worrying about panning across or navigating
     * within the internal focus of the currently selected item.)
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return whether selection was moved
     */
    boolean fullScroll(int direction) {
        forceValidFocusDirection(direction);

        boolean moved = false;
        if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
            if (mSelectedPosition != 0) {
                int position = lookForSelectablePosition(0, true);
                if (position >= 0) {
                    mLayoutMode = LAYOUT_FORCE_TOP;
                    setSelectionInt(position);
                    invokeOnItemScrollListener();
                }

                moved = true;
            }
        } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
            if (mSelectedPosition < mItemCount - 1) {
                int position = lookForSelectablePosition(mItemCount - 1, true);
                if (position >= 0) {
                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
                    setSelectionInt(position);
                    invokeOnItemScrollListener();
                }

                moved = true;
            }
        }

        if (moved && !awakenScrollbarsInternal()) {
            awakenScrollbarsInternal();
            invalidate();
        }

        return moved;
    }

    /**
     * To avoid horizontal/vertical focus searches changing the selected item,
     * we manually focus search within the selected item (as applicable), and
     * prevent focus from jumping to something within another item.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return Whether this consumes the key event.
     */
    private boolean handleFocusWithinItem(int direction) {
        forceValidInnerFocusDirection(direction);

        final int numChildren = getChildCount();

        if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
            final View selectedView = getSelectedView();

            if (selectedView != null && selectedView.hasFocus() &&
                    selectedView instanceof ViewGroup) {

                final View currentFocus = selectedView.findFocus();
                final View nextFocus = FocusFinder.getInstance().findNextFocus(
                        (ViewGroup) selectedView, currentFocus, direction);

                if (nextFocus != null) {
                    // Do the math to get interesting rect in next focus' coordinates
                    currentFocus.getFocusedRect(mTempRect);
                    offsetDescendantRectToMyCoords(currentFocus, mTempRect);
                    offsetRectIntoDescendantCoords(nextFocus, mTempRect);

                    if (nextFocus.requestFocus(direction, mTempRect)) {
                        return true;
                    }
                }

                // We are blocking the key from being handled (by returning true)
                // if the global result is going to be some other view within this
                // list. This is to achieve the overall goal of having horizontal/vertical
                // d-pad navigation remain in the current item depending on the current
                // orientation in this view.
                final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
                        (ViewGroup) getRootView(), currentFocus, direction);

                if (globalNextFocus != null) {
                    return isViewAncestorOf(globalNextFocus, this);
                }
            }
        }

        return false;
    }

    /**
     * Scrolls to the next or previous item if possible.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return whether selection was moved
     */
    private boolean arrowScroll(int direction) {
        forceValidFocusDirection(direction);

        try {
            mInLayout = true;

            final boolean handled = arrowScrollImpl(direction);
            if (handled) {
                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
            }

            return handled;
        } finally {
            mInLayout = false;
        }
    }

    /**
     * When selection changes, it is possible that the previously selected or the
     * next selected item will change its size.  If so, we need to offset some folks,
     * and re-layout the items as appropriate.
     *
     * @param selectedView The currently selected view (before changing selection).
     *   should be <code>null</code> if there was no previous selection.
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     * @param newSelectedPosition The position of the next selection.
     * @param newFocusAssigned whether new focus was assigned.  This matters because
     *        when something has focus, we don't want to show selection (ugh).
     */
    private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
            boolean newFocusAssigned) {
        forceValidFocusDirection(direction);

        if (newSelectedPosition == INVALID_POSITION) {
            throw new IllegalArgumentException("newSelectedPosition needs to be valid");
        }

        // Whether or not we are moving down/right or up/left, we want to preserve the
        // top/left of whatever view is at the start:
        // - moving down/right: the view that had selection
        // - moving up/left: the view that is getting selection
        final int selectedIndex = mSelectedPosition - mFirstPosition;
        final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
        int startViewIndex, endViewIndex;
        boolean topSelected = false;
        View startView;
        View endView;

        if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
            startViewIndex = nextSelectedIndex;
            endViewIndex = selectedIndex;
            startView = getChildAt(startViewIndex);
            endView = selectedView;
            topSelected = true;
        } else {
            startViewIndex = selectedIndex;
            endViewIndex = nextSelectedIndex;
            startView = selectedView;
            endView = getChildAt(endViewIndex);
        }

        final int numChildren = getChildCount();

        // start with top view: is it changing size?
        if (startView != null) {
            startView.setSelected(!newFocusAssigned && topSelected);
            measureAndAdjustDown(startView, startViewIndex, numChildren);
        }

        // is the bottom view changing size?
        if (endView != null) {
            endView.setSelected(!newFocusAssigned && !topSelected);
            measureAndAdjustDown(endView, endViewIndex, numChildren);
        }
    }

    /**
     * Re-measure a child, and if its height changes, lay it out preserving its
     * top, and adjust the children below it appropriately.
     *
     * @param child The child
     * @param childIndex The view group index of the child.
     * @param numChildren The number of children in the view group.
     */
    private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
        int oldSize = getChildSize(child);
        measureChild(child);

        if (getChildMeasuredSize(child) == oldSize) {
            return;
        }

        // lay out the view, preserving its top
        relayoutMeasuredChild(child);

        // adjust views below appropriately
        final int sizeDelta = getChildMeasuredSize(child) - oldSize;
        for (int i = childIndex + 1; i < numChildren; i++) {
            getChildAt(i).offsetTopAndBottom(sizeDelta);
        }
    }

    /**
     * Do an arrow scroll based on focus searching.  If a new view is
     * given focus, return the selection delta and amount to scroll via
     * an {@link ArrowScrollFocusResult}, otherwise, return null.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return The result if focus has changed, or <code>null</code>.
     */
    private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
        forceValidFocusDirection(direction);

        final View selectedView = getSelectedView();
        final View newFocus;
        final int searchPoint;

        if (selectedView != null && selectedView.hasFocus()) {
            View oldFocus = selectedView.findFocus();
            newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
        } else {
            if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
                boolean fadingEdgeShowing = (mFirstPosition > 0);
                final int start = getStartEdge() +
                        (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);

                final int selectedStart;
                if (selectedView != null) {
                    selectedStart = getChildStartEdge(selectedView);
                } else {
                    selectedStart = start;
                }

                searchPoint = Math.max(selectedStart, start);
            } else {
                final boolean fadingEdgeShowing =
                        (mFirstPosition + getChildCount() - 1) < mItemCount;
                final int end = getEndEdge() - (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);

                final int selectedEnd;
                if (selectedView != null) {
                    selectedEnd = getChildEndEdge(selectedView);
                } else {
                    selectedEnd = end;
                }

                searchPoint = Math.min(selectedEnd, end);
            }

            final int x = (mIsVertical ? 0 : searchPoint);
            final int y = (mIsVertical ? searchPoint : 0);
            mTempRect.set(x, y, x, y);

            newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
        }

        if (newFocus != null) {
            final int positionOfNewFocus = positionOfNewFocus(newFocus);

            // If the focus change is in a different new position, make sure
            // we aren't jumping over another selectable position.
            if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
                final int selectablePosition = lookForSelectablePositionOnScreen(direction);

                final boolean movingForward =
                        (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT);
                final boolean movingBackward =
                        (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT);

                if (selectablePosition != INVALID_POSITION &&
                        ((movingForward && selectablePosition < positionOfNewFocus) ||
                         (movingBackward && selectablePosition > positionOfNewFocus))) {
                    return null;
                }
            }

            int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);

            final int maxScrollAmount = getMaxScrollAmount();
            if (focusScroll < maxScrollAmount) {
                // Not moving too far, safe to give next view focus
                newFocus.requestFocus(direction);
                mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
                return mArrowScrollFocusResult;
            } else if (distanceToView(newFocus) < maxScrollAmount){
                // Case to consider:
                // Too far to get entire next focusable on screen, but by going
                // max scroll amount, we are getting it at least partially in view,
                // so give it focus and scroll the max amount.
                newFocus.requestFocus(direction);
                mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
                return mArrowScrollFocusResult;
            }
        }

        return null;
    }

    /**
     * @return The maximum amount a list view will scroll in response to
     *   an arrow event.
     */
    public int getMaxScrollAmount() {
        return (int) (MAX_SCROLL_FACTOR * getSize());
    }

    /**
     * @return The amount to preview next items when arrow scrolling.
     */
    private int getArrowScrollPreviewLength() {
        return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, getFadingEdgeLength());
    }

    /**
     * @param newFocus The view that would have focus.
     * @return the position that contains newFocus
     */
    private int positionOfNewFocus(View newFocus) {
        final int numChildren = getChildCount();

        for (int i = 0; i < numChildren; i++) {
            final View child = getChildAt(i);
            if (isViewAncestorOf(newFocus, child)) {
                return mFirstPosition + i;
            }
        }

        throw new IllegalArgumentException("newFocus is not a child of any of the"
                + " children of the list!");
    }

    /**
     * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
     * whether there are focusable items, etc.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return Whether any scrolling, selection or focus change occurred.
     */
    private boolean arrowScrollImpl(int direction) {
        forceValidFocusDirection(direction);

        if (getChildCount() <= 0) {
            return false;
        }

        View selectedView = getSelectedView();
        int selectedPos = mSelectedPosition;

        int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
        int amountToScroll = amountToScroll(direction, nextSelectedPosition);

        // If we are moving focus, we may OVERRIDE the default behaviour
        final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null);
        if (focusResult != null) {
            nextSelectedPosition = focusResult.getSelectedPosition();
            amountToScroll = focusResult.getAmountToScroll();
        }

        boolean needToRedraw = (focusResult != null);
        if (nextSelectedPosition != INVALID_POSITION) {
            handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);

            setSelectedPositionInt(nextSelectedPosition);
            setNextSelectedPositionInt(nextSelectedPosition);

            selectedView = getSelectedView();
            selectedPos = nextSelectedPosition;

            if (mItemsCanFocus && focusResult == null) {
                // There was no new view found to take focus, make sure we
                // don't leave focus with the old selection.
                final View focused = getFocusedChild();
                if (focused != null) {
                    focused.clearFocus();
                }
            }

            needToRedraw = true;
            checkSelectionChanged();
        }

        if (amountToScroll > 0) {
            scrollListItemsBy(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
                    amountToScroll : -amountToScroll);
            needToRedraw = true;
        }

        // If we didn't find a new focusable, make sure any existing focused
        // item that was panned off screen gives up focus.
        if (mItemsCanFocus && focusResult == null &&
                selectedView != null && selectedView.hasFocus()) {
            final View focused = selectedView.findFocus();
            if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
                focused.clearFocus();
            }
        }

        // If the current selection is panned off, we need to remove the selection
        if (nextSelectedPosition == INVALID_POSITION && selectedView != null
                && !isViewAncestorOf(selectedView, this)) {
            selectedView = null;
            hideSelector();

            // But we don't want to set the ressurect position (that would make subsequent
            // unhandled key events bring back the item we just scrolled off)
            mResurrectToPosition = INVALID_POSITION;
        }

        if (needToRedraw) {
            if (selectedView != null) {
                positionSelector(selectedPos, selectedView);
                mSelectedStart = getChildStartEdge(selectedView);
            }

            if (!awakenScrollbarsInternal()) {
                invalidate();
            }

            invokeOnItemScrollListener();
            return true;
        }

        return false;
    }

    /**
     * Determine how much we need to scroll in order to get the next selected view
     * visible. The amount is capped at {@link #getMaxScrollAmount()}.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     * @param nextSelectedPosition The position of the next selection, or
     *        {@link #INVALID_POSITION} if there is no next selectable position
     *
     * @return The amount to scroll. Note: this is always positive!  Direction
     *         needs to be taken into account when actually scrolling.
     */
    private int amountToScroll(int direction, int nextSelectedPosition) {
        forceValidFocusDirection(direction);

        final int numChildren = getChildCount();

        if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
            final int end = getEndEdge();

            int indexToMakeVisible = numChildren - 1;
            if (nextSelectedPosition != INVALID_POSITION) {
                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
            }

            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
            final View viewToMakeVisible = getChildAt(indexToMakeVisible);

            int goalEnd = end;
            if (positionToMakeVisible < mItemCount - 1) {
                goalEnd -= getArrowScrollPreviewLength();
            }

            final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
            final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);

            if (viewToMakeVisibleEnd <= goalEnd) {
                // Target item is fully visible
                return 0;
            }

            if (nextSelectedPosition != INVALID_POSITION &&
                    (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) {
                // Item already has enough of it visible, changing selection is good enough
                return 0;
            }

            int amountToScroll = (viewToMakeVisibleEnd - goalEnd);

            if (mFirstPosition + numChildren == mItemCount) {
                final int lastChildEnd = getChildEndEdge(getChildAt(numChildren - 1));

                // Last is last in list -> Make sure we don't scroll past it
                final int max = lastChildEnd - end;
                amountToScroll = Math.min(amountToScroll, max);
            }

            return Math.min(amountToScroll, getMaxScrollAmount());
        } else {
            final int start = getStartEdge();

            int indexToMakeVisible = 0;
            if (nextSelectedPosition != INVALID_POSITION) {
                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
            }

            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
            final View viewToMakeVisible = getChildAt(indexToMakeVisible);

            int goalStart = start;
            if (positionToMakeVisible > 0) {
                goalStart += getArrowScrollPreviewLength();
            }

            final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
            final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);

            if (viewToMakeVisibleStart >= goalStart) {
                // Item is fully visible
                return 0;
            }

            if (nextSelectedPosition != INVALID_POSITION &&
                    (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) {
                // Item already has enough of it visible, changing selection is good enough
                return 0;
            }

            int amountToScroll = (goalStart - viewToMakeVisibleStart);

            if (mFirstPosition == 0) {
                final int firstChildStart = getChildStartEdge(getChildAt(0));

                // First is first in list -> make sure we don't scroll past it
                final int max = start - firstChildStart;
                amountToScroll = Math.min(amountToScroll,  max);
            }

            return Math.min(amountToScroll, getMaxScrollAmount());
        }
    }

    /**
     * Determine how much we need to scroll in order to get newFocus in view.
     *
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     * @param newFocus The view that would take focus.
     * @param positionOfNewFocus The position of the list item containing newFocus
     *
     * @return The amount to scroll. Note: this is always positive! Direction
     *   needs to be taken into account when actually scrolling.
     */
    private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
        forceValidFocusDirection(direction);

        int amountToScroll = 0;

        newFocus.getDrawingRect(mTempRect);
        offsetDescendantRectToMyCoords(newFocus, mTempRect);

        if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
            final int start = getStartEdge();
            final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);

            if (newFocusStart < start) {
                amountToScroll = start - newFocusStart;
                if (positionOfNewFocus > 0) {
                    amountToScroll += getArrowScrollPreviewLength();
                }
            }
        } else {
            final int end = getEndEdge();
            final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);

            if (newFocusEnd > end) {
                amountToScroll = newFocusEnd - end;
                if (positionOfNewFocus < mItemCount - 1) {
                    amountToScroll += getArrowScrollPreviewLength();
                }
            }
        }

        return amountToScroll;
    }

    /**
     * Determine the distance to the nearest edge of a view in a particular
     * direction.
     *
     * @param descendant A descendant of this list.
     * @return The distance, or 0 if the nearest edge is already on screen.
     */
    private int distanceToView(View descendant) {
        descendant.getDrawingRect(mTempRect);
        offsetDescendantRectToMyCoords(descendant, mTempRect);

        final int start = getStartEdge();
        final int end = getEndEdge();

        final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
        final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);

        int distance = 0;
        if (viewEnd < start) {
            distance = start - viewEnd;
        } else if (viewStart > end) {
            distance = viewStart - end;
        }

        return distance;
    }

    private boolean handleKeyScroll(KeyEvent event, int count, int direction) {
        boolean handled = false;

        if (KeyEventCompat.hasNoModifiers(event)) {
            handled = resurrectSelectionIfNeeded();
            if (!handled) {
                while (count-- > 0) {
                    if (arrowScroll(direction)) {
                        handled = true;
                    } else {
                        break;
                    }
                }
            }
        } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
            handled = resurrectSelectionIfNeeded() || fullScroll(direction);
        }

        return handled;
    }

    private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) {
        if (mAdapter == null || !mIsAttached) {
            return false;
        }

        if (mDataChanged) {
            layoutChildren();
        }

        boolean handled = false;
        final int action = event.getAction();

        if (action != KeyEvent.ACTION_UP) {
            switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
                if (mIsVertical) {
                    handled = handleKeyScroll(event, count, View.FOCUS_UP);
                } else if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = handleFocusWithinItem(View.FOCUS_UP);
                }
                break;

            case KeyEvent.KEYCODE_DPAD_DOWN: {
                if (mIsVertical) {
                    handled = handleKeyScroll(event, count, View.FOCUS_DOWN);
                } else if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = handleFocusWithinItem(View.FOCUS_DOWN);
                }
                break;
            }

            case KeyEvent.KEYCODE_DPAD_LEFT:
                if (!mIsVertical) {
                    handled = handleKeyScroll(event, count, View.FOCUS_LEFT);
                } else if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = handleFocusWithinItem(View.FOCUS_LEFT);
                }
                break;

            case KeyEvent.KEYCODE_DPAD_RIGHT:
                if (!mIsVertical) {
                    handled = handleKeyScroll(event, count, View.FOCUS_RIGHT);
                } else if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = handleFocusWithinItem(View.FOCUS_RIGHT);
                }
                break;

            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded();
                    if (!handled
                            && event.getRepeatCount() == 0 && getChildCount() > 0) {
                        keyPressed();
                        handled = true;
                    }
                }
                break;

            case KeyEvent.KEYCODE_SPACE:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded() ||
                            pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
                } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
                    handled = resurrectSelectionIfNeeded() ||
                            fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
                }

                handled = true;
                break;

            case KeyEvent.KEYCODE_PAGE_UP:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded() ||
                            pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
                } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
                    handled = resurrectSelectionIfNeeded() ||
                            fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
                }
                break;

            case KeyEvent.KEYCODE_PAGE_DOWN:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded() ||
                            pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
                } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
                    handled = resurrectSelectionIfNeeded() ||
                            fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
                }
                break;

            case KeyEvent.KEYCODE_MOVE_HOME:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded() ||
                            fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
                }
                break;

            case KeyEvent.KEYCODE_MOVE_END:
                if (KeyEventCompat.hasNoModifiers(event)) {
                    handled = resurrectSelectionIfNeeded() ||
                            fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
                }
                break;
            }
        }

        if (handled) {
            return true;
        }

        switch (action) {
        case KeyEvent.ACTION_DOWN:
            return super.onKeyDown(keyCode, event);

        case KeyEvent.ACTION_UP:
            if (!isEnabled()) {
                return true;
            }

            if (isClickable() && isPressed() &&
                    mSelectedPosition >= 0 && mAdapter != null &&
                    mSelectedPosition < mAdapter.getCount()) {

                final View child = getChildAt(mSelectedPosition - mFirstPosition);
                if (child != null) {
                    performItemClick(child, mSelectedPosition, mSelectedRowId);
                    child.setPressed(false);
                }

                setPressed(false);
                return true;
            }

            return false;

        case KeyEvent.ACTION_MULTIPLE:
            return super.onKeyMultiple(keyCode, count, event);

        default:
            return false;
        }
    }

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * Notify our scroll listener (if there is one) of a change in scroll state
     */
    private void invokeOnItemScrollListener() {
        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
        }

        // Dummy values, View's implementation does not use these.
        onScrollChanged(0, 0, 0, 0);
    }

    private void reportScrollStateChange(int newState) {
        if (newState == mLastScrollState) {
            return;
        }

        if (mOnScrollListener != null) {
            mLastScrollState = newState;
            mOnScrollListener.onScrollStateChanged(this, newState);
        }
    }

    private boolean maybeStartScrolling(int delta) {
        final boolean isOverScroll = (mOverScroll != 0);
        if (Math.abs(delta) <= mTouchSlop && !isOverScroll) {
            return false;
        }

        if (isOverScroll) {
            mTouchMode = TOUCH_MODE_OVERSCROLL;
        } else {
            mTouchMode = TOUCH_MODE_DRAGGING;
        }

        // Time to start stealing events! Once we've stolen them, don't
        // let anyone steal from us.
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }

        cancelCheckForLongPress();

        setPressed(false);
        View motionView = getChildAt(mMotionPosition - mFirstPosition);
        if (motionView != null) {
            motionView.setPressed(false);
        }

        reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);

        return true;
    }

    private void maybeScroll(int delta) {
        if (mTouchMode == TOUCH_MODE_DRAGGING) {
            handleDragChange(delta);
        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
            handleOverScrollChange(delta);
        }
    }

    private void handleDragChange(int delta) {
        // Time to start stealing events! Once we've stolen them, don't
        // let anyone steal from us.
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(true);
        }

        final int motionIndex;
        if (mMotionPosition >= 0) {
            motionIndex = mMotionPosition - mFirstPosition;
        } else {
            // If we don't have a motion position that we can reliably track,
            // pick something in the middle to make a best guess at things below.
            motionIndex = getChildCount() / 2;
        }

        int motionViewPrevStart = 0;
        View motionView = this.getChildAt(motionIndex);
        if (motionView != null) {
            motionViewPrevStart = getChildStartEdge(motionView);
        }

        boolean atEdge = scrollListItemsBy(delta);

        motionView = this.getChildAt(motionIndex);
        if (motionView != null) {
            final int motionViewRealStart = getChildStartEdge(motionView);

            if (atEdge) {
                final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart);
                updateOverScrollState(delta, overscroll);
            }
        }
    }

    private void updateOverScrollState(int delta, int overscroll) {
        overScrollByInternal((mIsVertical ? 0 : overscroll),
                             (mIsVertical ? overscroll : 0),
                             (mIsVertical ? 0 : mOverScroll),
                             (mIsVertical ? mOverScroll : 0),
                             0, 0,
                             (mIsVertical ? 0 : mOverscrollDistance),
                             (mIsVertical ? mOverscrollDistance : 0),
                             true);

        if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) {
            // Break fling velocity if we impacted an edge
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }

        final int overscrollMode = ViewCompat.getOverScrollMode(this);
        if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
                (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
            mTouchMode = TOUCH_MODE_OVERSCROLL;

            float pull = (float) overscroll / getSize();
            if (delta > 0) {
                mStartEdge.onPull(pull);

                if (!mEndEdge.isFinished()) {
                    mEndEdge.onRelease();
                }
            } else if (delta < 0) {
                mEndEdge.onPull(pull);

                if (!mStartEdge.isFinished()) {
                    mStartEdge.onRelease();
                }
            }

            if (delta != 0) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

    private void handleOverScrollChange(int delta) {
        final int oldOverScroll = mOverScroll;
        final int newOverScroll = oldOverScroll - delta;

        int overScrollDistance = -delta;
        if ((newOverScroll < 0 && oldOverScroll >= 0) ||
                (newOverScroll > 0 && oldOverScroll <= 0)) {
            overScrollDistance = -oldOverScroll;
            delta += overScrollDistance;
        } else {
            delta = 0;
        }

        if (overScrollDistance != 0) {
            updateOverScrollState(delta, overScrollDistance);
        }

        if (delta != 0) {
            if (mOverScroll != 0) {
                mOverScroll = 0;
                ViewCompat.postInvalidateOnAnimation(this);
            }

            scrollListItemsBy(delta);
            mTouchMode = TOUCH_MODE_DRAGGING;

            // We did not scroll the full amount. Treat this essentially like the
            // start of a new touch scroll
            mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos);
            mTouchRemainderPos = 0;
        }
    }

    /**
     * What is the distance between the source and destination rectangles given the direction of
     * focus navigation between them? The direction basically helps figure out more quickly what is
     * self evident by the relationship between the rects...
     *
     * @param source the source rectangle
     * @param dest the destination rectangle
     * @param direction the direction
     * @return the distance between the rectangles
     */
    private static int getDistance(Rect source, Rect dest, int direction) {
        int sX, sY; // source x, y
        int dX, dY; // dest x, y

        switch (direction) {
        case View.FOCUS_RIGHT:
            sX = source.right;
            sY = source.top + source.height() / 2;
            dX = dest.left;
            dY = dest.top + dest.height() / 2;
            break;

        case View.FOCUS_DOWN:
            sX = source.left + source.width() / 2;
            sY = source.bottom;
            dX = dest.left + dest.width() / 2;
            dY = dest.top;
            break;

        case View.FOCUS_LEFT:
            sX = source.left;
            sY = source.top + source.height() / 2;
            dX = dest.right;
            dY = dest.top + dest.height() / 2;
            break;

        case View.FOCUS_UP:
            sX = source.left + source.width() / 2;
            sY = source.top;
            dX = dest.left + dest.width() / 2;
            dY = dest.bottom;
            break;

        case View.FOCUS_FORWARD:
        case View.FOCUS_BACKWARD:
            sX = source.right + source.width() / 2;
            sY = source.top + source.height() / 2;
            dX = dest.left + dest.width() / 2;
            dY = dest.top + dest.height() / 2;
            break;

        default:
            throw new IllegalArgumentException("direction must be one of "
                    + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
                    + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
        }

        int deltaX = dX - sX;
        int deltaY = dY - sY;

        return deltaY * deltaY + deltaX * deltaX;
    }

    private int findMotionRowOrColumn(int motionPos) {
        int childCount = getChildCount();
        if (childCount == 0) {
            return INVALID_POSITION;
        }

        for (int i = 0; i < childCount; i++) {
            final View v = getChildAt(i);
            if (motionPos <= getChildEndEdge(v)) {
                return mFirstPosition + i;
            }
        }

        return INVALID_POSITION;
    }

    private int findClosestMotionRowOrColumn(int motionPos) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return INVALID_POSITION;
        }

        final int motionRow = findMotionRowOrColumn(motionPos);
        if (motionRow != INVALID_POSITION) {
            return motionRow;
        } else {
            return mFirstPosition + childCount - 1;
        }
    }

    @TargetApi(9)
    private int getScaledOverscrollDistance(ViewConfiguration vc) {
        if (Build.VERSION.SDK_INT < 9) {
            return 0;
        }

        return vc.getScaledOverscrollDistance();
    }

    private int getStartEdge() {
        return (mIsVertical ? getPaddingTop() : getPaddingLeft());
    }

    private int getEndEdge() {
        if (mIsVertical) {
            return (getHeight() - getPaddingBottom());
        } else {
            return (getWidth() - getPaddingRight());
        }
    }

    private int getSize() {
        return (mIsVertical ? getHeight() : getWidth());
    }

    private int getAvailableSize() {
        if (mIsVertical) {
            return getHeight() - getPaddingBottom() - getPaddingTop();
        } else {
            return getWidth() - getPaddingRight() - getPaddingLeft();
        }
    }

    private int getChildStartEdge(View child) {
        return (mIsVertical ? child.getTop() : child.getLeft());
    }

    private int getChildEndEdge(View child) {
        return (mIsVertical ? child.getBottom() : child.getRight());
    }

    private int getChildSize(View child) {
        return (mIsVertical ? child.getHeight() : child.getWidth());
    }

    private int getChildMeasuredSize(View child) {
        return (mIsVertical ? child.getMeasuredHeight() : child.getMeasuredWidth());
    }

    private int getFadingEdgeLength() {
        return (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength());
    }

    private int getMinSelectionPixel(int start, int fadingEdgeLength, int selectedPosition) {
        // First pixel we can draw the selection into.
        int selectionPixelStart = start;
        if (selectedPosition > 0) {
            selectionPixelStart += fadingEdgeLength;
        }

        return selectionPixelStart;
    }

    private int getMaxSelectionPixel(int end, int fadingEdgeLength,
                                     int selectedPosition) {
        int selectionPixelEnd = end;
        if (selectedPosition != mItemCount - 1) {
            selectionPixelEnd -= fadingEdgeLength;
        }

        return selectionPixelEnd;
    }

    private boolean contentFits() {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return true;
        }

        if (childCount != mItemCount) {
            return false;
        }

        View first = getChildAt(0);
        View last = getChildAt(childCount - 1);

        return (getChildStartEdge(first) >= getStartEdge() &&
                getChildEndEdge(last) <= getEndEdge());
    }

    private void triggerCheckForTap() {
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();
        }

        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
    }

    private void cancelCheckForTap() {
        if (mPendingCheckForTap == null) {
            return;
        }

        removeCallbacks(mPendingCheckForTap);
    }

    private void triggerCheckForLongPress() {
        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = new CheckForLongPress();
        }

        mPendingCheckForLongPress.rememberWindowAttachCount();

        postDelayed(mPendingCheckForLongPress,
                ViewConfiguration.getLongPressTimeout());
    }

    private void cancelCheckForLongPress() {
        if (mPendingCheckForLongPress == null) {
            return;
        }

        removeCallbacks(mPendingCheckForLongPress);
    }

    private boolean scrollListItemsBy(int incrementalDelta) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return true;
        }

        final int firstStart = getChildStartEdge(getChildAt(0));
        final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));

        final int paddingTop = getPaddingTop();
        final int paddingLeft = getPaddingLeft();

        final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);

        final int spaceBefore = paddingStart - firstStart;
        final int end = getEndEdge();
        final int spaceAfter = lastEnd - end;

        final int size = getAvailableSize();

        if (incrementalDelta < 0) {
            incrementalDelta = Math.max(-(size - 1), incrementalDelta);
        } else {
            incrementalDelta = Math.min(size - 1, incrementalDelta);
        }

        final int firstPosition = mFirstPosition;

        final boolean cannotScrollDown = (firstPosition == 0 &&
                firstStart >= paddingStart && incrementalDelta >= 0);
        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
                lastEnd <= end && incrementalDelta <= 0);

        if (cannotScrollDown || cannotScrollUp) {
            return incrementalDelta != 0;
        }

        final boolean inTouchMode = isInTouchMode();
        if (inTouchMode) {
            hideSelector();
        }

        int start = 0;
        int count = 0;

        final boolean down = (incrementalDelta < 0);
        if (down) {
            int childrenStart = -incrementalDelta + paddingStart;

            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final int childEnd = getChildEndEdge(child);

                if (childEnd >= childrenStart) {
                    break;
                }

                count++;
                mRecycler.addScrapView(child, firstPosition + i);
            }
        } else {
            int childrenEnd = end - incrementalDelta;

            for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                final int childStart = getChildStartEdge(child);

                if (childStart <= childrenEnd) {
                    break;
                }

                start = i;
                count++;
                mRecycler.addScrapView(child, firstPosition + i);
            }
        }

        mBlockLayoutRequests = true;

        if (count > 0) {
            detachViewsFromParent(start, count);
        }

        // invalidate before moving the children to avoid unnecessary invalidate
        // calls to bubble up from the children all the way to the top
        if (!awakenScrollbarsInternal()) {
           invalidate();
        }

        offsetChildren(incrementalDelta);

        if (down) {
            mFirstPosition += count;
        }

        final int absIncrementalDelta = Math.abs(incrementalDelta);
        if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) {
            fillGap(down);
        }

        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
            final int childIndex = mSelectedPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(mSelectedPosition, getChildAt(childIndex));
            }
        } else if (mSelectorPosition != INVALID_POSITION) {
            final int childIndex = mSelectorPosition - mFirstPosition;
            if (childIndex >= 0 && childIndex < getChildCount()) {
                positionSelector(INVALID_POSITION, getChildAt(childIndex));
            }
        } else {
            mSelectorRect.setEmpty();
        }

        mBlockLayoutRequests = false;

        invokeOnItemScrollListener();

        return false;
    }

    @TargetApi(14)
    private final float getCurrVelocity() {
        if (Build.VERSION.SDK_INT >= 14) {
            return mScroller.getCurrVelocity();
        }

        return 0;
    }

    @TargetApi(5)
    private boolean awakenScrollbarsInternal() {
        return (Build.VERSION.SDK_INT >= 5) && super.awakenScrollBars();
    }

    @Override
    public void computeScroll() {
        if (!mScroller.computeScrollOffset()) {
            return;
        }

        final int pos;
        if (mIsVertical) {
            pos = mScroller.getCurrY();
        } else {
            pos = mScroller.getCurrX();
        }

        final int diff = (int) (pos - mLastTouchPos);
        mLastTouchPos = pos;

        final boolean stopped = scrollListItemsBy(diff);

        if (!stopped && !mScroller.isFinished()) {
            ViewCompat.postInvalidateOnAnimation(this);
        } else {
            if (stopped) {
                final int overScrollMode = ViewCompat.getOverScrollMode(this);
                if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
                    final EdgeEffectCompat edge =
                            (diff > 0 ? mStartEdge : mEndEdge);

                    boolean needsInvalidate =
                            edge.onAbsorb(Math.abs((int) getCurrVelocity()));

                    if (needsInvalidate) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }

                finishSmoothScrolling();
            }

            mTouchMode = TOUCH_MODE_REST;
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
        }
    }

    private void finishEdgeGlows() {
        if (mStartEdge != null) {
            mStartEdge.finish();
        }

        if (mEndEdge != null) {
            mEndEdge.finish();
        }
    }

    private boolean drawStartEdge(Canvas canvas) {
        if (mStartEdge.isFinished()) {
            return false;
        }

        if (mIsVertical) {
            return mStartEdge.draw(canvas);
        }

        final int restoreCount = canvas.save();
        final int height = getHeight();

        canvas.translate(0, height);
        canvas.rotate(270);

        final boolean needsInvalidate = mStartEdge.draw(canvas);
        canvas.restoreToCount(restoreCount);
        return needsInvalidate;
    }

    private boolean drawEndEdge(Canvas canvas) {
        if (mEndEdge.isFinished()) {
            return false;
        }

        final int restoreCount = canvas.save();
        final int width = getWidth();
        final int height = getHeight();

        if (mIsVertical) {
            canvas.translate(-width, height);
            canvas.rotate(180, width, 0);
        } else {
            canvas.translate(width, 0);
            canvas.rotate(90);
        }

        final boolean needsInvalidate = mEndEdge.draw(canvas);
        canvas.restoreToCount(restoreCount);
        return needsInvalidate;
    }

    private void finishSmoothScrolling() {
        mTouchMode = TOUCH_MODE_REST;
        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);

        mScroller.abortAnimation();
        if (mPositionScroller != null) {
            mPositionScroller.stop();
        }
    }

    private void drawSelector(Canvas canvas) {
        if (!mSelectorRect.isEmpty()) {
            final Drawable selector = mSelector;
            selector.setBounds(mSelectorRect);
            selector.draw(canvas);
        }
    }

    private void useDefaultSelector() {
        setSelector(getResources().getDrawable(
                android.R.drawable.list_selector_background));
    }

    private boolean shouldShowSelector() {
        return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
    }

    private void positionSelector(int position, View selected) {
        if (position != INVALID_POSITION) {
            mSelectorPosition = position;
        }

        mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(),
                selected.getBottom());

        final boolean isChildViewEnabled = mIsChildViewEnabled;
        if (selected.isEnabled() != isChildViewEnabled) {
            mIsChildViewEnabled = !isChildViewEnabled;

            if (getSelectedItemPosition() != INVALID_POSITION) {
                refreshDrawableState();
            }
        }
    }

    private void hideSelector() {
        if (mSelectedPosition != INVALID_POSITION) {
            if (mLayoutMode != LAYOUT_SPECIFIC) {
                mResurrectToPosition = mSelectedPosition;
            }

            if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
                mResurrectToPosition = mNextSelectedPosition;
            }

            setSelectedPositionInt(INVALID_POSITION);
            setNextSelectedPositionInt(INVALID_POSITION);

            mSelectedStart = 0;
        }
    }

    private void setSelectedPositionInt(int position) {
        mSelectedPosition = position;
        mSelectedRowId = getItemIdAtPosition(position);
    }

    private void setSelectionInt(int position) {
        setNextSelectedPositionInt(position);
        boolean awakeScrollbars = false;

        final int selectedPosition = mSelectedPosition;
        if (selectedPosition >= 0) {
            if (position == selectedPosition - 1) {
                awakeScrollbars = true;
            } else if (position == selectedPosition + 1) {
                awakeScrollbars = true;
            }
        }

        layoutChildren();

        if (awakeScrollbars) {
            awakenScrollbarsInternal();
        }
    }

    private void setNextSelectedPositionInt(int position) {
        mNextSelectedPosition = position;
        mNextSelectedRowId = getItemIdAtPosition(position);

        // If we are trying to sync to the selection, update that too
        if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
            mSyncPosition = position;
            mSyncRowId = mNextSelectedRowId;
        }
    }

    private boolean touchModeDrawsInPressedState() {
        switch (mTouchMode) {
        case TOUCH_MODE_TAP:
        case TOUCH_MODE_DONE_WAITING:
            return true;
        default:
            return false;
        }
    }

    /**
     * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
     * this is a long press.
     */
    private void keyPressed() {
        if (!isEnabled() || !isClickable()) {
            return;
        }

        final Drawable selector = mSelector;
        final Rect selectorRect = mSelectorRect;

        if (selector != null && (isFocused() || touchModeDrawsInPressedState())
                && !selectorRect.isEmpty()) {

            final View child = getChildAt(mSelectedPosition - mFirstPosition);

            if (child != null) {
                if (child.hasFocusable()) {
                    return;
                }

                child.setPressed(true);
            }

            setPressed(true);

            final boolean longClickable = isLongClickable();
            final Drawable d = selector.getCurrent();
            if (d != null && d instanceof TransitionDrawable) {
                if (longClickable) {
                    ((TransitionDrawable) d).startTransition(
                            ViewConfiguration.getLongPressTimeout());
                } else {
                    ((TransitionDrawable) d).resetTransition();
                }
            }

            if (longClickable && !mDataChanged) {
                if (mPendingCheckForKeyLongPress == null) {
                    mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
                }

                mPendingCheckForKeyLongPress.rememberWindowAttachCount();
                postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
            }
        }
    }

    private void updateSelectorState() {
        if (mSelector != null) {
            if (shouldShowSelector()) {
                mSelector.setState(getDrawableState());
            } else {
                mSelector.setState(STATE_NOTHING);
            }
        }
    }

    private void checkSelectionChanged() {
        if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
            selectionChanged();
            mOldSelectedPosition = mSelectedPosition;
            mOldSelectedRowId = mSelectedRowId;
        }
    }

    private void selectionChanged() {
        OnItemSelectedListener listener = getOnItemSelectedListener();
        if (listener == null) {
            return;
        }

        if (mInLayout || mBlockLayoutRequests) {
            // If we are in a layout traversal, defer notification
            // by posting. This ensures that the view tree is
            // in a consistent state and is able to accommodate
            // new layout or invalidate requests.
            if (mSelectionNotifier == null) {
                mSelectionNotifier = new SelectionNotifier();
            }

            post(mSelectionNotifier);
        } else {
            fireOnSelected();
            performAccessibilityActionsOnSelected();
        }
    }

    private void fireOnSelected() {
        OnItemSelectedListener listener = getOnItemSelectedListener();
        if (listener == null) {
            return;
        }

        final int selection = getSelectedItemPosition();
        if (selection >= 0) {
            View v = getSelectedView();
            listener.onItemSelected(this, v, selection,
                    mAdapter.getItemId(selection));
        } else {
            listener.onNothingSelected(this);
        }
    }

    private void performAccessibilityActionsOnSelected() {
        final int position = getSelectedItemPosition();
        if (position >= 0) {
            // We fire selection events here not in View
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
        }
    }

    private int lookForSelectablePosition(int position) {
        return lookForSelectablePosition(position, true);
    }

    private int lookForSelectablePosition(int position, boolean lookDown) {
        final ListAdapter adapter = mAdapter;
        if (adapter == null || isInTouchMode()) {
            return INVALID_POSITION;
        }

        final int itemCount = mItemCount;
        if (!mAreAllItemsSelectable) {
            if (lookDown) {
                position = Math.max(0, position);
                while (position < itemCount && !adapter.isEnabled(position)) {
                    position++;
                }
            } else {
                position = Math.min(position, itemCount - 1);
                while (position >= 0 && !adapter.isEnabled(position)) {
                    position--;
                }
            }

            if (position < 0 || position >= itemCount) {
                return INVALID_POSITION;
            }

            return position;
        } else {
            if (position < 0 || position >= itemCount) {
                return INVALID_POSITION;
            }

            return position;
        }
    }

    /**
     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
     *        current view orientation.
     *
     * @return The position of the next selectable position of the views that
     *         are currently visible, taking into account the fact that there might
     *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
     *         selectable view on screen in the given direction.
     */
    private int lookForSelectablePositionOnScreen(int direction) {
        forceValidFocusDirection(direction);

        final int firstPosition = mFirstPosition;
        final ListAdapter adapter = getAdapter();

        if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
            int startPos = (mSelectedPosition != INVALID_POSITION ?
                    mSelectedPosition + 1 : firstPosition);

            if (startPos >= adapter.getCount()) {
                return INVALID_POSITION;
            }

            if (startPos < firstPosition) {
                startPos = firstPosition;
            }

            final int lastVisiblePos = getLastVisiblePosition();

            for (int pos = startPos; pos <= lastVisiblePos; pos++) {
                if (adapter.isEnabled(pos)
                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
                    return pos;
                }
            }
        } else {
            final int last = firstPosition + getChildCount() - 1;

            int startPos = (mSelectedPosition != INVALID_POSITION) ?
                    mSelectedPosition - 1 : firstPosition + getChildCount() - 1;

            if (startPos < 0 || startPos >= adapter.getCount()) {
                return INVALID_POSITION;
            }

            if (startPos > last) {
                startPos = last;
            }

            for (int pos = startPos; pos >= firstPosition; pos--) {
                if (adapter.isEnabled(pos)
                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
                    return pos;
                }
            }
        }

        return INVALID_POSITION;
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        updateSelectorState();
    }

    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        // If the child view is enabled then do the default behavior.
        if (mIsChildViewEnabled) {
            // Common case
            return super.onCreateDrawableState(extraSpace);
        }

        // The selector uses this View's drawable state. The selected child view
        // is disabled, so we need to remove the enabled state from the drawable
        // states.
        final int enabledState = ENABLED_STATE_SET[0];

        // If we don't have any extra space, it will return one of the static state arrays,
        // and clearing the enabled state on those arrays is a bad thing!  If we specify
        // we need extra space, it will create+copy into a new array that safely mutable.
        int[] state = super.onCreateDrawableState(extraSpace + 1);
        int enabledPos = -1;
        for (int i = state.length - 1; i >= 0; i--) {
            if (state[i] == enabledState) {
                enabledPos = i;
                break;
            }
        }

        // Remove the enabled state
        if (enabledPos >= 0) {
            System.arraycopy(state, enabledPos + 1, state, enabledPos,
                    state.length - enabledPos - 1);
        }

        return state;
    }

    @Override
    protected boolean canAnimate() {
        return (super.canAnimate() && mItemCount > 0);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        final boolean drawSelectorOnTop = mDrawSelectorOnTop;
        if (!drawSelectorOnTop) {
            drawSelector(canvas);
        }

        super.dispatchDraw(canvas);

        if (drawSelectorOnTop) {
            drawSelector(canvas);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        boolean needsInvalidate = false;

        if (mStartEdge != null) {
            needsInvalidate |= drawStartEdge(canvas);
        }

        if (mEndEdge != null) {
            needsInvalidate |= drawEndEdge(canvas);
        }

        if (needsInvalidate) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    public void requestLayout() {
        if (!mInLayout && !mBlockLayoutRequests) {
            super.requestLayout();
        }
    }

    @Override
    public View getSelectedView() {
        if (mItemCount > 0 && mSelectedPosition >= 0) {
            return getChildAt(mSelectedPosition - mFirstPosition);
        } else {
            return null;
        }
    }

    @Override
    public void setSelection(int position) {
        setSelectionFromOffset(position, 0);
    }

    public void setSelectionFromOffset(int position, int offset) {
        if (mAdapter == null) {
            return;
        }

        if (!isInTouchMode()) {
            position = lookForSelectablePosition(position);
            if (position >= 0) {
                setNextSelectedPositionInt(position);
            }
        } else {
            mResurrectToPosition = position;
        }

        if (position >= 0) {
            mLayoutMode = LAYOUT_SPECIFIC;

            if (mIsVertical) {
                mSpecificStart = getPaddingTop() + offset;
            } else {
                mSpecificStart = getPaddingLeft() + offset;
            }

            if (mNeedSync) {
                mSyncPosition = position;
                mSyncRowId = mAdapter.getItemId(position);
            }

            requestLayout();
        }
    }

    public void scrollBy(int offset) {
        scrollListItemsBy(-offset);
    }

    /**
     * Smoothly scroll to the specified adapter position. The view will
     * scroll such that the indicated position is displayed.
     * @param position Scroll to this adapter position.
     */
    public void smoothScrollToPosition(int position) {
        if (mPositionScroller == null) {
            mPositionScroller = new PositionScroller();
        }
        mPositionScroller.start(position);
    }

    /**
     * Smoothly scroll to the specified adapter position. The view will scroll
     * such that the indicated position is displayed <code>offset</code> pixels from
     * the top/left edge of the view, according to the orientation. If this is
     * impossible, (e.g. the offset would scroll the first or last item beyond the boundaries
     * of the list) it will get as close as possible. The scroll will take
     * <code>duration</code> milliseconds to complete.
     *
     * @param position Position to scroll to
     * @param offset Desired distance in pixels of <code>position</code> from the top/left
     *               of the view when scrolling is finished
     * @param duration Number of milliseconds to use for the scroll
     */
    public void smoothScrollToPositionFromOffset(int position, int offset, int duration) {
        if (mPositionScroller == null) {
            mPositionScroller = new PositionScroller();
        }
        mPositionScroller.startWithOffset(position, offset, duration);
    }

    /**
     * Smoothly scroll to the specified adapter position. The view will scroll
     * such that the indicated position is displayed <code>offset</code> pixels from
     * the top edge of the view. If this is impossible, (e.g. the offset would scroll
     * the first or last item beyond the boundaries of the list) it will get as close
     * as possible.
     *
     * @param position Position to scroll to
     * @param offset Desired distance in pixels of <code>position</code> from the top
     *               of the view when scrolling is finished
     */
    public void smoothScrollToPositionFromOffset(int position, int offset) {
        if (mPositionScroller == null) {
            mPositionScroller = new PositionScroller();
        }
        mPositionScroller.startWithOffset(position, offset);
    }

    /**
     * Smoothly scroll to the specified adapter position. The view will
     * scroll such that the indicated position is displayed, but it will
     * stop early if scrolling further would scroll boundPosition out of
     * view.
     *
     * @param position Scroll to this adapter position.
     * @param boundPosition Do not scroll if it would move this adapter
     *          position out of view.
     */
    public void smoothScrollToPosition(int position, int boundPosition) {
        if (mPositionScroller == null) {
            mPositionScroller = new PositionScroller();
        }
        mPositionScroller.start(position, boundPosition);
    }

    /**
     * Smoothly scroll by distance pixels over duration milliseconds.
     * @param distance Distance to scroll in pixels.
     * @param duration Duration of the scroll animation in milliseconds.
     */
    public void smoothScrollBy(int distance, int duration) {
        // No sense starting to scroll if we're not going anywhere
        final int firstPosition = mFirstPosition;
        final int childCount = getChildCount();
        final int lastPosition = firstPosition + childCount;
        final int start = getStartEdge();
        final int end = getEndEdge();

        if (distance == 0 || mItemCount == 0 || childCount == 0 ||
                (firstPosition == 0 && getChildStartEdge(getChildAt(0)) == start && distance < 0) ||
                (lastPosition == mItemCount &&
                            getChildEndEdge(getChildAt(childCount - 1)) == end && distance > 0)) {
            finishSmoothScrolling();
        } else {
            mScroller.startScroll(0, 0,
                                  mIsVertical ? 0 : -distance,
                                  mIsVertical ? -distance : 0,
                                  duration);

            mLastTouchPos = 0;

            mTouchMode = TOUCH_MODE_FLINGING;
            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);

            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        // Dispatch in the normal way
        boolean handled = super.dispatchKeyEvent(event);
        if (!handled) {
            // If we didn't handle it...
            final View focused = getFocusedChild();
            if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
                // ... and our focused child didn't handle it
                // ... give it to ourselves so we can scroll if necessary
                handled = onKeyDown(event.getKeyCode(), event);
            }
        }

        return handled;
    }

    @Override
    protected void dispatchSetPressed(boolean pressed) {
        // Don't dispatch setPressed to our children. We call setPressed on ourselves to
        // get the selector in the right state, but we don't want to press each child.
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mSelector == null) {
            useDefaultSelector();
        }

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;

        mItemCount = (mAdapter == null ? 0 : mAdapter.getCount());
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
                heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            final int secondaryMeasureSpec =
                    (mIsVertical ? widthMeasureSpec : heightMeasureSpec);

            measureScrapChild(child, 0, secondaryMeasureSpec);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();

            if (recycleOnMeasure()) {
                mRecycler.addScrapView(child, -1);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = getPaddingLeft() + getPaddingRight() + childWidth;
            if (mIsVertical) {
                widthSize += getVerticalScrollbarWidth();
            }
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = getPaddingTop() + getPaddingBottom() + childHeight;
            if (!mIsVertical) {
                heightSize += getHorizontalScrollbarHeight();
            }
        }

        if (mIsVertical && heightMode == MeasureSpec.AT_MOST) {
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) {
            widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1);
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout = true;

        if (changed) {
            final int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).forceLayout();
            }

            mRecycler.markChildrenDirty();
        }

        layoutChildren();

        mInLayout = false;

        final int width = r - l - getPaddingLeft() - getPaddingRight();
        final int height = b - t - getPaddingTop() - getPaddingBottom();

        if (mStartEdge != null && mEndEdge != null) {
            if (mIsVertical) {
                mStartEdge.setSize(width, height);
                mEndEdge.setSize(width, height);
            } else {
                mStartEdge.setSize(height, width);
                mEndEdge.setSize(height, width);
            }
        }
    }

    private void layoutChildren() {
        if (getWidth() == 0 || getHeight() == 0) {
            return;
        }

        final boolean blockLayoutRequests = mBlockLayoutRequests;
        if (!blockLayoutRequests) {
            mBlockLayoutRequests = true;
        } else {
            return;
        }

        try {
            invalidate();

            if (mAdapter == null) {
                resetState();
                return;
            }

            final int start = getStartEdge();
            final int end = getEndEdge();

            int childCount = getChildCount();
            int index = 0;
            int delta = 0;

            View focusLayoutRestoreView = null;

            View selected = null;
            View oldSelected = null;
            View newSelected = null;
            View oldFirstChild = null;

            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                index = mNextSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    newSelected = getChildAt(index);
                }

                break;

            case LAYOUT_FORCE_TOP:
            case LAYOUT_FORCE_BOTTOM:
            case LAYOUT_SPECIFIC:
            case LAYOUT_SYNC:
                break;

            case LAYOUT_MOVE_SELECTION:
            default:
                // Remember the previously selected view
                index = mSelectedPosition - mFirstPosition;
                if (index >= 0 && index < childCount) {
                    oldSelected = getChildAt(index);
                }

                // Remember the previous first child
                oldFirstChild = getChildAt(0);

                if (mNextSelectedPosition >= 0) {
                    delta = mNextSelectedPosition - mSelectedPosition;
                }

                // Caution: newSelected might be null
                newSelected = getChildAt(index + delta);
            }

            final boolean dataChanged = mDataChanged;
            if (dataChanged) {
                handleDataChanged();
            }

            // Handle the empty set by removing all views that are visible
            // and calling it a day
            if (mItemCount == 0) {
                resetState();
                return;
            } else if (mItemCount != mAdapter.getCount()) {
                throw new IllegalStateException("The content of the adapter has changed but "
                        + "TwoWayView did not receive a notification. Make sure the content of "
                        + "your adapter is not modified from a background thread, but only "
                        + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass()
                        + ") with Adapter(" + mAdapter.getClass() + ")]");
            }

            setSelectedPositionInt(mNextSelectedPosition);

            // Reset the focus restoration
            View focusLayoutRestoreDirectChild = null;

            // Pull all children into the RecycleBin.
            // These views will be reused if possible
            final int firstPosition = mFirstPosition;
            final RecycleBin recycleBin = mRecycler;

            if (dataChanged) {
                for (int i = 0; i < childCount; i++) {
                    recycleBin.addScrapView(getChildAt(i), firstPosition + i);
                }
            } else {
                recycleBin.fillActiveViews(childCount, firstPosition);
            }

            // Take focus back to us temporarily to avoid the eventual
            // call to clear focus when removing the focused child below
            // from messing things up when ViewAncestor assigns focus back
            // to someone else.
            final View focusedChild = getFocusedChild();
            if (focusedChild != null) {
                // We can remember the focused view to restore after relayout if the
                // data hasn't changed, or if the focused position is a header or footer.
                if (!dataChanged) {
                    focusLayoutRestoreDirectChild = focusedChild;

                    // Remember the specific view that had focus
                    focusLayoutRestoreView = findFocus();
                    if (focusLayoutRestoreView != null) {
                        // Tell it we are going to mess with it
                        focusLayoutRestoreView.onStartTemporaryDetach();
                    }
                }

                requestFocus();
            }

            // FIXME: We need a way to save current accessibility focus here
            // so that it can be restored after we re-attach the children on each
            // layout round.

            detachAllViewsFromParent();

            switch (mLayoutMode) {
            case LAYOUT_SET_SELECTION:
                if (newSelected != null) {
                    final int newSelectedStart = getChildStartEdge(newSelected);
                    selected = fillFromSelection(newSelectedStart, start, end);
                } else {
                    selected = fillFromMiddle(start, end);
                }

                break;

            case LAYOUT_SYNC:
                selected = fillSpecific(mSyncPosition, mSpecificStart);
                break;

            case LAYOUT_FORCE_BOTTOM:
                selected = fillBefore(mItemCount - 1, end);
                adjustViewsStartOrEnd();
                break;

            case LAYOUT_FORCE_TOP:
                mFirstPosition = 0;
                selected = fillFromOffset(start);
                adjustViewsStartOrEnd();
                break;

            case LAYOUT_SPECIFIC:
                selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart);
                break;

            case LAYOUT_MOVE_SELECTION:
                selected = moveSelection(oldSelected, newSelected, delta, start, end);
                break;

            default:
                if (childCount == 0) {
                    final int position = lookForSelectablePosition(0);
                    setSelectedPositionInt(position);
                    selected = fillFromOffset(start);
                } else {
                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
                        int offset = start;
                        if (oldSelected != null) {
                            offset = getChildStartEdge(oldSelected);
                        }
                        selected = fillSpecific(mSelectedPosition, offset);
                    } else if (mFirstPosition < mItemCount) {
                        int offset = start;
                        if (oldFirstChild != null) {
                            offset = getChildStartEdge(oldFirstChild);
                        }

                        selected = fillSpecific(mFirstPosition, offset);
                    } else {
                        selected = fillSpecific(0, start);
                    }
                }

                break;

            }

            recycleBin.scrapActiveViews();

            if (selected != null) {
                if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
                    final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
                            focusLayoutRestoreView != null &&
                            focusLayoutRestoreView.requestFocus()) || selected.requestFocus();

                    if (!focusWasTaken) {
                        // Selected item didn't take focus, fine, but still want
                        // to make sure something else outside of the selected view
                        // has focus
                        final View focused = getFocusedChild();
                        if (focused != null) {
                            focused.clearFocus();
                        }

                        positionSelector(INVALID_POSITION, selected);
                    } else {
                        selected.setSelected(false);
                        mSelectorRect.setEmpty();
                    }
                } else {
                    positionSelector(INVALID_POSITION, selected);
                }

                mSelectedStart = getChildStartEdge(selected);
            } else {
                if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
                    View child = getChildAt(mMotionPosition - mFirstPosition);

                    if (child != null) {
                        positionSelector(mMotionPosition, child);
                    }
                } else {
                    mSelectedStart = 0;
                    mSelectorRect.setEmpty();
                }

                // Even if there is not selected position, we may need to restore
                // focus (i.e. something focusable in touch mode)
                if (hasFocus() && focusLayoutRestoreView != null) {
                    focusLayoutRestoreView.requestFocus();
                }
            }

            // Tell focus view we are done mucking with it, if it is still in
            // our view hierarchy.
            if (focusLayoutRestoreView != null
                    && focusLayoutRestoreView.getWindowToken() != null) {
                focusLayoutRestoreView.onFinishTemporaryDetach();
            }

            mLayoutMode = LAYOUT_NORMAL;
            mDataChanged = false;
            mNeedSync = false;

            setNextSelectedPositionInt(mSelectedPosition);
            if (mItemCount > 0) {
                checkSelectionChanged();
            }

            invokeOnItemScrollListener();
        } finally {
            if (!blockLayoutRequests) {
                mBlockLayoutRequests = false;
                mDataChanged = false;
            }
        }
    }

    protected boolean recycleOnMeasure() {
        return true;
    }

    private void offsetChildren(int offset) {
        final int childCount = getChildCount();

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);

            if (mIsVertical) {
                child.offsetTopAndBottom(offset);
            } else {
                child.offsetLeftAndRight(offset);
            }
        }
    }

    private View moveSelection(View oldSelected, View newSelected, int delta, int start,
            int end) {
        final int fadingEdgeLength = getFadingEdgeLength();
        final int selectedPosition = mSelectedPosition;

        final int oldSelectedStart = getChildStartEdge(oldSelected);
        final int oldSelectedEnd = getChildEndEdge(oldSelected);

        final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
        final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);

        View selected = null;

        if (delta > 0) {
            /*
             * Case 1: Scrolling down.
             */

            /*
             *     Before           After
             *    |       |        |       |
             *    +-------+        +-------+
             *    |   A   |        |   A   |
             *    |   1   |   =>   +-------+
             *    +-------+        |   B   |
             *    |   B   |        |   2   |
             *    +-------+        +-------+
             *    |       |        |       |
             *
             *    Try to keep the top of the previously selected item where it was.
             *    oldSelected = A
             *    selected = B
             */

            // Put oldSelected (A) where it belongs
            oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false);

            final int itemMargin = mItemMargin;

            // Now put the new selection (B) below that
            selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);

            final int selectedStart = getChildStartEdge(selected);
            final int selectedEnd = getChildEndEdge(selected);

            // Some of the newly selected item extends below the bottom of the list
            if (selectedEnd > end) {
                // Find space available above the selection into which we can scroll upwards
                final int spaceBefore = selectedStart - minStart;

                // Find space required to bring the bottom of the selected item fully into view
                final int spaceAfter = selectedEnd - maxEnd;

                // Don't scroll more than half the size of the list
                final int halfSpace = (end - start) / 2;
                int offset = Math.min(spaceBefore, spaceAfter);
                offset = Math.min(offset, halfSpace);

                if (mIsVertical) {
                    oldSelected.offsetTopAndBottom(-offset);
                    selected.offsetTopAndBottom(-offset);
                } else {
                    oldSelected.offsetLeftAndRight(-offset);
                    selected.offsetLeftAndRight(-offset);
                }
            }

            // Fill in views before and after
            fillBefore(mSelectedPosition - 2, selectedStart - itemMargin);
            adjustViewsStartOrEnd();
            fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin);
        } else if (delta < 0) {
            /*
             * Case 2: Scrolling up.
             */

            /*
             *     Before           After
             *    |       |        |       |
             *    +-------+        +-------+
             *    |   A   |        |   A   |
             *    +-------+   =>   |   1   |
             *    |   B   |        +-------+
             *    |   2   |        |   B   |
             *    +-------+        +-------+
             *    |       |        |       |
             *
             *    Try to keep the top of the item about to become selected where it was.
             *    newSelected = A
             *    olSelected = B
             */

            if (newSelected != null) {
                // Try to position the top of newSel (A) where it was before it was selected
                final int newSelectedStart = getChildStartEdge(newSelected);
                selected = makeAndAddView(selectedPosition, newSelectedStart, true, true);
            } else {
                // If (A) was not on screen and so did not have a view, position
                // it above the oldSelected (B)
                selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
            }

            final int selectedStart = getChildStartEdge(selected);
            final int selectedEnd = getChildEndEdge(selected);

            // Some of the newly selected item extends above the top of the list
            if (selectedStart < minStart) {
                // Find space required to bring the top of the selected item fully into view
                final int spaceBefore = minStart - selectedStart;

               // Find space available below the selection into which we can scroll downwards
                final int spaceAfter = maxEnd - selectedEnd;

                // Don't scroll more than half the height of the list
                final int halfSpace = (end - start) / 2;
                int offset = Math.min(spaceBefore, spaceAfter);
                offset = Math.min(offset, halfSpace);

                if (mIsVertical) {
                    selected.offsetTopAndBottom(offset);
                } else {
                    selected.offsetLeftAndRight(offset);
                }
            }

            // Fill in views above and below
            fillBeforeAndAfter(selected, selectedPosition);
        } else {
            /*
             * Case 3: Staying still
             */

            selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);

            final int selectedStart = getChildStartEdge(selected);
            final int selectedEnd = getChildEndEdge(selected);

            // We're staying still...
            if (oldSelectedStart < start) {
                // ... but the top of the old selection was off screen.
                // (This can happen if the data changes size out from under us)
                int newEnd = selectedEnd;
                if (newEnd < start + 20) {
                    // Not enough visible -- bring it onscreen
                    if (mIsVertical) {
                        selected.offsetTopAndBottom(start - selectedStart);
                    } else {
                        selected.offsetLeftAndRight(start - selectedStart);
                    }
                }
            }

            // Fill in views above and below
            fillBeforeAndAfter(selected, selectedPosition);
        }

        return selected;
    }

    void confirmCheckedPositionsById() {
        // Clear out the positional check states, we'll rebuild it below from IDs.
        mCheckStates.clear();

        for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
            final long id = mCheckedIdStates.keyAt(checkedIndex);
            final int lastPos = mCheckedIdStates.valueAt(checkedIndex);

            final long lastPosId = mAdapter.getItemId(lastPos);
            if (id != lastPosId) {
                // Look around to see if the ID is nearby. If not, uncheck it.
                final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
                final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
                boolean found = false;

                for (int searchPos = start; searchPos < end; searchPos++) {
                    final long searchId = mAdapter.getItemId(searchPos);
                    if (id == searchId) {
                        found = true;
                        mCheckStates.put(searchPos, true);
                        mCheckedIdStates.setValueAt(checkedIndex, searchPos);
                        break;
                    }
                }

                if (!found) {
                    mCheckedIdStates.delete(id);
                    checkedIndex--;
                    mCheckedItemCount--;
                }
            } else {
                mCheckStates.put(lastPos, true);
            }
        }
    }

    private void handleDataChanged() {
        if (mChoiceMode != ChoiceMode.NONE && mAdapter != null && mAdapter.hasStableIds()) {
            confirmCheckedPositionsById();
        }

        mRecycler.clearTransientStateViews();

        final int itemCount = mItemCount;
        if (itemCount > 0) {
            int newPos;
            int selectablePos;

            // Find the row we are supposed to sync to
            if (mNeedSync) {
                // Update this first, since setNextSelectedPositionInt inspects it
                mNeedSync = false;
                mPendingSync = null;

                switch (mSyncMode) {
                case SYNC_SELECTED_POSITION:
                    if (isInTouchMode()) {
                        // We saved our state when not in touch mode. (We know this because
                        // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
                        // restore in touch mode. Just leave mSyncPosition as it is (possibly
                        // adjusting if the available range changed) and return.
                        mLayoutMode = LAYOUT_SYNC;
                        mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);

                        return;
                    } else {
                        // See if we can find a position in the new data with the same
                        // id as the old selection. This will change mSyncPosition.
                        newPos = findSyncPosition();
                        if (newPos >= 0) {
                            // Found it. Now verify that new selection is still selectable
                            selectablePos = lookForSelectablePosition(newPos, true);
                            if (selectablePos == newPos) {
                                // Same row id is selected
                                mSyncPosition = newPos;

                                if (mSyncSize == getSize()) {
                                    // If we are at the same height as when we saved state, try
                                    // to restore the scroll position too.
                                    mLayoutMode = LAYOUT_SYNC;
                                } else {
                                    // We are not the same height as when the selection was saved, so
                                    // don't try to restore the exact position
                                    mLayoutMode = LAYOUT_SET_SELECTION;
                                }

                                // Restore selection
                                setNextSelectedPositionInt(newPos);
                                return;
                            }
                        }
                    }
                    break;

                case SYNC_FIRST_POSITION:
                    // Leave mSyncPosition as it is -- just pin to available range
                    mLayoutMode = LAYOUT_SYNC;
                    mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);

                    return;
                }
            }

            if (!isInTouchMode()) {
                // We couldn't find matching data -- try to use the same position
                newPos = getSelectedItemPosition();

                // Pin position to the available range
                if (newPos >= itemCount) {
                    newPos = itemCount - 1;
                }
                if (newPos < 0) {
                    newPos = 0;
                }

                // Make sure we select something selectable -- first look down
                selectablePos = lookForSelectablePosition(newPos, true);

                if (selectablePos >= 0) {
                    setNextSelectedPositionInt(selectablePos);
                    return;
                } else {
                    // Looking down didn't work -- try looking up
                    selectablePos = lookForSelectablePosition(newPos, false);
                    if (selectablePos >= 0) {
                        setNextSelectedPositionInt(selectablePos);
                        return;
                    }
                }
            } else {
                // We already know where we want to resurrect the selection
                if (mResurrectToPosition >= 0) {
                    return;
                }
            }
        }

        // Nothing is selected. Give up and reset everything.
        mLayoutMode = LAYOUT_FORCE_TOP;
        mSelectedPosition = INVALID_POSITION;
        mSelectedRowId = INVALID_ROW_ID;
        mNextSelectedPosition = INVALID_POSITION;
        mNextSelectedRowId = INVALID_ROW_ID;
        mNeedSync = false;
        mPendingSync = null;
        mSelectorPosition = INVALID_POSITION;

        checkSelectionChanged();
    }

    private int reconcileSelectedPosition() {
        int position = mSelectedPosition;
        if (position < 0) {
            position = mResurrectToPosition;
        }

        position = Math.max(0, position);
        position = Math.min(position, mItemCount - 1);

        return position;
    }

    boolean resurrectSelection() {
        final int childCount = getChildCount();
        if (childCount <= 0) {
            return false;
        }

        int selectedStart = 0;
        int selectedPosition;

        int start = getStartEdge();
        int end = getEndEdge();

        final int firstPosition = mFirstPosition;
        final int toPosition = mResurrectToPosition;
        boolean down = true;

        if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
            selectedPosition = toPosition;

            final View selected = getChildAt(selectedPosition - mFirstPosition);
            selectedStart = getChildStartEdge(selected);

            final int selectedEnd = getChildEndEdge(selected);

            // We are scrolled, don't get in the fade
            if (selectedStart < start) {
                selectedStart = start + getFadingEdgeLength();
            } else if (selectedEnd > end) {
                selectedStart = end - getChildMeasuredSize(selected) - getFadingEdgeLength();
            }
        } else if (toPosition < firstPosition) {
            // Default to selecting whatever is first
            selectedPosition = firstPosition;

            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final int childStart = getChildStartEdge(child);

                if (i == 0) {
                    // Remember the position of the first item
                    selectedStart = childStart;

                    // See if we are scrolled at all
                    if (firstPosition > 0 || childStart < start) {
                        // If we are scrolled, don't select anything that is
                        // in the fade region
                        start += getFadingEdgeLength();
                    }
                }

                if (childStart >= start) {
                    // Found a view whose top is fully visible
                    selectedPosition = firstPosition + i;
                    selectedStart = childStart;
                    break;
                }
            }
        } else {
            final int itemCount = mItemCount;
            selectedPosition = firstPosition + childCount - 1;
            down = false;

            for (int i = childCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                final int childStart = getChildStartEdge(child);
                final int childEnd = getChildEndEdge(child);

                if (i == childCount - 1) {
                    selectedStart = childStart;

                    if (firstPosition + childCount < itemCount || childEnd > end) {
                        end -= getFadingEdgeLength();
                    }
                }

                if (childEnd <= end) {
                    selectedPosition = firstPosition + i;
                    selectedStart = childStart;
                    break;
                }
            }
        }

        mResurrectToPosition = INVALID_POSITION;

        finishSmoothScrolling();

        mTouchMode = TOUCH_MODE_REST;
        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);

        mSpecificStart = selectedStart;

        selectedPosition = lookForSelectablePosition(selectedPosition, down);
        if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) {
            mLayoutMode = LAYOUT_SPECIFIC;
            updateSelectorState();
            setSelectionInt(selectedPosition);
            invokeOnItemScrollListener();
        } else {
            selectedPosition = INVALID_POSITION;
        }

        return selectedPosition >= 0;
    }

    /**
     * If there is a selection returns false.
     * Otherwise resurrects the selection and returns true if resurrected.
     */
    boolean resurrectSelectionIfNeeded() {
        if (mSelectedPosition < 0 && resurrectSelection()) {
            updateSelectorState();
            return true;
        }

        return false;
    }

    private int getChildWidthMeasureSpec(LayoutParams lp) {
        if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) {
            return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        } else if (mIsVertical) {
            final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight();
            return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
        } else {
            return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
        }
    }

    private int getChildHeightMeasureSpec(LayoutParams lp) {
        if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) {
            return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        } else if (!mIsVertical) {
            final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom();
            return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
        } else {
            return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
        }
    }

    private void measureChild(View child) {
        measureChild(child, (LayoutParams) child.getLayoutParams());
    }

    private void measureChild(View child, LayoutParams lp) {
        final int widthSpec = getChildWidthMeasureSpec(lp);
        final int heightSpec = getChildHeightMeasureSpec(lp);
        child.measure(widthSpec, heightSpec);
    }

    private void relayoutMeasuredChild(View child) {
        final int w = child.getMeasuredWidth();
        final int h = child.getMeasuredHeight();

        final int childLeft = getPaddingLeft();
        final int childRight = childLeft + w;
        final int childTop = child.getTop();
        final int childBottom = childTop + h;

        child.layout(childLeft, childTop, childRight, childBottom);
    }

    private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) {
        LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams();
        if (lp == null) {
            lp = generateDefaultLayoutParams();
            scrapChild.setLayoutParams(lp);
        }

        lp.viewType = mAdapter.getItemViewType(position);
        lp.forceAdd = true;

        final int widthMeasureSpec;
        final int heightMeasureSpec;
        if (mIsVertical) {
            widthMeasureSpec = secondaryMeasureSpec;
            heightMeasureSpec = getChildHeightMeasureSpec(lp);
        } else {
            widthMeasureSpec = getChildWidthMeasureSpec(lp);
            heightMeasureSpec = secondaryMeasureSpec;
        }

        scrapChild.measure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * Measures the height of the given range of children (inclusive) and
     * returns the height with this TwoWayView's padding and item margin heights
     * included. If maxHeight is provided, the measuring will stop when the
     * current height reaches maxHeight.
     *
     * @param widthMeasureSpec The width measure spec to be given to a child's
     *            {@link View#measure(int, int)}.
     * @param startPosition The position of the first child to be shown.
     * @param endPosition The (inclusive) position of the last child to be
     *            shown. Specify {@link #NO_POSITION} if the last child should be
     *            the last available child from the adapter.
     * @param maxHeight The maximum height that will be returned (if all the
     *            children don't fit in this value, this value will be
     *            returned).
     * @param disallowPartialChildPosition In general, whether the returned
     *            height should only contain entire children. This is more
     *            powerful--it is the first inclusive position at which partial
     *            children will not be allowed. Example: it looks nice to have
     *            at least 3 completely visible children, and in portrait this
     *            will most likely fit; but in landscape there could be times
     *            when even 2 children can not be completely shown, so a value
     *            of 2 (remember, inclusive) would be good (assuming
     *            startPosition is 0).
     * @return The height of this TwoWayView with the given children.
     */
    private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            final int maxHeight, int disallowPartialChildPosition) {

        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();

        final ListAdapter adapter = mAdapter;
        if (adapter == null) {
            return paddingTop + paddingBottom;
        }

        // Include the padding of the list
        int returnedHeight = paddingTop + paddingBottom;
        final int itemMargin = mItemMargin;

        // The previous height value that was less than maxHeight and contained
        // no partial children
        int prevHeightWithoutPartialChild = 0;
        int i;
        View child;

        // mItemCount - 1 since endPosition parameter is inclusive
        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
        final RecycleBin recycleBin = mRecycler;
        final boolean shouldRecycle = recycleOnMeasure();
        final boolean[] isScrap = mIsScrap;

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);

            measureScrapChild(child, i, widthMeasureSpec);

            if (i > 0) {
                // Count the item margin for all but one child
                returnedHeight += itemMargin;
            }

            // Recycle the view before we possibly return from the method
            if (shouldRecycle) {
                recycleBin.addScrapView(child, -1);
            }

            returnedHeight += child.getMeasuredHeight();

            if (returnedHeight >= maxHeight) {
                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
                            && (returnedHeight != maxHeight) // i'th child did not fit completely
                        ? prevHeightWithoutPartialChild
                        : maxHeight;
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                prevHeightWithoutPartialChild = returnedHeight;
            }
        }

        // At this point, we went through the range of children, and they each
        // completely fit, so return the returnedHeight
        return returnedHeight;
    }

    /**
     * Measures the width of the given range of children (inclusive) and
     * returns the width with this TwoWayView's padding and item margin widths
     * included. If maxWidth is provided, the measuring will stop when the
     * current width reaches maxWidth.
     *
     * @param heightMeasureSpec The height measure spec to be given to a child's
     *            {@link View#measure(int, int)}.
     * @param startPosition The position of the first child to be shown.
     * @param endPosition The (inclusive) position of the last child to be
     *            shown. Specify {@link #NO_POSITION} if the last child should be
     *            the last available child from the adapter.
     * @param maxWidth The maximum width that will be returned (if all the
     *            children don't fit in this value, this value will be
     *            returned).
     * @param disallowPartialChildPosition In general, whether the returned
     *            width should only contain entire children. This is more
     *            powerful--it is the first inclusive position at which partial
     *            children will not be allowed. Example: it looks nice to have
     *            at least 3 completely visible children, and in portrait this
     *            will most likely fit; but in landscape there could be times
     *            when even 2 children can not be completely shown, so a value
     *            of 2 (remember, inclusive) would be good (assuming
     *            startPosition is 0).
     * @return The width of this TwoWayView with the given children.
     */
    private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition,
            final int maxWidth, int disallowPartialChildPosition) {

        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();

        final ListAdapter adapter = mAdapter;
        if (adapter == null) {
            return paddingLeft + paddingRight;
        }

        // Include the padding of the list
        int returnedWidth = paddingLeft + paddingRight;
        final int itemMargin = mItemMargin;

        // The previous height value that was less than maxHeight and contained
        // no partial children
        int prevWidthWithoutPartialChild = 0;
        int i;
        View child;

        // mItemCount - 1 since endPosition parameter is inclusive
        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
        final RecycleBin recycleBin = mRecycler;
        final boolean shouldRecycle = recycleOnMeasure();
        final boolean[] isScrap = mIsScrap;

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);

            measureScrapChild(child, i, heightMeasureSpec);

            if (i > 0) {
                // Count the item margin for all but one child
                returnedWidth += itemMargin;
            }

            // Recycle the view before we possibly return from the method
            if (shouldRecycle) {
                recycleBin.addScrapView(child, -1);
            }

            returnedWidth += child.getMeasuredWidth();

            if (returnedWidth >= maxWidth) {
                // We went over, figure out which width to return.  If returnedWidth > maxWidth,
                // then the i'th position did not fit completely.
                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                            && (i > disallowPartialChildPosition) // We've past the min pos
                            && (prevWidthWithoutPartialChild > 0) // We have a prev width
                            && (returnedWidth != maxWidth) // i'th child did not fit completely
                        ? prevWidthWithoutPartialChild
                        : maxWidth;
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                prevWidthWithoutPartialChild = returnedWidth;
            }
        }

        // At this point, we went through the range of children, and they each
        // completely fit, so return the returnedWidth
        return returnedWidth;
    }

    private View makeAndAddView(int position, int offset, boolean flow, boolean selected) {
        final int top;
        final int left;

        // Compensate item margin on the first item of the list if the item margin
        // is negative to avoid incorrect offset for the very first child.
        if (mIsVertical) {
            top = offset;
            left = getPaddingLeft();
        } else {
            top = getPaddingTop();
            left = offset;
        }

        if (!mDataChanged) {
            // Try to use an existing view for this position
            final View activeChild = mRecycler.getActiveView(position);
            if (activeChild != null) {
                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(activeChild, position, top, left, flow, selected, true);

                return activeChild;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        final View child = obtainView(position, mIsScrap);

        // This needs to be positioned and measured
        setupChild(child, position, top, left, flow, selected, mIsScrap[0]);

        return child;
    }

    @TargetApi(11)
    private void setupChild(View child, int position, int top, int left,
            boolean flow, boolean selected, boolean recycled) {
        final boolean isSelected = selected && shouldShowSelector();
        final boolean updateChildSelected = isSelected != child.isSelected();
        final int touchMode = mTouchMode;

        final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING &&
                mMotionPosition == position;

        final boolean updateChildPressed = isPressed != child.isPressed();
        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();

        // Respect layout params that are already in the view. Otherwise make some up...
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (lp == null) {
            lp = generateDefaultLayoutParams();
        }

        lp.viewType = mAdapter.getItemViewType(position);

        if (recycled && !lp.forceAdd) {
            attachViewToParent(child, (flow ? -1 : 0), lp);
        } else {
            lp.forceAdd = false;
            addViewInLayout(child, (flow ? -1 : 0), lp, true);
        }

        if (updateChildSelected) {
            child.setSelected(isSelected);
        }

        if (updateChildPressed) {
            child.setPressed(isPressed);
        }

        if (mChoiceMode != ChoiceMode.NONE && mCheckStates != null) {
            if (child instanceof Checkable) {
                ((Checkable) child).setChecked(mCheckStates.get(position));
            } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
                child.setActivated(mCheckStates.get(position));
            }
        }

        if (needToMeasure) {
            measureChild(child, lp);
        } else {
            cleanupLayoutState(child);
        }

        final int w = child.getMeasuredWidth();
        final int h = child.getMeasuredHeight();

        final int childTop = (mIsVertical && !flow ? top - h : top);
        final int childLeft = (!mIsVertical && !flow ? left - w : left);

        if (needToMeasure) {
            final int childRight = childLeft + w;
            final int childBottom = childTop + h;

            child.layout(childLeft, childTop, childRight, childBottom);
        } else {
            child.offsetLeftAndRight(childLeft - child.getLeft());
            child.offsetTopAndBottom(childTop - child.getTop());
        }
    }

    void fillGap(boolean down) {
        final int childCount = getChildCount();

        if (down) {
            final int start = getStartEdge();
            final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
            final int offset = (childCount > 0 ? lastEnd + mItemMargin : start);
            fillAfter(mFirstPosition + childCount, offset);
            correctTooHigh(getChildCount());
        } else {
            final int end = getEndEdge();
            final int firstStart = getChildStartEdge(getChildAt(0));
            final int offset = (childCount > 0 ? firstStart - mItemMargin : end);
            fillBefore(mFirstPosition - 1, offset);
            correctTooLow(getChildCount());
        }
    }

    private View fillBefore(int pos, int nextOffset) {
        View selectedView = null;

        final int start = getStartEdge();

        while (nextOffset > start && pos >= 0) {
            boolean isSelected = (pos == mSelectedPosition);

            View child = makeAndAddView(pos, nextOffset, false, isSelected);
            nextOffset = getChildStartEdge(child) - mItemMargin;

            if (isSelected) {
                selectedView = child;
            }

            pos--;
        }

        mFirstPosition = pos + 1;

        return selectedView;
    }

    private View fillAfter(int pos, int nextOffset) {
        View selectedView = null;

        final int end = getEndEdge();

        while (nextOffset < end && pos < mItemCount) {
            boolean selected = (pos == mSelectedPosition);

            View child = makeAndAddView(pos, nextOffset, true, selected);
            nextOffset = getChildEndEdge(child) + mItemMargin;

            if (selected) {
                selectedView = child;
            }

            pos++;
        }

        return selectedView;
    }

    private View fillSpecific(int position, int offset) {
        final boolean tempIsSelected = (position == mSelectedPosition);
        View temp = makeAndAddView(position, offset, true, tempIsSelected);

        // Possibly changed again in fillBefore if we add rows above this one.
        mFirstPosition = position;

        final int offsetBefore = getChildStartEdge(temp) - mItemMargin;
        final View before = fillBefore(position - 1, offsetBefore);

        // This will correct for the top of the first view not touching the top of the list
        adjustViewsStartOrEnd();

        final int offsetAfter = getChildEndEdge(temp) + mItemMargin;
        final View after = fillAfter(position + 1, offsetAfter);

        final int childCount = getChildCount();
        if (childCount > 0) {
            correctTooHigh(childCount);
        }

        if (tempIsSelected) {
            return temp;
        } else if (before != null) {
            return before;
        } else {
            return after;
        }
    }

    private View fillFromOffset(int nextOffset) {
        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);

        if (mFirstPosition < 0) {
            mFirstPosition = 0;
        }

        return fillAfter(mFirstPosition, nextOffset);
    }

    private View fillFromMiddle(int start, int end) {
        final int size = end - start;
        int position = reconcileSelectedPosition();

        View selected = makeAndAddView(position, start, true, true);
        mFirstPosition = position;

        if (mIsVertical) {
            int selectedHeight = selected.getMeasuredHeight();
            if (selectedHeight <= size) {
                selected.offsetTopAndBottom((size - selectedHeight) / 2);
            }
        } else {
            int selectedWidth = selected.getMeasuredWidth();
            if (selectedWidth <= size) {
                selected.offsetLeftAndRight((size - selectedWidth) / 2);
            }
        }

        fillBeforeAndAfter(selected, position);
        correctTooHigh(getChildCount());

        return selected;
    }

    private void fillBeforeAndAfter(View selected, int position) {
        final int offsetBefore = getChildStartEdge(selected) + mItemMargin;
        fillBefore(position - 1, offsetBefore);

        adjustViewsStartOrEnd();

        final int offsetAfter = getChildEndEdge(selected) + mItemMargin;
        fillAfter(position + 1, offsetAfter);
    }

    private View fillFromSelection(int selectedTop, int start, int end) {
        int fadingEdgeLength = getFadingEdgeLength();
        final int selectedPosition = mSelectedPosition;

        final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
        final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);

        View selected = makeAndAddView(selectedPosition, selectedTop, true, true);

        final int selectedStart = getChildStartEdge(selected);
        final int selectedEnd = getChildEndEdge(selected);

        // Some of the newly selected item extends below the bottom of the list
        if (selectedEnd > maxEnd) {
            // Find space available above the selection into which we can scroll
            // upwards
            final int spaceAbove = selectedStart - minStart;

            // Find space required to bring the bottom of the selected item
            // fully into view
            final int spaceBelow = selectedEnd - maxEnd;

            final int offset = Math.min(spaceAbove, spaceBelow);

            // Now offset the selected item to get it into view
            selected.offsetTopAndBottom(-offset);
        } else if (selectedStart < minStart) {
            // Find space required to bring the top of the selected item fully
            // into view
            final int spaceAbove = minStart - selectedStart;

            // Find space available below the selection into which we can scroll
            // downwards
            final int spaceBelow = maxEnd - selectedEnd;

            final int offset = Math.min(spaceAbove, spaceBelow);

            // Offset the selected item to get it into view
            selected.offsetTopAndBottom(offset);
        }

        // Fill in views above and below
        fillBeforeAndAfter(selected, selectedPosition);
        correctTooHigh(getChildCount());

        return selected;
    }

    private void correctTooHigh(int childCount) {
        // First see if the last item is visible. If it is not, it is OK for the
        // top of the list to be pushed up.
        final int lastPosition = mFirstPosition + childCount - 1;
        if (lastPosition != mItemCount - 1 || childCount == 0) {
            return;
        }

        // Get the last child end edge
        final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));

        // This is bottom of our drawable area
        final int start = getStartEdge();
        final int end = getEndEdge();

        // This is how far the end edge of the last view is from the end of the
        // drawable area
        int endOffset = end - lastEnd;

        View firstChild = getChildAt(0);
        int firstStart = getChildStartEdge(firstChild);

        // Make sure we are 1) Too high, and 2) Either there are more rows above the
        // first row or the first row is scrolled off the top of the drawable area
        if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start))  {
            if (mFirstPosition == 0) {
                // Don't pull the top too far down
                endOffset = Math.min(endOffset, start - firstStart);
            }

            // Move everything down
            offsetChildren(endOffset);

            if (mFirstPosition > 0) {
                firstStart = getChildStartEdge(firstChild);

                // Fill the gap that was opened above mFirstPosition with more rows, if
                // possible
                fillBefore(mFirstPosition - 1, firstStart - mItemMargin);

                // Close up the remaining gap
                adjustViewsStartOrEnd();
            }
        }
    }

    private void correctTooLow(int childCount) {
        // First see if the first item is visible. If it is not, it is OK for the
        // bottom of the list to be pushed down.
        if (mFirstPosition != 0 || childCount == 0) {
            return;
        }

        final int firstStart = getChildStartEdge(getChildAt(0));

        final int start = getStartEdge();
        final int end = getEndEdge();

        // This is how far the start edge of the first view is from the start of the
        // drawable area
        int startOffset = firstStart - start;

        View last = getChildAt(childCount - 1);
        int lastEnd = getChildEndEdge(last);

        int lastPosition = mFirstPosition + childCount - 1;

        // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the
        // last column/row or the last column/row is scrolled off the end of the
        // drawable area
        if (startOffset > 0) {
            if (lastPosition < mItemCount - 1 || lastEnd > end)  {
                if (lastPosition == mItemCount - 1) {
                    // Don't pull the bottom too far up
                    startOffset = Math.min(startOffset, lastEnd - end);
                }

                // Move everything up
                offsetChildren(-startOffset);

                if (lastPosition < mItemCount - 1) {
                    lastEnd = getChildEndEdge(last);

                    // Fill the gap that was opened below the last position with more rows, if
                    // possible
                    fillAfter(lastPosition + 1, lastEnd + mItemMargin);

                    // Close up the remaining gap
                    adjustViewsStartOrEnd();
                }
            } else if (lastPosition == mItemCount - 1) {
                adjustViewsStartOrEnd();
            }
        }
    }

    private void adjustViewsStartOrEnd() {
        if (getChildCount() == 0) {
            return;
        }

        int delta = getChildStartEdge(getChildAt(0)) - getStartEdge();

        // If item margin is negative we shouldn't apply it in the
        // first item of the list to avoid offsetting it incorrectly.
        if (mItemMargin >= 0 || mFirstPosition != 0) {
            delta -= mItemMargin;
        }

        if (delta < 0) {
            // We only are looking to see if we are too low, not too high
            delta = 0;
        }

        if (delta != 0) {
            offsetChildren(-delta);
        }
    }

    @TargetApi(14)
    private SparseBooleanArray cloneCheckStates() {
        if (mCheckStates == null) {
            return null;
        }

        SparseBooleanArray checkedStates;

        if (Build.VERSION.SDK_INT >= 14) {
            checkedStates = mCheckStates.clone();
        } else {
            checkedStates = new SparseBooleanArray();

            for (int i = 0; i < mCheckStates.size(); i++) {
                checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
            }
        }

        return checkedStates;
    }

    private int findSyncPosition() {
        int itemCount = mItemCount;

        if (itemCount == 0) {
            return INVALID_POSITION;
        }

        final long idToMatch = mSyncRowId;

        // If there isn't a selection don't hunt for it
        if (idToMatch == INVALID_ROW_ID) {
            return INVALID_POSITION;
        }

        // Pin seed to reasonable values
        int seed = mSyncPosition;
        seed = Math.max(0, seed);
        seed = Math.min(itemCount - 1, seed);

        long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;

        long rowId;

        // first position scanned so far
        int first = seed;

        // last position scanned so far
        int last = seed;

        // True if we should move down on the next iteration
        boolean next = false;

        // True when we have looked at the first item in the data
        boolean hitFirst;

        // True when we have looked at the last item in the data
        boolean hitLast;

        // Get the item ID locally (instead of getItemIdAtPosition), so
        // we need the adapter
        final ListAdapter adapter = mAdapter;
        if (adapter == null) {
            return INVALID_POSITION;
        }

        while (SystemClock.uptimeMillis() <= endTime) {
            rowId = adapter.getItemId(seed);
            if (rowId == idToMatch) {
                // Found it!
                return seed;
            }

            hitLast = (last == itemCount - 1);
            hitFirst = (first == 0);

            if (hitLast && hitFirst) {
                // Looked at everything
                break;
            }

            if (hitFirst || (next && !hitLast)) {
                // Either we hit the top, or we are trying to move down
                last++;
                seed = last;

                // Try going up next time
                next = false;
            } else if (hitLast || (!next && !hitFirst)) {
                // Either we hit the bottom, or we are trying to move up
                first--;
                seed = first;

                // Try going down next time
                next = true;
            }
        }

        return INVALID_POSITION;
    }

    @TargetApi(16)
    private View obtainView(int position, boolean[] isScrap) {
        isScrap[0] = false;

        View scrapView = mRecycler.getTransientStateView(position);
        if (scrapView != null) {
            return scrapView;
        }

        scrapView = mRecycler.getScrapView(position);

        final View child;
        if (scrapView != null) {
            child = mAdapter.getView(position, scrapView, this);

            if (child != scrapView) {
                mRecycler.addScrapView(scrapView, position);
            } else {
                isScrap[0] = true;
            }
        } else {
            child = mAdapter.getView(position, null, this);
        }

        if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        if (mHasStableIds) {
            LayoutParams lp = (LayoutParams) child.getLayoutParams();

            if (lp == null) {
                lp = generateDefaultLayoutParams();
            } else if (!checkLayoutParams(lp)) {
                lp = generateLayoutParams(lp);
            }

            lp.id = mAdapter.getItemId(position);

            child.setLayoutParams(lp);
        }

        if (mAccessibilityDelegate == null) {
            mAccessibilityDelegate = new ListItemAccessibilityDelegate();
        }

        ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);

        return child;
    }

    void resetState() {
        mScroller.forceFinished(true);

        removeAllViewsInLayout();

        mSelectedStart = 0;
        mFirstPosition = 0;
        mDataChanged = false;
        mNeedSync = false;
        mPendingSync = null;
        mOldSelectedPosition = INVALID_POSITION;
        mOldSelectedRowId = INVALID_ROW_ID;

        mOverScroll = 0;

        setSelectedPositionInt(INVALID_POSITION);
        setNextSelectedPositionInt(INVALID_POSITION);

        mSelectorPosition = INVALID_POSITION;
        mSelectorRect.setEmpty();

        invalidate();
    }

    private void rememberSyncState() {
        if (getChildCount() == 0) {
            return;
        }

        mNeedSync = true;

        if (mSelectedPosition >= 0) {
            View child = getChildAt(mSelectedPosition - mFirstPosition);

            mSyncRowId = mNextSelectedRowId;
            mSyncPosition = mNextSelectedPosition;

            if (child != null) {
                mSpecificStart = getChildStartEdge(child);
            }

            mSyncMode = SYNC_SELECTED_POSITION;
        } else {
            // Sync the based on the offset of the first view
            View child = getChildAt(0);
            ListAdapter adapter = getAdapter();

            if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
                mSyncRowId = adapter.getItemId(mFirstPosition);
            } else {
                mSyncRowId = NO_ID;
            }

            mSyncPosition = mFirstPosition;

            if (child != null) {
                mSpecificStart = getChildStartEdge(child);
            }

            mSyncMode = SYNC_FIRST_POSITION;
        }
    }

    private ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
        return new AdapterContextMenuInfo(view, position, id);
    }

    @TargetApi(11)
    private void updateOnScreenCheckedViews() {
        final int firstPos = mFirstPosition;
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            final int position = firstPos + i;

            if (child instanceof Checkable) {
                ((Checkable) child).setChecked(mCheckStates.get(position));
            } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
                child.setActivated(mCheckStates.get(position));
            }
        }
    }

    @Override
    public boolean performItemClick(View view, int position, long id) {
        boolean checkedStateChanged = false;

        if (mChoiceMode == ChoiceMode.MULTIPLE) {
            boolean checked = !mCheckStates.get(position, false);
            mCheckStates.put(position, checked);

            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                if (checked) {
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                } else {
                    mCheckedIdStates.delete(mAdapter.getItemId(position));
                }
            }

            if (checked) {
                mCheckedItemCount++;
            } else {
                mCheckedItemCount--;
            }

            checkedStateChanged = true;
        } else if (mChoiceMode == ChoiceMode.SINGLE) {
            boolean checked = !mCheckStates.get(position, false);
            if (checked) {
                mCheckStates.clear();
                mCheckStates.put(position, true);

                if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
                    mCheckedIdStates.clear();
                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
                }

                mCheckedItemCount = 1;
            } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
                mCheckedItemCount = 0;
            }

            checkedStateChanged = true;
        }

        if (checkedStateChanged) {
            updateOnScreenCheckedViews();
        }

        return super.performItemClick(view, position, id);
    }

    private boolean performLongPress(final View child,
            final int longPressPosition, final long longPressId) {
        // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
        boolean handled = false;

        OnItemLongClickListener listener = getOnItemLongClickListener();
        if (listener != null) {
            handled = listener.onItemLongClick(TwoWayView.this, child,
                    longPressPosition, longPressId);
        }

        if (!handled) {
            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
            handled = super.showContextMenuForChild(TwoWayView.this);
        }

        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }

        return handled;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        if (mIsVertical) {
            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        } else {
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
        }
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new LayoutParams(lp);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected ContextMenuInfo getContextMenuInfo() {
        return mContextMenuInfo;
    }

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);

        if (mPendingSync != null) {
            ss.selectedId = mPendingSync.selectedId;
            ss.firstId = mPendingSync.firstId;
            ss.viewStart = mPendingSync.viewStart;
            ss.position = mPendingSync.position;
            ss.size = mPendingSync.size;

            return ss;
        }

        boolean haveChildren = (getChildCount() > 0 && mItemCount > 0);
        long selectedId = getSelectedItemId();
        ss.selectedId = selectedId;
        ss.size = getSize();

        if (selectedId >= 0) {
            ss.viewStart = mSelectedStart;
            ss.position = getSelectedItemPosition();
            ss.firstId = INVALID_POSITION;
        } else if (haveChildren && mFirstPosition > 0) {
            // Remember the position of the first child.
            // We only do this if we are not currently at the top of
            // the list, for two reasons:
            //
            // (1) The list may be in the process of becoming empty, in
            // which case mItemCount may not be 0, but if we try to
            // ask for any information about position 0 we will crash.
            //
            // (2) Being "at the top" seems like a special case, anyway,
            // and the user wouldn't expect to end up somewhere else when
            // they revisit the list even if its content has changed.

            ss.viewStart = getChildStartEdge(getChildAt(0));

            int firstPos = mFirstPosition;
            if (firstPos >= mItemCount) {
                firstPos = mItemCount - 1;
            }

            ss.position = firstPos;
            ss.firstId = mAdapter.getItemId(firstPos);
        } else {
            ss.viewStart = 0;
            ss.firstId = INVALID_POSITION;
            ss.position = 0;
        }

        if (mCheckStates != null) {
            ss.checkState = cloneCheckStates();
        }

        if (mCheckedIdStates != null) {
            final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();

            final int count = mCheckedIdStates.size();
            for (int i = 0; i < count; i++) {
                idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
            }

            ss.checkIdState = idState;
        }

        ss.checkedItemCount = mCheckedItemCount;

        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        mDataChanged = true;
        mSyncSize = ss.size;

        if (ss.selectedId >= 0) {
            mNeedSync = true;
            mPendingSync = ss;
            mSyncRowId = ss.selectedId;
            mSyncPosition = ss.position;
            mSpecificStart = ss.viewStart;
            mSyncMode = SYNC_SELECTED_POSITION;
        } else if (ss.firstId >= 0) {
            setSelectedPositionInt(INVALID_POSITION);

            // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
            setNextSelectedPositionInt(INVALID_POSITION);

            mSelectorPosition = INVALID_POSITION;
            mNeedSync = true;
            mPendingSync = ss;
            mSyncRowId = ss.firstId;
            mSyncPosition = ss.position;
            mSpecificStart = ss.viewStart;
            mSyncMode = SYNC_FIRST_POSITION;
        }

        if (ss.checkState != null) {
            mCheckStates = ss.checkState;
        }

        if (ss.checkIdState != null) {
            mCheckedIdStates = ss.checkIdState;
        }

        mCheckedItemCount = ss.checkedItemCount;

        requestLayout();
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        /**
         * Type of this view as reported by the adapter
         */
        int viewType;

        /**
         * The stable ID of the item this view displays
         */
        long id = -1;

        /**
         * The position the view was removed from when pulled out of the
         * scrap heap.
         * @hide
         */
        int scrappedFromPosition;

        /**
         * When a TwoWayView is measured with an AT_MOST measure spec, it needs
         * to obtain children views to measure itself. When doing so, the children
         * are not attached to the window, but put in the recycler which assumes
         * they've been attached before. Setting this flag will force the reused
         * view to be attached to the window rather than just attached to the
         * parent.
         */
        boolean forceAdd;

        public LayoutParams(int width, int height) {
            super(width, height);

            if (this.width == MATCH_PARENT) {
                Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.width = WRAP_CONTENT;
            }

            if (this.height == MATCH_PARENT) {
                Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            if (this.width == MATCH_PARENT) {
                Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.width = MATCH_PARENT;
            }

            if (this.height == MATCH_PARENT) {
                Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }

        public LayoutParams(ViewGroup.LayoutParams other) {
            super(other);

            if (this.width == MATCH_PARENT) {
                Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT - " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.width = WRAP_CONTENT;
            }

            if (this.height == MATCH_PARENT) {
                Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
                        "does not make much sense as the view might change orientation. " +
                        "Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }
    }

    class RecycleBin {
        private RecyclerListener mRecyclerListener;
        private int mFirstActivePosition;
        private View[] mActiveViews = new View[0];
        private ArrayList<View>[] mScrapViews;
        private int mViewTypeCount;
        private ArrayList<View> mCurrentScrap;
        private SparseArrayCompat<View> mTransientStateViews;

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
            }

            @SuppressWarnings({"unchecked", "rawtypes"})
            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }

            mViewTypeCount = viewTypeCount;
            mCurrentScrap = scrapViews[0];
            mScrapViews = scrapViews;
        }

        public void markChildrenDirty() {
            if (mViewTypeCount == 1) {
                final ArrayList<View> scrap = mCurrentScrap;
                final int scrapCount = scrap.size();

                for (int i = 0; i < scrapCount; i++) {
                    scrap.get(i).forceLayout();
                }
            } else {
                final int typeCount = mViewTypeCount;
                for (int i = 0; i < typeCount; i++) {
                    for (View scrap : mScrapViews[i]) {
                        scrap.forceLayout();
                    }
                }
            }

            if (mTransientStateViews != null) {
                final int count = mTransientStateViews.size();
                for (int i = 0; i < count; i++) {
                    mTransientStateViews.valueAt(i).forceLayout();
                }
            }
        }

        public boolean shouldRecycleViewType(int viewType) {
            return viewType >= 0;
        }

        void clear() {
            if (mViewTypeCount == 1) {
                final ArrayList<View> scrap = mCurrentScrap;
                final int scrapCount = scrap.size();

                for (int i = 0; i < scrapCount; i++) {
                    removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
                }
            } else {
                final int typeCount = mViewTypeCount;
                for (int i = 0; i < typeCount; i++) {
                    final ArrayList<View> scrap = mScrapViews[i];
                    final int scrapCount = scrap.size();

                    for (int j = 0; j < scrapCount; j++) {
                        removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
                    }
                }
            }

            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        void fillActiveViews(int childCount, int firstActivePosition) {
            if (mActiveViews.length < childCount) {
                mActiveViews = new View[childCount];
            }

            mFirstActivePosition = firstActivePosition;

            final View[] activeViews = mActiveViews;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);

                // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
                //        However, we will NOT place them into scrap views.
                activeViews[i] = child;
            }
        }

        View getActiveView(int position) {
            final int index = position - mFirstActivePosition;
            final View[] activeViews = mActiveViews;

            if (index >= 0 && index < activeViews.length) {
                final View match = activeViews[index];
                activeViews[index] = null;

                return match;
            }

            return null;
        }

        View getTransientStateView(int position) {
            if (mTransientStateViews == null) {
                return null;
            }

            final int index = mTransientStateViews.indexOfKey(position);
            if (index < 0) {
                return null;
            }

            final View result = mTransientStateViews.valueAt(index);
            mTransientStateViews.removeAt(index);

            return result;
        }

        void clearTransientStateViews() {
            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        View getScrapView(int position) {
            if (mViewTypeCount == 1) {
                return retrieveFromScrap(mCurrentScrap, position);
            } else {
                int whichScrap = mAdapter.getItemViewType(position);
                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
                    return retrieveFromScrap(mScrapViews[whichScrap], position);
                }
            }

            return null;
        }

        @TargetApi(14)
        void addScrapView(View scrap, int position) {
            LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
            if (lp == null) {
                return;
            }

            lp.scrappedFromPosition = position;

            final int viewType = lp.viewType;
            final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);

            // Don't put views that should be ignored into the scrap heap
            if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
                if (scrapHasTransientState) {
                    if (mTransientStateViews == null) {
                        mTransientStateViews = new SparseArrayCompat<View>();
                    }

                    mTransientStateViews.put(position, scrap);
                }

                return;
            }

            if (mViewTypeCount == 1) {
                mCurrentScrap.add(scrap);
            } else {
                mScrapViews[viewType].add(scrap);
            }

            // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
            // null delegates.
            if (Build.VERSION.SDK_INT >= 14) {
                scrap.setAccessibilityDelegate(null);
            }

            if (mRecyclerListener != null) {
                mRecyclerListener.onMovedToScrapHeap(scrap);
            }
        }

        @TargetApi(14)
        void scrapActiveViews() {
            final View[] activeViews = mActiveViews;
            final boolean multipleScraps = (mViewTypeCount > 1);

            ArrayList<View> scrapViews = mCurrentScrap;
            final int count = activeViews.length;

            for (int i = count - 1; i >= 0; i--) {
                final View victim = activeViews[i];
                if (victim != null) {
                    final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
                    int whichScrap = lp.viewType;

                    activeViews[i] = null;

                    final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
                    if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) {
                        if (scrapHasTransientState) {
                            removeDetachedView(victim, false);

                            if (mTransientStateViews == null) {
                                mTransientStateViews = new SparseArrayCompat<View>();
                            }

                            mTransientStateViews.put(mFirstActivePosition + i, victim);
                        }

                        continue;
                    }

                    if (multipleScraps) {
                        scrapViews = mScrapViews[whichScrap];
                    }

                    lp.scrappedFromPosition = mFirstActivePosition + i;
                    scrapViews.add(victim);

                    // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
                    // null delegates.
                    if (Build.VERSION.SDK_INT >= 14) {
                        victim.setAccessibilityDelegate(null);
                    }

                    if (mRecyclerListener != null) {
                        mRecyclerListener.onMovedToScrapHeap(victim);
                    }
                }
            }

            pruneScrapViews();
        }

        private void pruneScrapViews() {
            final int maxViews = mActiveViews.length;
            final int viewTypeCount = mViewTypeCount;
            final ArrayList<View>[] scrapViews = mScrapViews;

            for (int i = 0; i < viewTypeCount; ++i) {
                final ArrayList<View> scrapPile = scrapViews[i];
                int size = scrapPile.size();
                final int extras = size - maxViews;

                size--;

                for (int j = 0; j < extras; j++) {
                    removeDetachedView(scrapPile.remove(size--), false);
                }
            }

            if (mTransientStateViews != null) {
                for (int i = 0; i < mTransientStateViews.size(); i++) {
                    final View v = mTransientStateViews.valueAt(i);
                    if (!ViewCompat.hasTransientState(v)) {
                        mTransientStateViews.removeAt(i);
                        i--;
                    }
                }
            }
        }

        void reclaimScrapViews(List<View> views) {
            if (mViewTypeCount == 1) {
                views.addAll(mCurrentScrap);
            } else {
                final int viewTypeCount = mViewTypeCount;
                final ArrayList<View>[] scrapViews = mScrapViews;

                for (int i = 0; i < viewTypeCount; ++i) {
                    final ArrayList<View> scrapPile = scrapViews[i];
                    views.addAll(scrapPile);
                }
            }
        }

        View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
            int size = scrapViews.size();
            if (size <= 0) {
                return null;
            }

            for (int i = 0; i < size; i++) {
                final View scrapView = scrapViews.get(i);
                final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams();

                if (lp.scrappedFromPosition == position) {
                    scrapViews.remove(i);
                    return scrapView;
                }
            }

            return scrapViews.remove(size - 1);
        }
    }

    @Override
    public void setEmptyView(View emptyView) {
        super.setEmptyView(emptyView);
        mEmptyView = emptyView;
        updateEmptyStatus();
    }

    @Override
    public void setFocusable(boolean focusable) {
        final ListAdapter adapter = getAdapter();
        final boolean empty = (adapter == null || adapter.getCount() == 0);

        mDesiredFocusableState = focusable;
        if (!focusable) {
            mDesiredFocusableInTouchModeState = false;
        }

        super.setFocusable(focusable && !empty);
    }

    @Override
    public void setFocusableInTouchMode(boolean focusable) {
        final ListAdapter adapter = getAdapter();
        final boolean empty = (adapter == null || adapter.getCount() == 0);

        mDesiredFocusableInTouchModeState = focusable;
        if (focusable) {
            mDesiredFocusableState = true;
        }

        super.setFocusableInTouchMode(focusable && !empty);
    }

    private void checkFocus() {
        final ListAdapter adapter = getAdapter();
        final boolean focusable = (adapter != null && adapter.getCount() > 0);

        // The order in which we set focusable in touch mode/focusable may matter
        // for the client, see View.setFocusableInTouchMode() comments for more
        // details
        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
        super.setFocusable(focusable && mDesiredFocusableState);

        if (mEmptyView != null) {
            updateEmptyStatus();
        }
    }

    private void updateEmptyStatus() {
        final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());

        if (isEmpty) {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.VISIBLE);
                setVisibility(View.GONE);
            } else {
                // If the caller just removed our empty view, make sure the list
                // view is visible
                setVisibility(View.VISIBLE);
            }

            // We are now GONE, so pending layouts will not be dispatched.
            // Force one here to make sure that the state of the list matches
            // the state of the adapter.
            if (mDataChanged) {
                layout(getLeft(), getTop(), getRight(), getBottom());
            }
        } else {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.GONE);
            }

            setVisibility(View.VISIBLE);
        }
    }

    private class AdapterDataSetObserver extends DataSetObserver {
        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount();

            // Detect the case where a cursor that was previously invalidated has
            // been re-populated with new data.
            if (TwoWayView.this.mHasStableIds && mInstanceState != null
                    && mOldItemCount == 0 && mItemCount > 0) {
                TwoWayView.this.onRestoreInstanceState(mInstanceState);
                mInstanceState = null;
            } else {
                rememberSyncState();
            }

            checkFocus();
            requestLayout();
        }

        @Override
        public void onInvalidated() {
            mDataChanged = true;

            if (TwoWayView.this.mHasStableIds) {
                // Remember the current state for the case where our hosting activity is being
                // stopped and later restarted
                mInstanceState = TwoWayView.this.onSaveInstanceState();
            }

            // Data is invalid so we should reset our state
            mOldItemCount = mItemCount;
            mItemCount = 0;

            mSelectedPosition = INVALID_POSITION;
            mSelectedRowId = INVALID_ROW_ID;

            mNextSelectedPosition = INVALID_POSITION;
            mNextSelectedRowId = INVALID_ROW_ID;

            mNeedSync = false;

            checkFocus();
            requestLayout();
        }
    }

    static class SavedState extends BaseSavedState {
        long selectedId;
        long firstId;
        int viewStart;
        int position;
        int size;
        int checkedItemCount;
        SparseBooleanArray checkState;
        LongSparseArray<Integer> checkIdState;

        /**
         * Constructor called from {@link TwoWayView#onSaveInstanceState()}
         */
        SavedState(Parcelable superState) {
            super(superState);
        }

        /**
         * Constructor called from {@link #CREATOR}
         */
        private SavedState(Parcel in) {
            super(in);

            selectedId = in.readLong();
            firstId = in.readLong();
            viewStart = in.readInt();
            position = in.readInt();
            size = in.readInt();

            checkedItemCount = in.readInt();
            checkState = in.readSparseBooleanArray();

            final int N = in.readInt();
            if (N > 0) {
                checkIdState = new LongSparseArray<Integer>();
                for (int i = 0; i < N; i++) {
                    final long key = in.readLong();
                    final int value = in.readInt();
                    checkIdState.put(key, value);
                }
            }
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);

            out.writeLong(selectedId);
            out.writeLong(firstId);
            out.writeInt(viewStart);
            out.writeInt(position);
            out.writeInt(size);

            out.writeInt(checkedItemCount);
            out.writeSparseBooleanArray(checkState);

            final int N = checkIdState != null ? checkIdState.size() : 0;
            out.writeInt(N);

            for (int i = 0; i < N; i++) {
                out.writeLong(checkIdState.keyAt(i));
                out.writeInt(checkIdState.valueAt(i));
            }
        }

        @Override
        public String toString() {
            return "TwoWayView.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " selectedId=" + selectedId
                    + " firstId=" + firstId
                    + " viewStart=" + viewStart
                    + " size=" + size
                    + " position=" + position
                    + " checkState=" + checkState + "}";
        }

        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    private class SelectionNotifier implements Runnable {
        @Override
        public void run() {
            if (mDataChanged) {
                // Data has changed between when this SelectionNotifier
                // was posted and now. We need to wait until the AdapterView
                // has been synched to the new data.
                if (mAdapter != null) {
                    post(this);
                }
            } else {
                fireOnSelected();
                performAccessibilityActionsOnSelected();
            }
        }
    }

    private class WindowRunnable {
        private int mOriginalAttachCount;

        public void rememberWindowAttachCount() {
            mOriginalAttachCount = getWindowAttachCount();
        }

        public boolean sameWindow() {
            return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
        }
    }

    private class PerformClick extends WindowRunnable implements Runnable {
        int mClickMotionPosition;

        @Override
        public void run() {
            if (mDataChanged) {
                return;
            }

            final ListAdapter adapter = mAdapter;
            final int motionPosition = mClickMotionPosition;

            if (adapter != null && mItemCount > 0 &&
                motionPosition != INVALID_POSITION &&
                motionPosition < adapter.getCount() && sameWindow()) {

                final View child = getChildAt(motionPosition - mFirstPosition);
                if (child != null) {
                    performItemClick(child, motionPosition, adapter.getItemId(motionPosition));
                }
            }
        }
    }

    private final class CheckForTap implements Runnable {
        @Override
        public void run() {
            if (mTouchMode != TOUCH_MODE_DOWN) {
                return;
            }

            mTouchMode = TOUCH_MODE_TAP;

            final View child = getChildAt(mMotionPosition - mFirstPosition);
            if (child != null && !child.hasFocusable()) {
                mLayoutMode = LAYOUT_NORMAL;

                if (!mDataChanged) {
                    setPressed(true);
                    child.setPressed(true);

                    layoutChildren();
                    positionSelector(mMotionPosition, child);
                    refreshDrawableState();

                    positionSelector(mMotionPosition, child);
                    refreshDrawableState();

                    final boolean longClickable = isLongClickable();

                    if (mSelector != null) {
                        Drawable d = mSelector.getCurrent();

                        if (d != null && d instanceof TransitionDrawable) {
                            if (longClickable) {
                                final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
                                ((TransitionDrawable) d).startTransition(longPressTimeout);
                            } else {
                                ((TransitionDrawable) d).resetTransition();
                            }
                        }
                    }

                    if (longClickable) {
                        triggerCheckForLongPress();
                    } else {
                        mTouchMode = TOUCH_MODE_DONE_WAITING;
                    }
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                }
            }
        }
    }

    private class CheckForLongPress extends WindowRunnable implements Runnable {
        @Override
        public void run() {
            final int motionPosition = mMotionPosition;
            final View child = getChildAt(motionPosition - mFirstPosition);

            if (child != null) {
                final long longPressId = mAdapter.getItemId(mMotionPosition);

                boolean handled = false;
                if (sameWindow() && !mDataChanged) {
                    handled = performLongPress(child, motionPosition, longPressId);
                }

                if (handled) {
                    mTouchMode = TOUCH_MODE_REST;
                    setPressed(false);
                    child.setPressed(false);
                } else {
                    mTouchMode = TOUCH_MODE_DONE_WAITING;
                }
            }
        }
    }

    private class CheckForKeyLongPress extends WindowRunnable implements Runnable {
        public void run() {
            if (!isPressed() || mSelectedPosition < 0) {
                return;
            }

            final int index = mSelectedPosition - mFirstPosition;
            final View v = getChildAt(index);

            if (!mDataChanged) {
                boolean handled = false;

                if (sameWindow()) {
                    handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
                }

                if (handled) {
                    setPressed(false);
                    v.setPressed(false);
                }
            } else {
                setPressed(false);

                if (v != null) {
                    v.setPressed(false);
                }
            }
        }
    }

    private static class ArrowScrollFocusResult {
        private int mSelectedPosition;
        private int mAmountToScroll;

        /**
         * How {@link TwoWayView#arrowScrollFocused} returns its values.
         */
        void populate(int selectedPosition, int amountToScroll) {
            mSelectedPosition = selectedPosition;
            mAmountToScroll = amountToScroll;
        }

        public int getSelectedPosition() {
            return mSelectedPosition;
        }

        public int getAmountToScroll() {
            return mAmountToScroll;
        }
    }

    private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(host, info);

            final int position = getPositionForView(host);
            final ListAdapter adapter = getAdapter();

            // Cannot perform actions on invalid items
            if (position == INVALID_POSITION || adapter == null) {
                return;
            }

            // Cannot perform actions on disabled items
            if (!isEnabled() || !adapter.isEnabled(position)) {
                return;
            }

            if (position == getSelectedItemPosition()) {
                info.setSelected(true);
                info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
            } else {
                info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
            }

            if (isClickable()) {
                info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
                info.setClickable(true);
            }

            if (isLongClickable()) {
                info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
                info.setLongClickable(true);
            }
        }

        @Override
        public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
            if (super.performAccessibilityAction(host, action, arguments)) {
                return true;
            }

            final int position = getPositionForView(host);
            final ListAdapter adapter = getAdapter();

            // Cannot perform actions on invalid items
            if (position == INVALID_POSITION || adapter == null) {
                return false;
            }

            // Cannot perform actions on disabled items
            if (!isEnabled() || !adapter.isEnabled(position)) {
                return false;
            }

            final long id = getItemIdAtPosition(position);

            switch (action) {
            case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
                if (getSelectedItemPosition() == position) {
                    setSelection(INVALID_POSITION);
                    return true;
                }
                return false;

            case AccessibilityNodeInfoCompat.ACTION_SELECT:
                if (getSelectedItemPosition() != position) {
                    setSelection(position);
                    return true;
                }
                return false;

            case AccessibilityNodeInfoCompat.ACTION_CLICK:
                return isClickable() && performItemClick(host, position, id);

            case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
                return isLongClickable() && performLongPress(host, position, id);
            }

            return false;
        }
    }

    private class PositionScroller implements Runnable {
        private static final int SCROLL_DURATION = 200;

        private static final int MOVE_AFTER_POS = 1;
        private static final int MOVE_BEFORE_POS = 2;
        private static final int MOVE_AFTER_BOUND = 3;
        private static final int MOVE_BEFORE_BOUND = 4;
        private static final int MOVE_OFFSET = 5;

        private int mMode;
        private int mTargetPosition;
        private int mBoundPosition;
        private int mLastSeenPosition;
        private int mScrollDuration;
        private final int mExtraScroll;

        private int mOffsetFromStart;

        PositionScroller() {
            mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
        }

        void start(final int position) {
            stop();

            if (mDataChanged) {
                // Wait until we're back in a stable state to try this.
                mPositionScrollAfterLayout = new Runnable() {
                    @Override public void run() {
                        start(position);
                    }
                };

                return;
            }

            final int childCount = getChildCount();
            if (childCount == 0) {
                // Can't scroll without children.
                return;
            }

            final int firstPosition = mFirstPosition;
            final int lastPosition = firstPosition + childCount - 1;

            final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));

            final int viewTravelCount;
            if (clampedPosition < firstPosition) {
                viewTravelCount = firstPosition - clampedPosition + 1;
                mMode = MOVE_BEFORE_POS;
            } else if (clampedPosition > lastPosition) {
                viewTravelCount = clampedPosition - lastPosition + 1;
                mMode = MOVE_AFTER_POS;
            } else {
                scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION);
                return;
            }

            if (viewTravelCount > 0) {
                mScrollDuration = SCROLL_DURATION / viewTravelCount;
            } else {
                mScrollDuration = SCROLL_DURATION;
            }

            mTargetPosition = clampedPosition;
            mBoundPosition = INVALID_POSITION;
            mLastSeenPosition = INVALID_POSITION;

            ViewCompat.postOnAnimation(TwoWayView.this, this);
        }

        void start(final int position, final int boundPosition) {
            stop();

            if (boundPosition == INVALID_POSITION) {
                start(position);
                return;
            }

            if (mDataChanged) {
                // Wait until we're back in a stable state to try this.
                mPositionScrollAfterLayout = new Runnable() {
                    @Override public void run() {
                        start(position, boundPosition);
                    }
                };

                return;
            }

            final int childCount = getChildCount();
            if (childCount == 0) {
                // Can't scroll without children.
                return;
            }

            final int firstPosition = mFirstPosition;
            final int lastPosition = firstPosition + childCount - 1;

            final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));

            final int viewTravelCount;
            if (clampedPosition < firstPosition) {
                final int boundPositionFromLast = lastPosition - boundPosition;
                if (boundPositionFromLast < 1) {
                    // Moving would shift our bound position off the screen. Abort.
                    return;
                }

                final int positionTravel = firstPosition - clampedPosition + 1;
                final int boundTravel = boundPositionFromLast - 1;
                if (boundTravel < positionTravel) {
                    viewTravelCount = boundTravel;
                    mMode = MOVE_BEFORE_BOUND;
                } else {
                    viewTravelCount = positionTravel;
                    mMode = MOVE_BEFORE_POS;
                }
            } else if (clampedPosition > lastPosition) {
                final int boundPositionFromFirst = boundPosition - firstPosition;
                if (boundPositionFromFirst < 1) {
                    // Moving would shift our bound position off the screen. Abort.
                    return;
                }

                final int positionTravel = clampedPosition - lastPosition + 1;
                final int boundTravel = boundPositionFromFirst - 1;
                if (boundTravel < positionTravel) {
                    viewTravelCount = boundTravel;
                    mMode = MOVE_AFTER_BOUND;
                } else {
                    viewTravelCount = positionTravel;
                    mMode = MOVE_AFTER_POS;
                }
            } else {
                scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION);
                return;
            }

            if (viewTravelCount > 0) {
                mScrollDuration = SCROLL_DURATION / viewTravelCount;
            } else {
                mScrollDuration = SCROLL_DURATION;
            }

            mTargetPosition = clampedPosition;
            mBoundPosition = boundPosition;
            mLastSeenPosition = INVALID_POSITION;

            ViewCompat.postOnAnimation(TwoWayView.this, this);
        }

        void startWithOffset(int position, int offset) {
            startWithOffset(position, offset, SCROLL_DURATION);
        }

        void startWithOffset(final int position, int offset, final int duration) {
            stop();

            if (mDataChanged) {
                // Wait until we're back in a stable state to try this.
                final int postOffset = offset;
                mPositionScrollAfterLayout = new Runnable() {
                    @Override public void run() {
                        startWithOffset(position, postOffset, duration);
                    }
                };

                return;
            }

            final int childCount = getChildCount();
            if (childCount == 0) {
                // Can't scroll without children.
                return;
            }

            offset += getStartEdge();

            mTargetPosition = Math.max(0, Math.min(getCount() - 1, position));
            mOffsetFromStart = offset;
            mBoundPosition = INVALID_POSITION;
            mLastSeenPosition = INVALID_POSITION;
            mMode = MOVE_OFFSET;

            final int firstPosition = mFirstPosition;
            final int lastPosition = firstPosition + childCount - 1;

            final int viewTravelCount;
            if (mTargetPosition < firstPosition) {
                viewTravelCount = firstPosition - mTargetPosition;
            } else if (mTargetPosition > lastPosition) {
                viewTravelCount = mTargetPosition - lastPosition;
            } else {
                // On-screen, just scroll.
                final View targetView = getChildAt(mTargetPosition - firstPosition);
                final int targetStart = getChildStartEdge(targetView);
                smoothScrollBy(targetStart - offset, duration);
                return;
            }

            // Estimate how many screens we should travel
            final float screenTravelCount = (float) viewTravelCount / childCount;
            mScrollDuration = screenTravelCount < 1 ?
                    duration : (int) (duration / screenTravelCount);
            mLastSeenPosition = INVALID_POSITION;

            ViewCompat.postOnAnimation(TwoWayView.this, this);
        }

        /**
         * Scroll such that targetPos is in the visible padded region without scrolling
         * boundPos out of view. Assumes targetPos is onscreen.
         */
        void scrollToVisible(int targetPosition, int boundPosition, int duration) {
            final int childCount = getChildCount();
            final int firstPosition = mFirstPosition;
            final int lastPosition = firstPosition + childCount - 1;

            final int start = getStartEdge();
            final int end = getEndEdge();

            if (targetPosition < firstPosition || targetPosition > lastPosition) {
                Log.w(LOGTAG, "scrollToVisible called with targetPosition " + targetPosition +
                        " not visible [" + firstPosition + ", " + lastPosition + "]");
            }

            if (boundPosition < firstPosition || boundPosition > lastPosition) {
                // boundPos doesn't matter, it's already offscreen.
                boundPosition = INVALID_POSITION;
            }

            final View targetChild = getChildAt(targetPosition - firstPosition);
            final int targetStart = getChildStartEdge(targetChild);
            final int targetEnd = getChildEndEdge(targetChild);

            int scrollBy = 0;
            if (targetEnd > end) {
                scrollBy = targetEnd - end;
            }
            if (targetStart < start) {
                scrollBy = targetStart - start;
            }

            if (scrollBy == 0) {
                return;
            }

            if (boundPosition >= 0) {
                final View boundChild = getChildAt(boundPosition - firstPosition);
                final int boundStart = getChildStartEdge(boundChild);
                final int boundEnd = getChildEndEdge(boundChild);
                final int absScroll = Math.abs(scrollBy);

                if (scrollBy < 0 && boundEnd + absScroll > end) {
                    // Don't scroll the bound view off the end of the screen.
                    scrollBy = Math.max(0, boundEnd - end);
                } else if (scrollBy > 0 && boundStart - absScroll < start) {
                    // Don't scroll the bound view off the top of the screen.
                    scrollBy = Math.min(0, boundStart - start);
                }
            }

            smoothScrollBy(scrollBy, duration);
        }

        void stop() {
            removeCallbacks(this);
        }

        @Override
        public void run() {
            final int size = getAvailableSize();
            final int firstPosition = mFirstPosition;

            final int startPadding = (mIsVertical ? getPaddingTop() : getPaddingLeft());
            final int endPadding = (mIsVertical ? getPaddingBottom() : getPaddingRight());

            switch (mMode) {
                case MOVE_AFTER_POS: {
                    final int lastViewIndex = getChildCount() - 1;
                    if (lastViewIndex < 0) {
                        return;
                    }

                    final int lastPosition = firstPosition + lastViewIndex;
                    if (lastPosition == mLastSeenPosition) {
                        // No new views, let things keep going.
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                        return;
                    }

                    final View lastView = getChildAt(lastViewIndex);
                    final int lastViewSize = getChildSize(lastView);
                    final int lastViewStart = getChildStartEdge(lastView);
                    final int lastViewPixelsShowing = size - lastViewStart;
                    final int extraScroll = lastPosition < mItemCount - 1 ?
                            Math.max(endPadding, mExtraScroll) : endPadding;

                    final int scrollBy = lastViewSize - lastViewPixelsShowing + extraScroll;
                    smoothScrollBy(scrollBy, mScrollDuration);

                    mLastSeenPosition = lastPosition;
                    if (lastPosition < mTargetPosition) {
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    }

                    break;
                }

                case MOVE_AFTER_BOUND: {
                    final int nextViewIndex = 1;
                    final int childCount = getChildCount();
                    if (firstPosition == mBoundPosition ||
                        childCount <= nextViewIndex ||
                        firstPosition + childCount >= mItemCount) {
                        return;
                    }

                    final int nextPosition = firstPosition + nextViewIndex;

                    if (nextPosition == mLastSeenPosition) {
                        // No new views, let things keep going.
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                        return;
                    }

                    final View nextView = getChildAt(nextViewIndex);
                    final int nextViewSize = getChildSize(nextView);
                    final int nextViewStart = getChildStartEdge(nextView);
                    final int extraScroll = Math.max(endPadding, mExtraScroll);
                    if (nextPosition < mBoundPosition) {
                        smoothScrollBy(Math.max(0, nextViewSize + nextViewStart - extraScroll),
                                mScrollDuration);
                        mLastSeenPosition = nextPosition;
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    } else  {
                        if (nextViewSize > extraScroll) {
                            smoothScrollBy(nextViewSize - extraScroll, mScrollDuration);
                        }
                    }

                    break;
                }

                case MOVE_BEFORE_POS: {
                    if (firstPosition == mLastSeenPosition) {
                        // No new views, let things keep going.
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                        return;
                    }

                    final View firstView = getChildAt(0);
                    if (firstView == null) {
                        return;
                    }

                    final int firstViewTop = getChildStartEdge(firstView);
                    final int extraScroll = firstPosition > 0 ?
                            Math.max(mExtraScroll, startPadding) : startPadding;

                    smoothScrollBy(firstViewTop - extraScroll, mScrollDuration);
                    mLastSeenPosition = firstPosition;

                    if (firstPosition > mTargetPosition) {
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    }

                    break;
                }

                case MOVE_BEFORE_BOUND: {
                    final int lastViewIndex = getChildCount() - 2;
                    if (lastViewIndex < 0) {
                        return;
                    }

                    final int lastPosition = firstPosition + lastViewIndex;

                    if (lastPosition == mLastSeenPosition) {
                        // No new views, let things keep going.
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                        return;
                    }

                    final View lastView = getChildAt(lastViewIndex);
                    final int lastViewSize = getChildSize(lastView);
                    final int lastViewStart = getChildStartEdge(lastView);
                    final int lastViewPixelsShowing = size - lastViewStart;
                    final int extraScroll = Math.max(startPadding, mExtraScroll);

                    mLastSeenPosition = lastPosition;

                    if (lastPosition > mBoundPosition) {
                        smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration);
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    } else {
                        final int end = size - extraScroll;
                        final int lastViewEnd = lastViewStart + lastViewSize;
                        if (end > lastViewEnd) {
                            smoothScrollBy(-(end - lastViewEnd), mScrollDuration);
                        }
                    }

                    break;
                }

                case MOVE_OFFSET: {
                    if (mLastSeenPosition == firstPosition) {
                        // No new views, let things keep going.
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                        return;
                    }

                    mLastSeenPosition = firstPosition;

                    final int childCount = getChildCount();
                    final int position = mTargetPosition;
                    final int lastPos = firstPosition + childCount - 1;

                    int viewTravelCount = 0;
                    if (position < firstPosition) {
                        viewTravelCount = firstPosition - position + 1;
                    } else if (position > lastPos) {
                        viewTravelCount = position - lastPos;
                    }

                    // Estimate how many screens we should travel
                    final float screenTravelCount = (float) viewTravelCount / childCount;

                    final float modifier = Math.min(Math.abs(screenTravelCount), 1.f);
                    if (position < firstPosition) {
                        final int distance = (int) (-getSize() * modifier);
                        final int duration = (int) (mScrollDuration * modifier);
                        smoothScrollBy(distance, duration);
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    } else if (position > lastPos) {
                        final int distance = (int) (getSize() * modifier);
                        final int duration = (int) (mScrollDuration * modifier);
                        smoothScrollBy(distance, duration);
                        ViewCompat.postOnAnimation(TwoWayView.this, this);
                    } else {
                        // On-screen, just scroll.
                        final View targetView = getChildAt(position - firstPosition);
                        final int targetStart = getChildStartEdge(targetView);
                        final int distance = targetStart - mOffsetFromStart;
                        final int duration = (int) (mScrollDuration *
                                ((float) Math.abs(distance) / getSize()));
                        smoothScrollBy(distance, duration);
                    }

                    break;
                }

                default:
                    break;
            }
        }
    }
}
