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.support.v17.leanback.R;
17import android.support.v17.leanback.transition.LeanbackTransitionHelper;
18import android.support.v17.leanback.transition.TransitionHelper;
19import android.support.v17.leanback.transition.TransitionListener;
20import android.support.v17.leanback.widget.BrowseFrameLayout;
21import android.support.v17.leanback.widget.HorizontalGridView;
22import android.support.v17.leanback.widget.ItemBridgeAdapter;
23import android.support.v17.leanback.widget.OnItemViewClickedListener;
24import android.support.v17.leanback.widget.OnItemViewSelectedListener;
25import android.support.v17.leanback.widget.Presenter;
26import android.support.v17.leanback.widget.PresenterSelector;
27import android.support.v17.leanback.widget.RowPresenter;
28import android.support.v17.leanback.widget.TitleView;
29import android.support.v17.leanback.widget.VerticalGridView;
30import android.support.v17.leanback.widget.Row;
31import android.support.v17.leanback.widget.ObjectAdapter;
32import android.support.v17.leanback.widget.OnItemSelectedListener;
33import android.support.v17.leanback.widget.OnItemClickedListener;
34import android.support.v17.leanback.widget.SearchOrbView;
35import android.support.v4.view.ViewCompat;
36import android.util.Log;
37import android.app.Activity;
38import android.app.Fragment;
39import android.app.FragmentManager;
40import android.app.FragmentManager.BackStackEntry;
41import android.content.Context;
42import android.content.res.TypedArray;
43import android.os.Bundle;
44import android.view.LayoutInflater;
45import android.view.View;
46import android.view.View.OnClickListener;
47import android.view.ViewGroup;
48import android.view.ViewGroup.MarginLayoutParams;
49import android.view.ViewTreeObserver;
50import android.graphics.Color;
51import android.graphics.Rect;
52import android.graphics.drawable.Drawable;
53
54import static android.support.v7.widget.RecyclerView.NO_POSITION;
55
56/**
57 * A fragment for creating Leanback browse screens. It is composed of a
58 * RowsFragment and a HeadersFragment.
59 * <p>
60 * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
61 * of rows in a vertical list. The elements in this adapter must be subclasses
62 * of {@link Row}.
63 * <p>
64 * The HeadersFragment can be set to be either shown or hidden by default, or
65 * may be disabled entirely. See {@link #setHeadersState} for details.
66 * <p>
67 * By default the BrowseFragment includes support for returning to the headers
68 * when the user presses Back. For Activities that customize {@link
69 * android.app.Activity#onBackPressed()}, you must disable this default Back key support by
70 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
71 * use {@link BrowseFragment.BrowseTransitionListener} and
72 * {@link #startHeadersTransition(boolean)}.
73 */
74public class BrowseFragment extends BaseFragment {
75
76    // BUNDLE attribute for saving header show/hide status when backstack is used:
77    static final String HEADER_STACK_INDEX = "headerStackIndex";
78    // BUNDLE attribute for saving header show/hide status when backstack is not used:
79    static final String HEADER_SHOW = "headerShow";
80    // BUNDLE attribute for title is showing
81    static final String TITLE_SHOW = "titleShow";
82
83    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
84        int mLastEntryCount;
85        int mIndexOfHeadersBackStack;
86
87        BackStackListener() {
88            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
89            mIndexOfHeadersBackStack = -1;
90        }
91
92        void load(Bundle savedInstanceState) {
93            if (savedInstanceState != null) {
94                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
95                mShowingHeaders = mIndexOfHeadersBackStack == -1;
96            } else {
97                if (!mShowingHeaders) {
98                    getFragmentManager().beginTransaction()
99                            .addToBackStack(mWithHeadersBackStackName).commit();
100                }
101            }
102        }
103
104        void save(Bundle outState) {
105            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
106        }
107
108
109        @Override
110        public void onBackStackChanged() {
111            if (getFragmentManager() == null) {
112                Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
113                return;
114            }
115            int count = getFragmentManager().getBackStackEntryCount();
116            // if backstack is growing and last pushed entry is "headers" backstack,
117            // remember the index of the entry.
118            if (count > mLastEntryCount) {
119                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
120                if (mWithHeadersBackStackName.equals(entry.getName())) {
121                    mIndexOfHeadersBackStack = count - 1;
122                }
123            } else if (count < mLastEntryCount) {
124                // if popped "headers" backstack, initiate the show header transition if needed
125                if (mIndexOfHeadersBackStack >= count) {
126                    mIndexOfHeadersBackStack = -1;
127                    if (!mShowingHeaders) {
128                        startHeadersTransitionInternal(true);
129                    }
130                }
131            }
132            mLastEntryCount = count;
133        }
134    }
135
136    /**
137     * Listener for transitions between browse headers and rows.
138     */
139    public static class BrowseTransitionListener {
140        /**
141         * Callback when headers transition starts.
142         *
143         * @param withHeaders True if the transition will result in headers
144         *        being shown, false otherwise.
145         */
146        public void onHeadersTransitionStart(boolean withHeaders) {
147        }
148        /**
149         * Callback when headers transition stops.
150         *
151         * @param withHeaders True if the transition will result in headers
152         *        being shown, false otherwise.
153         */
154        public void onHeadersTransitionStop(boolean withHeaders) {
155        }
156    }
157
158    private static final String TAG = "BrowseFragment";
159
160    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
161
162    private static boolean DEBUG = false;
163
164    /** The headers fragment is enabled and shown by default. */
165    public static final int HEADERS_ENABLED = 1;
166
167    /** The headers fragment is enabled and hidden by default. */
168    public static final int HEADERS_HIDDEN = 2;
169
170    /** The headers fragment is disabled and will never be shown. */
171    public static final int HEADERS_DISABLED = 3;
172
173    private static final float SLIDE_DISTANCE_FACTOR = 2;
174
175    private RowsFragment mRowsFragment;
176    private HeadersFragment mHeadersFragment;
177
178    private ObjectAdapter mAdapter;
179
180    private String mTitle;
181    private Drawable mBadgeDrawable;
182    private int mHeadersState = HEADERS_ENABLED;
183    private int mBrandColor = Color.TRANSPARENT;
184    private boolean mBrandColorSet;
185
186    private BrowseFrameLayout mBrowseFrame;
187    private TitleView mTitleView;
188    private boolean mShowingTitle = true;
189    private boolean mHeadersBackStackEnabled = true;
190    private String mWithHeadersBackStackName;
191    private boolean mShowingHeaders = true;
192    private boolean mCanShowHeaders = true;
193    private int mContainerListMarginStart;
194    private int mContainerListAlignTop;
195    private boolean mRowScaleEnabled = true;
196    private SearchOrbView.Colors mSearchAffordanceColors;
197    private boolean mSearchAffordanceColorSet;
198    private OnItemSelectedListener mExternalOnItemSelectedListener;
199    private OnClickListener mExternalOnSearchClickedListener;
200    private OnItemClickedListener mOnItemClickedListener;
201    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
202    private OnItemViewClickedListener mOnItemViewClickedListener;
203    private int mSelectedPosition = -1;
204
205    private PresenterSelector mHeaderPresenterSelector;
206
207    // transition related:
208    private Object mSceneWithTitle;
209    private Object mSceneWithoutTitle;
210    private Object mSceneWithHeaders;
211    private Object mSceneWithoutHeaders;
212    private Object mSceneAfterEntranceTransition;
213    private Object mTitleUpTransition;
214    private Object mTitleDownTransition;
215    private Object mHeadersTransition;
216    private BackStackListener mBackStackChangedListener;
217    private BrowseTransitionListener mBrowseTransitionListener;
218
219    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
220    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
221    private static final String ARG_HEADERS_STATE =
222        BrowseFragment.class.getCanonicalName() + ".headersState";
223
224    /**
225     * Create arguments for a browse fragment.
226     *
227     * @param args The Bundle to place arguments into, or null if the method
228     *        should return a new Bundle.
229     * @param title The title of the BrowseFragment.
230     * @param headersState The initial state of the headers of the
231     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
232     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
233     * @return A Bundle with the given arguments for creating a BrowseFragment.
234     */
235    public static Bundle createArgs(Bundle args, String title, int headersState) {
236        if (args == null) {
237            args = new Bundle();
238        }
239        args.putString(ARG_TITLE, title);
240        args.putInt(ARG_HEADERS_STATE, headersState);
241        return args;
242    }
243
244    /**
245     * Sets the brand color for the browse fragment. The brand color is used as
246     * the primary color for UI elements in the browse fragment. For example,
247     * the background color of the headers fragment uses the brand color.
248     *
249     * @param color The color to use as the brand color of the fragment.
250     */
251    public void setBrandColor(int color) {
252        mBrandColor = color;
253        mBrandColorSet = true;
254
255        if (mHeadersFragment != null) {
256            mHeadersFragment.setBackgroundColor(mBrandColor);
257        }
258    }
259
260    /**
261     * Returns the brand color for the browse fragment.
262     * The default is transparent.
263     */
264    public int getBrandColor() {
265        return mBrandColor;
266    }
267
268    /**
269     * Sets the adapter containing the rows for the fragment.
270     *
271     * <p>The items referenced by the adapter must be be derived from
272     * {@link Row}. These rows will be used by the rows fragment and the headers
273     * fragment (if not disabled) to render the browse rows.
274     *
275     * @param adapter An ObjectAdapter for the browse rows. All items must
276     *        derive from {@link Row}.
277     */
278    public void setAdapter(ObjectAdapter adapter) {
279        mAdapter = adapter;
280        if (mRowsFragment != null) {
281            mRowsFragment.setAdapter(adapter);
282            mHeadersFragment.setAdapter(adapter);
283        }
284    }
285
286    /**
287     * Returns the adapter containing the rows for the fragment.
288     */
289    public ObjectAdapter getAdapter() {
290        return mAdapter;
291    }
292
293    /**
294     * Sets an item selection listener. This listener will be called when an
295     * item or row is selected by a user.
296     *
297     * @param listener The listener to call when an item or row is selected.
298     * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
299     */
300    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
301        mExternalOnItemSelectedListener = listener;
302    }
303
304    /**
305     * Sets an item selection listener.
306     */
307    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
308        mExternalOnItemViewSelectedListener = listener;
309    }
310
311    /**
312     * Returns an item selection listener.
313     */
314    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
315        return mExternalOnItemViewSelectedListener;
316    }
317
318    /**
319     * Sets an item clicked listener on the fragment.
320     *
321     * <p>OnItemClickedListener will override {@link View.OnClickListener} that
322     * an item presenter may set during
323     * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you
324     * should choose to use an {@link OnItemClickedListener} or a
325     * {@link View.OnClickListener} on your item views, but not both.
326     *
327     * @param listener The listener to call when an item is clicked.
328     * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
329     */
330    public void setOnItemClickedListener(OnItemClickedListener listener) {
331        mOnItemClickedListener = listener;
332        if (mRowsFragment != null) {
333            mRowsFragment.setOnItemClickedListener(listener);
334        }
335    }
336
337    /**
338     * Returns the item clicked listener.
339     * @deprecated Use {@link #getOnItemViewClickedListener()}
340     */
341    public OnItemClickedListener getOnItemClickedListener() {
342        return mOnItemClickedListener;
343    }
344
345    /**
346     * Sets an item clicked listener on the fragment.
347     * OnItemViewClickedListener will override {@link View.OnClickListener} that
348     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
349     * So in general,  developer should choose one of the listeners but not both.
350     */
351    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
352        mOnItemViewClickedListener = listener;
353        if (mRowsFragment != null) {
354            mRowsFragment.setOnItemViewClickedListener(listener);
355        }
356    }
357
358    /**
359     * Returns the item Clicked listener.
360     */
361    public OnItemViewClickedListener getOnItemViewClickedListener() {
362        return mOnItemViewClickedListener;
363    }
364
365    /**
366     * Sets a click listener for the search affordance.
367     *
368     * <p>The presence of a listener will change the visibility of the search
369     * affordance in the fragment title. When set to non-null, the title will
370     * contain an element that a user may click to begin a search.
371     *
372     * <p>The listener's {@link View.OnClickListener#onClick onClick} method
373     * will be invoked when the user clicks on the search element.
374     *
375     * @param listener The listener to call when the search element is clicked.
376     */
377    public void setOnSearchClickedListener(View.OnClickListener listener) {
378        mExternalOnSearchClickedListener = listener;
379        if (mTitleView != null) {
380            mTitleView.setOnSearchClickedListener(listener);
381        }
382    }
383
384    /**
385     * Sets the {@link SearchOrbView.Colors} used to draw the search affordance.
386     */
387    public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
388        mSearchAffordanceColors = colors;
389        mSearchAffordanceColorSet = true;
390        if (mTitleView != null) {
391            mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
392        }
393    }
394
395    /**
396     * Returns the {@link SearchOrbView.Colors} used to draw the search affordance.
397     */
398    public SearchOrbView.Colors getSearchAffordanceColors() {
399        if (mSearchAffordanceColorSet) {
400            return mSearchAffordanceColors;
401        }
402        if (mTitleView == null) {
403            throw new IllegalStateException("Fragment views not yet created");
404        }
405        return mTitleView.getSearchAffordanceColors();
406    }
407
408    /**
409     * Sets the color used to draw the search affordance.
410     * A default brighter color will be set by the framework.
411     *
412     * @param color The color to use for the search affordance.
413     */
414    public void setSearchAffordanceColor(int color) {
415        setSearchAffordanceColors(new SearchOrbView.Colors(color));
416    }
417
418    /**
419     * Returns the color used to draw the search affordance.
420     */
421    public int getSearchAffordanceColor() {
422        return getSearchAffordanceColors().color;
423    }
424
425    /**
426     * Start a headers transition.
427     *
428     * <p>This method will begin a transition to either show or hide the
429     * headers, depending on the value of withHeaders. If headers are disabled
430     * for this browse fragment, this method will throw an exception.
431     *
432     * @param withHeaders True if the headers should transition to being shown,
433     *        false if the transition should result in headers being hidden.
434     */
435    public void startHeadersTransition(boolean withHeaders) {
436        if (!mCanShowHeaders) {
437            throw new IllegalStateException("Cannot start headers transition");
438        }
439        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
440            return;
441        }
442        startHeadersTransitionInternal(withHeaders);
443    }
444
445    /**
446     * Returns true if the headers transition is currently running.
447     */
448    public boolean isInHeadersTransition() {
449        return mHeadersTransition != null;
450    }
451
452    /**
453     * Returns true if headers are shown.
454     */
455    public boolean isShowingHeaders() {
456        return mShowingHeaders;
457    }
458
459    /**
460     * Set a listener for browse fragment transitions.
461     *
462     * @param listener The listener to call when a browse headers transition
463     *        begins or ends.
464     */
465    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
466        mBrowseTransitionListener = listener;
467    }
468
469    /**
470     * Enables scaling of rows when headers are present.
471     * By default enabled to increase density.
472     *
473     * @param enable true to enable row scaling
474     */
475    public void enableRowScaling(boolean enable) {
476        mRowScaleEnabled = enable;
477        if (mRowsFragment != null) {
478            mRowsFragment.enableRowScaling(mRowScaleEnabled);
479        }
480    }
481
482    private void startHeadersTransitionInternal(final boolean withHeaders) {
483        if (getFragmentManager().isDestroyed()) {
484            return;
485        }
486        mShowingHeaders = withHeaders;
487        mRowsFragment.onExpandTransitionStart(!withHeaders, new Runnable() {
488            @Override
489            public void run() {
490                mHeadersFragment.onTransitionStart();
491                createHeadersTransition();
492                if (mBrowseTransitionListener != null) {
493                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
494                }
495                sTransitionHelper.runTransition(withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders,
496                        mHeadersTransition);
497                if (mHeadersBackStackEnabled) {
498                    if (!withHeaders) {
499                        getFragmentManager().beginTransaction()
500                                .addToBackStack(mWithHeadersBackStackName).commit();
501                    } else {
502                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
503                        if (index >= 0) {
504                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
505                            getFragmentManager().popBackStackImmediate(entry.getId(),
506                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
507                        }
508                    }
509                }
510            }
511        });
512    }
513
514    private boolean isVerticalScrolling() {
515        // don't run transition
516        return mHeadersFragment.getVerticalGridView().getScrollState()
517                != HorizontalGridView.SCROLL_STATE_IDLE
518                || mRowsFragment.getVerticalGridView().getScrollState()
519                != HorizontalGridView.SCROLL_STATE_IDLE;
520    }
521
522    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
523            new BrowseFrameLayout.OnFocusSearchListener() {
524        @Override
525        public View onFocusSearch(View focused, int direction) {
526            // if headers is running transition,  focus stays
527            if (mCanShowHeaders && isInHeadersTransition()) {
528                return focused;
529            }
530            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
531
532            final View searchOrbView = mTitleView.getSearchAffordanceView();
533            if (focused == searchOrbView && direction == View.FOCUS_DOWN) {
534                return mCanShowHeaders && mShowingHeaders ?
535                        mHeadersFragment.getVerticalGridView() :
536                        mRowsFragment.getVerticalGridView();
537            } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
538                    && direction == View.FOCUS_UP) {
539                return searchOrbView;
540            }
541
542            // If headers fragment is disabled, just return null.
543            if (!mCanShowHeaders) {
544                return null;
545            }
546            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
547            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
548            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
549            if (direction == towardStart) {
550                if (isVerticalScrolling() || mShowingHeaders) {
551                    return focused;
552                }
553                return mHeadersFragment.getVerticalGridView();
554            } else if (direction == towardEnd) {
555                if (isVerticalScrolling() || !mShowingHeaders) {
556                    return focused;
557                }
558                return mRowsFragment.getVerticalGridView();
559            } else {
560                return null;
561            }
562        }
563    };
564
565    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
566            new BrowseFrameLayout.OnChildFocusListener() {
567
568        @Override
569        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
570            if (getChildFragmentManager().isDestroyed()) {
571                return true;
572            }
573            // Make sure not changing focus when requestFocus() is called.
574            if (mCanShowHeaders && mShowingHeaders) {
575                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
576                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
577                    return true;
578                }
579            }
580            if (mRowsFragment != null && mRowsFragment.getView() != null &&
581                    mRowsFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
582                return true;
583            }
584            if (mTitleView != null &&
585                    mTitleView.requestFocus(direction, previouslyFocusedRect)) {
586                return true;
587            }
588            return false;
589        };
590
591        @Override
592        public void onRequestChildFocus(View child, View focused) {
593            if (getChildFragmentManager().isDestroyed()) {
594                return;
595            }
596            if (!mCanShowHeaders || isInHeadersTransition()) return;
597            int childId = child.getId();
598            if (childId == R.id.browse_container_dock && mShowingHeaders) {
599                startHeadersTransitionInternal(false);
600            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
601                startHeadersTransitionInternal(true);
602            }
603        }
604    };
605
606    @Override
607    public void onSaveInstanceState(Bundle outState) {
608        if (mBackStackChangedListener != null) {
609            mBackStackChangedListener.save(outState);
610        } else {
611            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
612        }
613        outState.putBoolean(TITLE_SHOW, mShowingTitle);
614    }
615
616    @Override
617    public void onCreate(Bundle savedInstanceState) {
618        super.onCreate(savedInstanceState);
619        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
620        mContainerListMarginStart = (int) ta.getDimension(
621                R.styleable.LeanbackTheme_browseRowsMarginStart, 0);
622        mContainerListAlignTop = (int) ta.getDimension(
623                R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
624        ta.recycle();
625
626        readArguments(getArguments());
627
628        if (mCanShowHeaders) {
629            if (mHeadersBackStackEnabled) {
630                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
631                mBackStackChangedListener = new BackStackListener();
632                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
633                mBackStackChangedListener.load(savedInstanceState);
634            } else {
635                if (savedInstanceState != null) {
636                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
637                }
638            }
639        }
640
641    }
642
643    @Override
644    public void onDestroy() {
645        if (mBackStackChangedListener != null) {
646            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
647        }
648        super.onDestroy();
649    }
650
651    @Override
652    public View onCreateView(LayoutInflater inflater, ViewGroup container,
653            Bundle savedInstanceState) {
654        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
655            mRowsFragment = new RowsFragment();
656            mHeadersFragment = new HeadersFragment();
657            getChildFragmentManager().beginTransaction()
658                    .replace(R.id.browse_headers_dock, mHeadersFragment)
659                    .replace(R.id.browse_container_dock, mRowsFragment).commit();
660        } else {
661            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
662                    .findFragmentById(R.id.browse_headers_dock);
663            mRowsFragment = (RowsFragment) getChildFragmentManager()
664                    .findFragmentById(R.id.browse_container_dock);
665        }
666
667        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
668
669        mRowsFragment.setAdapter(mAdapter);
670        if (mHeaderPresenterSelector != null) {
671            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
672        }
673        mHeadersFragment.setAdapter(mAdapter);
674
675        mRowsFragment.enableRowScaling(mRowScaleEnabled);
676        mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
677        mRowsFragment.setOnItemViewSelectedListener(mRowViewSelectedListener);
678        mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
679        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
680        mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
681        mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
682
683        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
684
685        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
686        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
687        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
688
689        mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
690        mTitleView.setTitle(mTitle);
691        mTitleView.setBadgeDrawable(mBadgeDrawable);
692        if (mSearchAffordanceColorSet) {
693            mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
694        }
695        if (mExternalOnSearchClickedListener != null) {
696            mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener);
697        }
698
699        if (mBrandColorSet) {
700            mHeadersFragment.setBackgroundColor(mBrandColor);
701        }
702
703        mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
704            @Override
705            public void run() {
706                mTitleView.setVisibility(View.VISIBLE);
707            }
708        });
709        mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
710            @Override
711            public void run() {
712                mTitleView.setVisibility(View.INVISIBLE);
713            }
714        });
715        mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
716            @Override
717            public void run() {
718                showHeaders(true);
719            }
720        });
721        mSceneWithoutHeaders =  sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
722            @Override
723            public void run() {
724                showHeaders(false);
725            }
726        });
727        mSceneAfterEntranceTransition = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
728            @Override
729            public void run() {
730                setEntranceTransitionEndState();
731            }
732        });
733        Context context = getActivity();
734        mTitleUpTransition = LeanbackTransitionHelper.loadTitleOutTransition(context,
735                sTransitionHelper);
736        mTitleDownTransition = LeanbackTransitionHelper.loadTitleInTransition(context,
737                sTransitionHelper);
738
739        if (savedInstanceState != null) {
740            mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);
741        }
742        mTitleView.setVisibility(mShowingTitle ? View.VISIBLE: View.INVISIBLE);
743
744        return root;
745    }
746
747    private void createHeadersTransition() {
748        mHeadersTransition = sTransitionHelper.loadTransition(getActivity(),
749                mShowingHeaders ?
750                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
751
752        sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() {
753            @Override
754            public void onTransitionStart(Object transition) {
755            }
756            @Override
757            public void onTransitionEnd(Object transition) {
758                mHeadersTransition = null;
759                mRowsFragment.onTransitionEnd();
760                mHeadersFragment.onTransitionEnd();
761                if (mShowingHeaders) {
762                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
763                    if (headerGridView != null && !headerGridView.hasFocus()) {
764                        headerGridView.requestFocus();
765                    }
766                } else {
767                    VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView();
768                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
769                        rowsGridView.requestFocus();
770                    }
771                }
772                if (mBrowseTransitionListener != null) {
773                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
774                }
775            }
776        });
777    }
778
779    /**
780     * Sets the {@link PresenterSelector} used to render the row headers.
781     *
782     * @param headerPresenterSelector The PresenterSelector that will determine
783     *        the Presenter for each row header.
784     */
785    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
786        mHeaderPresenterSelector = headerPresenterSelector;
787        if (mHeadersFragment != null) {
788            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
789        }
790    }
791
792    private void setRowsAlignedLeft(boolean alignLeft) {
793        MarginLayoutParams lp;
794        View containerList;
795        containerList = mRowsFragment.getView();
796        lp = (MarginLayoutParams) containerList.getLayoutParams();
797        lp.setMarginStart(alignLeft ? 0 : mContainerListMarginStart);
798        containerList.setLayoutParams(lp);
799    }
800
801    private void setHeadersOnScreen(boolean onScreen) {
802        MarginLayoutParams lp;
803        View containerList;
804        containerList = mHeadersFragment.getView();
805        lp = (MarginLayoutParams) containerList.getLayoutParams();
806        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
807        containerList.setLayoutParams(lp);
808    }
809
810    private void showHeaders(boolean show) {
811        if (DEBUG) Log.v(TAG, "showHeaders " + show);
812        mHeadersFragment.setHeadersEnabled(show);
813        setHeadersOnScreen(show);
814        setRowsAlignedLeft(!show);
815        mRowsFragment.setExpand(!show);
816    }
817
818    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
819        new HeadersFragment.OnHeaderClickedListener() {
820            @Override
821            public void onHeaderClicked() {
822                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
823                    return;
824                }
825                startHeadersTransitionInternal(false);
826                mRowsFragment.getVerticalGridView().requestFocus();
827            }
828        };
829
830    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
831        @Override
832        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
833                RowPresenter.ViewHolder rowViewHolder, Row row) {
834            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
835            if (DEBUG) Log.v(TAG, "row selected position " + position);
836            onRowSelected(position);
837            if (mExternalOnItemViewSelectedListener != null) {
838                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
839                        rowViewHolder, row);
840            }
841        }
842    };
843
844    private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
845        @Override
846        public void onItemSelected(Object item, Row row) {
847            if (mExternalOnItemSelectedListener != null) {
848                mExternalOnItemSelectedListener.onItemSelected(item, row);
849            }
850        }
851    };
852
853    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
854        @Override
855        public void onItemSelected(Object item, Row row) {
856            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
857            if (DEBUG) Log.v(TAG, "header selected position " + position);
858            onRowSelected(position);
859        }
860    };
861
862    private void onRowSelected(int position) {
863        if (position != mSelectedPosition) {
864            mSetSelectionRunnable.mPosition = position;
865            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
866
867            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
868                if (!mShowingTitle) {
869                    sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
870                    mShowingTitle = true;
871                }
872            } else if (mShowingTitle) {
873                sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
874                mShowingTitle = false;
875            }
876        }
877    }
878
879    private class SetSelectionRunnable implements Runnable {
880        int mPosition;
881        boolean mSmooth = true;
882        @Override
883        public void run() {
884            setSelection(mPosition, mSmooth);
885        }
886    }
887
888    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
889
890    private void setSelection(int position, boolean smooth) {
891        if (position != NO_POSITION) {
892            mRowsFragment.setSelectedPosition(position, smooth);
893            mHeadersFragment.setSelectedPosition(position, smooth);
894        }
895        mSelectedPosition = position;
896    }
897
898    /**
899     * Sets the selected row position with smooth animation.
900     */
901    public void setSelectedPosition(int position) {
902        setSelectedPosition(position, true);
903    }
904
905    /**
906     * Sets the selected row position.
907     */
908    public void setSelectedPosition(int position, boolean smooth) {
909        mSetSelectionRunnable.mPosition = position;
910        mSetSelectionRunnable.mSmooth = smooth;
911        mBrowseFrame.getHandler().post(mSetSelectionRunnable);
912    }
913
914    @Override
915    public void onStart() {
916        super.onStart();
917        mHeadersFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
918        mHeadersFragment.setItemAlignment();
919        mRowsFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
920        mRowsFragment.setItemAlignment();
921
922        mRowsFragment.setScalePivots(0, mContainerListAlignTop);
923
924        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
925            mHeadersFragment.getView().requestFocus();
926        } else if ((!mCanShowHeaders || !mShowingHeaders)
927                && mRowsFragment.getView() != null) {
928            mRowsFragment.getView().requestFocus();
929        }
930        if (mCanShowHeaders) {
931            showHeaders(mShowingHeaders);
932        }
933        if (isEntranceTransitionEnabled()) {
934            setEntranceTransitionStartState();
935        }
936    }
937
938    @Override
939    public void onPause() {
940        mTitleView.enableAnimation(false);
941        super.onPause();
942    }
943
944    @Override
945    public void onResume() {
946        super.onResume();
947        mTitleView.enableAnimation(true);
948    }
949
950    /**
951     * Enable/disable headers transition on back key support. This is enabled by
952     * default. The BrowseFragment will add a back stack entry when headers are
953     * showing. Running a headers transition when the back key is pressed only
954     * works when the headers state is {@link #HEADERS_ENABLED} or
955     * {@link #HEADERS_HIDDEN}.
956     * <p>
957     * NOTE: If an Activity has its own onBackPressed() handling, you must
958     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
959     * and {@link BrowseTransitionListener} in your own back stack handling.
960     */
961    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
962        mHeadersBackStackEnabled = headersBackStackEnabled;
963    }
964
965    /**
966     * Returns true if headers transition on back key support is enabled.
967     */
968    public final boolean isHeadersTransitionOnBackEnabled() {
969        return mHeadersBackStackEnabled;
970    }
971
972    private void readArguments(Bundle args) {
973        if (args == null) {
974            return;
975        }
976        if (args.containsKey(ARG_TITLE)) {
977            setTitle(args.getString(ARG_TITLE));
978        }
979        if (args.containsKey(ARG_HEADERS_STATE)) {
980            setHeadersState(args.getInt(ARG_HEADERS_STATE));
981        }
982    }
983
984    /**
985     * Sets the drawable displayed in the browse fragment title.
986     *
987     * @param drawable The Drawable to display in the browse fragment title.
988     */
989    public void setBadgeDrawable(Drawable drawable) {
990        if (mBadgeDrawable != drawable) {
991            mBadgeDrawable = drawable;
992            if (mTitleView != null) {
993                mTitleView.setBadgeDrawable(drawable);
994            }
995        }
996    }
997
998    /**
999     * Returns the badge drawable used in the fragment title.
1000     */
1001    public Drawable getBadgeDrawable() {
1002        return mBadgeDrawable;
1003    }
1004
1005    /**
1006     * Sets a title for the browse fragment.
1007     *
1008     * @param title The title of the browse fragment.
1009     */
1010    public void setTitle(String title) {
1011        mTitle = title;
1012        if (mTitleView != null) {
1013            mTitleView.setTitle(title);
1014        }
1015    }
1016
1017    /**
1018     * Returns the title for the browse fragment.
1019     */
1020    public String getTitle() {
1021        return mTitle;
1022    }
1023
1024    /**
1025     * Sets the state for the headers column in the browse fragment. Must be one
1026     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1027     * {@link #HEADERS_DISABLED}.
1028     *
1029     * @param headersState The state of the headers for the browse fragment.
1030     */
1031    public void setHeadersState(int headersState) {
1032        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1033            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1034        }
1035        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1036
1037        if (headersState != mHeadersState) {
1038            mHeadersState = headersState;
1039            switch (headersState) {
1040                case HEADERS_ENABLED:
1041                    mCanShowHeaders = true;
1042                    mShowingHeaders = true;
1043                    break;
1044                case HEADERS_HIDDEN:
1045                    mCanShowHeaders = true;
1046                    mShowingHeaders = false;
1047                    break;
1048                case HEADERS_DISABLED:
1049                    mCanShowHeaders = false;
1050                    mShowingHeaders = false;
1051                    break;
1052                default:
1053                    Log.w(TAG, "Unknown headers state: " + headersState);
1054                    break;
1055            }
1056            if (mHeadersFragment != null) {
1057                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1058            }
1059        }
1060    }
1061
1062    /**
1063     * Returns the state of the headers column in the browse fragment.
1064     */
1065    public int getHeadersState() {
1066        return mHeadersState;
1067    }
1068
1069    @Override
1070    protected Object createEntranceTransition() {
1071        return sTransitionHelper.loadTransition(getActivity(),
1072                R.transition.lb_browse_entrance_transition);
1073    }
1074
1075    @Override
1076    protected void runEntranceTransition(Object entranceTransition) {
1077        sTransitionHelper.runTransition(mSceneAfterEntranceTransition,
1078                entranceTransition);
1079    }
1080
1081    @Override
1082    protected void onEntranceTransitionStart() {
1083        mHeadersFragment.onTransitionStart();
1084        mRowsFragment.onTransitionStart();
1085    }
1086
1087    @Override
1088    protected void onEntranceTransitionEnd() {
1089        mRowsFragment.onTransitionEnd();
1090        mHeadersFragment.onTransitionEnd();
1091    }
1092
1093    void setSearchOrbViewOnScreen(boolean onScreen) {
1094        View searchOrbView = mTitleView.getSearchAffordanceView();
1095        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1096        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1097        searchOrbView.setLayoutParams(lp);
1098    }
1099
1100    void setEntranceTransitionStartState() {
1101        setHeadersOnScreen(false);
1102        setSearchOrbViewOnScreen(false);
1103        mRowsFragment.setEntranceTransitionState(false);
1104    }
1105
1106    void setEntranceTransitionEndState() {
1107        setHeadersOnScreen(mShowingHeaders);
1108        setSearchOrbViewOnScreen(true);
1109        mRowsFragment.setEntranceTransitionState(true);
1110    }
1111
1112}
1113
1114