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