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