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