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