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