Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.settings.widget;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorInflater;
     21 import android.animation.ObjectAnimator;
     22 import android.content.Context;
     23 import android.content.res.TypedArray;
     24 import android.database.DataSetObserver;
     25 import android.graphics.Canvas;
     26 import android.graphics.Rect;
     27 import android.os.Bundle;
     28 import android.os.Parcel;
     29 import android.os.Parcelable;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.view.FocusFinder;
     33 import android.view.accessibility.AccessibilityEvent;
     34 import android.view.KeyEvent;
     35 import android.view.MotionEvent;
     36 import android.view.SoundEffectConstants;
     37 import android.view.View;
     38 import android.view.ViewGroup;
     39 import android.view.ViewParent;
     40 import android.widget.AdapterView;
     41 import android.widget.Adapter;
     42 
     43 import com.android.tv.settings.R;
     44 
     45 import java.util.ArrayList;
     46 import java.util.List;
     47 
     48 /**
     49  * A scrollable AdapterView, similar to {@link android.widget.Gallery}. Features include:
     50  * <p>
     51  * Supports "expandable" views by supplying a Adapter that implements
     52  * {@link ScrollAdapter#getExpandAdapter()}. Generally you could see two expanded views at most: one
     53  * fade in, one fade out.
     54  * <p>
     55  * Supports {@link #HORIZONTAL} and {@link #VERTICAL} set by {@link #setOrientation(int)}.
     56  * So you could have a vertical ScrollAdapterView with a nested expanding Horizontal ScrollAdapterView.
     57  * <p>
     58  * Supports Grid view style, see {@link #setGridSetting(int)}.
     59  * <p>
     60  * Supports Different strategies of scrolling viewport, see
     61  * {@link ScrollController#SCROLL_CENTER_IN_MIDDLE},
     62  * {@link ScrollController#SCROLL_CENTER_FIXED}, and
     63  * {@link ScrollController#SCROLL_CENTER_FIXED_PERCENT}.
     64  * Also take a look of {@link #adjustSystemScrollPos()} for better understanding how Center
     65  * is translated to android View scroll position.
     66  * <p>
     67  * Expandable items animation is based on distance to the center. Motivation behind not using two
     68  * time based animations for focusing/onfocusing is that in a fast scroll, there is no better way to
     69  * synchronize these two animations with scroller animation; so you will end up with situation that
     70  * scale animated item cannot be kept in the center because scroll animation is too fast/too slow.
     71  * By using distance to the scroll center, the animation of focus/unfocus will be accurately synced
     72  * with scroller animation. {@link #setLowItemTransform(Animator)} transforms items that are left or
     73  * up to scroll center position; {@link #setHighItemTransform(Animator)} transforms items that are
     74  * right or down to the scroll center position. It's recommended to use xml resource ref
     75  * "highItemTransform" and "lowItemTransform" attributes to load the animation from xml. The
     76  * animation duration which android by default is a duration of milliseconds is interpreted as dip
     77  * to the center. Here is an example that scales the center item to "1.2" of original size, any item
     78  * far from 60dip to scroll center has normal scale (scale = 1):
     79  * <pre>{@code
     80  * <set xmlns:android="http://schemas.android.com/apk/res/android" >
     81  *   <objectAnimator
     82  *       android:startOffset="0"
     83  *       android:duration="60"
     84  *       android:valueFrom="1.2"
     85  *       android:valueTo="1"
     86  *       android:valueType="floatType"
     87  *       android:propertyName="scaleX" />
     88  *   <objectAnimator
     89  *       android:startOffset="0"
     90  *       android:duration="60"
     91  *       android:valueFrom="1.2"
     92  *       android:valueTo="1"
     93  *       android:valueType="floatType"
     94  *       android:propertyName="scaleY"/>
     95  * </set>
     96  * } </pre>
     97  * When using an animation that expands the selected item room has to be made in the view for
     98  * the scale animation. To accomplish this set right/left and/or top/bottom padding values
     99  * for the ScrollAdapterView and also set its clipToPadding value to false. Another option is
    100  * to include padding in the item view itself.
    101  * <p>
    102  * Expanded items animation uses "normal" animation: duration is duration. Use xml attribute
    103  * expandedItemInAnim and expandedItemOutAnim for animation. A best practice is specify startOffset
    104  * for expandedItemInAnim to avoid showing half loaded expanded items during a fast scroll of
    105  * expandable items.
    106  */
    107 public final class ScrollAdapterView extends AdapterView<Adapter> {
    108 
    109     /** Callback interface for changing state of selected item */
    110     public static interface OnItemChangeListener {
    111         /**
    112          * In contrast to standard onFocusChange, the event is fired only when scrolling stops
    113          * @param view the view focusing to
    114          * @param position index in ScrollAdapter
    115          * @param targetCenter final center position of view to the left edge of ScrollAdapterView
    116          */
    117         public void onItemSelected(View view, int position, int targetCenter);
    118     }
    119 
    120     /**
    121      * Callback interface when there is scrolling happened, this function is called before
    122      * applying transformations ({@link ScrollAdapterTransform}).  This listener can be a
    123      * replacement of {@link ScrollAdapterTransform}.  The difference is that this listener
    124      * is called once when scroll position changes, {@link ScrollAdapterTransform} is called
    125      * on each child view.
    126      */
    127     public static interface OnScrollListener {
    128         /**
    129          * @param view the view focusing to
    130          * @param position index in ScrollAdapter
    131          * @param mainPosition position in the main axis 0(inclusive) ~ 1(exclusive)
    132          * @param secondPosition position in the second axis 0(inclusive) ~ 1(exclusive)
    133          */
    134         public void onScrolled(View view, int position, float mainPosition, float secondPosition);
    135     }
    136 
    137     // Hardcoded from InputDevice in sdk 18.
    138     private static final int SOURCE_TOUCH_NAV = 0x00200000;
    139 
    140     private static final String TAG = "ScrollAdapterView";
    141 
    142     private static final boolean DBG = false;
    143     private static final boolean DEBUG_FOCUS = false;
    144 
    145     private static final int MAX_RECYCLED_VIEWS = 10;
    146     private static final int MAX_RECYCLED_EXPANDED_VIEWS = 3;
    147 
    148     // search range for stable id, see {@link #heuristicGetPersistentIndex()}
    149     private static final int SEARCH_ID_RANGE = 30;
    150 
    151     /**
    152      * {@link ScrollAdapterView} fills horizontally
    153      */
    154     public static final int HORIZONTAL = 0;
    155 
    156     /**
    157      * {@link ScrollAdapterView} fills vertically
    158      */
    159     public static final int VERTICAL = 1;
    160 
    161     /** calculate number of items on second axis by "parentSize / childSize" */
    162     public static final int GRID_SETTING_AUTO = 0;
    163     /** single item on second axis (i.e. not a grid view) */
    164     public static final int GRID_SETTING_SINGLE = 1;
    165 
    166     private int mOrientation = HORIZONTAL;
    167 
    168     /** saved measuredSpec to pass to child views */
    169     private int mMeasuredSpec = -1;
    170 
    171     /** the Adapter used to create views */
    172     private ScrollAdapter mAdapter;
    173     private ScrollAdapterCustomSize mAdapterCustomSize;
    174     private ScrollAdapterCustomAlign mAdapterCustomAlign;
    175     private ScrollAdapterErrorHandler mAdapterErrorHandler;
    176     private int mSelectedSize;
    177 
    178     // flag that we have made initial selection during refreshing ScrollAdapterView
    179     private boolean mMadeInitialSelection = false;
    180 
    181     /** allow animate expanded size change when Scroller is stopped */
    182     private boolean mAnimateLayoutChange = true;
    183 
    184     private static class RecycledViews {
    185         List<View>[] mViews;
    186         int mMaxRecycledViews;
    187         ScrollAdapterBase mAdapter;
    188 
    189         RecycledViews(int max) {
    190             mMaxRecycledViews = max;
    191         }
    192 
    193         void updateAdapter(ScrollAdapterBase adapter) {
    194             if (adapter != null) {
    195                 int typeCount = adapter.getViewTypeCount();
    196                 if (mViews == null || typeCount != mViews.length) {
    197                     mViews = new List[typeCount];
    198                     for (int i = 0; i < typeCount; i++) {
    199                         mViews[i] = new ArrayList<View>();
    200                     }
    201                 }
    202             }
    203             mAdapter = adapter;
    204         }
    205 
    206         void recycleView(View child, int type) {
    207             if (mAdapter != null) {
    208                 mAdapter.viewRemoved(child);
    209             }
    210             if (mViews != null && type >=0 && type < mViews.length
    211                     && mViews[type].size() < mMaxRecycledViews) {
    212                 mViews[type].add(child);
    213             }
    214         }
    215 
    216         View getView(int type) {
    217             if (mViews != null && type >= 0 && type < mViews.length) {
    218                 List<View> array = mViews[type];
    219                 return array.size() > 0 ? array.remove(array.size() - 1) : null;
    220             }
    221             return null;
    222         }
    223     }
    224 
    225     private RecycledViews mRecycleViews = new RecycledViews(MAX_RECYCLED_VIEWS);
    226 
    227     private RecycledViews mRecycleExpandedViews = new RecycledViews(MAX_RECYCLED_EXPANDED_VIEWS);
    228 
    229     /** exclusive index of view on the left */
    230     private int mLeftIndex;
    231     /** exclusive index of view on the right */
    232     private int mRightIndex;
    233 
    234     /** space between two items */
    235     private int mSpace;
    236     private int mSpaceLow;
    237     private int mSpaceHigh;
    238 
    239     private int mGridSetting = GRID_SETTING_SINGLE;
    240     /** effective number of items on 2nd axis, calculated in {@link #onMeasure} */
    241     private int mItemsOnOffAxis;
    242 
    243     /** latch on centered item automatically when scroller velocity is less than LATCH_THRESHOLD*/
    244     private static final float LATCH_THRESHOLD = 1000f;
    245 
    246     /** maintains the scroller information */
    247     private ScrollController mScroll;
    248 
    249     private ArrayList<OnItemChangeListener> mOnItemChangeListeners =
    250             new ArrayList<OnItemChangeListener>();
    251     private ArrayList<OnScrollListener> mOnScrollListeners =
    252             new ArrayList<OnScrollListener>();
    253 
    254     private final static boolean DEFAULT_NAVIGATE_OUT_ALLOWED = true;
    255     private final static boolean DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED = true;
    256 
    257     private final static boolean DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED = true;
    258 
    259     final class ExpandableChildStates extends ViewsStateBundle {
    260         ExpandableChildStates() {
    261             super(SAVE_NO_CHILD, 0);
    262         }
    263         @Override
    264         protected void saveVisibleViewsUnchecked() {
    265             for (int i = firstExpandableIndex(), last = lastExpandableIndex(); i < last; i++) {
    266                 saveViewUnchecked(getChildAt(i), getAdapterIndex(i));
    267             }
    268         }
    269     }
    270     final class ExpandedChildStates extends ViewsStateBundle {
    271         ExpandedChildStates() {
    272             super(SAVE_LIMITED_CHILD, SAVE_LIMITED_CHILD_DEFAULT_VALUE);
    273         }
    274         @Override
    275         protected void saveVisibleViewsUnchecked() {
    276             for (int i = 0, size = mExpandedViews.size(); i < size; i++) {
    277                 ExpandedView v = mExpandedViews.get(i);
    278                 saveViewUnchecked(v.expandedView, v.index);
    279             }
    280         }
    281     }
    282 
    283     private static class ChildViewHolder {
    284         int mItemViewType;
    285         int mMaxSize; // max size in mainAxis of the same offaxis
    286         int mExtraSpaceLow; // extra space added before the view
    287         float mLocationInParent; // temp variable used in animating expanded view size change
    288         float mLocation; // temp variable used in animating expanded view size change
    289         int mScrollCenter; // cached scroll center
    290 
    291         ChildViewHolder(int t) {
    292             mItemViewType = t;
    293         }
    294     }
    295 
    296     /**
    297      * set in {@link #onRestoreInstanceState(Parcelable)} which triggers a re-layout
    298      * and ScrollAdapterView restores states in {@link #onLayout}
    299      */
    300     private AdapterViewState mLoadingState;
    301 
    302     /** saves all expandable child states */
    303     final private ExpandableChildStates mExpandableChildStates = new ExpandableChildStates();
    304 
    305     /** saves all expanded child states */
    306     final private ExpandedChildStates mExpandedChildStates = new ExpandedChildStates();
    307 
    308     private ScrollAdapterTransform mItemTransform;
    309 
    310     /** flag for data changed, {@link #onLayout} will cleaning the whole view */
    311     private boolean mDataSetChangedFlag;
    312 
    313     // current selected view adapter index, this is the final position to scroll to
    314     private int mSelectedIndex;
    315 
    316     private static class ScrollInfo {
    317         int index;
    318         long id;
    319         float mainPos;
    320         float secondPos;
    321         int viewLocation;
    322         ScrollInfo() {
    323             clear();
    324         }
    325         boolean isValid() {
    326             return index >= 0;
    327         }
    328         void clear() {
    329             index = -1;
    330             id = INVALID_ROW_ID;
    331         }
    332         void copyFrom(ScrollInfo other) {
    333             index = other.index;
    334             id = other.id;
    335             mainPos = other.mainPos;
    336             secondPos = other.secondPos;
    337             viewLocation = other.viewLocation;
    338         }
    339     }
    340 
    341     // positions that current scrolled to
    342     private final ScrollInfo mCurScroll = new ScrollInfo();
    343     private int mItemSelected = -1;
    344 
    345     private int mPendingSelection = -1;
    346     private float mPendingScrollPosition = 0f;
    347 
    348     private final ScrollInfo mScrollBeforeReset = new ScrollInfo();
    349 
    350     private boolean mScrollTaskRunning;
    351 
    352     private ScrollAdapterBase mExpandAdapter;
    353 
    354     /** used for measuring the size of {@link ScrollAdapterView} */
    355     private int mScrapWidth;
    356     private int mScrapHeight;
    357 
    358     /** Animator for showing expanded item */
    359     private Animator mExpandedItemInAnim = null;
    360 
    361     /** Animator for hiding expanded item */
    362     private Animator mExpandedItemOutAnim = null;
    363 
    364     private boolean mNavigateOutOfOffAxisAllowed = DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED;
    365     private boolean mNavigateOutAllowed = DEFAULT_NAVIGATE_OUT_ALLOWED;
    366 
    367     private boolean mNavigateInAnimationAllowed = DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED;
    368 
    369     /**
    370      * internal structure maintaining status of expanded views
    371      */
    372     final class ExpandedView {
    373         private static final int ANIM_DURATION = 450;
    374         ExpandedView(View v, int i, int t) {
    375             expandedView = v;
    376             index = i;
    377             viewType = t;
    378         }
    379 
    380         int index; // "Adapter index" of the expandable view
    381         int viewType;
    382         View expandedView; // expanded view
    383         float progress = 0f; // 0 ~ 1, indication if it's expanding or shrinking
    384         Animator grow_anim;
    385         Animator shrink_anim;
    386 
    387         Animator createFadeInAnimator() {
    388             if (mExpandedItemInAnim == null) {
    389                 expandedView.setAlpha(0);
    390                 ObjectAnimator anim1 = ObjectAnimator.ofFloat(null, "alpha", 1);
    391                 anim1.setStartDelay(ANIM_DURATION / 2);
    392                 anim1.setDuration(ANIM_DURATION * 2);
    393                 return anim1;
    394             } else {
    395                 return mExpandedItemInAnim.clone();
    396             }
    397         }
    398 
    399         Animator createFadeOutAnimator() {
    400             if (mExpandedItemOutAnim == null) {
    401                 ObjectAnimator anim1 = ObjectAnimator.ofFloat(null, "alpha", 0);
    402                 anim1.setDuration(ANIM_DURATION);
    403                 return anim1;
    404             } else {
    405                 return mExpandedItemOutAnim.clone();
    406             }
    407         }
    408 
    409         void setProgress(float p) {
    410             boolean growing = p > progress;
    411             boolean shrinking = p < progress;
    412             progress = p;
    413             if (growing) {
    414                 if (shrink_anim != null) {
    415                     shrink_anim.cancel();
    416                     shrink_anim = null;
    417                 }
    418                 if (grow_anim == null) {
    419                     grow_anim = createFadeInAnimator();
    420                     grow_anim.setTarget(expandedView);
    421                     grow_anim.start();
    422                 }
    423                 if (!mAnimateLayoutChange) {
    424                     grow_anim.end();
    425                 }
    426             } else if (shrinking) {
    427                 if (grow_anim != null) {
    428                     grow_anim.cancel();
    429                     grow_anim = null;
    430                 }
    431                 if (shrink_anim == null) {
    432                     shrink_anim = createFadeOutAnimator();
    433                     shrink_anim.setTarget(expandedView);
    434                     shrink_anim.start();
    435                 }
    436                 if (!mAnimateLayoutChange) {
    437                     shrink_anim.end();
    438                 }
    439             }
    440         }
    441 
    442         void close() {
    443             if (shrink_anim != null) {
    444                 shrink_anim.cancel();
    445                 shrink_anim = null;
    446             }
    447             if (grow_anim != null) {
    448                 grow_anim.cancel();
    449                 grow_anim = null;
    450             }
    451         }
    452     }
    453 
    454     /** list of ExpandedView structure */
    455     private final ArrayList<ExpandedView> mExpandedViews = new ArrayList<ExpandedView>(4);
    456 
    457     /** no scrolling */
    458     private static final int NO_SCROLL = 0;
    459     /** scrolling and centering a known focused view */
    460     private static final int SCROLL_AND_CENTER_FOCUS = 3;
    461 
    462     /**
    463      * internal state machine for scrolling, typical scenario: <br>
    464      * DPAD up/down is pressed: -> {@link #SCROLL_AND_CENTER_FOCUS} -> {@link #NO_SCROLL} <br>
    465      */
    466     private int mScrollerState;
    467 
    468     Rect mTempRect = new Rect(); // temp variable used in UI thread
    469 
    470     // Controls whether or not sounds should be played when scrolling/clicking
    471     private boolean mPlaySoundEffects = true;
    472 
    473     public ScrollAdapterView(Context context, AttributeSet attrs) {
    474         super(context, attrs);
    475         mScroll = new ScrollController(getContext());
    476         setChildrenDrawingOrderEnabled(true);
    477         setSoundEffectsEnabled(true);
    478         setWillNotDraw(true);
    479         initFromAttributes(context, attrs);
    480         reset();
    481     }
    482 
    483     private void initFromAttributes(Context context, AttributeSet attrs) {
    484         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollAdapterView);
    485 
    486         setOrientation(a.getInt(R.styleable.ScrollAdapterView_orientation, HORIZONTAL));
    487 
    488         mScroll.setScrollItemAlign(a.getInt(R.styleable.ScrollAdapterView_scrollItemAlign,
    489                 ScrollController.SCROLL_ITEM_ALIGN_CENTER));
    490 
    491         setGridSetting(a.getInt(R.styleable.ScrollAdapterView_gridSetting, 1));
    492 
    493         if (a.hasValue(R.styleable.ScrollAdapterView_lowItemTransform)) {
    494             setLowItemTransform(AnimatorInflater.loadAnimator(getContext(),
    495                     a.getResourceId(R.styleable.ScrollAdapterView_lowItemTransform, -1)));
    496         }
    497 
    498         if (a.hasValue(R.styleable.ScrollAdapterView_highItemTransform)) {
    499             setHighItemTransform(AnimatorInflater.loadAnimator(getContext(),
    500                     a.getResourceId(R.styleable.ScrollAdapterView_highItemTransform, -1)));
    501         }
    502 
    503         if (a.hasValue(R.styleable.ScrollAdapterView_expandedItemInAnim)) {
    504             mExpandedItemInAnim = AnimatorInflater.loadAnimator(getContext(),
    505                     a.getResourceId(R.styleable.ScrollAdapterView_expandedItemInAnim, -1));
    506         }
    507 
    508         if (a.hasValue(R.styleable.ScrollAdapterView_expandedItemOutAnim)) {
    509             mExpandedItemOutAnim = AnimatorInflater.loadAnimator(getContext(),
    510                     a.getResourceId(R.styleable.ScrollAdapterView_expandedItemOutAnim, -1));
    511         }
    512 
    513         setSpace(a.getDimensionPixelSize(R.styleable.ScrollAdapterView_space, 0));
    514 
    515         setSelectedTakesMoreSpace(a.getBoolean(
    516                 R.styleable.ScrollAdapterView_selectedTakesMoreSpace, false));
    517 
    518         setSelectedSize(a.getDimensionPixelSize(
    519                 R.styleable.ScrollAdapterView_selectedSize, 0));
    520 
    521         setScrollCenterStrategy(a.getInt(R.styleable.ScrollAdapterView_scrollCenterStrategy, 0));
    522 
    523         setScrollCenterOffset(a.getDimensionPixelSize(
    524                 R.styleable.ScrollAdapterView_scrollCenterOffset, 0));
    525 
    526         setScrollCenterOffsetPercent(a.getInt(
    527                 R.styleable.ScrollAdapterView_scrollCenterOffsetPercent, 0));
    528 
    529         setNavigateOutAllowed(a.getBoolean(
    530                 R.styleable.ScrollAdapterView_navigateOutAllowed, DEFAULT_NAVIGATE_OUT_ALLOWED));
    531 
    532         setNavigateOutOfOffAxisAllowed(a.getBoolean(
    533                 R.styleable.ScrollAdapterView_navigateOutOfOffAxisAllowed,
    534                 DEFAULT_NAVIGATE_OUT_OF_OFF_AXIS_ALLOWED));
    535 
    536         setNavigateInAnimationAllowed(a.getBoolean(
    537                 R.styleable.ScrollAdapterView_navigateInAnimationAllowed,
    538                 DEFAULT_NAVIGATE_IN_ANIMATION_ALLOWED));
    539 
    540         mScroll.lerper().setDivisor(a.getFloat(
    541                 R.styleable.ScrollAdapterView_lerperDivisor, Lerper.DEFAULT_DIVISOR));
    542 
    543         a.recycle();
    544     }
    545 
    546     public void setOrientation(int orientation) {
    547         mOrientation = orientation;
    548         mScroll.setOrientation(orientation);
    549     }
    550 
    551     public int getOrientation() {
    552         return mOrientation;
    553     }
    554 
    555     @SuppressWarnings("unchecked")
    556     private void reset() {
    557         mScrollBeforeReset.copyFrom(mCurScroll);
    558         mLeftIndex = -1;
    559         mRightIndex = 0;
    560         mDataSetChangedFlag = false;
    561         for (int i = 0, c = mExpandedViews.size(); i < c; i++) {
    562             ExpandedView v = mExpandedViews.get(i);
    563             v.close();
    564             removeViewInLayout(v.expandedView);
    565             mRecycleExpandedViews.recycleView(v.expandedView, v.viewType);
    566         }
    567         mExpandedViews.clear();
    568         for (int i = getChildCount() - 1; i >= 0; i--) {
    569             View child = getChildAt(i);
    570             removeViewInLayout(child);
    571             recycleExpandableView(child);
    572         }
    573         mRecycleViews.updateAdapter(mAdapter);
    574         mRecycleExpandedViews.updateAdapter(mExpandAdapter);
    575         mSelectedIndex = -1;
    576         mCurScroll.clear();
    577         mMadeInitialSelection = false;
    578     }
    579 
    580     /** find the view that containing scrollCenter or the next view */
    581     private int findViewIndexContainingScrollCenter(int scrollCenter, int scrollCenterOffAxis,
    582             boolean findNext) {
    583         final int lastExpandable = lastExpandableIndex();
    584         for (int i = firstExpandableIndex(); i < lastExpandable; i ++) {
    585             View view = getChildAt(i);
    586             int centerOffAxis = getCenterInOffAxis(view);
    587             int viewSizeOffAxis;
    588             if (mOrientation == HORIZONTAL) {
    589                 viewSizeOffAxis = view.getHeight();
    590             } else {
    591                 viewSizeOffAxis = view.getWidth();
    592             }
    593             int centerMain = getScrollCenter(view);
    594             if (hasScrollPosition(centerMain, getSize(view), scrollCenter)
    595                     && (mItemsOnOffAxis == 1 ||  hasScrollPositionSecondAxis(
    596                             scrollCenterOffAxis, viewSizeOffAxis, centerOffAxis))) {
    597                 if (findNext) {
    598                     if (mScroll.isMainAxisMovingForward() && centerMain < scrollCenter) {
    599                         if (i + mItemsOnOffAxis < lastExpandableIndex()) {
    600                             i = i + mItemsOnOffAxis;
    601                         }
    602                     } else if (!mScroll.isMainAxisMovingForward() && centerMain > scrollCenter) {
    603                         if (i - mItemsOnOffAxis >= firstExpandableIndex()) {
    604                             i = i - mItemsOnOffAxis;
    605                         }
    606                     }
    607                     if (mItemsOnOffAxis == 1) {
    608                         // don't look in second axis if it's not grid
    609                     } else if (mScroll.isSecondAxisMovingForward() &&
    610                             centerOffAxis < scrollCenterOffAxis) {
    611                         if (i + 1 < lastExpandableIndex()) {
    612                             i += 1;
    613                         }
    614                     } else if (!mScroll.isSecondAxisMovingForward() &&
    615                             centerOffAxis < scrollCenterOffAxis) {
    616                         if (i - 1 >= firstExpandableIndex()) {
    617                             i -= 1;
    618                         }
    619                     }
    620                 }
    621                 return i;
    622             }
    623         }
    624         return -1;
    625     }
    626 
    627     private int findViewIndexContainingScrollCenter() {
    628         return findViewIndexContainingScrollCenter(mScroll.mainAxis().getScrollCenter(),
    629                 mScroll.secondAxis().getScrollCenter(), false);
    630     }
    631 
    632     @Override
    633     public int getFirstVisiblePosition() {
    634         int first = firstExpandableIndex();
    635         return lastExpandableIndex() == first ? -1 : getAdapterIndex(first);
    636     }
    637 
    638     @Override
    639     public int getLastVisiblePosition() {
    640         int last = lastExpandableIndex();
    641         return firstExpandableIndex() == last ? -1 : getAdapterIndex(last - 1);
    642     }
    643 
    644     @Override
    645     public void setSelection(int position) {
    646         setSelectionInternal(position, 0f, true);
    647     }
    648 
    649     public void setSelection(int position, float offset) {
    650         setSelectionInternal(position, offset, true);
    651     }
    652 
    653     public int getCurrentAnimationDuration() {
    654         return mScroll.getCurrentAnimationDuration();
    655     }
    656 
    657     public void setSelectionSmooth(int index) {
    658         setSelectionSmooth(index, 0);
    659     }
    660 
    661     /** set selection using animation with a given duration, use 0 duration for auto  */
    662     public void setSelectionSmooth(int index, int duration) {
    663         int currentExpandableIndex = indexOfChild(getSelectedView());
    664         if (currentExpandableIndex < 0) {
    665             return;
    666         }
    667         int adapterIndex = getAdapterIndex(currentExpandableIndex);
    668         if (index == adapterIndex) {
    669             return;
    670         }
    671         boolean isGrowing = index > adapterIndex;
    672         View nextTop = null;
    673         if (isGrowing) {
    674             do {
    675                 if (index < getAdapterIndex(lastExpandableIndex())) {
    676                     nextTop = getChildAt(expandableIndexFromAdapterIndex(index));
    677                     break;
    678                 }
    679             } while (fillOneRightChildView(false));
    680         } else {
    681             do {
    682                 if (index >= getAdapterIndex(firstExpandableIndex())) {
    683                     nextTop = getChildAt(expandableIndexFromAdapterIndex(index));
    684                     break;
    685                 }
    686             } while (fillOneLeftChildView(false));
    687         }
    688         if (nextTop == null) {
    689             return;
    690         }
    691         int direction = isGrowing ?
    692                 (mOrientation == HORIZONTAL ? View.FOCUS_RIGHT : View.FOCUS_DOWN) :
    693                 (mOrientation == HORIZONTAL ? View.FOCUS_LEFT : View.FOCUS_UP);
    694         scrollAndFocusTo(nextTop, direction, false, duration, false);
    695     }
    696 
    697     private void fireDataSetChanged() {
    698         // set flag and trigger a scroll task
    699         mDataSetChangedFlag = true;
    700         scheduleScrollTask();
    701     }
    702 
    703     private DataSetObserver mDataObserver = new DataSetObserver() {
    704 
    705         @Override
    706         public void onChanged() {
    707             fireDataSetChanged();
    708         }
    709 
    710         @Override
    711         public void onInvalidated() {
    712             fireDataSetChanged();
    713         }
    714 
    715     };
    716 
    717     @Override
    718     public Adapter getAdapter() {
    719         return mAdapter;
    720     }
    721 
    722     /**
    723      * Adapter must be an implementation of {@link ScrollAdapter}.
    724      */
    725     @Override
    726     public void setAdapter(Adapter adapter) {
    727         if (mAdapter != null) {
    728             mAdapter.unregisterDataSetObserver(mDataObserver);
    729         }
    730         mAdapter = (ScrollAdapter) adapter;
    731         mExpandAdapter = mAdapter.getExpandAdapter();
    732         mAdapter.registerDataSetObserver(mDataObserver);
    733         mAdapterCustomSize = adapter instanceof ScrollAdapterCustomSize ?
    734                 (ScrollAdapterCustomSize) adapter : null;
    735         mAdapterCustomAlign = adapter instanceof ScrollAdapterCustomAlign ?
    736                 (ScrollAdapterCustomAlign) adapter : null;
    737         mMeasuredSpec = -1;
    738         mLoadingState = null;
    739         mPendingSelection = -1;
    740         mExpandableChildStates.clear();
    741         mExpandedChildStates.clear();
    742         mCurScroll.clear();
    743         mScrollBeforeReset.clear();
    744         fireDataSetChanged();
    745     }
    746 
    747     public void setErrorHandler(ScrollAdapterErrorHandler errorHandler) {
    748         mAdapterErrorHandler = errorHandler;
    749     }
    750 
    751     @Override
    752     public View getSelectedView() {
    753         return mSelectedIndex >= 0 ?
    754                 getChildAt(expandableIndexFromAdapterIndex(mSelectedIndex)) : null;
    755     }
    756 
    757     public View getSelectedExpandedView() {
    758         ExpandedView ev = findExpandedView(mExpandedViews, getSelectedItemPosition());
    759         return ev == null ? null : ev.expandedView;
    760     }
    761 
    762     public View getViewContainingScrollCenter() {
    763         return getChildAt(findViewIndexContainingScrollCenter());
    764     }
    765 
    766     public int getIndexContainingScrollCenter() {
    767         return getAdapterIndex(findViewIndexContainingScrollCenter());
    768     }
    769 
    770     @Override
    771     public int getSelectedItemPosition() {
    772         return mSelectedIndex;
    773     }
    774 
    775     @Override
    776     public Object getSelectedItem() {
    777         int index = getSelectedItemPosition();
    778         if (index < 0) return null;
    779         return getAdapter().getItem(index);
    780     }
    781 
    782     @Override
    783     public long getSelectedItemId() {
    784         if (mAdapter != null) {
    785             int index = getSelectedItemPosition();
    786             if (index < 0) return INVALID_ROW_ID;
    787             return mAdapter.getItemId(index);
    788         }
    789         return INVALID_ROW_ID;
    790     }
    791 
    792     public View getItemView(int position) {
    793         int index = expandableIndexFromAdapterIndex(position);
    794         if (index >= firstExpandableIndex() && index < lastExpandableIndex()) {
    795             return getChildAt(index);
    796         }
    797         return null;
    798     }
    799 
    800     /**
    801      * set system scroll position from our scroll position,
    802      */
    803     private void adjustSystemScrollPos() {
    804         scrollTo(mScroll.horizontal.getSystemScrollPos(), mScroll.vertical.getSystemScrollPos());
    805     }
    806 
    807     @Override
    808     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    809         mScroll.horizontal.setSize(w);
    810         mScroll.vertical.setSize(h);
    811         scheduleScrollTask();
    812     }
    813 
    814     /**
    815      * called from onLayout() to adjust all children's transformation based on how far they are from
    816      * {@link ScrollController.Axis#getScrollCenter()}
    817      */
    818     private void applyTransformations() {
    819         if (mItemTransform == null) {
    820             return;
    821         }
    822         int lastExpandable = lastExpandableIndex();
    823         for (int i = firstExpandableIndex(); i < lastExpandable; i++) {
    824             View child = getChildAt(i);
    825             mItemTransform.transform(child, getScrollCenter(child)
    826                     - mScroll.mainAxis().getScrollCenter(), mItemsOnOffAxis == 1 ? 0
    827                     : getCenterInOffAxis(child) - mScroll.secondAxis().getScrollCenter());
    828         }
    829     }
    830 
    831     @Override
    832     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    833         super.onLayout(changed, left, top, right, bottom);
    834         updateViewsLocations(true);
    835     }
    836 
    837     private void scheduleScrollTask() {
    838         if (!mScrollTaskRunning) {
    839             mScrollTaskRunning = true;
    840             postOnAnimation(mScrollTask);
    841         }
    842     }
    843 
    844     Runnable mScrollTask = new Runnable() {
    845         @Override
    846         public void run() {
    847             try {
    848                 scrollTaskRunInternal();
    849             } catch (RuntimeException ex) {
    850                 reset();
    851                 if (mAdapterErrorHandler != null) {
    852                     mAdapterErrorHandler.onError(ex);
    853                 } else {
    854                     ex.printStackTrace();
    855                 }
    856             }
    857         }
    858     };
    859 
    860     private void scrollTaskRunInternal() {
    861         mScrollTaskRunning = false;
    862         // 1. adjust mScrollController and system Scroll position
    863         if (mDataSetChangedFlag) {
    864             reset();
    865         }
    866         if (mAdapter == null || mAdapter.getCount() == 0) {
    867             invalidate();
    868             if (mAdapter != null) {
    869                 fireItemChange();
    870             }
    871             return;
    872         }
    873         if (mMeasuredSpec == -1) {
    874             // not layout yet
    875             requestLayout();
    876             scheduleScrollTask();
    877             return;
    878         }
    879         restoreLoadingState();
    880         mScroll.computeAndSetScrollPosition();
    881 
    882         boolean noChildBeforeFill = getChildCount() == 0;
    883 
    884         if (!noChildBeforeFill) {
    885             updateViewsLocations(false);
    886             adjustSystemScrollPos();
    887         }
    888 
    889         // 2. prune views that scroll out of visible area
    890         pruneInvisibleViewsInLayout();
    891 
    892         // 3. fill views in blank area
    893         fillVisibleViewsInLayout();
    894 
    895         if (noChildBeforeFill && getChildCount() > 0) {
    896             // if this is the first time add child(ren), we will get the initial value of
    897             // mScrollCenter after fillVisibleViewsInLayout(), and we need initalize the system
    898             // scroll position
    899             updateViewsLocations(false);
    900             adjustSystemScrollPos();
    901         }
    902 
    903         // 4. perform scroll position based animation
    904         // TODO remove vars once b/11602506 is fixed
    905         int index = mCurScroll.index;
    906         float mainPos = mCurScroll.mainPos;
    907         float secondPos = mCurScroll.secondPos;
    908         fireScrollChange();
    909         if (DEBUG_FOCUS && mScroll.isFinished()) {
    910             Log.d(TAG, "Scroll event finished,  index " + index + " -> " + mCurScroll.index
    911                     + " mainPos " + mainPos + " -> " + mCurScroll.mainPos + " secondPos "
    912                     + secondPos + " -> " + mCurScroll.secondPos);
    913         }
    914         applyTransformations();
    915 
    916         // 5. trigger another layout until the scroll stops
    917         if (!mScroll.isFinished()) {
    918             scheduleScrollTask();
    919         } else {
    920             // force ScrollAdapterView to reorder child order and call getChildDrawingOrder()
    921             invalidate();
    922             fireItemChange();
    923         }
    924     }
    925 
    926     @Override
    927     public void requestChildFocus(View child, View focused) {
    928         boolean receiveFocus = getFocusedChild() == null && child != null;
    929         super.requestChildFocus(child, focused);
    930         if (receiveFocus && mScroll.isFinished()) {
    931             // schedule {@link #updateViewsLocations()} for focus transition into expanded view
    932             scheduleScrollTask();
    933         }
    934     }
    935 
    936     private void recycleExpandableView(View child) {
    937         ChildViewHolder holder = ((ChildViewHolder)child.getTag(R.id.ScrollAdapterViewChild));
    938         if (holder != null) {
    939             mRecycleViews.recycleView(child, holder.mItemViewType);
    940         }
    941     }
    942 
    943     private void pruneInvisibleViewsInLayout() {
    944         View selectedView = getSelectedView();
    945         if (mScroll.isFinished() || mScroll.isMainAxisMovingForward()) {
    946             while (true) {
    947                 int firstIndex = firstExpandableIndex();
    948                 View child = getChildAt(firstIndex);
    949                 if (child == selectedView) {
    950                     break;
    951                 }
    952                 View nextChild = getChildAt(firstIndex + mItemsOnOffAxis);
    953                 if (nextChild == null) {
    954                     break;
    955                 }
    956                 View last = getChildAt(lastExpandableIndex() - 1);
    957                 if (mOrientation == HORIZONTAL) {
    958                     if (child.getRight() - getScrollX() > 0) {
    959                         // don't prune the first view if it's visible
    960                         break;
    961                     }
    962                 } else {
    963                     // VERTICAL is symmetric to HORIZONTAL, see comments above
    964                     if (child.getBottom() - getScrollY() > 0) {
    965                         break;
    966                     }
    967                 }
    968                 boolean foundFocus = false;
    969                 for (int i = 0; i < mItemsOnOffAxis; i++){
    970                     int childIndex = firstIndex + i;
    971                     if (childHasFocus(childIndex)) {
    972                         foundFocus = true;
    973                         break;
    974                     }
    975                 }
    976                 if (foundFocus) {
    977                     break;
    978                 }
    979                 for (int i = 0; i < mItemsOnOffAxis; i++){
    980                     child = getChildAt(firstExpandableIndex());
    981                     mExpandableChildStates.saveInvisibleView(child, mLeftIndex + 1);
    982                     removeViewInLayout(child);
    983                     recycleExpandableView(child);
    984                     mLeftIndex++;
    985                 }
    986             }
    987         }
    988         if (mScroll.isFinished() || !mScroll.isMainAxisMovingForward()) {
    989             while (true) {
    990                 int count = mRightIndex % mItemsOnOffAxis;
    991                 if (count == 0) {
    992                     count = mItemsOnOffAxis;
    993                 }
    994                 if (count > mRightIndex - mLeftIndex - 1) {
    995                     break;
    996                 }
    997                 int lastIndex = lastExpandableIndex();
    998                 View child = getChildAt(lastIndex - 1);
    999                 if (child == selectedView) {
   1000                     break;
   1001                 }
   1002                 View first = getChildAt(firstExpandableIndex());
   1003                 if (mOrientation == HORIZONTAL) {
   1004                     if (child.getLeft() - getScrollX() < getWidth()) {
   1005                         // don't prune the last view if it's visible
   1006                         break;
   1007                     }
   1008                 } else {
   1009                     // VERTICAL is symmetric to HORIZONTAL, see comments above
   1010                     if (child.getTop() - getScrollY() < getHeight()) {
   1011                         break;
   1012                     }
   1013                 }
   1014                 boolean foundFocus = false;
   1015                 for (int i = 0; i < count; i++){
   1016                     int childIndex = lastIndex - 1 - i;
   1017                     if (childHasFocus(childIndex)) {
   1018                         foundFocus = true;
   1019                         break;
   1020                     }
   1021                 }
   1022                 if (foundFocus) {
   1023                     break;
   1024                 }
   1025                 for (int i = 0; i < count; i++){
   1026                     child = getChildAt(lastExpandableIndex() - 1);
   1027                     mExpandableChildStates.saveInvisibleView(child, mRightIndex - 1);
   1028                     removeViewInLayout(child);
   1029                     recycleExpandableView(child);
   1030                     mRightIndex--;
   1031                 }
   1032             }
   1033         }
   1034     }
   1035 
   1036     /** check if expandable view or related expanded view has focus */
   1037     private boolean childHasFocus(int expandableViewIndex) {
   1038         View child = getChildAt(expandableViewIndex);
   1039         if (child.hasFocus()) {
   1040             return true;
   1041         }
   1042         ExpandedView v = findExpandedView(mExpandedViews, getAdapterIndex(expandableViewIndex));
   1043         if (v != null && v.expandedView.hasFocus()) {
   1044             return true;
   1045         }
   1046         return false;
   1047     }
   1048 
   1049     /**
   1050      * @param gridSetting <br>
   1051      * {@link #GRID_SETTING_SINGLE}: single item on second axis, i.e. not a grid view <br>
   1052      * {@link #GRID_SETTING_AUTO}: auto calculate number of items on second axis <br>
   1053      * >1: shown as a grid view, with given fixed number of items on second axis <br>
   1054      */
   1055     public void setGridSetting(int gridSetting) {
   1056         mGridSetting = gridSetting;
   1057         requestLayout();
   1058     }
   1059 
   1060     public int getGridSetting() {
   1061         return mGridSetting;
   1062     }
   1063 
   1064     private void fillVisibleViewsInLayout() {
   1065         while (fillOneRightChildView(true)) {
   1066         }
   1067         while (fillOneLeftChildView(true)) {
   1068         }
   1069         if (mRightIndex >= 0 && mLeftIndex == -1) {
   1070             // first child available
   1071             View child = getChildAt(firstExpandableIndex());
   1072             int scrollCenter = getScrollCenter(child);
   1073             mScroll.mainAxis().updateScrollMin(scrollCenter, getScrollLow(scrollCenter, child));
   1074         } else {
   1075             mScroll.mainAxis().invalidateScrollMin();
   1076         }
   1077         if (mRightIndex == mAdapter.getCount()) {
   1078             // last child available
   1079             View child = getChildAt(lastExpandableIndex() - 1);
   1080             int scrollCenter = getScrollCenter(child);
   1081             mScroll.mainAxis().updateScrollMax(scrollCenter, getScrollHigh(scrollCenter, child));
   1082         } else {
   1083             mScroll.mainAxis().invalidateScrollMax();
   1084         }
   1085     }
   1086 
   1087     /**
   1088      * try to add one left/top child view, returning false tells caller can stop loop
   1089      */
   1090     private boolean fillOneLeftChildView(boolean stopOnInvisible) {
   1091         // 1. check if we still need add view
   1092         if (mLeftIndex < 0) {
   1093             return false;
   1094         }
   1095         int left = Integer.MAX_VALUE;
   1096         int top = Integer.MAX_VALUE;
   1097         if (lastExpandableIndex() - firstExpandableIndex() > 0) {
   1098             int childIndex = firstExpandableIndex();
   1099             int last = Math.min(lastExpandableIndex(), childIndex + mItemsOnOffAxis);
   1100             for (int i = childIndex; i < last; i++) {
   1101                 View v = getChildAt(i);
   1102                 if (mOrientation == HORIZONTAL) {
   1103                     if (v.getLeft() < left) {
   1104                         left = v.getLeft();
   1105                     }
   1106                 } else {
   1107                     if (v.getTop() < top) {
   1108                         top = v.getTop();
   1109                     }
   1110                 }
   1111             }
   1112             boolean itemInvisible;
   1113             if (mOrientation == HORIZONTAL) {
   1114                 left -= mSpace;
   1115                 itemInvisible = left - getScrollX() <= 0;
   1116                 top = getPaddingTop();
   1117             } else {
   1118                 top -= mSpace;
   1119                 itemInvisible = top - getScrollY() <= 0;
   1120                 left = getPaddingLeft();
   1121             }
   1122             if (itemInvisible && stopOnInvisible) {
   1123                 return false;
   1124             }
   1125         } else {
   1126             return false;
   1127         }
   1128         // 2. create view and layout
   1129         return fillOneAxis(left, top, false, true);
   1130     }
   1131 
   1132     private View addAndMeasureExpandableView(int adapterIndex, int insertIndex) {
   1133         int type = mAdapter.getItemViewType(adapterIndex);
   1134         View recycleView = mRecycleViews.getView(type);
   1135         View child = mAdapter.getView(adapterIndex, recycleView, this);
   1136         if (child == null) {
   1137             return null;
   1138         }
   1139         child.setTag(R.id.ScrollAdapterViewChild, new ChildViewHolder(type));
   1140         addViewInLayout(child, insertIndex, child.getLayoutParams(), true);
   1141         measureChild(child);
   1142         return child;
   1143     }
   1144 
   1145     private void measureScrapChild(View child, int widthMeasureSpec, int heightMeasureSpec) {
   1146         LayoutParams p = child.getLayoutParams();
   1147         if (p == null) {
   1148             p = generateDefaultLayoutParams();
   1149             child.setLayoutParams(p);
   1150         }
   1151 
   1152         int childWidthSpec, childHeightSpec;
   1153         if (mOrientation == VERTICAL) {
   1154             childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec, 0, p.width);
   1155             int lpHeight = p.height;
   1156             if (lpHeight > 0) {
   1157                 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
   1158             } else {
   1159                 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
   1160             }
   1161         } else {
   1162             childHeightSpec = ViewGroup.getChildMeasureSpec(heightMeasureSpec, 0, p.height);
   1163             int lpWidth = p.width;
   1164             if (lpWidth > 0) {
   1165                 childWidthSpec = MeasureSpec.makeMeasureSpec(lpWidth, MeasureSpec.EXACTLY);
   1166             } else {
   1167                 childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
   1168             }
   1169         }
   1170         child.measure(childWidthSpec, childHeightSpec);
   1171     }
   1172 
   1173     @Override
   1174     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1175         if (mAdapter == null) {
   1176             Log.e(TAG, "onMeasure: Adapter not available ");
   1177             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   1178             return;
   1179         }
   1180         mScroll.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
   1181         mScroll.vertical.setPadding(getPaddingTop(), getPaddingBottom());
   1182 
   1183         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   1184         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
   1185         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   1186         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
   1187         int clientWidthSize = widthSize - getPaddingLeft() - getPaddingRight();
   1188         int clientHeightSize = heightSize - getPaddingTop() - getPaddingBottom();
   1189 
   1190         if (mMeasuredSpec == -1) {
   1191             View scrapView = mAdapter.getScrapView(this);
   1192             measureScrapChild(scrapView, MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
   1193             mScrapWidth = scrapView.getMeasuredWidth();
   1194             mScrapHeight = scrapView.getMeasuredHeight();
   1195         }
   1196 
   1197         mItemsOnOffAxis = mGridSetting > 0 ? mGridSetting
   1198             : mOrientation == HORIZONTAL ?
   1199                 (heightMode == MeasureSpec.UNSPECIFIED ? 1 : clientHeightSize / mScrapHeight)
   1200                 : (widthMode == MeasureSpec.UNSPECIFIED ? 1 : clientWidthSize / mScrapWidth);
   1201         if (mItemsOnOffAxis == 0) {
   1202             mItemsOnOffAxis = 1;
   1203         }
   1204 
   1205         if (mLoadingState != null && mItemsOnOffAxis != mLoadingState.itemsOnOffAxis) {
   1206             mLoadingState = null;
   1207         }
   1208 
   1209         // see table below "height handling"
   1210         if (widthMode == MeasureSpec.UNSPECIFIED ||
   1211                 (widthMode == MeasureSpec.AT_MOST && mOrientation == VERTICAL)) {
   1212             int size = mOrientation == VERTICAL ? mScrapWidth * mItemsOnOffAxis
   1213                     + mSpace * (mItemsOnOffAxis - 1) : mScrapWidth;
   1214             size += getPaddingLeft() + getPaddingRight();
   1215             widthSize = widthMode == MeasureSpec.AT_MOST ? Math.min(size, widthSize) : size;
   1216         }
   1217         // table of height handling
   1218         // heightMode:   UNSPECIFIED              AT_MOST                              EXACTLY
   1219         // HOROZINTAL    items*childHeight        min(items * childHeight, height)     height
   1220         // VERTICAL      childHeight              height                               height
   1221         if (heightMode == MeasureSpec.UNSPECIFIED ||
   1222                 (heightMode == MeasureSpec.AT_MOST && mOrientation == HORIZONTAL)) {
   1223             int size = mOrientation == HORIZONTAL ?
   1224                     mScrapHeight * mItemsOnOffAxis + mSpace * (mItemsOnOffAxis - 1) : mScrapHeight;
   1225             size += getPaddingTop() + getPaddingBottom();
   1226             heightSize = heightMode == MeasureSpec.AT_MOST ? Math.min(size, heightSize) : size;
   1227         }
   1228         mMeasuredSpec = mOrientation == HORIZONTAL ? heightMeasureSpec : widthMeasureSpec;
   1229 
   1230         setMeasuredDimension(widthSize, heightSize);
   1231 
   1232         // we allow scroll from padding low to padding high in the second axis
   1233         int scrollMin = mScroll.secondAxis().getPaddingLow();
   1234         int scrollMax = (mOrientation == HORIZONTAL ? heightSize : widthSize) -
   1235                 mScroll.secondAxis().getPaddingHigh();
   1236         mScroll.secondAxis().updateScrollMin(scrollMin, scrollMin);
   1237         mScroll.secondAxis().updateScrollMax(scrollMax, scrollMax);
   1238 
   1239         for (int j = 0, size = mExpandedViews.size(); j < size; j++) {
   1240             ExpandedView v = mExpandedViews.get(j);
   1241             measureChild(v.expandedView);
   1242         }
   1243 
   1244         for (int i = firstExpandableIndex(); i < lastExpandableIndex(); i++) {
   1245             View v = getChildAt(i);
   1246             if (v.isLayoutRequested()) {
   1247                 measureChild(v);
   1248             }
   1249         }
   1250     }
   1251 
   1252     /**
   1253      * override to draw from two sides, center item is draw at last
   1254      */
   1255     @Override
   1256     protected int getChildDrawingOrder(int childCount, int i) {
   1257         int minDistance = Integer.MAX_VALUE;
   1258         int focusIndex = mSelectedIndex < 0 ? -1 :
   1259                 expandableIndexFromAdapterIndex(mSelectedIndex);
   1260         if (focusIndex < 0) {
   1261             return i;
   1262         }
   1263         // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
   1264         // drawing order is 0 1 2 3 9 8 7 6 5 4
   1265         if (i < focusIndex) {
   1266             return i;
   1267         } else if (i < childCount - 1) {
   1268             return focusIndex + childCount - 1 - i;
   1269         } else {
   1270             return focusIndex;
   1271         }
   1272     }
   1273 
   1274     /**
   1275      * fill one off-axis views, the left/top of main axis will be interpreted as right/bottom if
   1276      * leftToRight is false
   1277      */
   1278     private boolean fillOneAxis(int left, int top, boolean leftToRight, boolean setInitialPos) {
   1279         // 2. create view and layout
   1280         int viewIndex = lastExpandableIndex();
   1281         int itemsToAdd = leftToRight ? Math.min(mItemsOnOffAxis, mAdapter.getCount() - mRightIndex)
   1282                 : mItemsOnOffAxis;
   1283         int maxSize = 0;
   1284         int maxSelectedSize = 0;
   1285         for (int i = 0; i < itemsToAdd; i++) {
   1286             View child = leftToRight ? addAndMeasureExpandableView(mRightIndex + i, -1) :
   1287                 addAndMeasureExpandableView(mLeftIndex - i, firstExpandableIndex());
   1288             if (child == null) {
   1289                 return false;
   1290             }
   1291             maxSize = Math.max(maxSize, mOrientation == HORIZONTAL ? child.getMeasuredWidth() :
   1292                     child.getMeasuredHeight());
   1293             maxSelectedSize = Math.max(
   1294                     maxSelectedSize, getSelectedItemSize(mLeftIndex - i, child));
   1295         }
   1296         if (!leftToRight) {
   1297             viewIndex = firstExpandableIndex();
   1298             if (mOrientation == HORIZONTAL) {
   1299                 left = left - maxSize;
   1300             } else {
   1301                 top = top - maxSize;
   1302             }
   1303         }
   1304         for (int i = 0; i < itemsToAdd; i++) {
   1305             View child = getChildAt(viewIndex + i);
   1306             ChildViewHolder h = (ChildViewHolder) child.getTag(R.id.ScrollAdapterViewChild);
   1307             h.mMaxSize = maxSize;
   1308             if (mOrientation == HORIZONTAL) {
   1309                 switch (mScroll.getScrollItemAlign()) {
   1310                 case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   1311                     child.layout(left + maxSize / 2 - child.getMeasuredWidth() / 2, top,
   1312                             left + maxSize / 2 + child.getMeasuredWidth() / 2,
   1313                             top + child.getMeasuredHeight());
   1314                     break;
   1315                 case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   1316                     child.layout(left, top, left + child.getMeasuredWidth(),
   1317                             top + child.getMeasuredHeight());
   1318                     break;
   1319                 case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   1320                     child.layout(left + maxSize - child.getMeasuredWidth(), top, left + maxSize,
   1321                             top + child.getMeasuredHeight());
   1322                     break;
   1323                 }
   1324                 top += child.getMeasuredHeight();
   1325                 top += mSpace;
   1326             } else {
   1327                 switch (mScroll.getScrollItemAlign()) {
   1328                 case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   1329                     child.layout(left, top + maxSize / 2 - child.getMeasuredHeight() / 2,
   1330                             left + child.getMeasuredWidth(),
   1331                             top + maxSize / 2 + child.getMeasuredHeight() / 2);
   1332                     break;
   1333                 case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   1334                     child.layout(left, top, left + child.getMeasuredWidth(),
   1335                             top + child.getMeasuredHeight());
   1336                     break;
   1337                 case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   1338                     child.layout(left, top + maxSize - child.getMeasuredHeight(),
   1339                             left + getMeasuredWidth(), top + maxSize);
   1340                     break;
   1341                 }
   1342                 left += child.getMeasuredWidth();
   1343                 left += mSpace;
   1344             }
   1345             if (leftToRight) {
   1346                 mExpandableChildStates.loadView(child, mRightIndex);
   1347                 mRightIndex++;
   1348             } else {
   1349                 mExpandableChildStates.loadView(child, mLeftIndex);
   1350                 mLeftIndex--;
   1351             }
   1352             h.mScrollCenter = computeScrollCenter(viewIndex + i);
   1353             if (setInitialPos && leftToRight &&
   1354                     mAdapter.isEnabled(mRightIndex - 1) && !mMadeInitialSelection) {
   1355                 // this is the first child being added
   1356                 int centerMain = getScrollCenter(child);
   1357                 int centerSecond = getCenterInOffAxis(child);
   1358                 if (mOrientation == HORIZONTAL) {
   1359                     mScroll.setScrollCenter(centerMain, centerSecond);
   1360                 } else {
   1361                     mScroll.setScrollCenter(centerSecond, centerMain);
   1362                 }
   1363                 mMadeInitialSelection = true;
   1364                 transferFocusTo(child, 0);
   1365             }
   1366         }
   1367         return true;
   1368     }
   1369     /**
   1370      * try to add one right/bottom child views, returning false tells caller can stop loop
   1371      */
   1372     private boolean fillOneRightChildView(boolean stopOnInvisible) {
   1373         // 1. check if we still need add view
   1374         if (mRightIndex >= mAdapter.getCount()) {
   1375             return false;
   1376         }
   1377         int left = getPaddingLeft();
   1378         int top = getPaddingTop();
   1379         boolean checkedChild = false;
   1380         if (lastExpandableIndex() - firstExpandableIndex() > 0) {
   1381             // position of new view should starts from the last child or expanded view of last
   1382             // child if it exists
   1383             int childIndex = lastExpandableIndex() - 1;
   1384             int gridPos = getAdapterIndex(childIndex) % mItemsOnOffAxis;
   1385             for (int i = childIndex - gridPos; i < lastExpandableIndex(); i++) {
   1386                 View v = getChildAt(i);
   1387                 int adapterIndex = getAdapterIndex(i);
   1388                 ExpandedView expandedView = findExpandedView(mExpandedViews, adapterIndex);
   1389                 if (expandedView != null) {
   1390                     if (mOrientation == HORIZONTAL) {
   1391                         left = expandedView.expandedView.getRight();
   1392                     } else {
   1393                         top = expandedView.expandedView.getBottom();
   1394                     }
   1395                     checkedChild = true;
   1396                     break;
   1397                 }
   1398                 if (mOrientation == HORIZONTAL) {
   1399                     if (!checkedChild) {
   1400                         checkedChild = true;
   1401                         left = v.getRight();
   1402                     } else if (v.getRight() > left) {
   1403                         left = v.getRight();
   1404                     }
   1405                 } else {
   1406                     if (!checkedChild) {
   1407                         checkedChild = true;
   1408                         top = v.getBottom();
   1409                     } else if (v.getBottom() > top) {
   1410                         top = v.getBottom();
   1411                     }
   1412                 }
   1413             }
   1414             boolean itemInvisible;
   1415             if (mOrientation == HORIZONTAL) {
   1416                 left += mSpace;
   1417                 itemInvisible = left - getScrollX() >= getWidth();
   1418                 top = getPaddingTop();
   1419             } else {
   1420                 top += mSpace;
   1421                 itemInvisible = top - getScrollY() >= getHeight();
   1422                 left = getPaddingLeft();
   1423             }
   1424             if (itemInvisible && stopOnInvisible) {
   1425                 return false;
   1426             }
   1427         }
   1428         // 2. create view and layout
   1429         return fillOneAxis(left, top, true, true);
   1430     }
   1431 
   1432     private int heuristicGetPersistentIndex() {
   1433         int selection = -1;
   1434         int c = mAdapter.getCount();
   1435         if (mScrollBeforeReset.id != INVALID_ROW_ID) {
   1436             if (mScrollBeforeReset.index < c
   1437                     && mAdapter.getItemId(mScrollBeforeReset.index) == mScrollBeforeReset.id) {
   1438                 return mScrollBeforeReset.index;
   1439             }
   1440             for (int i = 1; i <= SEARCH_ID_RANGE; i++) {
   1441                 int index = mScrollBeforeReset.index + i;
   1442                 if (index < c && mAdapter.getItemId(index) == mScrollBeforeReset.id) {
   1443                     return index;
   1444                 }
   1445                 index = mScrollBeforeReset.index - i;
   1446                 if (index >=0 && index < c && mAdapter.getItemId(index) == mScrollBeforeReset.id) {
   1447                     return index;
   1448                 }
   1449             }
   1450         }
   1451         return mScrollBeforeReset.index >= c ? c - 1 : mScrollBeforeReset.index;
   1452     }
   1453 
   1454     private void restoreLoadingState() {
   1455         int selection;
   1456         int viewLoc = Integer.MIN_VALUE;
   1457         float scrollPosition = 0f;
   1458         int fillWindowLeft = -1;
   1459         int fillWindowRight = -1;
   1460         boolean hasFocus = hasFocus();
   1461         int centerX = 0, centerY = 0;
   1462         Bundle expandableChildStates = null;
   1463         Bundle expandedChildStates = null;
   1464         if (mPendingSelection >= 0) {
   1465             // got setSelection calls
   1466             selection = mPendingSelection;
   1467             scrollPosition = mPendingScrollPosition;
   1468         } else if (mScrollBeforeReset.isValid()) {
   1469             // data was refreshed, try to recover where we were
   1470             selection = heuristicGetPersistentIndex();
   1471             viewLoc = mScrollBeforeReset.viewLocation;
   1472         } else if (mLoadingState != null) {
   1473             // scrollAdapterView is restoring from loading state
   1474             selection = mLoadingState.index;
   1475             expandableChildStates = mLoadingState.expandableChildStates;
   1476             expandedChildStates = mLoadingState.expandedChildStates;
   1477         } else {
   1478             return;
   1479         }
   1480         mPendingSelection = -1;
   1481         mScrollBeforeReset.clear();
   1482         mLoadingState = null;
   1483         if (selection < 0 || selection >= mAdapter.getCount()) {
   1484             Log.w(TAG, "invalid selection "+selection);
   1485             return;
   1486         }
   1487 
   1488         // startIndex is the first child in the same offAxis of selection
   1489         // We add this view first because we don't know "selection" position in offAxis
   1490         int startIndex = selection - selection % mItemsOnOffAxis;
   1491         int left, top;
   1492         if (mOrientation == HORIZONTAL) {
   1493             // estimation of left
   1494             left = viewLoc != Integer.MIN_VALUE ? viewLoc: mScroll.horizontal.getPaddingLow()
   1495                     + mScrapWidth * (selection / mItemsOnOffAxis);
   1496             top = mScroll.vertical.getPaddingLow();
   1497         } else {
   1498             left = mScroll.horizontal.getPaddingLow();
   1499             // estimation of top
   1500             top = viewLoc != Integer.MIN_VALUE ? viewLoc: mScroll.vertical.getPaddingLow()
   1501                     + mScrapHeight * (selection / mItemsOnOffAxis);
   1502         }
   1503         mRightIndex = startIndex;
   1504         mLeftIndex = mRightIndex - 1;
   1505         fillOneAxis(left, top, true, false);
   1506         mMadeInitialSelection = true;
   1507         // fill all views, should include the "selection" view
   1508         fillVisibleViewsInLayout();
   1509         View child = getExpandableView(selection);
   1510         if (child == null) {
   1511             Log.w(TAG, "unable to restore selection view");
   1512             return;
   1513         }
   1514         mExpandableChildStates.loadView(child, selection);
   1515         if (viewLoc != Integer.MIN_VALUE && mScrollerState == SCROLL_AND_CENTER_FOCUS) {
   1516             // continue scroll animation but since the views and sizes might change, we need
   1517             // update the scrolling final target
   1518             int finalLocation = (mOrientation == HORIZONTAL) ? mScroll.getFinalX() :
   1519                     mScroll.getFinalY();
   1520             mSelectedIndex = getAdapterIndex(indexOfChild(child));
   1521             int scrollCenter = getScrollCenter(child);
   1522             if (mScroll.mainAxis().getScrollCenter() <= finalLocation) {
   1523                 while (scrollCenter < finalLocation) {
   1524                     int nextAdapterIndex = mSelectedIndex + mItemsOnOffAxis;
   1525                     View nextView = getExpandableView(nextAdapterIndex);
   1526                     if (nextView == null) {
   1527                         if (!fillOneRightChildView(false)) {
   1528                             break;
   1529                         }
   1530                         nextView = getExpandableView(nextAdapterIndex);
   1531                     }
   1532                     int nextScrollCenter = getScrollCenter(nextView);
   1533                     if (nextScrollCenter > finalLocation) {
   1534                         break;
   1535                     }
   1536                     mSelectedIndex = nextAdapterIndex;
   1537                     scrollCenter = nextScrollCenter;
   1538                 }
   1539             } else {
   1540                 while (scrollCenter > finalLocation) {
   1541                     int nextAdapterIndex = mSelectedIndex - mItemsOnOffAxis;
   1542                     View nextView = getExpandableView(nextAdapterIndex);
   1543                     if (nextView == null) {
   1544                         if (!fillOneLeftChildView(false)) {
   1545                             break;
   1546                         }
   1547                         nextView = getExpandableView(nextAdapterIndex);
   1548                     }
   1549                     int nextScrollCenter = getScrollCenter(nextView);
   1550                     if (nextScrollCenter < finalLocation) {
   1551                         break;
   1552                     }
   1553                     mSelectedIndex = nextAdapterIndex;
   1554                     scrollCenter = nextScrollCenter;
   1555                 }
   1556             }
   1557             if (mOrientation == HORIZONTAL) {
   1558                 mScroll.setFinalX(scrollCenter);
   1559             } else {
   1560                 mScroll.setFinalY(scrollCenter);
   1561             }
   1562         } else {
   1563             // otherwise center focus to the view and stop animation
   1564             setSelectionInternal(selection, scrollPosition, false);
   1565         }
   1566     }
   1567 
   1568     private void measureChild(View child) {
   1569         LayoutParams p = child.getLayoutParams();
   1570         if (p == null) {
   1571             p = generateDefaultLayoutParams();
   1572             child.setLayoutParams(p);
   1573         }
   1574         if (mOrientation == VERTICAL) {
   1575             int childWidthSpec = ViewGroup.getChildMeasureSpec(mMeasuredSpec, 0, p.width);
   1576             int lpHeight = p.height;
   1577             int childHeightSpec;
   1578             if (lpHeight > 0) {
   1579                 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
   1580             } else {
   1581                 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
   1582             }
   1583             child.measure(childWidthSpec, childHeightSpec);
   1584         } else {
   1585             int childHeightSpec = ViewGroup.getChildMeasureSpec(mMeasuredSpec, 0, p.height);
   1586             int lpWidth = p.width;
   1587             int childWidthSpec;
   1588             if (lpWidth > 0) {
   1589                 childWidthSpec = MeasureSpec.makeMeasureSpec(lpWidth, MeasureSpec.EXACTLY);
   1590             } else {
   1591                 childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
   1592             }
   1593             child.measure(childWidthSpec, childHeightSpec);
   1594         }
   1595     }
   1596 
   1597     @Override
   1598     public boolean dispatchKeyEvent(KeyEvent event) {
   1599         // passing key event to focused child, which has chance to stop event processing by
   1600         // returning true.
   1601         // If child does not handle the event, we handle DPAD etc.
   1602         return super.dispatchKeyEvent(event) || event.dispatch(this, null, null);
   1603     }
   1604 
   1605     protected boolean internalKeyDown(int keyCode, KeyEvent event) {
   1606         switch (keyCode) {
   1607             case KeyEvent.KEYCODE_DPAD_LEFT:
   1608                 if (handleArrowKey(View.FOCUS_LEFT, 0, false, false)) {
   1609                     return true;
   1610                 }
   1611                 break;
   1612             case KeyEvent.KEYCODE_DPAD_RIGHT:
   1613                 if (handleArrowKey(View.FOCUS_RIGHT, 0, false, false)) {
   1614                     return true;
   1615                 }
   1616                 break;
   1617             case KeyEvent.KEYCODE_DPAD_UP:
   1618                 if (handleArrowKey(View.FOCUS_UP, 0, false, false)) {
   1619                     return true;
   1620                 }
   1621                 break;
   1622             case KeyEvent.KEYCODE_DPAD_DOWN:
   1623                 if (handleArrowKey(View.FOCUS_DOWN, 0, false, false)) {
   1624                     return true;
   1625                 }
   1626                 break;
   1627         }
   1628         return super.onKeyDown(keyCode, event);
   1629     }
   1630 
   1631     @Override
   1632     public boolean onKeyDown(int keyCode, KeyEvent event) {
   1633         return internalKeyDown(keyCode, event);
   1634     }
   1635 
   1636     @Override
   1637     public boolean onKeyUp(int keyCode, KeyEvent event) {
   1638         switch (keyCode) {
   1639             case KeyEvent.KEYCODE_DPAD_CENTER:
   1640             case KeyEvent.KEYCODE_ENTER:
   1641                 if (getOnItemClickListener() != null) {
   1642                     int index = findViewIndexContainingScrollCenter();
   1643                     View child = getChildAt(index);
   1644                     if (child != null) {
   1645                         int adapterIndex = getAdapterIndex(index);
   1646                         getOnItemClickListener().onItemClick(this, child,
   1647                                 adapterIndex, mAdapter.getItemId(adapterIndex));
   1648                         return true;
   1649                     }
   1650                 }
   1651                 // otherwise fall back to default handling, typically handled by
   1652                 // the focused child view
   1653                 break;
   1654         }
   1655         return super.onKeyUp(keyCode, event);
   1656     }
   1657 
   1658     /**
   1659      * Scroll to next/last expandable view.
   1660      * @param direction The direction corresponding to the arrow key that was pressed
   1661      * @param repeats repeated count (0 means no repeat)
   1662      * @return True if we consumed the event, false otherwise
   1663      */
   1664     public boolean arrowScroll(int direction, int repeats) {
   1665         if (DBG) Log.d(TAG, "arrowScroll " + direction);
   1666         return handleArrowKey(direction, repeats, true, false);
   1667     }
   1668 
   1669     /** equivalent to arrowScroll(direction, 0) */
   1670     public boolean arrowScroll(int direction) {
   1671         return arrowScroll(direction, 0);
   1672     }
   1673 
   1674     public boolean isInScrolling() {
   1675         return !mScroll.isFinished();
   1676     }
   1677 
   1678     public boolean isInScrollingOrDragging() {
   1679         return mScrollerState != NO_SCROLL;
   1680     }
   1681 
   1682     public void setPlaySoundEffects(boolean playSoundEffects) {
   1683         mPlaySoundEffects = playSoundEffects;
   1684     }
   1685 
   1686     private static boolean isDirectionGrowing(int direction) {
   1687         return direction == View.FOCUS_RIGHT || direction == View.FOCUS_DOWN;
   1688     }
   1689 
   1690     private static boolean isDescendant(View parent, View v) {
   1691         while (v != null) {
   1692             ViewParent p = v.getParent();
   1693             if (p == parent) {
   1694                 return true;
   1695             }
   1696             if (!(p instanceof View)) {
   1697                 return false;
   1698             }
   1699             v = (View) p;
   1700         }
   1701         return false;
   1702     }
   1703 
   1704     private boolean requestNextFocus(int direction, View focused, View newFocus) {
   1705         focused.getFocusedRect(mTempRect);
   1706         offsetDescendantRectToMyCoords(focused, mTempRect);
   1707         offsetRectIntoDescendantCoords(newFocus, mTempRect);
   1708         return newFocus.requestFocus(direction, mTempRect);
   1709     }
   1710 
   1711     protected boolean handleArrowKey(int direction, int repeats, boolean forceFindNextExpandable,
   1712             boolean page) {
   1713         View currentTop = getFocusedChild();
   1714         View currentExpandable = getExpandableChild(currentTop);
   1715         View focused = findFocus();
   1716         if (currentTop == currentExpandable && focused != null && !forceFindNextExpandable) {
   1717             // find next focused inside expandable item
   1718             View v = focused.focusSearch(direction);
   1719             if (v != null && v != focused && isDescendant(currentTop, v)) {
   1720                 requestNextFocus(direction, focused, v);
   1721                 return true;
   1722             }
   1723         }
   1724         boolean isGrowing = isDirectionGrowing(direction);
   1725         boolean isOnOffAxis = false;
   1726         if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) {
   1727             isOnOffAxis = mOrientation == VERTICAL;
   1728         } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) {
   1729             isOnOffAxis = mOrientation == HORIZONTAL;
   1730         }
   1731 
   1732         if (currentTop != currentExpandable && !forceFindNextExpandable) {
   1733             // find next focused inside expanded item
   1734             View nextFocused = currentTop instanceof ViewGroup ? FocusFinder.getInstance()
   1735                     .findNextFocus((ViewGroup) currentTop, findFocus(), direction)
   1736                     : null;
   1737             View nextTop = getTopItem(nextFocused);
   1738             if (nextTop == currentTop) {
   1739                 // within same expanded item
   1740                 // ignore at this level, the key handler of expanded item will take care
   1741                 return false;
   1742             }
   1743         }
   1744 
   1745         // focus to next expandable item
   1746         int currentExpandableIndex = expandableIndexFromAdapterIndex(mSelectedIndex);
   1747         if (currentExpandableIndex < 0) {
   1748             return false;
   1749         }
   1750         View nextTop = null;
   1751         if (isOnOffAxis) {
   1752             if (isGrowing && currentExpandableIndex + 1 < lastExpandableIndex() &&
   1753                             getAdapterIndex(currentExpandableIndex) % mItemsOnOffAxis
   1754                             != mItemsOnOffAxis - 1) {
   1755                 nextTop = getChildAt(currentExpandableIndex + 1);
   1756             } else if (!isGrowing && currentExpandableIndex - 1 >= firstExpandableIndex()
   1757                     && getAdapterIndex(currentExpandableIndex) % mItemsOnOffAxis != 0) {
   1758                 nextTop = getChildAt(currentExpandableIndex - 1);
   1759             } else {
   1760                 return !mNavigateOutOfOffAxisAllowed;
   1761             }
   1762         } else {
   1763             int adapterIndex = getAdapterIndex(currentExpandableIndex);
   1764             int focusAdapterIndex = adapterIndex;
   1765             for (int totalCount = repeats + 1; totalCount > 0;) {
   1766                 int nextFocusAdapterIndex = isGrowing ? focusAdapterIndex + mItemsOnOffAxis:
   1767                     focusAdapterIndex - mItemsOnOffAxis;
   1768                 if ((isGrowing && nextFocusAdapterIndex >= mAdapter.getCount())
   1769                         || (!isGrowing && nextFocusAdapterIndex < 0)) {
   1770                     if (focusAdapterIndex == adapterIndex
   1771                             || !mAdapter.isEnabled(focusAdapterIndex)) {
   1772                         if (hasFocus() && mNavigateOutAllowed) {
   1773                             View view = getChildAt(
   1774                                     expandableIndexFromAdapterIndex(focusAdapterIndex));
   1775                             if (view != null && !view.hasFocus()) {
   1776                                 view.requestFocus();
   1777                             }
   1778                         }
   1779                         return !mNavigateOutAllowed;
   1780                     } else {
   1781                         break;
   1782                     }
   1783                 }
   1784                 focusAdapterIndex = nextFocusAdapterIndex;
   1785                 if (mAdapter.isEnabled(focusAdapterIndex)) {
   1786                     totalCount--;
   1787                 }
   1788             }
   1789             if (isGrowing) {
   1790                 do {
   1791                     if (focusAdapterIndex <= getAdapterIndex(lastExpandableIndex() - 1)) {
   1792                         nextTop = getChildAt(expandableIndexFromAdapterIndex(focusAdapterIndex));
   1793                         break;
   1794                     }
   1795                 } while (fillOneRightChildView(false));
   1796                 if (nextTop == null) {
   1797                     nextTop = getChildAt(lastExpandableIndex() - 1);
   1798                 }
   1799             } else {
   1800                 do {
   1801                     if (focusAdapterIndex >= getAdapterIndex(firstExpandableIndex())) {
   1802                         nextTop = getChildAt(expandableIndexFromAdapterIndex(focusAdapterIndex));
   1803                         break;
   1804                     }
   1805                 } while (fillOneLeftChildView(false));
   1806                 if (nextTop == null) {
   1807                     nextTop = getChildAt(firstExpandableIndex());
   1808                 }
   1809             }
   1810             if (nextTop == null) {
   1811                 return true;
   1812             }
   1813         }
   1814         scrollAndFocusTo(nextTop, direction, false, 0, page);
   1815         if (mPlaySoundEffects) {
   1816             playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
   1817         }
   1818         return true;
   1819     }
   1820 
   1821     private void fireItemChange() {
   1822         int childIndex = findViewIndexContainingScrollCenter();
   1823         View topItem = getChildAt(childIndex);
   1824         if (isFocused() && getDescendantFocusability() == FOCUS_AFTER_DESCENDANTS
   1825                 && topItem != null) {
   1826             // transfer focus to child for reset/restore
   1827             topItem.requestFocus();
   1828         }
   1829         if (mOnItemChangeListeners != null && !mOnItemChangeListeners.isEmpty()) {
   1830             if (topItem == null) {
   1831                 if (mItemSelected != -1) {
   1832                     for (OnItemChangeListener listener : mOnItemChangeListeners) {
   1833                         listener.onItemSelected(null, -1, 0);
   1834                     }
   1835                     mItemSelected = -1;
   1836                 }
   1837             } else {
   1838                 int adapterIndex = getAdapterIndex(childIndex);
   1839                 int scrollCenter = getScrollCenter(topItem);
   1840                 for (OnItemChangeListener listener : mOnItemChangeListeners) {
   1841                     listener.onItemSelected(topItem, adapterIndex, scrollCenter -
   1842                             mScroll.mainAxis().getSystemScrollPos(scrollCenter));
   1843                 }
   1844                 mItemSelected = adapterIndex;
   1845             }
   1846         }
   1847 
   1848         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
   1849     }
   1850 
   1851     private void updateScrollInfo(ScrollInfo info) {
   1852         int scrollCenter = mScroll.mainAxis().getScrollCenter();
   1853         int scrollCenterOff = mScroll.secondAxis().getScrollCenter();
   1854         int index = findViewIndexContainingScrollCenter(
   1855                 scrollCenter, scrollCenterOff, false);
   1856         if (index < 0) {
   1857             info.index = -1;
   1858             return;
   1859         }
   1860         View view = getChildAt(index);
   1861         int center = getScrollCenter(view);
   1862         if (scrollCenter > center) {
   1863             if (index + mItemsOnOffAxis < lastExpandableIndex()) {
   1864                 int nextCenter = getScrollCenter(getChildAt(index + mItemsOnOffAxis));
   1865                 info.mainPos = (float)(scrollCenter - center) / (nextCenter - center);
   1866             } else {
   1867                 // overscroll to right
   1868                 info.mainPos = (float)(scrollCenter - center) / getSize(view);
   1869             }
   1870         } else if (scrollCenter == center){
   1871             info.mainPos = 0;
   1872         } else {
   1873             if (index - mItemsOnOffAxis >= firstExpandableIndex()) {
   1874                 index = index - mItemsOnOffAxis;
   1875                 view = getChildAt(index);
   1876                 int previousCenter = getScrollCenter(view);
   1877                 info.mainPos = (float) (scrollCenter - previousCenter) /
   1878                         (center - previousCenter);
   1879             } else {
   1880                 // overscroll to left, negative value
   1881                 info.mainPos = (float) (scrollCenter - center) / getSize(view);
   1882             }
   1883         }
   1884         int centerOffAxis = getCenterInOffAxis(view);
   1885         if (scrollCenterOff > centerOffAxis) {
   1886             if (index + 1 < lastExpandableIndex()) {
   1887                 int nextCenter = getCenterInOffAxis(getChildAt(index + 1));
   1888                 info.secondPos = (float) (scrollCenterOff - centerOffAxis)
   1889                         / (nextCenter - centerOffAxis);
   1890             } else {
   1891                 // overscroll to right
   1892                 info.secondPos = (float) (scrollCenterOff - centerOffAxis) /
   1893                         getSizeInOffAxis(view);
   1894             }
   1895         } else if (scrollCenterOff == centerOffAxis) {
   1896             info.secondPos = 0;
   1897         } else {
   1898             if (index - 1 >= firstExpandableIndex()) {
   1899                 index = index - 1;
   1900                 view = getChildAt(index);
   1901                 int previousCenter = getCenterInOffAxis(view);
   1902                 info.secondPos = (float) (scrollCenterOff - previousCenter)
   1903                         / (centerOffAxis - previousCenter);
   1904             } else {
   1905                 // overscroll to left, negative value
   1906                 info.secondPos = (float) (scrollCenterOff - centerOffAxis) /
   1907                         getSizeInOffAxis(view);
   1908             }
   1909         }
   1910         info.index = getAdapterIndex(index);
   1911         info.viewLocation = mOrientation == HORIZONTAL ? view.getLeft() : view.getTop();
   1912         if (mAdapter.hasStableIds()) {
   1913             info.id = mAdapter.getItemId(info.index);
   1914         }
   1915     }
   1916 
   1917     private void fireScrollChange() {
   1918         int savedIndex = mCurScroll.index;
   1919         float savedMainPos = mCurScroll.mainPos;
   1920         float savedSecondPos = mCurScroll.secondPos;
   1921         updateScrollInfo(mCurScroll);
   1922         if (mOnScrollListeners != null && !mOnScrollListeners.isEmpty()
   1923                 &&(savedIndex != mCurScroll.index
   1924                 || savedMainPos != mCurScroll.mainPos || savedSecondPos != mCurScroll.secondPos)) {
   1925             if (mCurScroll.index >= 0) {
   1926                 for (OnScrollListener l : mOnScrollListeners) {
   1927                     l.onScrolled(getChildAt(expandableIndexFromAdapterIndex(
   1928                             mCurScroll.index)), mCurScroll.index,
   1929                             mCurScroll.mainPos, mCurScroll.secondPos);
   1930                 }
   1931             }
   1932         }
   1933     }
   1934 
   1935     private void fireItemSelected() {
   1936         OnItemSelectedListener listener = getOnItemSelectedListener();
   1937         if (listener != null) {
   1938             listener.onItemSelected(this, getSelectedView(), getSelectedItemPosition(),
   1939                     getSelectedItemId());
   1940         }
   1941         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
   1942     }
   1943 
   1944     /** manually set scroll position */
   1945     private void setSelectionInternal(int adapterIndex, float scrollPosition, boolean fireEvent) {
   1946         if (adapterIndex < 0 || mAdapter == null || adapterIndex >= mAdapter.getCount()
   1947                 || !mAdapter.isEnabled(adapterIndex)) {
   1948             Log.w(TAG, "invalid selection index = " + adapterIndex);
   1949             return;
   1950         }
   1951         int viewIndex = expandableIndexFromAdapterIndex(adapterIndex);
   1952         if (mDataSetChangedFlag || viewIndex < firstExpandableIndex() ||
   1953                 viewIndex >= lastExpandableIndex()) {
   1954             mPendingSelection = adapterIndex;
   1955             mPendingScrollPosition = scrollPosition;
   1956             fireDataSetChanged();
   1957             return;
   1958         }
   1959         View view = getChildAt(viewIndex);
   1960         int scrollCenter = getScrollCenter(view);
   1961         int scrollCenterOffAxis = getCenterInOffAxis(view);
   1962         int deltaMain;
   1963         if (scrollPosition > 0 && viewIndex + mItemsOnOffAxis < lastExpandableIndex()) {
   1964             int nextCenter = getScrollCenter(getChildAt(viewIndex + mItemsOnOffAxis));
   1965             deltaMain = (int) ((nextCenter - scrollCenter) * scrollPosition);
   1966         } else {
   1967             deltaMain = (int) (getSize(view) * scrollPosition);
   1968         }
   1969         if (mOrientation == HORIZONTAL) {
   1970             mScroll.setScrollCenter(scrollCenter + deltaMain, scrollCenterOffAxis);
   1971         } else {
   1972             mScroll.setScrollCenter(scrollCenterOffAxis, scrollCenter + deltaMain);
   1973         }
   1974         transferFocusTo(view, 0);
   1975         adjustSystemScrollPos();
   1976         applyTransformations();
   1977         if (fireEvent) {
   1978             updateViewsLocations(false);
   1979             fireScrollChange();
   1980             if (scrollPosition == 0) {
   1981                 fireItemChange();
   1982             }
   1983         }
   1984     }
   1985 
   1986     private void transferFocusTo(View topItem, int direction) {
   1987         View oldSelection = getSelectedView();
   1988         if (topItem == oldSelection) {
   1989             return;
   1990         }
   1991         mSelectedIndex = getAdapterIndex(indexOfChild(topItem));
   1992         View focused = findFocus();
   1993         if (focused != null) {
   1994             if (direction != 0) {
   1995                 requestNextFocus(direction, focused, topItem);
   1996             } else {
   1997                 topItem.requestFocus();
   1998             }
   1999         }
   2000         fireItemSelected();
   2001     }
   2002 
   2003     /** scroll And Focus To expandable item in the main direction */
   2004     public void scrollAndFocusTo(View topItem, int direction, boolean easeFling, int duration,
   2005             boolean page) {
   2006         if (topItem == null) {
   2007             mScrollerState = NO_SCROLL;
   2008             return;
   2009         }
   2010         int delta = getScrollCenter(topItem) - mScroll.mainAxis().getScrollCenter();
   2011         int deltaOffAxis = mItemsOnOffAxis == 1 ? 0 : // dont scroll 2nd axis for non-grid
   2012                 getCenterInOffAxis(topItem) - mScroll.secondAxis().getScrollCenter();
   2013         if (delta != 0 || deltaOffAxis != 0) {
   2014             mScrollerState = SCROLL_AND_CENTER_FOCUS;
   2015             mScroll.startScrollByMain(delta, deltaOffAxis, easeFling, duration, page);
   2016             // Instead of waiting scrolling animation finishes, we immediately change focus.
   2017             // This will cause focused item to be off center and benefit is to dealing multiple
   2018             // DPAD events without waiting animation finish.
   2019         } else {
   2020             mScrollerState = NO_SCROLL;
   2021         }
   2022 
   2023         transferFocusTo(topItem, direction);
   2024 
   2025         scheduleScrollTask();
   2026     }
   2027 
   2028     public int getScrollCenterStrategy() {
   2029         return mScroll.mainAxis().getScrollCenterStrategy();
   2030     }
   2031 
   2032     public void setScrollCenterStrategy(int scrollCenterStrategy) {
   2033         mScroll.mainAxis().setScrollCenterStrategy(scrollCenterStrategy);
   2034     }
   2035 
   2036     public int getScrollCenterOffset() {
   2037         return mScroll.mainAxis().getScrollCenterOffset();
   2038     }
   2039 
   2040     public void setScrollCenterOffset(int scrollCenterOffset) {
   2041         mScroll.mainAxis().setScrollCenterOffset(scrollCenterOffset);
   2042     }
   2043 
   2044     public void setScrollCenterOffsetPercent(int scrollCenterOffsetPercent) {
   2045         mScroll.mainAxis().setScrollCenterOffsetPercent(scrollCenterOffsetPercent);
   2046     }
   2047 
   2048     public void setItemTransform(ScrollAdapterTransform transform) {
   2049         mItemTransform = transform;
   2050     }
   2051 
   2052     public ScrollAdapterTransform getItemTransform() {
   2053         return mItemTransform;
   2054     }
   2055 
   2056     private void ensureSimpleItemTransform() {
   2057         if (! (mItemTransform instanceof SimpleScrollAdapterTransform)) {
   2058             mItemTransform = new SimpleScrollAdapterTransform(getContext());
   2059         }
   2060     }
   2061 
   2062     public void setLowItemTransform(Animator anim) {
   2063         ensureSimpleItemTransform();
   2064         ((SimpleScrollAdapterTransform)mItemTransform).setLowItemTransform(anim);
   2065     }
   2066 
   2067     public void setHighItemTransform(Animator anim) {
   2068         ensureSimpleItemTransform();
   2069         ((SimpleScrollAdapterTransform)mItemTransform).setHighItemTransform(anim);
   2070     }
   2071 
   2072     @Override
   2073     protected float getRightFadingEdgeStrength() {
   2074         if (mOrientation != HORIZONTAL || mAdapter == null || getChildCount() == 0) {
   2075             return 0;
   2076         }
   2077         if (mRightIndex == mAdapter.getCount()) {
   2078             View lastChild = getChildAt(lastExpandableIndex() - 1);
   2079             int maxEdge = lastChild.getRight();
   2080             if (getScrollX() + getWidth() >= maxEdge) {
   2081                 return 0;
   2082             }
   2083         }
   2084         return 1;
   2085     }
   2086 
   2087     @Override
   2088     protected float getBottomFadingEdgeStrength() {
   2089         if (mOrientation != HORIZONTAL || mAdapter == null || getChildCount() == 0) {
   2090             return 0;
   2091         }
   2092         if (mRightIndex == mAdapter.getCount()) {
   2093             View lastChild = getChildAt(lastExpandableIndex() - 1);
   2094             int maxEdge = lastChild.getBottom();
   2095             if (getScrollY() + getHeight() >= maxEdge) {
   2096                 return 0;
   2097             }
   2098         }
   2099         return 1;
   2100     }
   2101 
   2102     /**
   2103      * get the view which is ancestor of "v" and immediate child of root view return "v" if
   2104      * rootView is not ViewGroup or "v" is not in the subtree
   2105      */
   2106     private final View getTopItem(View v) {
   2107         ViewGroup root = this;
   2108         View ret = v;
   2109         while (ret != null && ret.getParent() != root) {
   2110             if (!(ret.getParent() instanceof View)) {
   2111                 break;
   2112             }
   2113             ret = (View) ret.getParent();
   2114         }
   2115         if (ret == null) {
   2116             return v;
   2117         } else {
   2118             return ret;
   2119         }
   2120     }
   2121 
   2122     private final int getCenter(View v) {
   2123         return mOrientation == HORIZONTAL ? (v.getLeft() + v.getRight()) / 2 : (v.getTop()
   2124                 + v.getBottom()) / 2;
   2125     }
   2126 
   2127     private final int getCenterInOffAxis(View v) {
   2128         return mOrientation == VERTICAL ? (v.getLeft() + v.getRight()) / 2 : (v.getTop()
   2129                 + v.getBottom()) / 2;
   2130     }
   2131 
   2132     private final int getSize(View v) {
   2133         return ((ChildViewHolder) v.getTag(R.id.ScrollAdapterViewChild)).mMaxSize;
   2134     }
   2135 
   2136     private final int getSizeInOffAxis(View v) {
   2137         return mOrientation == HORIZONTAL ? v.getHeight() : v.getWidth();
   2138     }
   2139 
   2140     public View getExpandableView(int adapterIndex) {
   2141         return getChildAt(expandableIndexFromAdapterIndex(adapterIndex));
   2142     }
   2143 
   2144     public int firstExpandableIndex() {
   2145         return mExpandedViews.size();
   2146     }
   2147 
   2148     public int lastExpandableIndex() {
   2149         return getChildCount();
   2150     }
   2151 
   2152     private int getAdapterIndex(int expandableViewIndex) {
   2153         return expandableViewIndex - firstExpandableIndex() + mLeftIndex + 1;
   2154     }
   2155 
   2156     private int expandableIndexFromAdapterIndex(int index) {
   2157         return firstExpandableIndex() + index - mLeftIndex - 1;
   2158     }
   2159 
   2160     View getExpandableChild(View view) {
   2161         if (view != null) {
   2162             for (int i = 0, size = mExpandedViews.size(); i < size; i++) {
   2163                 ExpandedView v = mExpandedViews.get(i);
   2164                 if (v.expandedView == view) {
   2165                     return getChildAt(expandableIndexFromAdapterIndex(v.index));
   2166                 }
   2167             }
   2168         }
   2169         return view;
   2170     }
   2171 
   2172     private static ExpandedView findExpandedView(ArrayList<ExpandedView> expandedView, int index) {
   2173         int expandedCount = expandedView.size();
   2174         for (int i = 0; i < expandedCount; i++) {
   2175             ExpandedView v = expandedView.get(i);
   2176             if (v.index == index) {
   2177                 return v;
   2178             }
   2179         }
   2180         return null;
   2181     }
   2182 
   2183     /**
   2184      * This function is only called from {@link #updateViewsLocations()} Returns existing
   2185      * ExpandedView or create a new one.
   2186      */
   2187     private ExpandedView getOrCreateExpandedView(int index) {
   2188         if (mExpandAdapter == null || index < 0) {
   2189             return null;
   2190         }
   2191         ExpandedView ret = findExpandedView(mExpandedViews, index);
   2192         if (ret != null) {
   2193             return ret;
   2194         }
   2195         int type = mExpandAdapter.getItemViewType(index);
   2196         View recycleView = mRecycleExpandedViews.getView(type);
   2197         View v = mExpandAdapter.getView(index, recycleView, ScrollAdapterView.this);
   2198         if (v == null) {
   2199             return null;
   2200         }
   2201         addViewInLayout(v, 0, v.getLayoutParams(), true);
   2202         mExpandedChildStates.loadView(v, index);
   2203         measureChild(v);
   2204         if (DBG) Log.d(TAG, "created new expanded View for " + index + " " + v);
   2205         ExpandedView view = new ExpandedView(v, index, type);
   2206         for (int i = 0, size = mExpandedViews.size(); i < size; i++) {
   2207             if (view.index < mExpandedViews.get(i).index) {
   2208                 mExpandedViews.add(i, view);
   2209                 return view;
   2210             }
   2211         }
   2212         mExpandedViews.add(view);
   2213         return view;
   2214     }
   2215 
   2216     public void setAnimateLayoutChange(boolean animateLayoutChange) {
   2217         mAnimateLayoutChange = animateLayoutChange;
   2218     }
   2219 
   2220     public boolean getAnimateLayoutChange() {
   2221         return mAnimateLayoutChange;
   2222     }
   2223 
   2224     /**
   2225      * Key function to update expandable views location and create/destroy expanded views
   2226      */
   2227     private void updateViewsLocations(boolean onLayout) {
   2228         int lastExpandable = lastExpandableIndex();
   2229         if (((mExpandAdapter == null && !selectedItemCanScale() && mAdapterCustomAlign == null)
   2230                 || lastExpandable == 0) &&
   2231                 (!onLayout || getChildCount() == 0)) {
   2232             return;
   2233         }
   2234 
   2235         int scrollCenter = mScroll.mainAxis().getScrollCenter();
   2236         int scrollCenterOffAxis = mScroll.secondAxis().getScrollCenter();
   2237         // 1 search center and nextCenter that contains mScrollCenter.
   2238         int expandedCount = mExpandedViews.size();
   2239         int center = -1;
   2240         int nextCenter = -1;
   2241         int expandIdx = -1;
   2242         int firstExpandable = firstExpandableIndex();
   2243         int alignExtraOffset = 0;
   2244         for (int idx = firstExpandable; idx < lastExpandable; idx++) {
   2245             View view = getChildAt(idx);
   2246             int centerMain = getScrollCenter(view);
   2247             int centerOffAxis = getCenterInOffAxis(view);
   2248             int viewSizeOffAxis = mOrientation == HORIZONTAL ? view.getHeight() : view.getWidth();
   2249             if (centerMain <= scrollCenter && (mItemsOnOffAxis == 1 || hasScrollPositionSecondAxis(
   2250                     scrollCenterOffAxis, viewSizeOffAxis, centerOffAxis))) {
   2251                 // find last one match the criteria,  we can optimize it..
   2252                 expandIdx = idx;
   2253                 center = centerMain;
   2254                 if (mAdapterCustomAlign != null) {
   2255                     alignExtraOffset = mAdapterCustomAlign.getItemAlignmentExtraOffset(
   2256                             getAdapterIndex(idx), view);
   2257                 }
   2258             }
   2259         }
   2260         if (expandIdx == -1) {
   2261             // mScrollCenter scrolls too fast, a fling action might cause this
   2262             return;
   2263         }
   2264         int nextExpandIdx = expandIdx + mItemsOnOffAxis;
   2265         int nextAlignExtraOffset = 0;
   2266         if (nextExpandIdx < lastExpandable) {
   2267             View nextView = getChildAt(nextExpandIdx);
   2268             nextCenter = getScrollCenter(nextView);
   2269             if (mAdapterCustomAlign != null) {
   2270                 nextAlignExtraOffset = mAdapterCustomAlign.getItemAlignmentExtraOffset(
   2271                         getAdapterIndex(nextExpandIdx), nextView);
   2272             }
   2273         } else {
   2274             nextExpandIdx = -1;
   2275         }
   2276         int previousExpandIdx = expandIdx - mItemsOnOffAxis;
   2277         if (previousExpandIdx < firstExpandable) {
   2278             previousExpandIdx = -1;
   2279         }
   2280 
   2281         // 2. prepare the expanded view, they could be new created or from existing.
   2282         int xindex = getAdapterIndex(expandIdx);
   2283         ExpandedView thisExpanded = getOrCreateExpandedView(xindex);
   2284         ExpandedView nextExpanded = null;
   2285         if (nextExpandIdx != -1) {
   2286             nextExpanded = getOrCreateExpandedView(xindex + mItemsOnOffAxis);
   2287         }
   2288         // cache one more expanded view before the visible one, it's always invisible
   2289         ExpandedView previousExpanded = null;
   2290         if (previousExpandIdx != -1) {
   2291             previousExpanded = getOrCreateExpandedView(xindex - mItemsOnOffAxis);
   2292         }
   2293 
   2294         // these count and index needs to be updated after we inserted new views
   2295         int newExpandedAdded = mExpandedViews.size() - expandedCount;
   2296         expandIdx += newExpandedAdded;
   2297         if (nextExpandIdx != -1) {
   2298             nextExpandIdx += newExpandedAdded;
   2299         }
   2300         expandedCount = mExpandedViews.size();
   2301         lastExpandable = lastExpandableIndex();
   2302 
   2303         // 3. calculate the expanded View size, and optional next expanded view size.
   2304         int expandedSize = 0;
   2305         int nextExpandedSize = 0;
   2306         float progress = 1;
   2307         if (expandIdx < lastExpandable - 1) {
   2308             progress = (float) (nextCenter - mScroll.mainAxis().getScrollCenter()) /
   2309                        (float) (nextCenter - center);
   2310             if (thisExpanded != null) {
   2311                 expandedSize =
   2312                         (mOrientation == HORIZONTAL ? thisExpanded.expandedView.getMeasuredWidth()
   2313                                 : thisExpanded.expandedView.getMeasuredHeight());
   2314                 expandedSize = (int) (progress * expandedSize);
   2315                 thisExpanded.setProgress(progress);
   2316             }
   2317             if (nextExpanded != null) {
   2318                 nextExpandedSize =
   2319                         (mOrientation == HORIZONTAL ? nextExpanded.expandedView.getMeasuredWidth()
   2320                                 : nextExpanded.expandedView.getMeasuredHeight());
   2321                 nextExpandedSize = (int) ((1f - progress) * nextExpandedSize);
   2322                 nextExpanded.setProgress(1f - progress);
   2323             }
   2324         } else {
   2325             if (thisExpanded != null) {
   2326                 expandedSize =
   2327                         (mOrientation == HORIZONTAL ? thisExpanded.expandedView.getMeasuredWidth()
   2328                                 : thisExpanded.expandedView.getMeasuredHeight());
   2329                 thisExpanded.setProgress(1f);
   2330             }
   2331         }
   2332 
   2333         int totalExpandedSize = expandedSize + nextExpandedSize;
   2334         int extraSpaceLow = 0, extraSpaceHigh = 0;
   2335         // 4. update expandable views positions
   2336         int low = Integer.MAX_VALUE;
   2337         int expandedStart = 0;
   2338         int nextExpandedStart = 0;
   2339         int numOffAxis = (lastExpandable - firstExpandableIndex() + mItemsOnOffAxis - 1)
   2340                 / mItemsOnOffAxis;
   2341         boolean canAnimateExpandedSize = mAnimateLayoutChange &&
   2342                 mScroll.isFinished() && mExpandAdapter != null;
   2343         for (int j = 0; j < numOffAxis; j++) {
   2344             int viewIndex = firstExpandableIndex() + j * mItemsOnOffAxis;
   2345             int endViewIndex = viewIndex + mItemsOnOffAxis - 1;
   2346             if (endViewIndex >= lastExpandable) {
   2347                 endViewIndex = lastExpandable - 1;
   2348             }
   2349             // get maxSize of the off-axis, get start position for first off-axis
   2350             int maxSize = 0;
   2351             for (int k = viewIndex; k <= endViewIndex; k++) {
   2352                 View view = getChildAt(k);
   2353                 ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild);
   2354                 if (canAnimateExpandedSize) {
   2355                     // remember last position in temporary variable
   2356                     if (mOrientation == HORIZONTAL) {
   2357                         h.mLocation = view.getLeft();
   2358                         h.mLocationInParent = h.mLocation + view.getTranslationX();
   2359                     } else {
   2360                         h.mLocation = view.getTop();
   2361                         h.mLocationInParent = h.mLocation + view.getTranslationY();
   2362                     }
   2363                 }
   2364                 maxSize = Math.max(maxSize, mOrientation == HORIZONTAL ? view.getMeasuredWidth() :
   2365                     view.getMeasuredHeight());
   2366                 if (j == 0) {
   2367                     int viewLow = mOrientation == HORIZONTAL ? view.getLeft() : view.getTop();
   2368                     // because we start over again,  we should remove the extraspace
   2369                     if (mScroll.mainAxis().getSelectedTakesMoreSpace()) {
   2370                         viewLow -= h.mExtraSpaceLow;
   2371                     }
   2372                     if (viewLow < low) {
   2373                         low = viewLow;
   2374                     }
   2375                 }
   2376             }
   2377             // layout views within the off axis and get the max right/bottom
   2378             int maxSelectedSize = Integer.MIN_VALUE;
   2379             int maxHigh = low + maxSize;
   2380             for (int k = viewIndex; k <= endViewIndex; k++) {
   2381                 View view = getChildAt(k);
   2382                 int viewStart = low;
   2383                 int viewMeasuredSize = mOrientation == HORIZONTAL ? view.getMeasuredWidth()
   2384                         : view.getMeasuredHeight();
   2385                 switch (mScroll.getScrollItemAlign()) {
   2386                 case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2387                     viewStart += maxSize / 2 - viewMeasuredSize / 2;
   2388                     break;
   2389                 case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2390                     viewStart += maxSize - viewMeasuredSize;
   2391                     break;
   2392                 case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2393                     break;
   2394                 }
   2395                 if (mOrientation == HORIZONTAL) {
   2396                     if (view.isLayoutRequested()) {
   2397                         measureChild(view);
   2398                         view.layout(viewStart, view.getTop(), viewStart + view.getMeasuredWidth(),
   2399                                 view.getTop() + view.getMeasuredHeight());
   2400                     } else {
   2401                         view.offsetLeftAndRight(viewStart - view.getLeft());
   2402                     }
   2403                 } else {
   2404                     if (view.isLayoutRequested()) {
   2405                         measureChild(view);
   2406                         view.layout(view.getLeft(), viewStart, view.getLeft() +
   2407                                 view.getMeasuredWidth(), viewStart + view.getMeasuredHeight());
   2408                     } else {
   2409                         view.offsetTopAndBottom(viewStart - view.getTop());
   2410                     }
   2411                 }
   2412                 if (selectedItemCanScale()) {
   2413                     maxSelectedSize = Math.max(maxSelectedSize,
   2414                             getSelectedItemSize(getAdapterIndex(k), view));
   2415                 }
   2416             }
   2417             // we might need update mMaxSize/mMaxSelectedSize in case a relayout happens
   2418             for (int k = viewIndex; k <= endViewIndex; k++) {
   2419                 View view = getChildAt(k);
   2420                 ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild);
   2421                 h.mMaxSize = maxSize;
   2422                 h.mExtraSpaceLow = 0;
   2423                 h.mScrollCenter = computeScrollCenter(k);
   2424             }
   2425             boolean isTransitionFrom = viewIndex <= expandIdx && expandIdx <= endViewIndex;
   2426             boolean isTransitionTo = viewIndex <= nextExpandIdx && nextExpandIdx <= endViewIndex;
   2427             // adding extra space
   2428             if (maxSelectedSize != Integer.MIN_VALUE) {
   2429                 int extraSpace = 0;
   2430                 if (isTransitionFrom) {
   2431                     extraSpace = (int) ((maxSelectedSize - maxSize) * progress);
   2432                 } else if (isTransitionTo) {
   2433                     extraSpace = (int) ((maxSelectedSize - maxSize) * (1 - progress));
   2434                 }
   2435                 if (extraSpace > 0) {
   2436                     int lowExtraSpace;
   2437                     if (mScroll.mainAxis().getSelectedTakesMoreSpace()) {
   2438                         maxHigh = maxHigh + extraSpace;
   2439                         totalExpandedSize = totalExpandedSize + extraSpace;
   2440                         switch (mScroll.getScrollItemAlign()) {
   2441                             case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2442                                 lowExtraSpace = extraSpace / 2; // extraSpace added low and high
   2443                                 break;
   2444                             case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2445                                 lowExtraSpace = extraSpace; // extraSpace added on the low
   2446                                 break;
   2447                             case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2448                             default:
   2449                                 lowExtraSpace = 0; // extraSpace is added on the high
   2450                                 break;
   2451                         }
   2452                     } else {
   2453                         // if we don't add extra space surrounding it,  the view should
   2454                         // grow evenly on low and high
   2455                         lowExtraSpace = extraSpace / 2;
   2456                     }
   2457                     extraSpaceLow += lowExtraSpace;
   2458                     extraSpaceHigh += (extraSpace - lowExtraSpace);
   2459                     for (int k = viewIndex; k <= endViewIndex; k++) {
   2460                         View view = getChildAt(k);
   2461                         if (mScroll.mainAxis().getSelectedTakesMoreSpace()) {
   2462                             if (mOrientation == HORIZONTAL) {
   2463                                 view.offsetLeftAndRight(lowExtraSpace);
   2464                             } else {
   2465                                 view.offsetTopAndBottom(lowExtraSpace);
   2466                             }
   2467                             ChildViewHolder h = (ChildViewHolder)
   2468                                     view.getTag(R.id.ScrollAdapterViewChild);
   2469                             h.mExtraSpaceLow = lowExtraSpace;
   2470                         }
   2471                     }
   2472                 }
   2473             }
   2474             // animate between different expanded view size
   2475             if (canAnimateExpandedSize) {
   2476                 for (int k = viewIndex; k <= endViewIndex; k++) {
   2477                     View view = getChildAt(k);
   2478                     ChildViewHolder h = (ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild);
   2479                     float target = (mOrientation == HORIZONTAL) ? view.getLeft() : view.getTop();
   2480                     if (h.mLocation != target) {
   2481                         if (mOrientation == HORIZONTAL) {
   2482                             view.setTranslationX(h.mLocationInParent - target);
   2483                             view.animate().translationX(0).start();
   2484                         } else {
   2485                             view.setTranslationY(h.mLocationInParent - target);
   2486                             view.animate().translationY(0).start();
   2487                         }
   2488                     }
   2489                 }
   2490             }
   2491             // adding expanded size
   2492             if (isTransitionFrom) {
   2493                 expandedStart = maxHigh;
   2494                 // "low" (next expandable start) is next to current one until fully expanded
   2495                 maxHigh += progress == 1f ? expandedSize : 0;
   2496             } else if (isTransitionTo) {
   2497                 nextExpandedStart = maxHigh;
   2498                 maxHigh += progress == 1f ? nextExpandedSize : expandedSize + nextExpandedSize;
   2499             }
   2500             // assign beginning position for next "off axis"
   2501             low = maxHigh + mSpace;
   2502         }
   2503         mScroll.mainAxis().setAlignExtraOffset(
   2504                 (int) (alignExtraOffset * progress + nextAlignExtraOffset * (1 - progress)));
   2505         mScroll.mainAxis().setExpandedSize(totalExpandedSize);
   2506         mScroll.mainAxis().setExtraSpaceLow(extraSpaceLow);
   2507         mScroll.mainAxis().setExtraSpaceHigh(extraSpaceHigh);
   2508 
   2509         // 5. update expanded views
   2510         for (int j = 0; j < expandedCount;) {
   2511             // remove views in mExpandedViews and are not newly created
   2512             ExpandedView v = mExpandedViews.get(j);
   2513             if (v!= thisExpanded && v!= nextExpanded && v != previousExpanded) {
   2514                 if (v.expandedView.hasFocus()) {
   2515                     View expandableView = getChildAt(expandableIndexFromAdapterIndex(v.index));
   2516                      expandableView.requestFocus();
   2517                 }
   2518                 v.close();
   2519                 mExpandedChildStates.saveInvisibleView(v.expandedView, v.index);
   2520                 removeViewInLayout(v.expandedView);
   2521                 mRecycleExpandedViews.recycleView(v.expandedView, v.viewType);
   2522                 mExpandedViews.remove(j);
   2523                 expandedCount--;
   2524             } else {
   2525                 j++;
   2526             }
   2527         }
   2528         for (int j = 0, size = mExpandedViews.size(); j < size; j++) {
   2529             ExpandedView v = mExpandedViews.get(j);
   2530             int start = v == thisExpanded ? expandedStart : nextExpandedStart;
   2531             if (!(v == previousExpanded || v == nextExpanded && progress == 1f)) {
   2532                 v.expandedView.setVisibility(VISIBLE);
   2533             }
   2534             if (mOrientation == HORIZONTAL) {
   2535                 if (v.expandedView.isLayoutRequested()) {
   2536                     measureChild(v.expandedView);
   2537                 }
   2538                 v.expandedView.layout(start, 0, start + v.expandedView.getMeasuredWidth(),
   2539                         v.expandedView.getMeasuredHeight());
   2540             } else {
   2541                 if (v.expandedView.isLayoutRequested()) {
   2542                     measureChild(v.expandedView);
   2543                 }
   2544                 v.expandedView.layout(0, start, v.expandedView.getMeasuredWidth(),
   2545                         start + v.expandedView.getMeasuredHeight());
   2546             }
   2547         }
   2548         for (int j = 0, size = mExpandedViews.size(); j < size; j++) {
   2549             ExpandedView v = mExpandedViews.get(j);
   2550             int start = v == thisExpanded ? expandedStart : nextExpandedStart;
   2551             if (v == previousExpanded || v == nextExpanded && progress == 1f) {
   2552                 v.expandedView.setVisibility(View.INVISIBLE);
   2553             }
   2554         }
   2555 
   2556         // 6. move focus from expandable view to expanded view, disable expandable view after it's
   2557         // expanded
   2558         if (mExpandAdapter != null && hasFocus()) {
   2559             View focusedChild = getFocusedChild();
   2560             int focusedIndex = indexOfChild(focusedChild);
   2561             if (focusedIndex >= firstExpandableIndex()) {
   2562                 for (int j = 0, size = mExpandedViews.size(); j < size; j++) {
   2563                     ExpandedView v = mExpandedViews.get(j);
   2564                     if (expandableIndexFromAdapterIndex(v.index) == focusedIndex
   2565                             && v.expandedView.getVisibility() == View.VISIBLE) {
   2566                         v.expandedView.requestFocus();
   2567                     }
   2568                 }
   2569             }
   2570         }
   2571     }
   2572 
   2573     @Override
   2574     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
   2575         View view = getSelectedExpandedView();
   2576         if (view != null) {
   2577             return view.requestFocus(direction, previouslyFocusedRect);
   2578         }
   2579         view = getSelectedView();
   2580         if (view != null) {
   2581             return view.requestFocus(direction, previouslyFocusedRect);
   2582         }
   2583         return false;
   2584     }
   2585 
   2586     private int getScrollCenter(View view) {
   2587         return ((ChildViewHolder) view.getTag(R.id.ScrollAdapterViewChild)).mScrollCenter;
   2588     }
   2589 
   2590     public int getScrollItemAlign() {
   2591         return mScroll.getScrollItemAlign();
   2592     }
   2593 
   2594     private boolean hasScrollPosition(int scrollCenter, int maxSize, int scrollPosInMain) {
   2595         switch (mScroll.getScrollItemAlign()) {
   2596         case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2597             return scrollCenter - maxSize / 2 - mSpaceLow < scrollPosInMain &&
   2598                     scrollPosInMain < scrollCenter + maxSize / 2 + mSpaceHigh;
   2599         case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2600             return scrollCenter - mSpaceLow <= scrollPosInMain &&
   2601                     scrollPosInMain < scrollCenter + maxSize + mSpaceHigh;
   2602         case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2603             return scrollCenter - maxSize - mSpaceLow < scrollPosInMain &&
   2604                     scrollPosInMain <= scrollCenter + mSpaceHigh;
   2605         }
   2606         return false;
   2607     }
   2608 
   2609     private boolean hasScrollPositionSecondAxis(int scrollCenterOffAxis, int viewSizeOffAxis,
   2610             int centerOffAxis) {
   2611         return centerOffAxis - viewSizeOffAxis / 2 - mSpaceLow <= scrollCenterOffAxis
   2612                 && scrollCenterOffAxis <= centerOffAxis + viewSizeOffAxis / 2 + mSpaceHigh;
   2613     }
   2614 
   2615     /**
   2616      * Get the center of expandable view in the state that all expandable views are collapsed, i.e.
   2617      * expanded views are excluded from calculating.  The space is included in calculation
   2618      */
   2619     private int computeScrollCenter(int expandViewIndex) {
   2620         int lastIndex = lastExpandableIndex();
   2621         int firstIndex = firstExpandableIndex();
   2622         View firstView = getChildAt(firstIndex);
   2623         int center = 0;
   2624         switch (mScroll.getScrollItemAlign()) {
   2625         case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2626             center = getCenter(firstView);
   2627             break;
   2628         case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2629             center = mOrientation == HORIZONTAL ? firstView.getLeft() : firstView.getTop();
   2630             break;
   2631         case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2632             center = mOrientation == HORIZONTAL ? firstView.getRight() : firstView.getBottom();
   2633             break;
   2634         }
   2635         if (mScroll.mainAxis().getSelectedTakesMoreSpace()) {
   2636             center -= ((ChildViewHolder) firstView.getTag(
   2637                     R.id.ScrollAdapterViewChild)).mExtraSpaceLow;
   2638         }
   2639         int nextCenter = -1;
   2640         for (int idx = firstIndex; idx < lastIndex; idx += mItemsOnOffAxis) {
   2641             View view = getChildAt(idx);
   2642             if (idx <= expandViewIndex && expandViewIndex < idx + mItemsOnOffAxis) {
   2643                 return center;
   2644             }
   2645             if (idx < lastIndex - mItemsOnOffAxis) {
   2646                 // nextView is never null if scrollCenter is larger than center of current view
   2647                 View nextView = getChildAt(idx + mItemsOnOffAxis);
   2648                 switch (mScroll.getScrollItemAlign()) { // fixme
   2649                     case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2650                         nextCenter = center + (getSize(view) + getSize(nextView)) / 2;
   2651                         break;
   2652                     case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2653                         nextCenter = center + getSize(view);
   2654                         break;
   2655                     case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2656                         nextCenter = center + getSize(nextView);
   2657                         break;
   2658                 }
   2659                 nextCenter += mSpace;
   2660             } else {
   2661                 nextCenter = Integer.MAX_VALUE;
   2662             }
   2663             center = nextCenter;
   2664         }
   2665         assertFailure("Scroll out of range?");
   2666         return 0;
   2667     }
   2668 
   2669     private int getScrollLow(int scrollCenter, View view) {
   2670         ChildViewHolder holder = (ChildViewHolder)view.getTag(R.id.ScrollAdapterViewChild);
   2671         switch (mScroll.getScrollItemAlign()) {
   2672         case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2673             return scrollCenter - holder.mMaxSize / 2;
   2674         case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2675             return scrollCenter;
   2676         case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2677             return scrollCenter - holder.mMaxSize;
   2678         }
   2679         return 0;
   2680     }
   2681 
   2682     private int getScrollHigh(int scrollCenter, View view) {
   2683         ChildViewHolder holder = (ChildViewHolder)view.getTag(R.id.ScrollAdapterViewChild);
   2684         switch (mScroll.getScrollItemAlign()) {
   2685         case ScrollController.SCROLL_ITEM_ALIGN_CENTER:
   2686             return scrollCenter + holder.mMaxSize / 2;
   2687         case ScrollController.SCROLL_ITEM_ALIGN_LOW:
   2688             return scrollCenter + holder.mMaxSize;
   2689         case ScrollController.SCROLL_ITEM_ALIGN_HIGH:
   2690             return scrollCenter;
   2691         }
   2692         return 0;
   2693     }
   2694 
   2695     /**
   2696      * saves the current item index and scroll information for fully restore from
   2697      */
   2698     final static class AdapterViewState {
   2699         int itemsOnOffAxis;
   2700         int index; // index inside adapter of the current view
   2701         Bundle expandedChildStates = Bundle.EMPTY;
   2702         Bundle expandableChildStates = Bundle.EMPTY;
   2703     }
   2704 
   2705     final static class SavedState extends BaseSavedState {
   2706 
   2707         final AdapterViewState theState = new AdapterViewState();
   2708 
   2709         public SavedState(Parcelable superState) {
   2710             super(superState);
   2711         }
   2712 
   2713         @Override
   2714         public void writeToParcel(Parcel out, int flags) {
   2715             super.writeToParcel(out, flags);
   2716             out.writeInt(theState.itemsOnOffAxis);
   2717             out.writeInt(theState.index);
   2718             out.writeBundle(theState.expandedChildStates);
   2719             out.writeBundle(theState.expandableChildStates);
   2720         }
   2721 
   2722         @SuppressWarnings("hiding")
   2723         public static final Parcelable.Creator<SavedState> CREATOR =
   2724                 new Parcelable.Creator<SavedState>() {
   2725                     @Override
   2726                     public SavedState createFromParcel(Parcel in) {
   2727                         return new SavedState(in);
   2728                     }
   2729 
   2730                     @Override
   2731                     public SavedState[] newArray(int size) {
   2732                         return new SavedState[size];
   2733                     }
   2734                 };
   2735 
   2736         SavedState(Parcel in) {
   2737             super(in);
   2738             theState.itemsOnOffAxis = in.readInt();
   2739             theState.index = in.readInt();
   2740             ClassLoader loader = ScrollAdapterView.class.getClassLoader();
   2741             theState.expandedChildStates = in.readBundle(loader);
   2742             theState.expandableChildStates = in.readBundle(loader);
   2743         }
   2744     }
   2745 
   2746     @Override
   2747     protected Parcelable onSaveInstanceState() {
   2748         Parcelable superState = super.onSaveInstanceState();
   2749         SavedState ss = new SavedState(superState);
   2750         int lastIndex = lastExpandableIndex();
   2751         int index = findViewIndexContainingScrollCenter();
   2752         if (index < 0) {
   2753             return superState;
   2754         }
   2755         mExpandedChildStates.saveVisibleViews();
   2756         mExpandableChildStates.saveVisibleViews();
   2757         ss.theState.itemsOnOffAxis = mItemsOnOffAxis;
   2758         ss.theState.index = getAdapterIndex(index);
   2759         View view = getChildAt(index);
   2760         ss.theState.expandedChildStates = mExpandedChildStates.getChildStates();
   2761         ss.theState.expandableChildStates = mExpandableChildStates.getChildStates();
   2762         return ss;
   2763     }
   2764 
   2765     @Override
   2766     protected void onRestoreInstanceState(Parcelable state) {
   2767         if (!(state instanceof SavedState)) {
   2768             super.onRestoreInstanceState(state);
   2769             return;
   2770         }
   2771         SavedState ss = (SavedState)state;
   2772         super.onRestoreInstanceState(ss.getSuperState());
   2773         mLoadingState = ss.theState;
   2774         fireDataSetChanged();
   2775     }
   2776 
   2777     /**
   2778      * Returns expandable children states policy, returns one of
   2779      * {@link ViewsStateBundle#SAVE_NO_CHILD} {@link ViewsStateBundle#SAVE_VISIBLE_CHILD}
   2780      * {@link ViewsStateBundle#SAVE_LIMITED_CHILD} {@link ViewsStateBundle#SAVE_ALL_CHILD}
   2781      */
   2782     public int getSaveExpandableViewsPolicy() {
   2783         return mExpandableChildStates.getSavePolicy();
   2784     }
   2785 
   2786     /** See explanation in {@link #getSaveExpandableViewsPolicy()} */
   2787     public void setSaveExpandableViewsPolicy(int saveExpandablePolicy) {
   2788         mExpandableChildStates.setSavePolicy(saveExpandablePolicy);
   2789     }
   2790 
   2791     /**
   2792      * Returns the limited number of expandable children that will be saved when
   2793      * {@link #getSaveExpandableViewsPolicy()} is {@link ViewsStateBundle#SAVE_LIMITED_CHILD}
   2794      */
   2795     public int getSaveExpandableViewsLimit() {
   2796         return mExpandableChildStates.getLimitNumber();
   2797     }
   2798 
   2799     /** See explanation in {@link #getSaveExpandableViewsLimit()} */
   2800     public void setSaveExpandableViewsLimit(int saveExpandableChildNumber) {
   2801         mExpandableChildStates.setLimitNumber(saveExpandableChildNumber);
   2802     }
   2803 
   2804     /**
   2805      * Returns expanded children states policy, returns one of
   2806      * {@link ViewsStateBundle#SAVE_NO_CHILD} {@link ViewsStateBundle#SAVE_VISIBLE_CHILD}
   2807      * {@link ViewsStateBundle#SAVE_LIMITED_CHILD} {@link ViewsStateBundle#SAVE_ALL_CHILD}
   2808      */
   2809     public int getSaveExpandedViewsPolicy() {
   2810         return mExpandedChildStates.getSavePolicy();
   2811     }
   2812 
   2813     /** See explanation in {@link #getSaveExpandedViewsPolicy} */
   2814     public void setSaveExpandedViewsPolicy(int saveExpandedChildPolicy) {
   2815         mExpandedChildStates.setSavePolicy(saveExpandedChildPolicy);
   2816     }
   2817 
   2818     /**
   2819      * Returns the limited number of expanded children that will be saved when
   2820      * {@link #getSaveExpandedViewsPolicy()} is {@link ViewsStateBundle#SAVE_LIMITED_CHILD}
   2821      */
   2822     public int getSaveExpandedViewsLimit() {
   2823         return mExpandedChildStates.getLimitNumber();
   2824     }
   2825 
   2826     /** See explanation in {@link #getSaveExpandedViewsLimit()} */
   2827     public void setSaveExpandedViewsLimit(int mSaveExpandedNumber) {
   2828         mExpandedChildStates.setLimitNumber(mSaveExpandedNumber);
   2829     }
   2830 
   2831     public ArrayList<OnItemChangeListener> getOnItemChangeListeners() {
   2832         return mOnItemChangeListeners;
   2833     }
   2834 
   2835     public void setOnItemChangeListener(OnItemChangeListener onItemChangeListener) {
   2836         mOnItemChangeListeners.clear();
   2837         addOnItemChangeListener(onItemChangeListener);
   2838     }
   2839 
   2840     public void addOnItemChangeListener(OnItemChangeListener onItemChangeListener) {
   2841         if (!mOnItemChangeListeners.contains(onItemChangeListener)) {
   2842             mOnItemChangeListeners.add(onItemChangeListener);
   2843         }
   2844     }
   2845 
   2846     public ArrayList<OnScrollListener> getOnScrollListeners() {
   2847         return mOnScrollListeners;
   2848     }
   2849 
   2850     public void setOnScrollListener(OnScrollListener onScrollListener) {
   2851         mOnScrollListeners.clear();
   2852         addOnScrollListener(onScrollListener);
   2853     }
   2854 
   2855     public void addOnScrollListener(OnScrollListener onScrollListener) {
   2856         if (!mOnScrollListeners.contains(onScrollListener)) {
   2857             mOnScrollListeners.add(onScrollListener);
   2858         }
   2859     }
   2860 
   2861     public void setExpandedItemInAnim(Animator animator) {
   2862         mExpandedItemInAnim = animator;
   2863     }
   2864 
   2865     public Animator getExpandedItemInAnim() {
   2866         return mExpandedItemInAnim;
   2867     }
   2868 
   2869     public void setExpandedItemOutAnim(Animator animator) {
   2870         mExpandedItemOutAnim = animator;
   2871     }
   2872 
   2873     public Animator getExpandedItemOutAnim() {
   2874         return mExpandedItemOutAnim;
   2875     }
   2876 
   2877     public boolean isNavigateOutOfOffAxisAllowed() {
   2878         return mNavigateOutOfOffAxisAllowed;
   2879     }
   2880 
   2881     public boolean isNavigateOutAllowed() {
   2882         return mNavigateOutAllowed;
   2883     }
   2884 
   2885     /**
   2886      * if allow DPAD key in secondary axis to navigate out of ScrollAdapterView
   2887      */
   2888     public void setNavigateOutOfOffAxisAllowed(boolean navigateOut) {
   2889         mNavigateOutOfOffAxisAllowed = navigateOut;
   2890     }
   2891 
   2892     /**
   2893      * if allow DPAD key in main axis to navigate out of ScrollAdapterView
   2894      */
   2895     public void setNavigateOutAllowed(boolean navigateOut) {
   2896         mNavigateOutAllowed = navigateOut;
   2897     }
   2898 
   2899     public boolean isNavigateInAnimationAllowed() {
   2900         return mNavigateInAnimationAllowed;
   2901     }
   2902 
   2903     /**
   2904      * if {@code true} allow DPAD event from trackpadNavigation when ScrollAdapterView is in
   2905      * animation, this does not affect physical keyboard or manually calling arrowScroll()
   2906      */
   2907     public void setNavigateInAnimationAllowed(boolean navigateInAnimation) {
   2908         mNavigateInAnimationAllowed = navigateInAnimation;
   2909     }
   2910 
   2911     /** set space in pixels between two items */
   2912     public void setSpace(int space) {
   2913         mSpace = space;
   2914         // mSpace may not be evenly divided by 2
   2915         mSpaceLow = mSpace / 2;
   2916         mSpaceHigh = mSpace - mSpaceLow;
   2917     }
   2918 
   2919     /** get space in pixels between two items */
   2920     public int getSpace() {
   2921         return mSpace;
   2922     }
   2923 
   2924     /** set pixels of selected item, use {@link ScrollAdapterCustomSize} for more complicated case */
   2925     public void setSelectedSize(int selectedScale) {
   2926         mSelectedSize = selectedScale;
   2927     }
   2928 
   2929     /** get pixels of selected item */
   2930     public int getSelectedSize() {
   2931         return mSelectedSize;
   2932     }
   2933 
   2934     public void setSelectedTakesMoreSpace(boolean selectedTakesMoreSpace) {
   2935         mScroll.mainAxis().setSelectedTakesMoreSpace(selectedTakesMoreSpace);
   2936     }
   2937 
   2938     public boolean getSelectedTakesMoreSpace() {
   2939         return mScroll.mainAxis().getSelectedTakesMoreSpace();
   2940     }
   2941 
   2942     private boolean selectedItemCanScale() {
   2943         return mSelectedSize != 0 || mAdapterCustomSize != null;
   2944     }
   2945 
   2946     private int getSelectedItemSize(int adapterIndex, View view) {
   2947         if (mSelectedSize != 0) {
   2948             return mSelectedSize;
   2949         } else if (mAdapterCustomSize != null) {
   2950             return mAdapterCustomSize.getSelectItemSize(adapterIndex, view);
   2951         }
   2952         return 0;
   2953     }
   2954 
   2955     private static void assertFailure(String msg) {
   2956         throw new RuntimeException(msg);
   2957     }
   2958 
   2959 }
   2960