BrowseSupportFragment.java revision 5ae73b45620a3d535b2d279512d5d34603e2bdb1
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.support.v4.view.ViewCompat;
37import android.util.Log;
38import android.support.v4.app.FragmentActivity;
39import android.support.v4.app.Fragment;
40import android.support.v4.app.FragmentManager;
41import android.support.v4.app.FragmentManager.BackStackEntry;
42import android.content.Context;
43import android.content.res.TypedArray;
44import android.os.Bundle;
45import android.view.LayoutInflater;
46import android.view.View;
47import android.view.View.OnClickListener;
48import android.view.ViewGroup;
49import android.view.ViewGroup.MarginLayoutParams;
50import android.view.ViewTreeObserver;
51import android.graphics.Color;
52import android.graphics.Rect;
53import android.graphics.drawable.Drawable;
54
55import static android.support.v7.widget.RecyclerView.NO_POSITION;
56
57/**
58 * A fragment for creating Leanback browse screens. It is composed of a
59 * RowsSupportFragment and a HeadersSupportFragment.
60 * <p>
61 * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set
62 * of rows in a vertical list. The elements in this adapter must be subclasses
63 * of {@link Row}.
64 * <p>
65 * The HeadersSupportFragment can be set to be either shown or hidden by default, or
66 * may be disabled entirely. See {@link #setHeadersState} for details.
67 * <p>
68 * By default the BrowseSupportFragment includes support for returning to the headers
69 * when the user presses Back. For Activities that customize {@link
70 * android.support.v4.app.FragmentActivity#onBackPressed()}, you must disable this default Back key support by
71 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
72 * use {@link BrowseSupportFragment.BrowseTransitionListener} and
73 * {@link #startHeadersTransition(boolean)}.
74 */
75public class BrowseSupportFragment extends BaseSupportFragment {
76
77    // BUNDLE attribute for saving header show/hide status when backstack is used:
78    static final String HEADER_STACK_INDEX = "headerStackIndex";
79    // BUNDLE attribute for saving header show/hide status when backstack is not used:
80    static final String HEADER_SHOW = "headerShow";
81    // BUNDLE attribute for title is showing
82    static final String TITLE_SHOW = "titleShow";
83
84    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
85        int mLastEntryCount;
86        int mIndexOfHeadersBackStack;
87
88        BackStackListener() {
89            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
90            mIndexOfHeadersBackStack = -1;
91        }
92
93        void load(Bundle savedInstanceState) {
94            if (savedInstanceState != null) {
95                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
96                mShowingHeaders = mIndexOfHeadersBackStack == -1;
97            } else {
98                if (!mShowingHeaders) {
99                    getFragmentManager().beginTransaction()
100                            .addToBackStack(mWithHeadersBackStackName).commit();
101                }
102            }
103        }
104
105        void save(Bundle outState) {
106            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
107        }
108
109
110        @Override
111        public void onBackStackChanged() {
112            if (getFragmentManager() == null) {
113                Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
114                return;
115            }
116            int count = getFragmentManager().getBackStackEntryCount();
117            // if backstack is growing and last pushed entry is "headers" backstack,
118            // remember the index of the entry.
119            if (count > mLastEntryCount) {
120                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
121                if (mWithHeadersBackStackName.equals(entry.getName())) {
122                    mIndexOfHeadersBackStack = count - 1;
123                }
124            } else if (count < mLastEntryCount) {
125                // if popped "headers" backstack, initiate the show header transition if needed
126                if (mIndexOfHeadersBackStack >= count) {
127                    mIndexOfHeadersBackStack = -1;
128                    if (!mShowingHeaders) {
129                        startHeadersTransitionInternal(true);
130                    }
131                }
132            }
133            mLastEntryCount = count;
134        }
135    }
136
137    /**
138     * Listener for transitions between browse headers and rows.
139     */
140    public static class BrowseTransitionListener {
141        /**
142         * Callback when headers transition starts.
143         *
144         * @param withHeaders True if the transition will result in headers
145         *        being shown, false otherwise.
146         */
147        public void onHeadersTransitionStart(boolean withHeaders) {
148        }
149        /**
150         * Callback when headers transition stops.
151         *
152         * @param withHeaders True if the transition will result in headers
153         *        being shown, false otherwise.
154         */
155        public void onHeadersTransitionStop(boolean withHeaders) {
156        }
157    }
158
159    private static final String TAG = "BrowseSupportFragment";
160
161    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
162
163    private static boolean DEBUG = false;
164
165    /** The headers fragment is enabled and shown by default. */
166    public static final int HEADERS_ENABLED = 1;
167
168    /** The headers fragment is enabled and hidden by default. */
169    public static final int HEADERS_HIDDEN = 2;
170
171    /** The headers fragment is disabled and will never be shown. */
172    public static final int HEADERS_DISABLED = 3;
173
174    private static final float SLIDE_DISTANCE_FACTOR = 2;
175
176    private RowsSupportFragment mRowsSupportFragment;
177    private HeadersSupportFragment mHeadersSupportFragment;
178
179    private ObjectAdapter mAdapter;
180
181    private String mTitle;
182    private Drawable mBadgeDrawable;
183    private int mHeadersState = HEADERS_ENABLED;
184    private int mBrandColor = Color.TRANSPARENT;
185    private boolean mBrandColorSet;
186
187    private BrowseFrameLayout mBrowseFrame;
188    private TitleView mTitleView;
189    private boolean mShowingTitle = true;
190    private boolean mHeadersBackStackEnabled = true;
191    private String mWithHeadersBackStackName;
192    private boolean mShowingHeaders = true;
193    private boolean mCanShowHeaders = true;
194    private int mContainerListMarginStart;
195    private int mContainerListAlignTop;
196    private boolean mRowScaleEnabled = true;
197    private SearchOrbView.Colors mSearchAffordanceColors;
198    private boolean mSearchAffordanceColorSet;
199    private OnItemSelectedListener mExternalOnItemSelectedListener;
200    private OnClickListener mExternalOnSearchClickedListener;
201    private OnItemClickedListener mOnItemClickedListener;
202    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
203    private OnItemViewClickedListener mOnItemViewClickedListener;
204    private int mSelectedPosition = -1;
205
206    private PresenterSelector mHeaderPresenterSelector;
207
208    // transition related:
209    private Object mSceneWithTitle;
210    private Object mSceneWithoutTitle;
211    private Object mSceneWithHeaders;
212    private Object mSceneWithoutHeaders;
213    private Object mSceneAfterEntranceTransition;
214    private Object mTitleUpTransition;
215    private Object mTitleDownTransition;
216    private Object mHeadersTransition;
217    private BackStackListener mBackStackChangedListener;
218    private BrowseTransitionListener mBrowseTransitionListener;
219
220    private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title";
221    private static final String ARG_BADGE_URI = BrowseSupportFragment.class.getCanonicalName() + ".badge";
222    private static final String ARG_HEADERS_STATE =
223        BrowseSupportFragment.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 BrowseSupportFragment.
231     * @param headersState The initial state of the headers of the
232     *        BrowseSupportFragment. 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 BrowseSupportFragment.
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 (mHeadersSupportFragment != null) {
257            mHeadersSupportFragment.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 (mRowsSupportFragment != null) {
282            mRowsSupportFragment.setAdapter(adapter);
283            mHeadersSupportFragment.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 (mRowsSupportFragment != null) {
334            mRowsSupportFragment.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 (mRowsSupportFragment != null) {
355            mRowsSupportFragment.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 (mRowsSupportFragment != null) {
479            mRowsSupportFragment.enableRowScaling(mRowScaleEnabled);
480        }
481    }
482
483    private void startHeadersTransitionInternal(final boolean withHeaders) {
484        if (getFragmentManager().isDestroyed()) {
485            return;
486        }
487        mShowingHeaders = withHeaders;
488        mRowsSupportFragment.onExpandTransitionStart(!withHeaders, new Runnable() {
489            @Override
490            public void run() {
491                mHeadersSupportFragment.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 mHeadersSupportFragment.getVerticalGridView().getScrollState()
518                != HorizontalGridView.SCROLL_STATE_IDLE
519                || mRowsSupportFragment.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 is running transition,  focus stays
528            if (mCanShowHeaders && isInHeadersTransition()) {
529                return focused;
530            }
531            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
532
533            final View searchOrbView = mTitleView.getSearchAffordanceView();
534            if (focused == searchOrbView && direction == View.FOCUS_DOWN) {
535                return mCanShowHeaders && mShowingHeaders ?
536                        mHeadersSupportFragment.getVerticalGridView() :
537                        mRowsSupportFragment.getVerticalGridView();
538            } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
539                    && direction == View.FOCUS_UP) {
540                return searchOrbView;
541            }
542
543            // If headers fragment is disabled, just return null.
544            if (!mCanShowHeaders) {
545                return null;
546            }
547            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
548            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
549            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
550            if (direction == towardStart) {
551                if (isVerticalScrolling() || mShowingHeaders) {
552                    return focused;
553                }
554                return mHeadersSupportFragment.getVerticalGridView();
555            } else if (direction == towardEnd) {
556                if (isVerticalScrolling() || !mShowingHeaders) {
557                    return focused;
558                }
559                return mRowsSupportFragment.getVerticalGridView();
560            } else {
561                return null;
562            }
563        }
564    };
565
566    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
567            new BrowseFrameLayout.OnChildFocusListener() {
568
569        @Override
570        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
571            if (getChildFragmentManager().isDestroyed()) {
572                return true;
573            }
574            // Make sure not changing focus when requestFocus() is called.
575            if (mCanShowHeaders && mShowingHeaders) {
576                if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null &&
577                        mHeadersSupportFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
578                    return true;
579                }
580            }
581            if (mRowsSupportFragment != null && mRowsSupportFragment.getView() != null &&
582                    mRowsSupportFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
583                return true;
584            }
585            if (mTitleView != null &&
586                    mTitleView.requestFocus(direction, previouslyFocusedRect)) {
587                return true;
588            }
589            return false;
590        };
591
592        @Override
593        public void onRequestChildFocus(View child, View focused) {
594            if (getChildFragmentManager().isDestroyed()) {
595                return;
596            }
597            if (!mCanShowHeaders || isInHeadersTransition()) return;
598            int childId = child.getId();
599            if (childId == R.id.browse_container_dock && mShowingHeaders) {
600                startHeadersTransitionInternal(false);
601            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
602                startHeadersTransitionInternal(true);
603            }
604        }
605    };
606
607    @Override
608    public void onSaveInstanceState(Bundle outState) {
609        if (mBackStackChangedListener != null) {
610            mBackStackChangedListener.save(outState);
611        } else {
612            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
613        }
614        outState.putBoolean(TITLE_SHOW, mShowingTitle);
615    }
616
617    @Override
618    public void onCreate(Bundle savedInstanceState) {
619        super.onCreate(savedInstanceState);
620        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
621        mContainerListMarginStart = (int) ta.getDimension(
622                R.styleable.LeanbackTheme_browseRowsMarginStart, 0);
623        mContainerListAlignTop = (int) ta.getDimension(
624                R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
625        ta.recycle();
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            mRowsSupportFragment = new RowsSupportFragment();
657            mHeadersSupportFragment = new HeadersSupportFragment();
658            getChildFragmentManager().beginTransaction()
659                    .replace(R.id.browse_headers_dock, mHeadersSupportFragment)
660                    .replace(R.id.browse_container_dock, mRowsSupportFragment).commit();
661        } else {
662            mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager()
663                    .findFragmentById(R.id.browse_headers_dock);
664            mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager()
665                    .findFragmentById(R.id.browse_container_dock);
666        }
667
668        mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
669
670        mRowsSupportFragment.setAdapter(mAdapter);
671        if (mHeaderPresenterSelector != null) {
672            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
673        }
674        mHeadersSupportFragment.setAdapter(mAdapter);
675
676        mRowsSupportFragment.enableRowScaling(mRowScaleEnabled);
677        mRowsSupportFragment.setOnItemSelectedListener(mRowSelectedListener);
678        mRowsSupportFragment.setOnItemViewSelectedListener(mRowViewSelectedListener);
679        mHeadersSupportFragment.setOnItemSelectedListener(mHeaderSelectedListener);
680        mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener);
681        mRowsSupportFragment.setOnItemClickedListener(mOnItemClickedListener);
682        mRowsSupportFragment.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            mHeadersSupportFragment.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        Context context = getActivity();
735        mTitleUpTransition = sTransitionHelper.loadTransition(context, R.transition.lb_title_out);
736        mTitleDownTransition = sTransitionHelper.loadTransition(context, R.transition.lb_title_in);
737
738        if (savedInstanceState != null) {
739            mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW);
740        }
741        mTitleView.setVisibility(mShowingTitle ? View.VISIBLE: View.INVISIBLE);
742
743        return root;
744    }
745
746    private void createHeadersTransition() {
747        mHeadersTransition = sTransitionHelper.loadTransition(getActivity(),
748                mShowingHeaders ?
749                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
750
751        sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() {
752            @Override
753            public void onTransitionStart(Object transition) {
754            }
755            @Override
756            public void onTransitionEnd(Object transition) {
757                mHeadersTransition = null;
758                mRowsSupportFragment.onTransitionEnd();
759                mHeadersSupportFragment.onTransitionEnd();
760                if (mShowingHeaders) {
761                    VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView();
762                    if (headerGridView != null && !headerGridView.hasFocus()) {
763                        headerGridView.requestFocus();
764                    }
765                } else {
766                    VerticalGridView rowsGridView = mRowsSupportFragment.getVerticalGridView();
767                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
768                        rowsGridView.requestFocus();
769                    }
770                }
771                if (mBrowseTransitionListener != null) {
772                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
773                }
774            }
775        });
776    }
777
778    /**
779     * Sets the {@link PresenterSelector} used to render the row headers.
780     *
781     * @param headerPresenterSelector The PresenterSelector that will determine
782     *        the Presenter for each row header.
783     */
784    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
785        mHeaderPresenterSelector = headerPresenterSelector;
786        if (mHeadersSupportFragment != null) {
787            mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector);
788        }
789    }
790
791    private void setRowsAlignedLeft(boolean alignLeft) {
792        MarginLayoutParams lp;
793        View containerList;
794        containerList = mRowsSupportFragment.getView();
795        lp = (MarginLayoutParams) containerList.getLayoutParams();
796        lp.setMarginStart(alignLeft ? 0 : mContainerListMarginStart);
797        containerList.setLayoutParams(lp);
798    }
799
800    private void setHeadersOnScreen(boolean onScreen) {
801        MarginLayoutParams lp;
802        View containerList;
803        containerList = mHeadersSupportFragment.getView();
804        lp = (MarginLayoutParams) containerList.getLayoutParams();
805        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
806        containerList.setLayoutParams(lp);
807    }
808
809    private void showHeaders(boolean show) {
810        if (DEBUG) Log.v(TAG, "showHeaders " + show);
811        mHeadersSupportFragment.setHeadersEnabled(show);
812        setHeadersOnScreen(show);
813        setRowsAlignedLeft(!show);
814        mRowsSupportFragment.setExpand(!show);
815    }
816
817    private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener =
818        new HeadersSupportFragment.OnHeaderClickedListener() {
819            @Override
820            public void onHeaderClicked() {
821                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
822                    return;
823                }
824                startHeadersTransitionInternal(false);
825                mRowsSupportFragment.getVerticalGridView().requestFocus();
826            }
827        };
828
829    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
830        @Override
831        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
832                RowPresenter.ViewHolder rowViewHolder, Row row) {
833            int position = mRowsSupportFragment.getVerticalGridView().getSelectedPosition();
834            if (DEBUG) Log.v(TAG, "row selected position " + position);
835            onRowSelected(position);
836            if (mExternalOnItemViewSelectedListener != null) {
837                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
838                        rowViewHolder, row);
839            }
840        }
841    };
842
843    private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
844        @Override
845        public void onItemSelected(Object item, Row row) {
846            if (mExternalOnItemSelectedListener != null) {
847                mExternalOnItemSelectedListener.onItemSelected(item, row);
848            }
849        }
850    };
851
852    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
853        @Override
854        public void onItemSelected(Object item, Row row) {
855            int position = mHeadersSupportFragment.getVerticalGridView().getSelectedPosition();
856            if (DEBUG) Log.v(TAG, "header selected position " + position);
857            onRowSelected(position);
858        }
859    };
860
861    private void onRowSelected(int position) {
862        if (position != mSelectedPosition) {
863            mSetSelectionRunnable.mPosition = position;
864            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
865
866            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
867                if (!mShowingTitle) {
868                    sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
869                    mShowingTitle = true;
870                }
871            } else if (mShowingTitle) {
872                sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
873                mShowingTitle = false;
874            }
875        }
876    }
877
878    private class SetSelectionRunnable implements Runnable {
879        int mPosition;
880        boolean mSmooth = true;
881        @Override
882        public void run() {
883            setSelection(mPosition, mSmooth);
884        }
885    }
886
887    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
888
889    private void setSelection(int position, boolean smooth) {
890        if (position != NO_POSITION) {
891            mRowsSupportFragment.setSelectedPosition(position, smooth);
892            mHeadersSupportFragment.setSelectedPosition(position, smooth);
893        }
894        mSelectedPosition = position;
895    }
896
897    /**
898     * Sets the selected row position with smooth animation.
899     */
900    public void setSelectedPosition(int position) {
901        setSelectedPosition(position, true);
902    }
903
904    /**
905     * Sets the selected row position.
906     */
907    public void setSelectedPosition(int position, boolean smooth) {
908        mSetSelectionRunnable.mPosition = position;
909        mSetSelectionRunnable.mSmooth = smooth;
910        mBrowseFrame.getHandler().post(mSetSelectionRunnable);
911    }
912
913    @Override
914    public void onStart() {
915        super.onStart();
916        mHeadersSupportFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
917        mHeadersSupportFragment.setItemAlignment();
918        mRowsSupportFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
919        mRowsSupportFragment.setItemAlignment();
920
921        mRowsSupportFragment.setScalePivots(0, mContainerListAlignTop);
922
923        if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment.getView() != null) {
924            mHeadersSupportFragment.getView().requestFocus();
925        } else if ((!mCanShowHeaders || !mShowingHeaders)
926                && mRowsSupportFragment.getView() != null) {
927            mRowsSupportFragment.getView().requestFocus();
928        }
929        if (mCanShowHeaders) {
930            showHeaders(mShowingHeaders);
931        }
932        if (isEntranceTransitionEnabled()) {
933            setEntranceTransitionStartState();
934        }
935    }
936
937    @Override
938    public void onPause() {
939        mTitleView.enableAnimation(false);
940        super.onPause();
941    }
942
943    @Override
944    public void onResume() {
945        super.onResume();
946        mTitleView.enableAnimation(true);
947    }
948
949    /**
950     * Enable/disable headers transition on back key support. This is enabled by
951     * default. The BrowseSupportFragment will add a back stack entry when headers are
952     * showing. Running a headers transition when the back key is pressed only
953     * works when the headers state is {@link #HEADERS_ENABLED} or
954     * {@link #HEADERS_HIDDEN}.
955     * <p>
956     * NOTE: If an Activity has its own onBackPressed() handling, you must
957     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
958     * and {@link BrowseTransitionListener} in your own back stack handling.
959     */
960    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
961        mHeadersBackStackEnabled = headersBackStackEnabled;
962    }
963
964    /**
965     * Returns true if headers transition on back key support is enabled.
966     */
967    public final boolean isHeadersTransitionOnBackEnabled() {
968        return mHeadersBackStackEnabled;
969    }
970
971    private void readArguments(Bundle args) {
972        if (args == null) {
973            return;
974        }
975        if (args.containsKey(ARG_TITLE)) {
976            setTitle(args.getString(ARG_TITLE));
977        }
978        if (args.containsKey(ARG_HEADERS_STATE)) {
979            setHeadersState(args.getInt(ARG_HEADERS_STATE));
980        }
981    }
982
983    /**
984     * Sets the drawable displayed in the browse fragment title.
985     *
986     * @param drawable The Drawable to display in the browse fragment title.
987     */
988    public void setBadgeDrawable(Drawable drawable) {
989        if (mBadgeDrawable != drawable) {
990            mBadgeDrawable = drawable;
991            if (mTitleView != null) {
992                mTitleView.setBadgeDrawable(drawable);
993            }
994        }
995    }
996
997    /**
998     * Returns the badge drawable used in the fragment title.
999     */
1000    public Drawable getBadgeDrawable() {
1001        return mBadgeDrawable;
1002    }
1003
1004    /**
1005     * Sets a title for the browse fragment.
1006     *
1007     * @param title The title of the browse fragment.
1008     */
1009    public void setTitle(String title) {
1010        mTitle = title;
1011        if (mTitleView != null) {
1012            mTitleView.setTitle(title);
1013        }
1014    }
1015
1016    /**
1017     * Returns the title for the browse fragment.
1018     */
1019    public String getTitle() {
1020        return mTitle;
1021    }
1022
1023    /**
1024     * Sets the state for the headers column in the browse fragment. Must be one
1025     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1026     * {@link #HEADERS_DISABLED}.
1027     *
1028     * @param headersState The state of the headers for the browse fragment.
1029     */
1030    public void setHeadersState(int headersState) {
1031        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1032            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1033        }
1034        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1035
1036        if (headersState != mHeadersState) {
1037            mHeadersState = headersState;
1038            switch (headersState) {
1039                case HEADERS_ENABLED:
1040                    mCanShowHeaders = true;
1041                    mShowingHeaders = true;
1042                    break;
1043                case HEADERS_HIDDEN:
1044                    mCanShowHeaders = true;
1045                    mShowingHeaders = false;
1046                    break;
1047                case HEADERS_DISABLED:
1048                    mCanShowHeaders = false;
1049                    mShowingHeaders = false;
1050                    break;
1051                default:
1052                    Log.w(TAG, "Unknown headers state: " + headersState);
1053                    break;
1054            }
1055            if (mHeadersSupportFragment != null) {
1056                mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders);
1057            }
1058        }
1059    }
1060
1061    /**
1062     * Returns the state of the headers column in the browse fragment.
1063     */
1064    public int getHeadersState() {
1065        return mHeadersState;
1066    }
1067
1068    @Override
1069    protected Object createEntranceTransition() {
1070        return sTransitionHelper.loadTransition(getActivity(),
1071                R.transition.lb_browse_entrance_transition);
1072    }
1073
1074    @Override
1075    protected void runEntranceTransition(Object entranceTransition) {
1076        sTransitionHelper.runTransition(mSceneAfterEntranceTransition,
1077                entranceTransition);
1078    }
1079
1080    @Override
1081    protected void onEntranceTransitionStart() {
1082        mHeadersSupportFragment.onTransitionStart();
1083        mRowsSupportFragment.onTransitionStart();
1084    }
1085
1086    @Override
1087    protected void onEntranceTransitionEnd() {
1088        mRowsSupportFragment.onTransitionEnd();
1089        mHeadersSupportFragment.onTransitionEnd();
1090    }
1091
1092    void setSearchOrbViewOnScreen(boolean onScreen) {
1093        View searchOrbView = mTitleView.getSearchAffordanceView();
1094        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1095        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1096        searchOrbView.setLayoutParams(lp);
1097    }
1098
1099    void setEntranceTransitionStartState() {
1100        setHeadersOnScreen(false);
1101        setSearchOrbViewOnScreen(false);
1102        mRowsSupportFragment.setEntranceTransitionState(false);
1103    }
1104
1105    void setEntranceTransitionEndState() {
1106        setHeadersOnScreen(mShowingHeaders);
1107        setSearchOrbViewOnScreen(true);
1108        mRowsSupportFragment.setEntranceTransitionState(true);
1109    }
1110
1111}
1112
1113