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