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