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