BrowseFragment.java revision bb0a680c10b84b83833a59634373140f8bd0750c
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.app;
15
16import static android.support.v7.widget.RecyclerView.NO_POSITION;
17
18import android.app.Fragment;
19import android.app.FragmentManager;
20import android.app.FragmentManager.BackStackEntry;
21import android.app.FragmentTransaction;
22import android.content.res.TypedArray;
23import android.graphics.Color;
24import android.graphics.Rect;
25import android.os.Bundle;
26import android.support.annotation.ColorInt;
27import android.support.v17.leanback.R;
28import android.support.v17.leanback.transition.TransitionHelper;
29import android.support.v17.leanback.transition.TransitionListener;
30import android.support.v17.leanback.widget.BrowseFrameLayout;
31import android.support.v17.leanback.widget.ListRow;
32import android.support.v17.leanback.widget.ObjectAdapter;
33import android.support.v17.leanback.widget.OnItemViewClickedListener;
34import android.support.v17.leanback.widget.OnItemViewSelectedListener;
35import android.support.v17.leanback.widget.PageRow;
36import android.support.v17.leanback.widget.Presenter;
37import android.support.v17.leanback.widget.PresenterSelector;
38import android.support.v17.leanback.widget.Row;
39import android.support.v17.leanback.widget.RowHeaderPresenter;
40import android.support.v17.leanback.widget.RowPresenter;
41import android.support.v17.leanback.widget.ScaleFrameLayout;
42import android.support.v17.leanback.widget.TitleView;
43import android.support.v17.leanback.widget.VerticalGridView;
44import android.support.v4.view.ViewCompat;
45import android.util.Log;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.view.ViewGroup;
49import android.view.ViewGroup.MarginLayoutParams;
50import android.view.ViewTreeObserver;
51
52import java.util.HashMap;
53import java.util.Map;
54
55/**
56 * A fragment for creating Leanback browse screens. It is composed of a
57 * RowsFragment and a HeadersFragment.
58 * <p>
59 * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
60 * of rows in a vertical list. The elements in this adapter must be subclasses
61 * of {@link Row}.
62 * <p>
63 * The HeadersFragment can be set to be either shown or hidden by default, or
64 * may be disabled entirely. See {@link #setHeadersState} for details.
65 * <p>
66 * By default the BrowseFragment includes support for returning to the headers
67 * when the user presses Back. For Activities that customize {@link
68 * android.app.Activity#onBackPressed()}, you must disable this default Back key support by
69 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
70 * use {@link BrowseFragment.BrowseTransitionListener} and
71 * {@link #startHeadersTransition(boolean)}.
72 * <p>
73 * The recommended theme to use with a BrowseFragment is
74 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
75 * </p>
76 */
77public class BrowseFragment extends BaseFragment {
78
79    // BUNDLE attribute for saving header show/hide status when backstack is used:
80    static final String HEADER_STACK_INDEX = "headerStackIndex";
81    // BUNDLE attribute for saving header show/hide status when backstack is not used:
82    static final String HEADER_SHOW = "headerShow";
83    private static final String IS_PAGE_ROW = "isPageRow";
84    private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
85
86    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
87        int mLastEntryCount;
88        int mIndexOfHeadersBackStack;
89
90        BackStackListener() {
91            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
92            mIndexOfHeadersBackStack = -1;
93        }
94
95        void load(Bundle savedInstanceState) {
96            if (savedInstanceState != null) {
97                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
98                mShowingHeaders = mIndexOfHeadersBackStack == -1;
99            } else {
100                if (!mShowingHeaders) {
101                    getFragmentManager().beginTransaction()
102                            .addToBackStack(mWithHeadersBackStackName).commit();
103                }
104            }
105        }
106
107        void save(Bundle outState) {
108            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
109        }
110
111
112        @Override
113        public void onBackStackChanged() {
114            if (getFragmentManager() == null) {
115                Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
116                return;
117            }
118            int count = getFragmentManager().getBackStackEntryCount();
119            // if backstack is growing and last pushed entry is "headers" backstack,
120            // remember the index of the entry.
121            if (count > mLastEntryCount) {
122                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
123                if (mWithHeadersBackStackName.equals(entry.getName())) {
124                    mIndexOfHeadersBackStack = count - 1;
125                }
126            } else if (count < mLastEntryCount) {
127                // if popped "headers" backstack, initiate the show header transition if needed
128                if (mIndexOfHeadersBackStack >= count) {
129                    mIndexOfHeadersBackStack = -1;
130                    if (!mShowingHeaders) {
131                        startHeadersTransitionInternal(true);
132                    }
133                }
134            }
135            mLastEntryCount = count;
136        }
137    }
138
139    /**
140     * Listener for transitions between browse headers and rows.
141     */
142    public static class BrowseTransitionListener {
143        /**
144         * Callback when headers transition starts.
145         *
146         * @param withHeaders True if the transition will result in headers
147         *        being shown, false otherwise.
148         */
149        public void onHeadersTransitionStart(boolean withHeaders) {
150        }
151        /**
152         * Callback when headers transition stops.
153         *
154         * @param withHeaders True if the transition will result in headers
155         *        being shown, false otherwise.
156         */
157        public void onHeadersTransitionStop(boolean withHeaders) {
158        }
159    }
160
161    private class SetSelectionRunnable implements Runnable {
162        static final int TYPE_INVALID = -1;
163        static final int TYPE_INTERNAL_SYNC = 0;
164        static final int TYPE_USER_REQUEST = 1;
165
166        private int mPosition;
167        private int mType;
168        private boolean mSmooth;
169
170        SetSelectionRunnable() {
171            reset();
172        }
173
174        void post(int position, int type, boolean smooth) {
175            // Posting the set selection, rather than calling it immediately, prevents an issue
176            // with adapter changes.  Example: a row is added before the current selected row;
177            // first the fast lane view updates its selection, then the rows fragment has that
178            // new selection propagated immediately; THEN the rows view processes the same adapter
179            // change and moves the selection again.
180            if (type >= mType) {
181                mPosition = position;
182                mType = type;
183                mSmooth = smooth;
184                mBrowseFrame.removeCallbacks(this);
185                mBrowseFrame.post(this);
186            }
187        }
188
189        @Override
190        public void run() {
191            setSelection(mPosition, mSmooth);
192            reset();
193        }
194
195        private void reset() {
196            mPosition = -1;
197            mType = TYPE_INVALID;
198            mSmooth = false;
199        }
200    }
201
202    /**
203     * Interface that defines the interaction between {@link BrowseFragment} and it's main
204     * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
205     * it will be used to get the fragment to be shown in the content section. Clients can
206     * provide any implementation of fragment and customize it's interaction with
207     * {@link BrowseFragment} by overriding the necessary methods.
208     *
209     * <p>
210     * Clients are expected to provide
211     * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
212     * implementations of {@link MainFragmentAdapter} for given content types. Currently
213     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
214     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
215     * {@link PageRow} - {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}.
216     *
217     * <p>
218     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
219     * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
220     * and provide that through {@link MainFragmentAdapterRegistry}.
221     * {@link MainFragmentAdapter} implementation can supply any fragment and override
222     * just those interactions that makes sense.
223     */
224    public static class MainFragmentAdapter<T extends Fragment> {
225        private boolean mScalingEnabled;
226        private final T mFragment;
227
228        public MainFragmentAdapter(T fragment) {
229            this.mFragment = fragment;
230        }
231
232        public final T getFragment() {
233            return mFragment;
234        }
235
236        /**
237         * Returns whether its scrolling.
238         */
239        public boolean isScrolling() {
240            return false;
241        }
242
243        /**
244         * Set the visibility of titles/hovercard of browse rows.
245         */
246        public void setExpand(boolean expand) {
247        }
248
249        /**
250         * For rows that willing to participate entrance transition,  this function
251         * hide views if afterTransition is true,  show views if afterTransition is false.
252         */
253        public void setEntranceTransitionState(boolean state) {
254        }
255
256        /**
257         * Sets the window alignment and also the pivots for scale operation.
258         */
259        public void setAlignment(int windowAlignOffsetFromTop) {
260        }
261
262        /**
263         * Callback indicating transition prepare start.
264         */
265        public boolean onTransitionPrepare() {
266            return false;
267        }
268
269        /**
270         * Callback indicating transition start.
271         */
272        public void onTransitionStart() {
273        }
274
275        /**
276         * Callback indicating transition end.
277         */
278        public void onTransitionEnd() {
279        }
280
281        /**
282         * Returns whether row scaling is enabled.
283         */
284        public boolean isScalingEnabled() {
285            return mScalingEnabled;
286        }
287
288        /**
289         * Sets the row scaling property.
290         */
291        public void setScalingEnabled(boolean scalingEnabled) {
292            this.mScalingEnabled = scalingEnabled;
293        }
294    }
295
296    /**
297     * This is used to pass information to {@link RowsFragment}.
298     * {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}
299     * would return an instance to connect the callbacks from {@link BrowseFragment} to
300     * {@link RowsFragment}.
301     */
302    public static class MainFragmentRowsAdapter<T extends Fragment> {
303        private final T mFragment;
304
305        public MainFragmentRowsAdapter(T fragment) {
306            if (fragment == null) {
307                throw new IllegalArgumentException("Fragment can't be null");
308            }
309            this.mFragment = fragment;
310        }
311
312        public final T getFragment() {
313            return mFragment;
314        }
315        /**
316         * Set the visibility titles/hover of browse rows.
317         */
318        public void setAdapter(ObjectAdapter adapter) {
319        }
320
321        /**
322         * Sets an item clicked listener on the fragment.
323         */
324        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
325        }
326
327        /**
328         * Sets an item selection listener.
329         */
330        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
331        }
332
333        /**
334         * Selects a Row and perform an optional task on the Row.
335         */
336        public void setSelectedPosition(int rowPosition,
337                                        boolean smooth,
338                                        final Presenter.ViewHolderTask rowHolderTask) {
339        }
340
341        /**
342         * Selects a Row.
343         */
344        public void setSelectedPosition(int rowPosition, boolean smooth) {
345        }
346
347        /**
348         * Returns the selected position.
349         */
350        public int getSelectedPosition() {
351            return 0;
352        }
353    }
354
355    private boolean createMainFragment(ObjectAdapter adapter, int position) {
356        Object item = null;
357        if (adapter == null || adapter.size() == 0) {
358            return false;
359        } else {
360            if (position < 0) {
361                position = 0;
362            } else if (position >= adapter.size()) {
363                throw new IllegalArgumentException(
364                        String.format("Invalid position %d requested", position));
365            }
366            item = adapter.get(position);
367        }
368
369        boolean oldIsPageRow = mIsPageRow;
370        mIsPageRow = item instanceof PageRow;
371        boolean swap;
372
373        if (mMainFragment == null) {
374            swap = true;
375        } else {
376            if (oldIsPageRow) {
377                swap = true;
378            } else {
379                swap = mIsPageRow;
380            }
381        }
382
383        if (swap) {
384            mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
385            mMainFragmentAdapter = (MainFragmentAdapter) ((Adaptable)mMainFragment)
386                    .getAdapter(MainFragmentAdapter.class);
387            if (!mIsPageRow) {
388                mMainFragmentRowsAdapter = (MainFragmentRowsAdapter) ((Adaptable)mMainFragment)
389                        .getAdapter(MainFragmentRowsAdapter.class);
390                mIsPageRow = mMainFragmentRowsAdapter == null;
391            } else {
392                mMainFragmentRowsAdapter = null;
393            }
394        }
395        return swap;
396    }
397
398    /**
399     * Factory class responsible for creating fragment given the current item. {@link ListRow}
400     * should returns {@link RowsFragment} or it's subclass whereas {@link PageRow}
401     * can return any fragment class.
402     */
403    public abstract static class FragmentFactory<T extends Fragment> {
404        public abstract T createFragment(Object row);
405    }
406
407    /**
408     * FragmentFactory implementation for {@link ListRow}.
409     */
410    public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {
411        @Override
412        public RowsFragment createFragment(Object row) {
413            return new RowsFragment();
414        }
415    }
416
417    /**
418     * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
419     * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
420     * handling {@link ListRow}. Developers can override that and also if they want to
421     * use custom fragment, they can register a custom {@link FragmentFactory}
422     * against {@link PageRow}.
423     */
424    public final static class MainFragmentAdapterRegistry {
425        private Map<Class, FragmentFactory> itemToFragmentFactoryMapping = new HashMap();
426
427        public void registerFragment(Class rowClass, FragmentFactory factory) {
428            itemToFragmentFactoryMapping.put(rowClass, factory);
429        }
430
431        public Fragment createFragment(Object item) {
432            if (item == null) {
433                throw new IllegalArgumentException("Item can't be null");
434            }
435
436            return itemToFragmentFactoryMapping.get(item.getClass()).createFragment(item);
437        }
438    }
439
440    private static final String TAG = "BrowseFragment";
441
442    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
443
444    private static boolean DEBUG = false;
445
446    /** The headers fragment is enabled and shown by default. */
447    public static final int HEADERS_ENABLED = 1;
448
449    /** The headers fragment is enabled and hidden by default. */
450    public static final int HEADERS_HIDDEN = 2;
451
452    /** The headers fragment is disabled and will never be shown. */
453    public static final int HEADERS_DISABLED = 3;
454
455    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry
456            = new MainFragmentAdapterRegistry();
457    private MainFragmentAdapter mMainFragmentAdapter;
458    private Fragment mMainFragment;
459    private HeadersFragment mHeadersFragment;
460    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
461
462    private ObjectAdapter mAdapter;
463
464    private int mHeadersState = HEADERS_ENABLED;
465    private int mBrandColor = Color.TRANSPARENT;
466    private boolean mBrandColorSet;
467
468    private BrowseFrameLayout mBrowseFrame;
469    private ScaleFrameLayout mScaleFrameLayout;
470    private boolean mHeadersBackStackEnabled = true;
471    private String mWithHeadersBackStackName;
472    private boolean mShowingHeaders = true;
473    private boolean mCanShowHeaders = true;
474    private int mContainerListMarginStart;
475    private int mContainerListAlignTop;
476    private boolean mMainFragmentScaleEnabled = true;
477    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
478    private OnItemViewClickedListener mOnItemViewClickedListener;
479    private int mSelectedPosition = -1;
480    private float mScaleFactor;
481    private boolean mIsPageRow;
482
483    private PresenterSelector mHeaderPresenterSelector;
484    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
485
486    // transition related:
487    private Object mSceneWithHeaders;
488    private Object mSceneWithoutHeaders;
489    private Object mSceneAfterEntranceTransition;
490    private Object mHeadersTransition;
491    private BackStackListener mBackStackChangedListener;
492    private BrowseTransitionListener mBrowseTransitionListener;
493
494    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
495    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
496    private static final String ARG_HEADERS_STATE =
497        BrowseFragment.class.getCanonicalName() + ".headersState";
498
499    /**
500     * Creates arguments for a browse fragment.
501     *
502     * @param args The Bundle to place arguments into, or null if the method
503     *        should return a new Bundle.
504     * @param title The title of the BrowseFragment.
505     * @param headersState The initial state of the headers of the
506     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
507     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
508     * @return A Bundle with the given arguments for creating a BrowseFragment.
509     */
510    public static Bundle createArgs(Bundle args, String title, int headersState) {
511        if (args == null) {
512            args = new Bundle();
513        }
514        args.putString(ARG_TITLE, title);
515        args.putInt(ARG_HEADERS_STATE, headersState);
516        return args;
517    }
518
519    /**
520     * Sets the brand color for the browse fragment. The brand color is used as
521     * the primary color for UI elements in the browse fragment. For example,
522     * the background color of the headers fragment uses the brand color.
523     *
524     * @param color The color to use as the brand color of the fragment.
525     */
526    public void setBrandColor(@ColorInt int color) {
527        mBrandColor = color;
528        mBrandColorSet = true;
529
530        if (mHeadersFragment != null) {
531            mHeadersFragment.setBackgroundColor(mBrandColor);
532        }
533    }
534
535    /**
536     * Returns the brand color for the browse fragment.
537     * The default is transparent.
538     */
539    @ColorInt
540    public int getBrandColor() {
541        return mBrandColor;
542    }
543
544    /**
545     * Sets the adapter containing the rows for the fragment.
546     *
547     * <p>The items referenced by the adapter must be be derived from
548     * {@link Row}. These rows will be used by the rows fragment and the headers
549     * fragment (if not disabled) to render the browse rows.
550     *
551     * @param adapter An ObjectAdapter for the browse rows. All items must
552     *        derive from {@link Row}.
553     */
554    public void setAdapter(ObjectAdapter adapter) {
555        mAdapter = adapter;
556        replaceMainFragment(mSelectedPosition);
557
558        if (adapter != null) {
559            if (mMainFragmentRowsAdapter != null) {
560                mMainFragmentRowsAdapter.setAdapter(adapter);
561            }
562            mHeadersFragment.setAdapter(adapter);
563        }
564    }
565
566    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
567        return mMainFragmentAdapterRegistry;
568    }
569
570    /**
571     * Returns the adapter containing the rows for the fragment.
572     */
573    public ObjectAdapter getAdapter() {
574        return mAdapter;
575    }
576
577    /**
578     * Sets an item selection listener.
579     */
580    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
581        mExternalOnItemViewSelectedListener = listener;
582    }
583
584    /**
585     * Returns an item selection listener.
586     */
587    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
588        return mExternalOnItemViewSelectedListener;
589    }
590
591    /**
592     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
593     * not been created yet or a different fragment is bound to it.
594     *
595     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
596     */
597    public RowsFragment getRowsFragment() {
598        if (mMainFragment instanceof RowsFragment) {
599            return (RowsFragment) mMainFragment;
600        }
601
602        return null;
603    }
604
605    /**
606     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
607     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
608     */
609    public HeadersFragment getHeadersFragment() {
610        return mHeadersFragment;
611    }
612
613    /**
614     * Sets an item clicked listener on the fragment.
615     * OnItemViewClickedListener will override {@link View.OnClickListener} that
616     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
617     * So in general,  developer should choose one of the listeners but not both.
618     */
619    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
620        mOnItemViewClickedListener = listener;
621        if (mMainFragmentAdapter != null) {
622            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
623        }
624    }
625
626    /**
627     * Returns the item Clicked listener.
628     */
629    public OnItemViewClickedListener getOnItemViewClickedListener() {
630        return mOnItemViewClickedListener;
631    }
632
633    /**
634     * Starts a headers transition.
635     *
636     * <p>This method will begin a transition to either show or hide the
637     * headers, depending on the value of withHeaders. If headers are disabled
638     * for this browse fragment, this method will throw an exception.
639     *
640     * @param withHeaders True if the headers should transition to being shown,
641     *        false if the transition should result in headers being hidden.
642     */
643    public void startHeadersTransition(boolean withHeaders) {
644        if (!mCanShowHeaders) {
645            throw new IllegalStateException("Cannot start headers transition");
646        }
647        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
648            return;
649        }
650        startHeadersTransitionInternal(withHeaders);
651    }
652
653    /**
654     * Returns true if the headers transition is currently running.
655     */
656    public boolean isInHeadersTransition() {
657        return mHeadersTransition != null;
658    }
659
660    /**
661     * Returns true if headers are shown.
662     */
663    public boolean isShowingHeaders() {
664        return mShowingHeaders;
665    }
666
667    /**
668     * Sets a listener for browse fragment transitions.
669     *
670     * @param listener The listener to call when a browse headers transition
671     *        begins or ends.
672     */
673    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
674        mBrowseTransitionListener = listener;
675    }
676
677    /**
678     * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
679     *
680     * @param enable true to enable row scaling
681     */
682    public void enableRowScaling(boolean enable) {
683        enableMainFragmentScaling(enable);
684    }
685
686    /**
687     * Enables scaling of main fragment when headers are present. For the page/row fragment,
688     * scaling is enabled only when both this method and
689     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
690     *
691     * @param enable true to enable row scaling
692     */
693    public void enableMainFragmentScaling(boolean enable) {
694        mMainFragmentScaleEnabled = enable;
695    }
696
697    private void startHeadersTransitionInternal(final boolean withHeaders) {
698        if (getFragmentManager().isDestroyed()) {
699            return;
700        }
701        mShowingHeaders = withHeaders;
702        mMainFragmentAdapter.onTransitionPrepare();
703        mMainFragmentAdapter.onTransitionStart();
704        onExpandTransitionStart(!withHeaders, new Runnable() {
705            @Override
706            public void run() {
707                mHeadersFragment.onTransitionPrepare();
708                mHeadersFragment.onTransitionStart();
709                createHeadersTransition();
710                if (mBrowseTransitionListener != null) {
711                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
712                }
713                TransitionHelper.runTransition(
714                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
715                if (mHeadersBackStackEnabled) {
716                    if (!withHeaders) {
717                        getFragmentManager().beginTransaction()
718                                .addToBackStack(mWithHeadersBackStackName).commit();
719                    } else {
720                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
721                        if (index >= 0) {
722                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
723                            getFragmentManager().popBackStackImmediate(entry.getId(),
724                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
725                        }
726                    }
727                }
728            }
729        });
730    }
731
732    boolean isVerticalScrolling() {
733        // don't run transition
734        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
735    }
736
737
738    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
739            new BrowseFrameLayout.OnFocusSearchListener() {
740        @Override
741        public View onFocusSearch(View focused, int direction) {
742            // if headers is running transition,  focus stays
743            if (mCanShowHeaders && isInHeadersTransition()) {
744                return focused;
745            }
746            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
747
748            if (getTitleView() != null && focused != getTitleView() &&
749                    direction == View.FOCUS_UP) {
750                return getTitleView();
751            }
752            if (getTitleView() != null && getTitleView().hasFocus() &&
753                    direction == View.FOCUS_DOWN) {
754                return mCanShowHeaders && mShowingHeaders ?
755                        mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
756            }
757
758            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
759            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
760            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
761            if (mCanShowHeaders && direction == towardStart) {
762                if (isVerticalScrolling() || mShowingHeaders) {
763                    return focused;
764                }
765                return mHeadersFragment.getVerticalGridView();
766            } else if (direction == towardEnd) {
767                if (isVerticalScrolling()) {
768                    return focused;
769                }
770                return mMainFragment.getView();
771            } else {
772                return null;
773            }
774        }
775    };
776
777    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
778            new BrowseFrameLayout.OnChildFocusListener() {
779
780        @Override
781        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
782            if (getChildFragmentManager().isDestroyed()) {
783                return true;
784            }
785            // Make sure not changing focus when requestFocus() is called.
786            if (mCanShowHeaders && mShowingHeaders) {
787                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
788                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
789                    return true;
790                }
791            }
792            if (mMainFragment != null && mMainFragment.getView() != null &&
793                    mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
794                return true;
795            }
796            if (getTitleView() != null &&
797                    getTitleView().requestFocus(direction, previouslyFocusedRect)) {
798                return true;
799            }
800            return false;
801        }
802
803        @Override
804        public void onRequestChildFocus(View child, View focused) {
805            if (getChildFragmentManager().isDestroyed()) {
806                return;
807            }
808            if (!mCanShowHeaders || isInHeadersTransition()) return;
809            int childId = child.getId();
810            if (childId == R.id.browse_container_dock && mShowingHeaders) {
811                startHeadersTransitionInternal(false);
812            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
813                startHeadersTransitionInternal(true);
814            }
815        }
816    };
817
818    @Override
819    public void onSaveInstanceState(Bundle outState) {
820        super.onSaveInstanceState(outState);
821        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
822        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
823
824        if (mBackStackChangedListener != null) {
825            mBackStackChangedListener.save(outState);
826        } else {
827            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
828        }
829    }
830
831    @Override
832    public void onCreate(Bundle savedInstanceState) {
833        super.onCreate(savedInstanceState);
834        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
835        mContainerListMarginStart = (int) ta.getDimension(
836                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
837                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
838        mContainerListAlignTop = (int) ta.getDimension(
839                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
840                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
841        ta.recycle();
842
843        readArguments(getArguments());
844
845        mMainFragmentAdapterRegistry.registerFragment(ListRow.class, new ListRowFragmentFactory());
846
847        if (mCanShowHeaders) {
848            if (mHeadersBackStackEnabled) {
849                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
850                mBackStackChangedListener = new BackStackListener();
851                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
852                mBackStackChangedListener.load(savedInstanceState);
853            } else {
854                if (savedInstanceState != null) {
855                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
856                }
857            }
858        }
859
860        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
861    }
862
863    @Override
864    public void onDestroyView() {
865        mMainFragmentRowsAdapter = null;
866        mMainFragmentAdapter = null;
867        mMainFragment = null;
868        mHeadersFragment = null;
869        super.onDestroyView();
870    }
871
872    @Override
873    public void onDestroy() {
874        if (mBackStackChangedListener != null) {
875            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
876        }
877        super.onDestroy();
878    }
879
880    @Override
881    public View onCreateView(LayoutInflater inflater, ViewGroup container,
882            Bundle savedInstanceState) {
883        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
884            mHeadersFragment = new HeadersFragment();
885
886            createMainFragment(mAdapter, mSelectedPosition);
887            FragmentTransaction ft = getChildFragmentManager().beginTransaction()
888                    .replace(R.id.browse_headers_dock, mHeadersFragment);
889
890            if (mMainFragment != null) {
891                ft.replace(R.id.scale_frame, mMainFragment);
892            } else {
893                // Empty adapter used to guard against lazy adapter loading. When this
894                // fragment is instantiated, mAdapter might not have the data or might not
895                // have been set. In either of those cases mFragmentAdapter will be null.
896                // This way we can maintain the invariant that mMainFragmentAdapter is never
897                // null and it avoids doing null checks all over the code.
898                mMainFragmentAdapter = new MainFragmentAdapter(null);
899            }
900
901            ft.commit();
902        } else {
903            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
904                    .findFragmentById(R.id.browse_headers_dock);
905            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
906            mMainFragmentAdapter = (MainFragmentAdapter) ((Adaptable)mMainFragment)
907                    .getAdapter(MainFragmentAdapter.class);
908
909            mIsPageRow = savedInstanceState != null ?
910                    savedInstanceState.getBoolean(IS_PAGE_ROW, false) : false;
911
912            mSelectedPosition = savedInstanceState != null ?
913                    savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
914            if (!mIsPageRow) {
915                mMainFragmentRowsAdapter = (MainFragmentRowsAdapter) ((Adaptable) mMainFragment)
916                        .getAdapter(MainFragmentRowsAdapter.class);
917            } else {
918                mMainFragmentRowsAdapter = null;
919            }
920        }
921
922        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
923        if (mHeaderPresenterSelector != null) {
924            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
925        }
926        mHeadersFragment.setAdapter(mAdapter);
927        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
928        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
929
930        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
931
932        setTitleView((TitleView) root.findViewById(R.id.browse_title_group));
933
934        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
935        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
936        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
937
938        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
939        mScaleFrameLayout.setPivotX(0);
940        mScaleFrameLayout.setPivotY(mContainerListAlignTop);
941
942        setupMainFragment();
943
944        if (mBrandColorSet) {
945            mHeadersFragment.setBackgroundColor(mBrandColor);
946        }
947
948        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
949            @Override
950            public void run() {
951                showHeaders(true);
952            }
953        });
954        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
955            @Override
956            public void run() {
957                showHeaders(false);
958            }
959        });
960        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
961            @Override
962            public void run() {
963                setEntranceTransitionEndState();
964            }
965        });
966        return root;
967    }
968
969    private void setupMainFragment() {
970        if (mMainFragmentRowsAdapter != null) {
971            mMainFragmentRowsAdapter.setAdapter(mAdapter);
972            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener);
973            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
974        }
975    }
976
977    private void createHeadersTransition() {
978        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
979                mShowingHeaders ?
980                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
981
982        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
983            @Override
984            public void onTransitionStart(Object transition) {
985            }
986            @Override
987            public void onTransitionEnd(Object transition) {
988                mHeadersTransition = null;
989                mMainFragmentAdapter.onTransitionEnd();
990                mHeadersFragment.onTransitionEnd();
991                if (mShowingHeaders) {
992                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
993                    if (headerGridView != null && !headerGridView.hasFocus()) {
994                        headerGridView.requestFocus();
995                    }
996                } else {
997                    View rowsGridView = mMainFragment.getView();
998                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
999                        rowsGridView.requestFocus();
1000                    }
1001                }
1002                if (mBrowseTransitionListener != null) {
1003                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
1004                }
1005            }
1006        });
1007    }
1008
1009    /**
1010     * Sets the {@link PresenterSelector} used to render the row headers.
1011     *
1012     * @param headerPresenterSelector The PresenterSelector that will determine
1013     *        the Presenter for each row header.
1014     */
1015    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
1016        mHeaderPresenterSelector = headerPresenterSelector;
1017        if (mHeadersFragment != null) {
1018            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
1019        }
1020    }
1021
1022    private void setHeadersOnScreen(boolean onScreen) {
1023        MarginLayoutParams lp;
1024        View containerList;
1025        containerList = mHeadersFragment.getView();
1026        lp = (MarginLayoutParams) containerList.getLayoutParams();
1027        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1028        containerList.setLayoutParams(lp);
1029    }
1030
1031    private void showHeaders(boolean show) {
1032        if (DEBUG) Log.v(TAG, "showHeaders " + show);
1033        mHeadersFragment.setHeadersEnabled(show);
1034        setHeadersOnScreen(show);
1035        expandMainFragment(!show);
1036    }
1037
1038    private void expandMainFragment(boolean expand) {
1039        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
1040        params.leftMargin = !expand ? mContainerListMarginStart : 0;
1041        mScaleFrameLayout.setLayoutParams(params);
1042        mMainFragmentAdapter.setExpand(expand);
1043
1044        setMainFragmentAlignment();
1045        final float scaleFactor = !expand
1046                && mMainFragmentScaleEnabled
1047                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
1048        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
1049        mScaleFrameLayout.setChildScale(scaleFactor);
1050    }
1051
1052    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
1053        new HeadersFragment.OnHeaderClickedListener() {
1054            @Override
1055            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1056                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
1057                    return;
1058                }
1059                startHeadersTransitionInternal(false);
1060                mMainFragment.getView().requestFocus();
1061            }
1062        };
1063
1064    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
1065        @Override
1066        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
1067                RowPresenter.ViewHolder rowViewHolder, Row row) {
1068            if (mMainFragment == null) {
1069                return;
1070            }
1071
1072            int position = ((RowsFragment) mMainFragment)
1073                    .getVerticalGridView().getSelectedPosition();
1074            if (DEBUG) Log.v(TAG, "row selected position " + position);
1075            onRowSelected(position);
1076            if (mExternalOnItemViewSelectedListener != null) {
1077                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
1078                        rowViewHolder, row);
1079            }
1080        }
1081    };
1082
1083    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
1084            new HeadersFragment.OnHeaderViewSelectedListener() {
1085        @Override
1086        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1087            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
1088            if (DEBUG) Log.v(TAG, "header selected position " + position);
1089            onRowSelected(position);
1090        }
1091    };
1092
1093    private void onRowSelected(int position) {
1094        if (position != mSelectedPosition) {
1095            mSetSelectionRunnable.post(
1096                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
1097
1098            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
1099                showTitle(true);
1100            } else {
1101                showTitle(false);
1102            }
1103        }
1104    }
1105
1106    private void setSelection(int position, boolean smooth) {
1107        if (position == NO_POSITION) {
1108            return;
1109        }
1110
1111        mHeadersFragment.setSelectedPosition(position, smooth);
1112        replaceMainFragment(position);
1113
1114        if (mMainFragmentRowsAdapter != null) {
1115            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
1116        }
1117        mSelectedPosition = position;
1118    }
1119
1120    private void replaceMainFragment(int position) {
1121        if (createMainFragment(mAdapter, position)) {
1122            swapBrowseContent(mMainFragment);
1123            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
1124            setupMainFragment();
1125        }
1126    }
1127
1128    private void swapBrowseContent(Fragment fragment) {
1129        getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit();
1130    }
1131
1132    /**
1133     * Sets the selected row position with smooth animation.
1134     */
1135    public void setSelectedPosition(int position) {
1136        setSelectedPosition(position, true);
1137    }
1138
1139    /**
1140     * Gets position of currently selected row.
1141     * @return Position of currently selected row.
1142     */
1143    public int getSelectedPosition() {
1144        return mSelectedPosition;
1145    }
1146
1147    /**
1148     * Sets the selected row position.
1149     */
1150    public void setSelectedPosition(int position, boolean smooth) {
1151        mSetSelectionRunnable.post(
1152                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
1153    }
1154
1155    /**
1156     * Selects a Row and perform an optional task on the Row. For example
1157     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
1158     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
1159     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
1160     * ViewGroup, Bundle)}).
1161     *
1162     * @param rowPosition Which row to select.
1163     * @param smooth True to scroll to the row, false for no animation.
1164     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
1165     * fragment will be collapsed.
1166     */
1167    public void setSelectedPosition(int rowPosition, boolean smooth,
1168            final Presenter.ViewHolderTask rowHolderTask) {
1169        if (mMainFragmentAdapterRegistry == null) {
1170            return;
1171        }
1172        if (rowHolderTask != null) {
1173            startHeadersTransition(false);
1174        }
1175        if (mMainFragmentRowsAdapter != null) {
1176            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
1177        }
1178    }
1179
1180    @Override
1181    public void onStart() {
1182        super.onStart();
1183        mHeadersFragment.setAlignment(mContainerListAlignTop);
1184        setMainFragmentAlignment();
1185
1186        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
1187            mHeadersFragment.getView().requestFocus();
1188        } else if ((!mCanShowHeaders || !mShowingHeaders)
1189                && mMainFragment.getView() != null) {
1190            mMainFragment.getView().requestFocus();
1191        }
1192
1193        if (mCanShowHeaders) {
1194            showHeaders(mShowingHeaders);
1195        }
1196
1197        if (isEntranceTransitionEnabled()) {
1198            setEntranceTransitionStartState();
1199        }
1200    }
1201
1202    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
1203        if (expand) {
1204            callback.run();
1205            return;
1206        }
1207        // Run a "pre" layout when we go non-expand, in order to get the initial
1208        // positions of added rows.
1209        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
1210    }
1211
1212    private void setMainFragmentAlignment() {
1213        int alignOffset = mContainerListAlignTop;
1214        if (mMainFragmentScaleEnabled
1215                && mMainFragmentAdapter.isScalingEnabled()
1216                && mShowingHeaders) {
1217            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
1218        }
1219        mMainFragmentAdapter.setAlignment(alignOffset);
1220    }
1221
1222    /**
1223     * Enables/disables headers transition on back key support. This is enabled by
1224     * default. The BrowseFragment will add a back stack entry when headers are
1225     * showing. Running a headers transition when the back key is pressed only
1226     * works when the headers state is {@link #HEADERS_ENABLED} or
1227     * {@link #HEADERS_HIDDEN}.
1228     * <p>
1229     * NOTE: If an Activity has its own onBackPressed() handling, you must
1230     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
1231     * and {@link BrowseTransitionListener} in your own back stack handling.
1232     */
1233    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
1234        mHeadersBackStackEnabled = headersBackStackEnabled;
1235    }
1236
1237    /**
1238     * Returns true if headers transition on back key support is enabled.
1239     */
1240    public final boolean isHeadersTransitionOnBackEnabled() {
1241        return mHeadersBackStackEnabled;
1242    }
1243
1244    private void readArguments(Bundle args) {
1245        if (args == null) {
1246            return;
1247        }
1248        if (args.containsKey(ARG_TITLE)) {
1249            setTitle(args.getString(ARG_TITLE));
1250        }
1251        if (args.containsKey(ARG_HEADERS_STATE)) {
1252            setHeadersState(args.getInt(ARG_HEADERS_STATE));
1253        }
1254    }
1255
1256    /**
1257     * Sets the state for the headers column in the browse fragment. Must be one
1258     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1259     * {@link #HEADERS_DISABLED}.
1260     *
1261     * @param headersState The state of the headers for the browse fragment.
1262     */
1263    public void setHeadersState(int headersState) {
1264        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1265            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1266        }
1267        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1268
1269        if (headersState != mHeadersState) {
1270            mHeadersState = headersState;
1271            switch (headersState) {
1272                case HEADERS_ENABLED:
1273                    mCanShowHeaders = true;
1274                    mShowingHeaders = true;
1275                    break;
1276                case HEADERS_HIDDEN:
1277                    mCanShowHeaders = true;
1278                    mShowingHeaders = false;
1279                    break;
1280                case HEADERS_DISABLED:
1281                    mCanShowHeaders = false;
1282                    mShowingHeaders = false;
1283                    break;
1284                default:
1285                    Log.w(TAG, "Unknown headers state: " + headersState);
1286                    break;
1287            }
1288            if (mHeadersFragment != null) {
1289                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1290            }
1291        }
1292    }
1293
1294    /**
1295     * Returns the state of the headers column in the browse fragment.
1296     */
1297    public int getHeadersState() {
1298        return mHeadersState;
1299    }
1300
1301    @Override
1302    protected Object createEntranceTransition() {
1303        return TransitionHelper.loadTransition(getActivity(),
1304                R.transition.lb_browse_entrance_transition);
1305    }
1306
1307    @Override
1308    protected void runEntranceTransition(Object entranceTransition) {
1309        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
1310    }
1311
1312    @Override
1313    protected void onEntranceTransitionPrepare() {
1314        mHeadersFragment.onTransitionPrepare();
1315        mMainFragmentAdapter.onTransitionPrepare();
1316    }
1317
1318    @Override
1319    protected void onEntranceTransitionStart() {
1320        mHeadersFragment.onTransitionStart();
1321        mMainFragmentAdapter.onTransitionStart();
1322    }
1323
1324    @Override
1325    protected void onEntranceTransitionEnd() {
1326        mMainFragmentAdapter.onTransitionEnd();
1327        mHeadersFragment.onTransitionEnd();
1328    }
1329
1330    void setSearchOrbViewOnScreen(boolean onScreen) {
1331        View searchOrbView = getTitleView().getSearchAffordanceView();
1332        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1333        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1334        searchOrbView.setLayoutParams(lp);
1335    }
1336
1337    void setEntranceTransitionStartState() {
1338        setHeadersOnScreen(false);
1339        setSearchOrbViewOnScreen(false);
1340        mMainFragmentAdapter.setEntranceTransitionState(false);
1341    }
1342
1343    void setEntranceTransitionEndState() {
1344        setHeadersOnScreen(mShowingHeaders);
1345        setSearchOrbViewOnScreen(true);
1346        mMainFragmentAdapter.setEntranceTransitionState(true);
1347    }
1348
1349    private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
1350
1351        private final View mView;
1352        private final Runnable mCallback;
1353        private int mState;
1354        private MainFragmentAdapter mainFragmentAdapter;
1355
1356        final static int STATE_INIT = 0;
1357        final static int STATE_FIRST_DRAW = 1;
1358        final static int STATE_SECOND_DRAW = 2;
1359
1360        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
1361            mView = view;
1362            mCallback = callback;
1363            mainFragmentAdapter = adapter;
1364        }
1365
1366        void execute() {
1367            mView.getViewTreeObserver().addOnPreDrawListener(this);
1368            mainFragmentAdapter.setExpand(false);
1369            mState = STATE_INIT;
1370        }
1371
1372        @Override
1373        public boolean onPreDraw() {
1374            if (mState == STATE_INIT) {
1375                mainFragmentAdapter.setExpand(true);
1376                mState = STATE_FIRST_DRAW;
1377            } else if (mState == STATE_FIRST_DRAW) {
1378                mCallback.run();
1379                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1380                mState = STATE_SECOND_DRAW;
1381            }
1382            return false;
1383        }
1384    }
1385}
1386