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