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