BrowseFragment.java revision b082c7277cff057bdff19e411e345c3d6fea2e12
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     * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom
204     * fragments can interact with {@link BrowseFragment} using this interface.
205     */
206    public interface FragmentHost {
207        /**
208         * Clients are required to invoke this callback once their view is created
209         * inside {@link Fragment#onStart} method. {@link BrowseFragment} starts the entrance
210         * animation only after receiving this callback. Failure to invoke this method
211         * will lead to fragment not showing up.
212         *
213         * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment.
214         */
215        void notifyViewCreated(MainFragmentAdapter fragmentAdapter);
216
217        /**
218         * Slides in the title view from top in {@link BrowseFragment}. This will only happen
219         * if either a. we are in fully expanded mode OR b. non expanded mode but on the first
220         * row.
221         *
222         * @param show Boolean indicating whether or not to show the title view.
223         */
224        void showTitleView(boolean show);
225    }
226
227    /**
228     * Default implementation of {@link FragmentHost} that is used only by
229     * {@link BrowseFragment}.
230     */
231    private final class FragmentHostImpl implements FragmentHost {
232
233        @Override
234        public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
235            processingPendingEntranceTransition();
236        }
237
238        @Override
239        public void showTitleView(boolean show) {
240            if (mShowingHeaders && !isShowingTitle()) {
241                mPendingShowTitleView = true;
242            }
243            else if (!mShowingHeaders && !isShowingTitle()) {
244                showTitle(true);
245            }
246        }
247    }
248
249    /**
250     * Interface that defines the interaction between {@link BrowseFragment} and it's main
251     * content fragment. The key method is {@link MainFragmentAdapter#getFragment()},
252     * it will be used to get the fragment to be shown in the content section. Clients can
253     * provide any implementation of fragment and customize it's interaction with
254     * {@link BrowseFragment} by overriding the necessary methods.
255     *
256     * <p>
257     * Clients are expected to provide
258     * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing
259     * implementations of {@link MainFragmentAdapter} for given content types. Currently
260     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
261     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
262     * {@link PageRow} - {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}.
263     *
264     * <p>
265     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
266     * design. Users will have to provide an implementation of {@link MainFragmentAdapter}
267     * and provide that through {@link MainFragmentAdapterRegistry}.
268     * {@link MainFragmentAdapter} implementation can supply any fragment and override
269     * just those interactions that makes sense.
270     */
271    public static class MainFragmentAdapter<T extends Fragment> {
272        private boolean mScalingEnabled;
273        private final T mFragment;
274        private FragmentHost mFragmentHost;
275
276        public MainFragmentAdapter(T fragment) {
277            this.mFragment = fragment;
278        }
279
280        public final T getFragment() {
281            return mFragment;
282        }
283
284        /**
285         * Returns whether its scrolling.
286         */
287        public boolean isScrolling() {
288            return false;
289        }
290
291        /**
292         * Set the visibility of titles/hovercard of browse rows.
293         */
294        public void setExpand(boolean expand) {
295        }
296
297        /**
298         * For rows that willing to participate entrance transition,  this function
299         * hide views if afterTransition is true,  show views if afterTransition is false.
300         */
301        public void setEntranceTransitionState(boolean state) {
302        }
303
304        /**
305         * Sets the window alignment and also the pivots for scale operation.
306         */
307        public void setAlignment(int windowAlignOffsetFromTop) {
308        }
309
310        /**
311         * Callback indicating transition prepare start.
312         */
313        public boolean onTransitionPrepare() {
314            return false;
315        }
316
317        /**
318         * Callback indicating transition start.
319         */
320        public void onTransitionStart() {
321        }
322
323        /**
324         * Callback indicating transition end.
325         */
326        public void onTransitionEnd() {
327        }
328
329        /**
330         * Returns whether row scaling is enabled.
331         */
332        public boolean isScalingEnabled() {
333            return mScalingEnabled;
334        }
335
336        /**
337         * Sets the row scaling property.
338         */
339        public void setScalingEnabled(boolean scalingEnabled) {
340            this.mScalingEnabled = scalingEnabled;
341        }
342
343        void setFragmentHost(FragmentHost fragmentHost) {
344            this.mFragmentHost = fragmentHost;
345        }
346
347        public final FragmentHost getFragmentHost() {
348            return mFragmentHost;
349        }
350    }
351
352    /**
353     * This is used to pass information to {@link RowsFragment}.
354     * {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}
355     * would return an instance to connect the callbacks from {@link BrowseFragment} to
356     * {@link RowsFragment}.
357     */
358    public static class MainFragmentRowsAdapter<T extends Fragment> {
359        private final T mFragment;
360
361        public MainFragmentRowsAdapter(T fragment) {
362            if (fragment == null) {
363                throw new IllegalArgumentException("Fragment can't be null");
364            }
365            this.mFragment = fragment;
366        }
367
368        public final T getFragment() {
369            return mFragment;
370        }
371        /**
372         * Set the visibility titles/hover of browse rows.
373         */
374        public void setAdapter(ObjectAdapter adapter) {
375        }
376
377        /**
378         * Sets an item clicked listener on the fragment.
379         */
380        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
381        }
382
383        /**
384         * Sets an item selection listener.
385         */
386        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
387        }
388
389        /**
390         * Selects a Row and perform an optional task on the Row.
391         */
392        public void setSelectedPosition(int rowPosition,
393                                        boolean smooth,
394                                        final Presenter.ViewHolderTask rowHolderTask) {
395        }
396
397        /**
398         * Selects a Row.
399         */
400        public void setSelectedPosition(int rowPosition, boolean smooth) {
401        }
402
403        /**
404         * Returns the selected position.
405         */
406        public int getSelectedPosition() {
407            return 0;
408        }
409    }
410
411    private boolean createMainFragment(ObjectAdapter adapter, int position) {
412        Object item = null;
413        if (adapter == null || adapter.size() == 0) {
414            return false;
415        } else {
416            if (position < 0) {
417                position = 0;
418            } else if (position >= adapter.size()) {
419                throw new IllegalArgumentException(
420                        String.format("Invalid position %d requested", position));
421            }
422            item = adapter.get(position);
423        }
424
425        mSelectedPosition = position;
426        boolean oldIsPageRow = mIsPageRow;
427        mIsPageRow = item instanceof PageRow;
428        boolean swap;
429
430        if (mMainFragment == null) {
431            swap = true;
432        } else {
433            if (oldIsPageRow) {
434                swap = true;
435            } else {
436                swap = mIsPageRow;
437            }
438        }
439
440        if (swap) {
441            mMainFragment = mMainFragmentAdapterRegistry.createFragment(item);
442            mMainFragmentAdapter = (MainFragmentAdapter) ((Adaptable)mMainFragment)
443                    .getAdapter(MainFragmentAdapter.class);
444            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
445            if (!mIsPageRow) {
446                mMainFragmentRowsAdapter = (MainFragmentRowsAdapter) ((Adaptable)mMainFragment)
447                        .getAdapter(MainFragmentRowsAdapter.class);
448                mIsPageRow = mMainFragmentRowsAdapter == null;
449            } else {
450                mMainFragmentRowsAdapter = null;
451            }
452        }
453        return swap;
454    }
455
456    /**
457     * Factory class responsible for creating fragment given the current item. {@link ListRow}
458     * should returns {@link RowsFragment} or it's subclass whereas {@link PageRow}
459     * can return any fragment class.
460     */
461    public abstract static class FragmentFactory<T extends Fragment> {
462        public abstract T createFragment(Object row);
463    }
464
465    /**
466     * FragmentFactory implementation for {@link ListRow}.
467     */
468    public static class ListRowFragmentFactory extends FragmentFactory<RowsFragment> {
469        @Override
470        public RowsFragment createFragment(Object row) {
471            return new RowsFragment();
472        }
473    }
474
475    /**
476     * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}.
477     * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for
478     * handling {@link ListRow}. Developers can override that and also if they want to
479     * use custom fragment, they can register a custom {@link FragmentFactory}
480     * against {@link PageRow}.
481     */
482    public final static class MainFragmentAdapterRegistry {
483        private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap();
484        private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory();
485
486        public MainFragmentAdapterRegistry() {
487            registerFragment(ListRow.class, sDefaultFragmentFactory);
488        }
489
490        public void registerFragment(Class rowClass, FragmentFactory factory) {
491            mItemToFragmentFactoryMapping.put(rowClass, factory);
492        }
493
494        public Fragment createFragment(Object item) {
495            if (item == null) {
496                throw new IllegalArgumentException("Item can't be null");
497            }
498
499            FragmentFactory fragmentFactory = mItemToFragmentFactoryMapping.get(item.getClass());
500            if (fragmentFactory == null && !(item instanceof PageRow)) {
501                fragmentFactory = sDefaultFragmentFactory;
502            }
503
504            return fragmentFactory.createFragment(item);
505        }
506    }
507
508    private static final String TAG = "BrowseFragment";
509
510    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
511
512    private static boolean DEBUG = false;
513
514    /** The headers fragment is enabled and shown by default. */
515    public static final int HEADERS_ENABLED = 1;
516
517    /** The headers fragment is enabled and hidden by default. */
518    public static final int HEADERS_HIDDEN = 2;
519
520    /** The headers fragment is disabled and will never be shown. */
521    public static final int HEADERS_DISABLED = 3;
522
523    private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry
524            = new MainFragmentAdapterRegistry();
525    private MainFragmentAdapter mMainFragmentAdapter;
526    private Fragment mMainFragment;
527    private HeadersFragment mHeadersFragment;
528    private MainFragmentRowsAdapter mMainFragmentRowsAdapter;
529
530    private ObjectAdapter mAdapter;
531
532    private int mHeadersState = HEADERS_ENABLED;
533    private int mBrandColor = Color.TRANSPARENT;
534    private boolean mBrandColorSet;
535
536    private BrowseFrameLayout mBrowseFrame;
537    private ScaleFrameLayout mScaleFrameLayout;
538    private boolean mHeadersBackStackEnabled = true;
539    private String mWithHeadersBackStackName;
540    private boolean mShowingHeaders = true;
541    private boolean mPendingShowTitleView;
542    private boolean mCanShowHeaders = true;
543    private int mContainerListMarginStart;
544    private int mContainerListAlignTop;
545    private boolean mMainFragmentScaleEnabled = true;
546    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
547    private OnItemViewClickedListener mOnItemViewClickedListener;
548    private int mSelectedPosition = -1;
549    private float mScaleFactor;
550    private boolean mIsPageRow;
551
552    private PresenterSelector mHeaderPresenterSelector;
553    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
554
555    // transition related:
556    private Object mSceneWithHeaders;
557    private Object mSceneWithoutHeaders;
558    private Object mSceneAfterEntranceTransition;
559    private Object mHeadersTransition;
560    private BackStackListener mBackStackChangedListener;
561    private BrowseTransitionListener mBrowseTransitionListener;
562
563    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
564    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
565    private static final String ARG_HEADERS_STATE =
566        BrowseFragment.class.getCanonicalName() + ".headersState";
567
568    /**
569     * Creates arguments for a browse fragment.
570     *
571     * @param args The Bundle to place arguments into, or null if the method
572     *        should return a new Bundle.
573     * @param title The title of the BrowseFragment.
574     * @param headersState The initial state of the headers of the
575     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
576     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
577     * @return A Bundle with the given arguments for creating a BrowseFragment.
578     */
579    public static Bundle createArgs(Bundle args, String title, int headersState) {
580        if (args == null) {
581            args = new Bundle();
582        }
583        args.putString(ARG_TITLE, title);
584        args.putInt(ARG_HEADERS_STATE, headersState);
585        return args;
586    }
587
588    /**
589     * Sets the brand color for the browse fragment. The brand color is used as
590     * the primary color for UI elements in the browse fragment. For example,
591     * the background color of the headers fragment uses the brand color.
592     *
593     * @param color The color to use as the brand color of the fragment.
594     */
595    public void setBrandColor(@ColorInt int color) {
596        mBrandColor = color;
597        mBrandColorSet = true;
598
599        if (mHeadersFragment != null) {
600            mHeadersFragment.setBackgroundColor(mBrandColor);
601        }
602    }
603
604    /**
605     * Returns the brand color for the browse fragment.
606     * The default is transparent.
607     */
608    @ColorInt
609    public int getBrandColor() {
610        return mBrandColor;
611    }
612
613    /**
614     * Sets the adapter containing the rows for the fragment.
615     *
616     * <p>The items referenced by the adapter must be be derived from
617     * {@link Row}. These rows will be used by the rows fragment and the headers
618     * fragment (if not disabled) to render the browse rows.
619     *
620     * @param adapter An ObjectAdapter for the browse rows. All items must
621     *        derive from {@link Row}.
622     */
623    public void setAdapter(ObjectAdapter adapter) {
624        mAdapter = adapter;
625        if (getView() == null) {
626            return;
627        }
628        replaceMainFragment(mSelectedPosition);
629
630        if (adapter != null) {
631            if (mMainFragmentRowsAdapter != null) {
632                mMainFragmentRowsAdapter.setAdapter(adapter);
633            }
634            mHeadersFragment.setAdapter(adapter);
635        }
636    }
637
638    public final MainFragmentAdapterRegistry getMainFragmentRegistry() {
639        return mMainFragmentAdapterRegistry;
640    }
641
642    /**
643     * Returns the adapter containing the rows for the fragment.
644     */
645    public ObjectAdapter getAdapter() {
646        return mAdapter;
647    }
648
649    /**
650     * Sets an item selection listener.
651     */
652    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
653        mExternalOnItemViewSelectedListener = listener;
654    }
655
656    /**
657     * Returns an item selection listener.
658     */
659    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
660        return mExternalOnItemViewSelectedListener;
661    }
662
663    /**
664     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
665     * not been created yet or a different fragment is bound to it.
666     *
667     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
668     */
669    public RowsFragment getRowsFragment() {
670        if (mMainFragment instanceof RowsFragment) {
671            return (RowsFragment) mMainFragment;
672        }
673
674        return null;
675    }
676
677    /**
678     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
679     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
680     */
681    public HeadersFragment getHeadersFragment() {
682        return mHeadersFragment;
683    }
684
685    /**
686     * Sets an item clicked listener on the fragment.
687     * OnItemViewClickedListener will override {@link View.OnClickListener} that
688     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
689     * So in general, developer should choose one of the listeners but not both.
690     */
691    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
692        mOnItemViewClickedListener = listener;
693        if (mMainFragmentRowsAdapter != null) {
694            mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener);
695        }
696    }
697
698    /**
699     * Returns the item Clicked listener.
700     */
701    public OnItemViewClickedListener getOnItemViewClickedListener() {
702        return mOnItemViewClickedListener;
703    }
704
705    /**
706     * Starts a headers transition.
707     *
708     * <p>This method will begin a transition to either show or hide the
709     * headers, depending on the value of withHeaders. If headers are disabled
710     * for this browse fragment, this method will throw an exception.
711     *
712     * @param withHeaders True if the headers should transition to being shown,
713     *        false if the transition should result in headers being hidden.
714     */
715    public void startHeadersTransition(boolean withHeaders) {
716        if (!mCanShowHeaders) {
717            throw new IllegalStateException("Cannot start headers transition");
718        }
719        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
720            return;
721        }
722        startHeadersTransitionInternal(withHeaders);
723    }
724
725    /**
726     * Returns true if the headers transition is currently running.
727     */
728    public boolean isInHeadersTransition() {
729        return mHeadersTransition != null;
730    }
731
732    /**
733     * Returns true if headers are shown.
734     */
735    public boolean isShowingHeaders() {
736        return mShowingHeaders;
737    }
738
739    /**
740     * Sets a listener for browse fragment transitions.
741     *
742     * @param listener The listener to call when a browse headers transition
743     *        begins or ends.
744     */
745    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
746        mBrowseTransitionListener = listener;
747    }
748
749    /**
750     * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
751     *
752     * @param enable true to enable row scaling
753     */
754    public void enableRowScaling(boolean enable) {
755        enableMainFragmentScaling(enable);
756    }
757
758    /**
759     * Enables scaling of main fragment when headers are present. For the page/row fragment,
760     * scaling is enabled only when both this method and
761     * {@link MainFragmentAdapter#isScalingEnabled()} are enabled.
762     *
763     * @param enable true to enable row scaling
764     */
765    public void enableMainFragmentScaling(boolean enable) {
766        mMainFragmentScaleEnabled = enable;
767    }
768
769    private void startHeadersTransitionInternal(final boolean withHeaders) {
770        if (getFragmentManager().isDestroyed()) {
771            return;
772        }
773        mShowingHeaders = withHeaders;
774        mMainFragmentAdapter.onTransitionPrepare();
775        mMainFragmentAdapter.onTransitionStart();
776        onExpandTransitionStart(!withHeaders, new Runnable() {
777            @Override
778            public void run() {
779                mHeadersFragment.onTransitionPrepare();
780                mHeadersFragment.onTransitionStart();
781                createHeadersTransition();
782                if (mBrowseTransitionListener != null) {
783                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
784                }
785                TransitionHelper.runTransition(
786                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
787                if (mHeadersBackStackEnabled) {
788                    if (!withHeaders) {
789                        getFragmentManager().beginTransaction()
790                                .addToBackStack(mWithHeadersBackStackName).commit();
791                    } else {
792                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
793                        if (index >= 0) {
794                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
795                            getFragmentManager().popBackStackImmediate(entry.getId(),
796                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
797                        }
798                    }
799                }
800            }
801        });
802    }
803
804    boolean isVerticalScrolling() {
805        // don't run transition
806        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
807    }
808
809
810    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
811            new BrowseFrameLayout.OnFocusSearchListener() {
812        @Override
813        public View onFocusSearch(View focused, int direction) {
814            // if headers is running transition,  focus stays
815            if (mCanShowHeaders && isInHeadersTransition()) {
816                return focused;
817            }
818            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
819
820            if (getTitleView() != null && focused != getTitleView() &&
821                    direction == View.FOCUS_UP) {
822                return getTitleView();
823            }
824            if (getTitleView() != null && getTitleView().hasFocus() &&
825                    direction == View.FOCUS_DOWN) {
826                return mCanShowHeaders && mShowingHeaders ?
827                        mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
828            }
829
830            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
831            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
832            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
833            if (mCanShowHeaders && direction == towardStart) {
834                if (isVerticalScrolling() || mShowingHeaders) {
835                    return focused;
836                }
837                return mHeadersFragment.getVerticalGridView();
838            } else if (direction == towardEnd) {
839                if (isVerticalScrolling()) {
840                    return focused;
841                }
842                return mMainFragment.getView();
843            } else {
844                return null;
845            }
846        }
847    };
848
849    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
850            new BrowseFrameLayout.OnChildFocusListener() {
851
852        @Override
853        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
854            if (getChildFragmentManager().isDestroyed()) {
855                return true;
856            }
857            // Make sure not changing focus when requestFocus() is called.
858            if (mCanShowHeaders && mShowingHeaders) {
859                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
860                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
861                    return true;
862                }
863            }
864            if (mMainFragment != null && mMainFragment.getView() != null &&
865                    mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
866                return true;
867            }
868            if (getTitleView() != null &&
869                    getTitleView().requestFocus(direction, previouslyFocusedRect)) {
870                return true;
871            }
872            return false;
873        }
874
875        @Override
876        public void onRequestChildFocus(View child, View focused) {
877            if (getChildFragmentManager().isDestroyed()) {
878                return;
879            }
880            if (!mCanShowHeaders || isInHeadersTransition()) return;
881            int childId = child.getId();
882            if (childId == R.id.browse_container_dock && mShowingHeaders) {
883                startHeadersTransitionInternal(false);
884            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
885                startHeadersTransitionInternal(true);
886            }
887        }
888    };
889
890    @Override
891    public void onSaveInstanceState(Bundle outState) {
892        super.onSaveInstanceState(outState);
893        outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition);
894        outState.putBoolean(IS_PAGE_ROW, mIsPageRow);
895
896        if (mBackStackChangedListener != null) {
897            mBackStackChangedListener.save(outState);
898        } else {
899            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
900        }
901    }
902
903    @Override
904    public void onCreate(Bundle savedInstanceState) {
905        super.onCreate(savedInstanceState);
906        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
907        mContainerListMarginStart = (int) ta.getDimension(
908                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
909                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
910        mContainerListAlignTop = (int) ta.getDimension(
911                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
912                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
913        ta.recycle();
914
915        readArguments(getArguments());
916
917        if (mCanShowHeaders) {
918            if (mHeadersBackStackEnabled) {
919                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
920                mBackStackChangedListener = new BackStackListener();
921                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
922                mBackStackChangedListener.load(savedInstanceState);
923            } else {
924                if (savedInstanceState != null) {
925                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
926                }
927            }
928        }
929
930        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
931    }
932
933    @Override
934    public void onDestroyView() {
935        mMainFragmentRowsAdapter = null;
936        mMainFragmentAdapter = null;
937        mMainFragment = null;
938        mHeadersFragment = null;
939        super.onDestroyView();
940    }
941
942    @Override
943    public void onDestroy() {
944        if (mBackStackChangedListener != null) {
945            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
946        }
947        super.onDestroy();
948    }
949
950    @Override
951    public View onCreateView(LayoutInflater inflater, ViewGroup container,
952            Bundle savedInstanceState) {
953        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
954            mHeadersFragment = new HeadersFragment();
955
956            createMainFragment(mAdapter, mSelectedPosition);
957            FragmentTransaction ft = getChildFragmentManager().beginTransaction()
958                    .replace(R.id.browse_headers_dock, mHeadersFragment);
959
960            if (mMainFragment != null) {
961                ft.replace(R.id.scale_frame, mMainFragment);
962            } else {
963                // Empty adapter used to guard against lazy adapter loading. When this
964                // fragment is instantiated, mAdapter might not have the data or might not
965                // have been set. In either of those cases mFragmentAdapter will be null.
966                // This way we can maintain the invariant that mMainFragmentAdapter is never
967                // null and it avoids doing null checks all over the code.
968                mMainFragmentAdapter = new MainFragmentAdapter(null);
969                mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
970            }
971
972            ft.commit();
973        } else {
974            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
975                    .findFragmentById(R.id.browse_headers_dock);
976            mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame);
977            mMainFragmentAdapter = (MainFragmentAdapter) ((Adaptable)mMainFragment)
978                    .getAdapter(MainFragmentAdapter.class);
979            mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl());
980
981            mIsPageRow = savedInstanceState != null ?
982                    savedInstanceState.getBoolean(IS_PAGE_ROW, false) : false;
983
984            mSelectedPosition = savedInstanceState != null ?
985                    savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0;
986            if (!mIsPageRow) {
987                mMainFragmentRowsAdapter = (MainFragmentRowsAdapter) ((Adaptable) mMainFragment)
988                        .getAdapter(MainFragmentRowsAdapter.class);
989            } else {
990                mMainFragmentRowsAdapter = null;
991            }
992        }
993
994        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
995        if (mHeaderPresenterSelector != null) {
996            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
997        }
998        mHeadersFragment.setAdapter(mAdapter);
999        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
1000        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
1001
1002        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
1003
1004        getProgressBarManager().setRootView((ViewGroup)root);
1005
1006        setTitleView((TitleView) root.findViewById(R.id.browse_title_group));
1007
1008        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
1009        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
1010        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
1011
1012        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
1013        mScaleFrameLayout.setPivotX(0);
1014        mScaleFrameLayout.setPivotY(mContainerListAlignTop);
1015
1016        setupMainFragment();
1017
1018        if (mBrandColorSet) {
1019            mHeadersFragment.setBackgroundColor(mBrandColor);
1020        }
1021
1022        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
1023            @Override
1024            public void run() {
1025                showHeaders(true);
1026            }
1027        });
1028        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
1029            @Override
1030            public void run() {
1031                showHeaders(false);
1032            }
1033        });
1034        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
1035            @Override
1036            public void run() {
1037                setEntranceTransitionEndState();
1038            }
1039        });
1040        return root;
1041    }
1042
1043    private void setupMainFragment() {
1044        if (mMainFragmentRowsAdapter != null) {
1045            mMainFragmentRowsAdapter.setAdapter(mAdapter);
1046            mMainFragmentRowsAdapter.setOnItemViewSelectedListener(
1047                    new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter));
1048            mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
1049        }
1050    }
1051
1052    @Override
1053    boolean isReadyForPrepareEntranceTransition() {
1054        return mMainFragment != null && mMainFragment.getView() != null;
1055    }
1056
1057    @Override
1058    boolean isReadyForStartEntranceTransition() {
1059        return mMainFragment != null && mMainFragment.getView() != null;
1060    }
1061
1062    void processingPendingEntranceTransition() {
1063        performPendingStates();
1064    }
1065
1066    private void createHeadersTransition() {
1067        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
1068                mShowingHeaders ?
1069                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
1070
1071        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
1072            @Override
1073            public void onTransitionStart(Object transition) {
1074            }
1075            @Override
1076            public void onTransitionEnd(Object transition) {
1077                mHeadersTransition = null;
1078                if (mMainFragmentAdapter != null) {
1079                    mMainFragmentAdapter.onTransitionEnd();
1080                    if (!mShowingHeaders && mMainFragment != null) {
1081                        View mainFragmentView = mMainFragment.getView();
1082                        if (mainFragmentView != null && !mainFragmentView.hasFocus()) {
1083                            mainFragmentView.requestFocus();
1084                        }
1085                    }
1086                }
1087                if (mHeadersFragment != null) {
1088                    mHeadersFragment.onTransitionEnd();
1089                    if (mShowingHeaders) {
1090                        VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
1091                        if (headerGridView != null && !headerGridView.hasFocus()) {
1092                            headerGridView.requestFocus();
1093                        }
1094                    }
1095
1096                    // Animate titleview once header animation is complete.
1097                    if (!mShowingHeaders && mPendingShowTitleView) {
1098                        mPendingShowTitleView = false;
1099                        showTitle(true);
1100                    } else if (mShowingHeaders && isShowingTitle() && mSelectedPosition != 0) {
1101                        mPendingShowTitleView = true;
1102                        showTitle(false);
1103                    }
1104                }
1105                if (mBrowseTransitionListener != null) {
1106                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
1107                }
1108            }
1109        });
1110    }
1111
1112    /**
1113     * Sets the {@link PresenterSelector} used to render the row headers.
1114     *
1115     * @param headerPresenterSelector The PresenterSelector that will determine
1116     *        the Presenter for each row header.
1117     */
1118    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
1119        mHeaderPresenterSelector = headerPresenterSelector;
1120        if (mHeadersFragment != null) {
1121            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
1122        }
1123    }
1124
1125    private void setHeadersOnScreen(boolean onScreen) {
1126        MarginLayoutParams lp;
1127        View containerList;
1128        containerList = mHeadersFragment.getView();
1129        lp = (MarginLayoutParams) containerList.getLayoutParams();
1130        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1131        containerList.setLayoutParams(lp);
1132    }
1133
1134    private void showHeaders(boolean show) {
1135        if (DEBUG) Log.v(TAG, "showHeaders " + show);
1136        mHeadersFragment.setHeadersEnabled(show);
1137        setHeadersOnScreen(show);
1138        expandMainFragment(!show);
1139    }
1140
1141    private void expandMainFragment(boolean expand) {
1142        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
1143        params.leftMargin = !expand ? mContainerListMarginStart : 0;
1144        mScaleFrameLayout.setLayoutParams(params);
1145        mMainFragmentAdapter.setExpand(expand);
1146
1147        setMainFragmentAlignment();
1148        final float scaleFactor = !expand
1149                && mMainFragmentScaleEnabled
1150                && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1;
1151        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
1152        mScaleFrameLayout.setChildScale(scaleFactor);
1153    }
1154
1155    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
1156        new HeadersFragment.OnHeaderClickedListener() {
1157            @Override
1158            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1159                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
1160                    return;
1161                }
1162                startHeadersTransitionInternal(false);
1163                mMainFragment.getView().requestFocus();
1164            }
1165        };
1166
1167    class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener {
1168        MainFragmentRowsAdapter mMainFragmentRowsAdapter;
1169
1170        public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) {
1171            mMainFragmentRowsAdapter = fragmentRowsAdapter;
1172        }
1173
1174        @Override
1175        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
1176                RowPresenter.ViewHolder rowViewHolder, Row row) {
1177            int position = mMainFragmentRowsAdapter.getSelectedPosition();
1178            if (DEBUG) Log.v(TAG, "row selected position " + position);
1179            onRowSelected(position);
1180            if (mExternalOnItemViewSelectedListener != null) {
1181                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
1182                        rowViewHolder, row);
1183            }
1184        }
1185    };
1186
1187    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
1188            new HeadersFragment.OnHeaderViewSelectedListener() {
1189        @Override
1190        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
1191            int position = mHeadersFragment.getSelectedPosition();
1192            if (DEBUG) Log.v(TAG, "header selected position " + position);
1193            onRowSelected(position);
1194        }
1195    };
1196
1197    private void onRowSelected(int position) {
1198        if (position != mSelectedPosition) {
1199            mSetSelectionRunnable.post(
1200                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
1201
1202            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
1203                showTitle(true);
1204            } else {
1205                showTitle(false);
1206            }
1207        }
1208    }
1209
1210    private void setSelection(int position, boolean smooth) {
1211        if (position == NO_POSITION) {
1212            return;
1213        }
1214
1215        if (position != mSelectedPosition) {
1216            mPendingShowTitleView = false;
1217        }
1218
1219        mHeadersFragment.setSelectedPosition(position, smooth);
1220        replaceMainFragment(position);
1221
1222        if (mMainFragmentRowsAdapter != null) {
1223            mMainFragmentRowsAdapter.setSelectedPosition(position, smooth);
1224        }
1225        mSelectedPosition = position;
1226    }
1227
1228    private void replaceMainFragment(int position) {
1229        if (createMainFragment(mAdapter, position)) {
1230            swapBrowseContent(mMainFragment);
1231            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
1232            setupMainFragment();
1233            processingPendingEntranceTransition();
1234        }
1235    }
1236
1237    private void swapBrowseContent(Fragment fragment) {
1238        getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit();
1239    }
1240
1241    /**
1242     * Sets the selected row position with smooth animation.
1243     */
1244    public void setSelectedPosition(int position) {
1245        setSelectedPosition(position, true);
1246    }
1247
1248    /**
1249     * Gets position of currently selected row.
1250     * @return Position of currently selected row.
1251     */
1252    public int getSelectedPosition() {
1253        return mSelectedPosition;
1254    }
1255
1256    /**
1257     * Sets the selected row position.
1258     */
1259    public void setSelectedPosition(int position, boolean smooth) {
1260        mSetSelectionRunnable.post(
1261                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
1262    }
1263
1264    /**
1265     * Selects a Row and perform an optional task on the Row. For example
1266     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
1267     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
1268     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
1269     * ViewGroup, Bundle)}).
1270     *
1271     * @param rowPosition Which row to select.
1272     * @param smooth True to scroll to the row, false for no animation.
1273     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
1274     * fragment will be collapsed.
1275     */
1276    public void setSelectedPosition(int rowPosition, boolean smooth,
1277            final Presenter.ViewHolderTask rowHolderTask) {
1278        if (mMainFragmentAdapterRegistry == null) {
1279            return;
1280        }
1281        if (rowHolderTask != null) {
1282            startHeadersTransition(false);
1283        }
1284        if (mMainFragmentRowsAdapter != null) {
1285            mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
1286        }
1287    }
1288
1289    @Override
1290    public void onStart() {
1291        super.onStart();
1292        mHeadersFragment.setAlignment(mContainerListAlignTop);
1293        setMainFragmentAlignment();
1294
1295        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
1296            mHeadersFragment.getView().requestFocus();
1297        } else if ((!mCanShowHeaders || !mShowingHeaders)
1298                && mMainFragment.getView() != null) {
1299            mMainFragment.getView().requestFocus();
1300        }
1301
1302        if (mCanShowHeaders) {
1303            showHeaders(mShowingHeaders);
1304        }
1305
1306        if (isEntranceTransitionEnabled()) {
1307            setEntranceTransitionStartState();
1308        }
1309        showTitle(false);
1310    }
1311
1312    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
1313        if (expand) {
1314            callback.run();
1315            return;
1316        }
1317        // Run a "pre" layout when we go non-expand, in order to get the initial
1318        // positions of added rows.
1319        new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute();
1320    }
1321
1322    private void setMainFragmentAlignment() {
1323        int alignOffset = mContainerListAlignTop;
1324        if (mMainFragmentScaleEnabled
1325                && mMainFragmentAdapter.isScalingEnabled()
1326                && mShowingHeaders) {
1327            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
1328        }
1329        mMainFragmentAdapter.setAlignment(alignOffset);
1330    }
1331
1332    /**
1333     * Enables/disables headers transition on back key support. This is enabled by
1334     * default. The BrowseFragment will add a back stack entry when headers are
1335     * showing. Running a headers transition when the back key is pressed only
1336     * works when the headers state is {@link #HEADERS_ENABLED} or
1337     * {@link #HEADERS_HIDDEN}.
1338     * <p>
1339     * NOTE: If an Activity has its own onBackPressed() handling, you must
1340     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
1341     * and {@link BrowseTransitionListener} in your own back stack handling.
1342     */
1343    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
1344        mHeadersBackStackEnabled = headersBackStackEnabled;
1345    }
1346
1347    /**
1348     * Returns true if headers transition on back key support is enabled.
1349     */
1350    public final boolean isHeadersTransitionOnBackEnabled() {
1351        return mHeadersBackStackEnabled;
1352    }
1353
1354    private void readArguments(Bundle args) {
1355        if (args == null) {
1356            return;
1357        }
1358        if (args.containsKey(ARG_TITLE)) {
1359            setTitle(args.getString(ARG_TITLE));
1360        }
1361        if (args.containsKey(ARG_HEADERS_STATE)) {
1362            setHeadersState(args.getInt(ARG_HEADERS_STATE));
1363        }
1364    }
1365
1366    /**
1367     * Sets the state for the headers column in the browse fragment. Must be one
1368     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1369     * {@link #HEADERS_DISABLED}.
1370     *
1371     * @param headersState The state of the headers for the browse fragment.
1372     */
1373    public void setHeadersState(int headersState) {
1374        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1375            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1376        }
1377        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1378
1379        if (headersState != mHeadersState) {
1380            mHeadersState = headersState;
1381            switch (headersState) {
1382                case HEADERS_ENABLED:
1383                    mCanShowHeaders = true;
1384                    mShowingHeaders = true;
1385                    break;
1386                case HEADERS_HIDDEN:
1387                    mCanShowHeaders = true;
1388                    mShowingHeaders = false;
1389                    break;
1390                case HEADERS_DISABLED:
1391                    mCanShowHeaders = false;
1392                    mShowingHeaders = false;
1393                    break;
1394                default:
1395                    Log.w(TAG, "Unknown headers state: " + headersState);
1396                    break;
1397            }
1398            if (mHeadersFragment != null) {
1399                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1400            }
1401        }
1402    }
1403
1404    /**
1405     * Returns the state of the headers column in the browse fragment.
1406     */
1407    public int getHeadersState() {
1408        return mHeadersState;
1409    }
1410
1411    @Override
1412    protected Object createEntranceTransition() {
1413        return TransitionHelper.loadTransition(getActivity(),
1414                R.transition.lb_browse_entrance_transition);
1415    }
1416
1417    @Override
1418    protected void runEntranceTransition(Object entranceTransition) {
1419        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
1420    }
1421
1422    @Override
1423    protected void onEntranceTransitionPrepare() {
1424        mHeadersFragment.onTransitionPrepare();
1425        // setEntranceTransitionStartState() might be called when mMainFragment is null,
1426        // make sure it is called.
1427        mMainFragmentAdapter.setEntranceTransitionState(false);
1428        mMainFragmentAdapter.onTransitionPrepare();
1429    }
1430
1431    @Override
1432    protected void onEntranceTransitionStart() {
1433        mHeadersFragment.onTransitionStart();
1434        mMainFragmentAdapter.onTransitionStart();
1435    }
1436
1437    @Override
1438    protected void onEntranceTransitionEnd() {
1439        if (mMainFragmentAdapter != null) {
1440            mMainFragmentAdapter.onTransitionEnd();
1441        }
1442
1443        if (mHeadersFragment != null) {
1444            mHeadersFragment.onTransitionEnd();
1445        }
1446    }
1447
1448    void setSearchOrbViewOnScreen(boolean onScreen) {
1449        View searchOrbView = getTitleView().getSearchAffordanceView();
1450        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1451        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1452        searchOrbView.setLayoutParams(lp);
1453    }
1454
1455    void setEntranceTransitionStartState() {
1456        setHeadersOnScreen(false);
1457        setSearchOrbViewOnScreen(false);
1458        mMainFragmentAdapter.setEntranceTransitionState(false);
1459    }
1460
1461    void setEntranceTransitionEndState() {
1462        setHeadersOnScreen(mShowingHeaders);
1463        setSearchOrbViewOnScreen(true);
1464        mMainFragmentAdapter.setEntranceTransitionState(true);
1465    }
1466
1467    private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
1468
1469        private final View mView;
1470        private final Runnable mCallback;
1471        private int mState;
1472        private MainFragmentAdapter mainFragmentAdapter;
1473
1474        final static int STATE_INIT = 0;
1475        final static int STATE_FIRST_DRAW = 1;
1476        final static int STATE_SECOND_DRAW = 2;
1477
1478        ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) {
1479            mView = view;
1480            mCallback = callback;
1481            mainFragmentAdapter = adapter;
1482        }
1483
1484        void execute() {
1485            mView.getViewTreeObserver().addOnPreDrawListener(this);
1486            mainFragmentAdapter.setExpand(false);
1487            mState = STATE_INIT;
1488        }
1489
1490        @Override
1491        public boolean onPreDraw() {
1492            if (getView() == null || getActivity() == null) {
1493                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1494                return true;
1495            }
1496            if (mState == STATE_INIT) {
1497                mainFragmentAdapter.setExpand(true);
1498                mState = STATE_FIRST_DRAW;
1499            } else if (mState == STATE_FIRST_DRAW) {
1500                mCallback.run();
1501                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1502                mState = STATE_SECOND_DRAW;
1503            }
1504            return false;
1505        }
1506    }
1507}
1508