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