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