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