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