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