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