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