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