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