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