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