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