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