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