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