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