BrowseFragment.java revision cfcb31c4895793dda843faf67d1b769268e3cce8
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 final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap();
426        private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
427
428        public MainFragmentAdapterRegistry() {
429            registerFragment(ListRow.class, sDefaultFragmentFactory);
430        }
431
432        public void registerFragment(Class rowClass, FragmentFactory factory) {
433            mItemToFragmentFactoryMapping.put(rowClass, factory);
434        }
435
436        public Fragment createFragment(Object item) {
437            if (item == null) {
438                throw new IllegalArgumentException("Item can't be null");
439            }
440
441            FragmentFactory fragmentFactory = mItemToFragmentFactoryMapping.get(item.getClass());
442            if (fragmentFactory == null && !(item instanceof PageRow)) {
443                fragmentFactory = sDefaultFragmentFactory;
444            }
445
446            return fragmentFactory.createFragment(item);
447        }
448    }
449
450    private static final String TAG = "BrowseFragment";
451
452    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
453
454    private static boolean DEBUG = false;
455
456    /** The headers fragment is enabled and shown by default. */
457    public static final int HEADERS_ENABLED = 1;
458
459    /** The headers fragment is enabled and hidden by default. */
460    public static final int HEADERS_HIDDEN = 2;
461
462    /** The headers fragment is disabled and will never be shown. */
463    public static final int HEADERS_DISABLED = 3;
464
465    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry
466            = new MainFragmentAdapterRegistry();
467    private MainFragmentAdapter mMainFragmentAdapter;
468    private Fragment mMainFragment;
469    private HeadersFragment mHeadersFragment;
470    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
471
472    private ObjectAdapter mAdapter;
473
474    private int mHeadersState = HEADERS_ENABLED;
475    private int mBrandColor = Color.TRANSPARENT;
476    private boolean mBrandColorSet;
477
478    private BrowseFrameLayout mBrowseFrame;
479    private ScaleFrameLayout mScaleFrameLayout;
480    private boolean mHeadersBackStackEnabled = true;
481    private String mWithHeadersBackStackName;
482    private boolean mShowingHeaders = true;
483    private boolean mCanShowHeaders = true;
484    private int mContainerListMarginStart;
485    private int mContainerListAlignTop;
486    private boolean mMainFragmentScaleEnabled = true;
487    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
488    private OnItemViewClickedListener mOnItemViewClickedListener;
489    private int mSelectedPosition = -1;
490    private float mScaleFactor;
491    private boolean mIsPageRow;
492
493    private PresenterSelector mHeaderPresenterSelector;
494    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
495
496    // transition related:
497    private Object mSceneWithHeaders;
498    private Object mSceneWithoutHeaders;
499    private Object mSceneAfterEntranceTransition;
500    private Object mHeadersTransition;
501    private BackStackListener mBackStackChangedListener;
502    private BrowseTransitionListener mBrowseTransitionListener;
503
504    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
505    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
506    private static final String ARG_HEADERS_STATE =
507        BrowseFragment.class.getCanonicalName() + ".headersState";
508
509    /**
510     * Creates arguments for a browse fragment.
511     *
512     * @param args The Bundle to place arguments into, or null if the method
513     *        should return a new Bundle.
514     * @param title The title of the BrowseFragment.
515     * @param headersState The initial state of the headers of the
516     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
517     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
518     * @return A Bundle with the given arguments for creating a BrowseFragment.
519     */
520    public static Bundle createArgs(Bundle args, String title, int headersState) {
521        if (args == null) {
522            args = new Bundle();
523        }
524        args.putString(ARG_TITLE, title);
525        args.putInt(ARG_HEADERS_STATE, headersState);
526        return args;
527    }
528
529    /**
530     * Sets the brand color for the browse fragment. The brand color is used as
531     * the primary color for UI elements in the browse fragment. For example,
532     * the background color of the headers fragment uses the brand color.
533     *
534     * @param color The color to use as the brand color of the fragment.
535     */
536    public void setBrandColor(@ColorInt int color) {
537        mBrandColor = color;
538        mBrandColorSet = true;
539
540        if (mHeadersFragment != null) {
541            mHeadersFragment.setBackgroundColor(mBrandColor);
542        }
543    }
544
545    /**
546     * Returns the brand color for the browse fragment.
547     * The default is transparent.
548     */
549    @ColorInt
550    public int getBrandColor() {
551        return mBrandColor;
552    }
553
554    /**
555     * Sets the adapter containing the rows for the fragment.
556     *
557     * <p>The items referenced by the adapter must be be derived from
558     * {@link Row}. These rows will be used by the rows fragment and the headers
559     * fragment (if not disabled) to render the browse rows.
560     *
561     * @param adapter An ObjectAdapter for the browse rows. All items must
562     *        derive from {@link Row}.
563     */
564    public void setAdapter(ObjectAdapter adapter) {
565        mAdapter = adapter;
566        if (getView() == null) {
567            return;
568        }
569        replaceMainFragment(mSelectedPosition);
570
571        if (adapter != null) {
572            if (mMainFragmentRowsAdapter != null) {
573                mMainFragmentRowsAdapter.setAdapter(adapter);
574            }
575            mHeadersFragment.setAdapter(adapter);
576        }
577    }
578
579    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
580        return mMainFragmentAdapterRegistry;
581    }
582
583    /**
584     * Returns the adapter containing the rows for the fragment.
585     */
586    public ObjectAdapter getAdapter() {
587        return mAdapter;
588    }
589
590    /**
591     * Sets an item selection listener.
592     */
593    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
594        mExternalOnItemViewSelectedListener = listener;
595    }
596
597    /**
598     * Returns an item selection listener.
599     */
600    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
601        return mExternalOnItemViewSelectedListener;
602    }
603
604    /**
605     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
606     * not been created yet or a different fragment is bound to it.
607     *
608     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
609     */
610    public RowsFragment getRowsFragment() {
611        if (mMainFragment instanceof RowsFragment) {
612            return (RowsFragment) mMainFragment;
613        }
614
615        return null;
616    }
617
618    /**
619     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
620     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
621     */
622    public HeadersFragment getHeadersFragment() {
623        return mHeadersFragment;
624    }
625
626    /**
627     * Sets an item clicked listener on the fragment.
628     * OnItemViewClickedListener will override {@link View.OnClickListener} that
629     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
630     * So in general,  developer should choose one of the listeners but not both.
631     */
632    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
633        mOnItemViewClickedListener = listener;
634        if (mMainFragmentRowsAdapter != null) {
635            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
636        }
637    }
638
639    /**
640     * Returns the item Clicked listener.
641     */
642    public OnItemViewClickedListener getOnItemViewClickedListener() {
643        return mOnItemViewClickedListener;
644    }
645
646    /**
647     * Starts a headers transition.
648     *
649     * <p>This method will begin a transition to either show or hide the
650     * headers, depending on the value of withHeaders. If headers are disabled
651     * for this browse fragment, this method will throw an exception.
652     *
653     * @param withHeaders True if the headers should transition to being shown,
654     *        false if the transition should result in headers being hidden.
655     */
656    public void startHeadersTransition(boolean withHeaders) {
657        if (!mCanShowHeaders) {
658            throw new IllegalStateException("Cannot start headers transition");
659        }
660        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
661            return;
662        }
663        startHeadersTransitionInternal(withHeaders);
664    }
665
666    /**
667     * Returns true if the headers transition is currently running.
668     */
669    public boolean isInHeadersTransition() {
670        return mHeadersTransition != null;
671    }
672
673    /**
674     * Returns true if headers are shown.
675     */
676    public boolean isShowingHeaders() {
677        return mShowingHeaders;
678    }
679
680    /**
681     * Sets a listener for browse fragment transitions.
682     *
683     * @param listener The listener to call when a browse headers transition
684     *        begins or ends.
685     */
686    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
687        mBrowseTransitionListener = listener;
688    }
689
690    /**
691     * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
692     *
693     * @param enable true to enable row scaling
694     */
695    public void enableRowScaling(boolean enable) {
696        enableMainFragmentScaling(enable);
697    }
698
699    /**
700     * Enables scaling of main fragment when headers are present. For the page/row fragment,
701     * scaling is enabled only when both this method and
702     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
703     *
704     * @param enable true to enable row scaling
705     */
706    public void enableMainFragmentScaling(boolean enable) {
707        mMainFragmentScaleEnabled = enable;
708    }
709
710    private void startHeadersTransitionInternal(final boolean withHeaders) {
711        if (getFragmentManager().isDestroyed()) {
712            return;
713        }
714        mShowingHeaders = withHeaders;
715        mMainFragmentAdapter.onTransitionPrepare();
716        mMainFragmentAdapter.onTransitionStart();
717        onExpandTransitionStart(!withHeaders, new Runnable() {
718            @Override
719            public void run() {
720                mHeadersFragment.onTransitionPrepare();
721                mHeadersFragment.onTransitionStart();
722                createHeadersTransition();
723                if (mBrowseTransitionListener != null) {
724                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
725                }
726                TransitionHelper.runTransition(
727                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
728                if (mHeadersBackStackEnabled) {
729                    if (!withHeaders) {
730                        getFragmentManager().beginTransaction()
731                                .addToBackStack(mWithHeadersBackStackName).commit();
732                    } else {
733                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
734                        if (index >= 0) {
735                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
736                            getFragmentManager().popBackStackImmediate(entry.getId(),
737                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
738                        }
739                    }
740                }
741            }
742        });
743    }
744
745    boolean isVerticalScrolling() {
746        // don't run transition
747        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
748    }
749
750
751    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
752            new BrowseFrameLayout.OnFocusSearchListener() {
753        @Override
754        public View onFocusSearch(View focused, int direction) {
755            // if headers is running transition,  focus stays
756            if (mCanShowHeaders && isInHeadersTransition()) {
757                return focused;
758            }
759            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
760
761            if (getTitleView() != null && focused != getTitleView() &&
762                    direction == View.FOCUS_UP) {
763                return getTitleView();
764            }
765            if (getTitleView() != null && getTitleView().hasFocus() &&
766                    direction == View.FOCUS_DOWN) {
767                return mCanShowHeaders && mShowingHeaders ?
768                        mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
769            }
770
771            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
772            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
773            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
774            if (mCanShowHeaders && direction == towardStart) {
775                if (isVerticalScrolling() || mShowingHeaders) {
776                    return focused;
777                }
778                return mHeadersFragment.getVerticalGridView();
779            } else if (direction == towardEnd) {
780                if (isVerticalScrolling()) {
781                    return focused;
782                }
783                return mMainFragment.getView();
784            } else {
785                return null;
786            }
787        }
788    };
789
790    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
791            new BrowseFrameLayout.OnChildFocusListener() {
792
793        @Override
794        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
795            if (getChildFragmentManager().isDestroyed()) {
796                return true;
797            }
798            // Make sure not changing focus when requestFocus() is called.
799            if (mCanShowHeaders && mShowingHeaders) {
800                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
801                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
802                    return true;
803                }
804            }
805            if (mMainFragment != null && mMainFragment.getView() != null &&
806                    mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
807                return true;
808            }
809            if (getTitleView() != null &&
810                    getTitleView().requestFocus(direction, previouslyFocusedRect)) {
811                return true;
812            }
813            return false;
814        }
815
816        @Override
817        public void onRequestChildFocus(View child, View focused) {
818            if (getChildFragmentManager().isDestroyed()) {
819                return;
820            }
821            if (!mCanShowHeaders || isInHeadersTransition()) return;
822            int childId = child.getId();
823            if (childId == R.id.browse_container_dock && mShowingHeaders) {
824                startHeadersTransitionInternal(false);
825            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
826                startHeadersTransitionInternal(true);
827            }
828        }
829    };
830
831    @Override
832    public void onSaveInstanceState(Bundle outState) {
833        super.onSaveInstanceState(outState);
834        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
835        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
836
837        if (mBackStackChangedListener != null) {
838            mBackStackChangedListener.save(outState);
839        } else {
840            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
841        }
842    }
843
844    @Override
845    public void onCreate(Bundle savedInstanceState) {
846        super.onCreate(savedInstanceState);
847        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
848        mContainerListMarginStart = (int) ta.getDimension(
849                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
850                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
851        mContainerListAlignTop = (int) ta.getDimension(
852                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
853                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
854        ta.recycle();
855
856        readArguments(getArguments());
857
858        if (mCanShowHeaders) {
859            if (mHeadersBackStackEnabled) {
860                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
861                mBackStackChangedListener = new BackStackListener();
862                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
863                mBackStackChangedListener.load(savedInstanceState);
864            } else {
865                if (savedInstanceState != null) {
866                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
867                }
868            }
869        }
870
871        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
872    }
873
874    @Override
875    public void onDestroyView() {
876        mMainFragmentRowsAdapter = null;
877        mMainFragmentAdapter = null;
878        mMainFragment = null;
879        mHeadersFragment = null;
880        super.onDestroyView();
881    }
882
883    @Override
884    public void onDestroy() {
885        if (mBackStackChangedListener != null) {
886            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
887        }
888        super.onDestroy();
889    }
890
891    @Override
892    public View onCreateView(LayoutInflater inflater, ViewGroup container,
893            Bundle savedInstanceState) {
894        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
895            mHeadersFragment = new HeadersFragment();
896
897            createMainFragment(mAdapter, mSelectedPosition);
898            FragmentTransaction ft = getChildFragmentManager().beginTransaction()
899                    .replace(R.id.browse_headers_dock, mHeadersFragment);
900
901            if (mMainFragment != null) {
902                ft.replace(R.id.scale_frame, mMainFragment);
903            } else {
904                // Empty adapter used to guard against lazy adapter loading. When this
905                // fragment is instantiated, mAdapter might not have the data or might not
906                // have been set. In either of those cases mFragmentAdapter will be null.
907                // This way we can maintain the invariant that mMainFragmentAdapter is never
908                // null and it avoids doing null checks all over the code.
909                mMainFragmentAdapter = new MainFragmentAdapter(null);
910            }
911
912            ft.commit();
913        } else {
914            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
915                    .findFragmentById(R.id.browse_headers_dock);
916            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
917            mMainFragmentAdapter = (MainFragmentAdapter) ((Adaptable)mMainFragment)
918                    .getAdapter(MainFragmentAdapter.class);
919
920            mIsPageRow = savedInstanceState != null ?
921                    savedInstanceState.getBoolean(IS_PAGE_ROW, false) : false;
922
923            mSelectedPosition = savedInstanceState != null ?
924                    savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
925            if (!mIsPageRow) {
926                mMainFragmentRowsAdapter = (MainFragmentRowsAdapter) ((Adaptable) mMainFragment)
927                        .getAdapter(MainFragmentRowsAdapter.class);
928            } else {
929                mMainFragmentRowsAdapter = null;
930            }
931        }
932
933        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
934        if (mHeaderPresenterSelector != null) {
935            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
936        }
937        mHeadersFragment.setAdapter(mAdapter);
938        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
939        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
940
941        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
942
943        setTitleView((TitleView) root.findViewById(R.id.browse_title_group));
944
945        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
946        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
947        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
948
949        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
950        mScaleFrameLayout.setPivotX(0);
951        mScaleFrameLayout.setPivotY(mContainerListAlignTop);
952
953        setupMainFragment();
954
955        if (mBrandColorSet) {
956            mHeadersFragment.setBackgroundColor(mBrandColor);
957        }
958
959        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
960            @Override
961            public void run() {
962                showHeaders(true);
963            }
964        });
965        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
966            @Override
967            public void run() {
968                showHeaders(false);
969            }
970        });
971        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
972            @Override
973            public void run() {
974                setEntranceTransitionEndState();
975            }
976        });
977        return root;
978    }
979
980    private void setupMainFragment() {
981        if (mMainFragmentRowsAdapter != null) {
982            mMainFragmentRowsAdapter.setAdapter(mAdapter);
983            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener);
984            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
985        }
986    }
987
988    private void createHeadersTransition() {
989        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
990                mShowingHeaders ?
991                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
992
993        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
994            @Override
995            public void onTransitionStart(Object transition) {
996            }
997            @Override
998            public void onTransitionEnd(Object transition) {
999                mHeadersTransition = null;
1000                mMainFragmentAdapter.onTransitionEnd();
1001                mHeadersFragment.onTransitionEnd();
1002                if (mShowingHeaders) {
1003                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
1004                    if (headerGridView != null && !headerGridView.hasFocus()) {
1005                        headerGridView.requestFocus();
1006                    }
1007                } else {
1008                    View rowsGridView = mMainFragment.getView();
1009                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
1010                        rowsGridView.requestFocus();
1011                    }
1012                }
1013                if (mBrowseTransitionListener != null) {
1014                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
1015                }
1016            }
1017        });
1018    }
1019
1020    /**
1021     * Sets the {@link PresenterSelector} used to render the row headers.
1022     *
1023     * @param headerPresenterSelector The PresenterSelector that will determine
1024     *        the Presenter for each row header.
1025     */
1026    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
1027        mHeaderPresenterSelector = headerPresenterSelector;
1028        if (mHeadersFragment != null) {
1029            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
1030        }
1031    }
1032
1033    private void setHeadersOnScreen(boolean onScreen) {
1034        MarginLayoutParams lp;
1035        View containerList;
1036        containerList = mHeadersFragment.getView();
1037        lp = (MarginLayoutParams) containerList.getLayoutParams();
1038        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1039        containerList.setLayoutParams(lp);
1040    }
1041
1042    private void showHeaders(boolean show) {
1043        if (DEBUG) Log.v(TAG, "showHeaders " + show);
1044        mHeadersFragment.setHeadersEnabled(show);
1045        setHeadersOnScreen(show);
1046        expandMainFragment(!show);
1047    }
1048
1049    private void expandMainFragment(boolean expand) {
1050        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
1051        params.leftMargin = !expand ? mContainerListMarginStart : 0;
1052        mScaleFrameLayout.setLayoutParams(params);
1053        mMainFragmentAdapter.setExpand(expand);
1054
1055        setMainFragmentAlignment();
1056        final float scaleFactor = !expand
1057                && mMainFragmentScaleEnabled
1058                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
1059        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
1060        mScaleFrameLayout.setChildScale(scaleFactor);
1061    }
1062
1063    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
1064        new HeadersFragment.OnHeaderClickedListener() {
1065            @Override
1066            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1067                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
1068                    return;
1069                }
1070                startHeadersTransitionInternal(false);
1071                mMainFragment.getView().requestFocus();
1072            }
1073        };
1074
1075    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
1076        @Override
1077        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
1078                RowPresenter.ViewHolder rowViewHolder, Row row) {
1079            if (mMainFragment == null) {
1080                return;
1081            }
1082
1083            int position = ((RowsFragment) mMainFragment)
1084                    .getVerticalGridView().getSelectedPosition();
1085            if (DEBUG) Log.v(TAG, "row selected position " + position);
1086            onRowSelected(position);
1087            if (mExternalOnItemViewSelectedListener != null) {
1088                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
1089                        rowViewHolder, row);
1090            }
1091        }
1092    };
1093
1094    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
1095            new HeadersFragment.OnHeaderViewSelectedListener() {
1096        @Override
1097        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1098            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
1099            if (DEBUG) Log.v(TAG, "header selected position " + position);
1100            onRowSelected(position);
1101        }
1102    };
1103
1104    private void onRowSelected(int position) {
1105        if (position != mSelectedPosition) {
1106            mSetSelectionRunnable.post(
1107                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
1108
1109            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
1110                showTitle(true);
1111            } else {
1112                showTitle(false);
1113            }
1114        }
1115    }
1116
1117    private void setSelection(int position, boolean smooth) {
1118        if (position == NO_POSITION) {
1119            return;
1120        }
1121
1122        mHeadersFragment.setSelectedPosition(position, smooth);
1123        replaceMainFragment(position);
1124
1125        if (mMainFragmentRowsAdapter != null) {
1126            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
1127        }
1128        mSelectedPosition = position;
1129    }
1130
1131    private void replaceMainFragment(int position) {
1132        if (createMainFragment(mAdapter, position)) {
1133            swapBrowseContent(mMainFragment);
1134            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
1135            setupMainFragment();
1136        }
1137    }
1138
1139    private void swapBrowseContent(Fragment fragment) {
1140        getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit();
1141    }
1142
1143    /**
1144     * Sets the selected row position with smooth animation.
1145     */
1146    public void setSelectedPosition(int position) {
1147        setSelectedPosition(position, true);
1148    }
1149
1150    /**
1151     * Gets position of currently selected row.
1152     * @return Position of currently selected row.
1153     */
1154    public int getSelectedPosition() {
1155        return mSelectedPosition;
1156    }
1157
1158    /**
1159     * Sets the selected row position.
1160     */
1161    public void setSelectedPosition(int position, boolean smooth) {
1162        mSetSelectionRunnable.post(
1163                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
1164    }
1165
1166    /**
1167     * Selects a Row and perform an optional task on the Row. For example
1168     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
1169     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
1170     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
1171     * ViewGroup, Bundle)}).
1172     *
1173     * @param rowPosition Which row to select.
1174     * @param smooth True to scroll to the row, false for no animation.
1175     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
1176     * fragment will be collapsed.
1177     */
1178    public void setSelectedPosition(int rowPosition, boolean smooth,
1179            final Presenter.ViewHolderTask rowHolderTask) {
1180        if (mMainFragmentAdapterRegistry == null) {
1181            return;
1182        }
1183        if (rowHolderTask != null) {
1184            startHeadersTransition(false);
1185        }
1186        if (mMainFragmentRowsAdapter != null) {
1187            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
1188        }
1189    }
1190
1191    @Override
1192    public void onStart() {
1193        super.onStart();
1194        mHeadersFragment.setAlignment(mContainerListAlignTop);
1195        setMainFragmentAlignment();
1196
1197        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
1198            mHeadersFragment.getView().requestFocus();
1199        } else if ((!mCanShowHeaders || !mShowingHeaders)
1200                && mMainFragment.getView() != null) {
1201            mMainFragment.getView().requestFocus();
1202        }
1203
1204        if (mCanShowHeaders) {
1205            showHeaders(mShowingHeaders);
1206        }
1207
1208        if (isEntranceTransitionEnabled()) {
1209            setEntranceTransitionStartState();
1210        }
1211    }
1212
1213    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
1214        if (expand) {
1215            callback.run();
1216            return;
1217        }
1218        // Run a "pre" layout when we go non-expand, in order to get the initial
1219        // positions of added rows.
1220        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
1221    }
1222
1223    private void setMainFragmentAlignment() {
1224        int alignOffset = mContainerListAlignTop;
1225        if (mMainFragmentScaleEnabled
1226                && mMainFragmentAdapter.isScalingEnabled()
1227                && mShowingHeaders) {
1228            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
1229        }
1230        mMainFragmentAdapter.setAlignment(alignOffset);
1231    }
1232
1233    /**
1234     * Enables/disables headers transition on back key support. This is enabled by
1235     * default. The BrowseFragment will add a back stack entry when headers are
1236     * showing. Running a headers transition when the back key is pressed only
1237     * works when the headers state is {@link #HEADERS_ENABLED} or
1238     * {@link #HEADERS_HIDDEN}.
1239     * <p>
1240     * NOTE: If an Activity has its own onBackPressed() handling, you must
1241     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
1242     * and {@link BrowseTransitionListener} in your own back stack handling.
1243     */
1244    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
1245        mHeadersBackStackEnabled = headersBackStackEnabled;
1246    }
1247
1248    /**
1249     * Returns true if headers transition on back key support is enabled.
1250     */
1251    public final boolean isHeadersTransitionOnBackEnabled() {
1252        return mHeadersBackStackEnabled;
1253    }
1254
1255    private void readArguments(Bundle args) {
1256        if (args == null) {
1257            return;
1258        }
1259        if (args.containsKey(ARG_TITLE)) {
1260            setTitle(args.getString(ARG_TITLE));
1261        }
1262        if (args.containsKey(ARG_HEADERS_STATE)) {
1263            setHeadersState(args.getInt(ARG_HEADERS_STATE));
1264        }
1265    }
1266
1267    /**
1268     * Sets the state for the headers column in the browse fragment. Must be one
1269     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1270     * {@link #HEADERS_DISABLED}.
1271     *
1272     * @param headersState The state of the headers for the browse fragment.
1273     */
1274    public void setHeadersState(int headersState) {
1275        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1276            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1277        }
1278        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1279
1280        if (headersState != mHeadersState) {
1281            mHeadersState = headersState;
1282            switch (headersState) {
1283                case HEADERS_ENABLED:
1284                    mCanShowHeaders = true;
1285                    mShowingHeaders = true;
1286                    break;
1287                case HEADERS_HIDDEN:
1288                    mCanShowHeaders = true;
1289                    mShowingHeaders = false;
1290                    break;
1291                case HEADERS_DISABLED:
1292                    mCanShowHeaders = false;
1293                    mShowingHeaders = false;
1294                    break;
1295                default:
1296                    Log.w(TAG, "Unknown headers state: " + headersState);
1297                    break;
1298            }
1299            if (mHeadersFragment != null) {
1300                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1301            }
1302        }
1303    }
1304
1305    /**
1306     * Returns the state of the headers column in the browse fragment.
1307     */
1308    public int getHeadersState() {
1309        return mHeadersState;
1310    }
1311
1312    @Override
1313    protected Object createEntranceTransition() {
1314        return TransitionHelper.loadTransition(getActivity(),
1315                R.transition.lb_browse_entrance_transition);
1316    }
1317
1318    @Override
1319    protected void runEntranceTransition(Object entranceTransition) {
1320        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
1321    }
1322
1323    @Override
1324    protected void onEntranceTransitionPrepare() {
1325        mHeadersFragment.onTransitionPrepare();
1326        mMainFragmentAdapter.onTransitionPrepare();
1327    }
1328
1329    @Override
1330    protected void onEntranceTransitionStart() {
1331        mHeadersFragment.onTransitionStart();
1332        mMainFragmentAdapter.onTransitionStart();
1333    }
1334
1335    @Override
1336    protected void onEntranceTransitionEnd() {
1337        mMainFragmentAdapter.onTransitionEnd();
1338        mHeadersFragment.onTransitionEnd();
1339    }
1340
1341    void setSearchOrbViewOnScreen(boolean onScreen) {
1342        View searchOrbView = getTitleView().getSearchAffordanceView();
1343        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1344        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1345        searchOrbView.setLayoutParams(lp);
1346    }
1347
1348    void setEntranceTransitionStartState() {
1349        setHeadersOnScreen(false);
1350        setSearchOrbViewOnScreen(false);
1351        mMainFragmentAdapter.setEntranceTransitionState(false);
1352    }
1353
1354    void setEntranceTransitionEndState() {
1355        setHeadersOnScreen(mShowingHeaders);
1356        setSearchOrbViewOnScreen(true);
1357        mMainFragmentAdapter.setEntranceTransitionState(true);
1358    }
1359
1360    private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
1361
1362        private final View mView;
1363        private final Runnable mCallback;
1364        private int mState;
1365        private MainFragmentAdapter mainFragmentAdapter;
1366
1367        final static int STATE_INIT = 0;
1368        final static int STATE_FIRST_DRAW = 1;
1369        final static int STATE_SECOND_DRAW = 2;
1370
1371        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
1372            mView = view;
1373            mCallback = callback;
1374            mainFragmentAdapter = adapter;
1375        }
1376
1377        void execute() {
1378            mView.getViewTreeObserver().addOnPreDrawListener(this);
1379            mainFragmentAdapter.setExpand(false);
1380            mState = STATE_INIT;
1381        }
1382
1383        @Override
1384        public boolean onPreDraw() {
1385            if (mState == STATE_INIT) {
1386                mainFragmentAdapter.setExpand(true);
1387                mState = STATE_FIRST_DRAW;
1388            } else if (mState == STATE_FIRST_DRAW) {
1389                mCallback.run();
1390                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1391                mState = STATE_SECOND_DRAW;
1392            }
1393            return false;
1394        }
1395    }
1396}
1397