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