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