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