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