BrowseFragment.java revision 5a983d70762d39d92501479cfc445be6cb1b569c
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.widget.HorizontalGridView;
18import android.support.v17.leanback.widget.Presenter;
19import android.support.v17.leanback.widget.PresenterSelector;
20import android.support.v17.leanback.widget.VerticalGridView;
21import android.support.v17.leanback.widget.Row;
22import android.support.v17.leanback.widget.ObjectAdapter;
23import android.support.v17.leanback.widget.OnItemSelectedListener;
24import android.support.v17.leanback.widget.OnItemClickedListener;
25import android.support.v17.leanback.widget.SearchOrbView;
26import android.util.Log;
27import android.util.TypedValue;
28import android.app.Activity;
29import android.app.Fragment;
30import android.app.FragmentManager;
31import android.app.FragmentManager.BackStackEntry;
32import android.content.res.TypedArray;
33import android.os.Bundle;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.View.OnClickListener;
37import android.view.ViewGroup;
38import android.view.ViewGroup.MarginLayoutParams;
39import android.view.animation.DecelerateInterpolator;
40import android.widget.ImageView;
41import android.widget.TextView;
42import android.graphics.Color;
43import android.graphics.drawable.Drawable;
44
45import static android.support.v7.widget.RecyclerView.NO_POSITION;
46
47/**
48 * Wrapper fragment for leanback browse screens. Composed of a
49 * RowsFragment and a HeadersFragment.
50 * <p>
51 * The fragment comes with default back key support to show headers.
52 * For app customized {@link Activity#onBackPressed()}, app must disable
53 * BrowseFragment's default back key support by calling
54 * {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and use
55 * {@link BrowseFragment.BrowseTransitionListener} and {@link #startHeadersTransition(boolean)}.
56 */
57public class BrowseFragment extends Fragment {
58
59    @Deprecated
60    public static class Params {
61        private String mTitle;
62        private Drawable mBadgeDrawable;
63        private int mHeadersState = HEADERS_ENABLED;
64
65        /**
66         * Sets the badge image.
67         */
68        public void setBadgeImage(Drawable drawable) {
69            mBadgeDrawable = drawable;
70        }
71
72        /**
73         * Returns the badge image.
74         */
75        public Drawable getBadgeImage() {
76            return mBadgeDrawable;
77        }
78
79        /**
80         * Sets a title for the browse fragment.
81         */
82        public void setTitle(String title) {
83            mTitle = title;
84        }
85
86        /**
87         * Returns the title for the browse fragment.
88         */
89        public String getTitle() {
90            return mTitle;
91        }
92
93        /**
94         * Sets the state for the headers column in the browse fragment.
95         */
96        public void setHeadersState(int headersState) {
97            if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
98                Log.e(TAG, "Invalid headers state: " + headersState
99                        + ", default to enabled and shown.");
100                mHeadersState = HEADERS_ENABLED;
101            } else {
102                mHeadersState = headersState;
103            }
104        }
105
106        /**
107         * Returns the state for the headers column in the browse fragment.
108         */
109        public int getHeadersState() {
110            return mHeadersState;
111        }
112    }
113
114    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
115        int mLastEntryCount;
116        int mIndexOfHeadersBackStack;
117
118        BackStackListener() {
119            reset();
120        }
121
122        void reset() {
123            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
124            mIndexOfHeadersBackStack = -1;
125        }
126
127        @Override
128        public void onBackStackChanged() {
129            int count = getFragmentManager().getBackStackEntryCount();
130            // if backstack is growing and last pushed entry is "headers" backstack,
131            // remember the index of the entry.
132            if (count > mLastEntryCount) {
133                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
134                if (mWithHeadersBackStackName.equals(entry.getName())) {
135                    mIndexOfHeadersBackStack = count - 1;
136                }
137            } else if (count < mLastEntryCount) {
138                // if popped "headers" backstack, initiate the show header transition if needed
139                if (mIndexOfHeadersBackStack >= count) {
140                    if (!mShowingHeaders) {
141                        startHeadersTransitionInternal(true);
142                    }
143                }
144            }
145            mLastEntryCount = count;
146        }
147    }
148
149    /**
150     * Listener for browse transitions.
151     */
152    public static class BrowseTransitionListener {
153        /**
154         * Callback when headers transition starts.
155         */
156        public void onHeadersTransitionStart(boolean withHeaders) {
157        }
158        /**
159         * Callback when headers transition stops.
160         */
161        public void onHeadersTransitionStop(boolean withHeaders) {
162        }
163    }
164
165    private static final String TAG = "BrowseFragment";
166
167    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
168
169    private static boolean DEBUG = false;
170
171    /** The headers fragment is enabled and shown by default. */
172    public static final int HEADERS_ENABLED = 1;
173
174    /** The headers fragment is enabled and hidden by default. */
175    public static final int HEADERS_HIDDEN = 2;
176
177    /** The headers fragment is disabled and will never be shown. */
178    public static final int HEADERS_DISABLED = 3;
179
180    private static final float SLIDE_DISTANCE_FACTOR = 2;
181
182    private RowsFragment mRowsFragment;
183    private HeadersFragment mHeadersFragment;
184
185    private ObjectAdapter mAdapter;
186
187    // TODO: remove Params
188    private Params mParams;
189
190    private String mTitle;
191    private Drawable mBadgeDrawable;
192    private int mHeadersState = HEADERS_ENABLED;
193    private int mBrandColor = Color.TRANSPARENT;
194    private boolean mBrandColorSet;
195
196    private BrowseFrameLayout mBrowseFrame;
197    private ImageView mBadgeView;
198    private TextView mTitleView;
199    private ViewGroup mBrowseTitle;
200    private SearchOrbView mSearchOrbView;
201    private boolean mShowingTitle = true;
202    private boolean mHeadersBackStackEnabled = true;
203    private String mWithHeadersBackStackName;
204    private boolean mShowingHeaders = true;
205    private boolean mCanShowHeaders = true;
206    private int mContainerListMarginLeft;
207    private int mContainerListAlignTop;
208    private int mSearchAffordanceColor;
209    private boolean mSearchAffordanceColorSet;
210    private OnItemSelectedListener mExternalOnItemSelectedListener;
211    private OnClickListener mExternalOnSearchClickedListener;
212    private OnItemClickedListener mOnItemClickedListener;
213    private int mSelectedPosition = -1;
214
215    private PresenterSelector mHeaderPresenterSelector;
216
217    // transition related:
218    private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
219    private int mReparentHeaderId = View.generateViewId();
220    private Object mSceneWithTitle;
221    private Object mSceneWithoutTitle;
222    private Object mSceneWithHeaders;
223    private Object mSceneWithoutHeaders;
224    private Object mTitleUpTransition;
225    private Object mTitleDownTransition;
226    private Object mHeadersTransition;
227    private int mHeadersTransitionStartDelay;
228    private int mHeadersTransitionDuration;
229    private BackStackListener mBackStackChangedListener;
230    private BrowseTransitionListener mBrowseTransitionListener;
231
232    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
233    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
234    private static final String ARG_HEADERS_STATE =
235        BrowseFragment.class.getCanonicalName() + ".headersState";
236
237    /**
238     * Create arguments for a browse fragment.
239     * @deprecated Use {@link #createArgs(Bundle args, String title, int headersState)}.
240     */
241    @Deprecated
242    public static Bundle createArgs(Bundle args, String title, String badgeUri) {
243        return createArgs(args, title, HEADERS_ENABLED);
244    }
245
246    /**
247     * Create arguments for a browse fragment.
248     * @deprecated Use {@link #createArgs(Bundle args, String title, int headersState)}.
249     */
250    @Deprecated
251    public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) {
252        return createArgs(args, title, headersState);
253    }
254
255    /**
256     * Create arguments for a browse fragment.
257     */
258    public static Bundle createArgs(Bundle args, String title, int headersState) {
259        if (args == null) {
260            args = new Bundle();
261        }
262        args.putString(ARG_TITLE, title);
263        args.putInt(ARG_HEADERS_STATE, headersState);
264        return args;
265    }
266
267    /**
268     * Set browse parameters.
269     * @deprecated Call methods on the fragment directly.
270     */
271    @Deprecated
272    public void setBrowseParams(Params params) {
273        mParams = params;
274        setBadgeDrawable(params.mBadgeDrawable);
275        setTitle(params.mTitle);
276        setHeadersState(params.mHeadersState);
277    }
278
279    /**
280     * Returns browse parameters.
281     * @deprecated Call methods on the fragment directly.
282     */
283    @Deprecated
284    public Params getBrowseParams() {
285        return mParams;
286    }
287
288    /**
289     * Sets the brand color for the browse fragment.
290     */
291    public void setBrandColor(int color) {
292        mBrandColor = color;
293        mBrandColorSet = true;
294
295        if (mHeadersFragment != null) {
296            mHeadersFragment.setBackgroundColor(mBrandColor);
297        }
298    }
299
300    /**
301     * Returns the brand color for the browse fragment.
302     * The default is transparent.
303     */
304    public int getBrandColor() {
305        return mBrandColor;
306    }
307
308    /**
309     * Sets the list of rows for the fragment.
310     */
311    public void setAdapter(ObjectAdapter adapter) {
312        mAdapter = adapter;
313        if (mRowsFragment != null) {
314            mRowsFragment.setAdapter(adapter);
315            mHeadersFragment.setAdapter(adapter);
316        }
317    }
318
319    /**
320     * Returns the list of rows.
321     */
322    public ObjectAdapter getAdapter() {
323        return mAdapter;
324    }
325
326    /**
327     * Sets an item selection listener.
328     */
329    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
330        mExternalOnItemSelectedListener = listener;
331    }
332
333    /**
334     * Sets an item clicked listener on the fragment.
335     * OnItemClickedListener will override {@link View.OnClickListener} that
336     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
337     * So in general,  developer should choose one of the listeners but not both.
338     */
339    public void setOnItemClickedListener(OnItemClickedListener listener) {
340        mOnItemClickedListener = listener;
341        if (mRowsFragment != null) {
342            mRowsFragment.setOnItemClickedListener(listener);
343        }
344    }
345
346    /**
347     * Returns the item Clicked listener.
348     */
349    public OnItemClickedListener getOnItemClickedListener() {
350        return mOnItemClickedListener;
351    }
352
353    /**
354     * Sets a click listener for the search affordance.
355     *
356     * The presence of a listener will change the visibility of the search affordance in the
357     * title area. When set to non-null the title area will contain a call to search action.
358     *
359     * The listener onClick method will be invoked when the user click on the search action.
360     *
361     * @param listener The listener.
362     */
363    public void setOnSearchClickedListener(View.OnClickListener listener) {
364        mExternalOnSearchClickedListener = listener;
365        if (mSearchOrbView != null) {
366            mSearchOrbView.setOnOrbClickedListener(listener);
367        }
368    }
369
370    /**
371     * Sets the color used to draw the search affordance.
372     */
373    public void setSearchAffordanceColor(int color) {
374        mSearchAffordanceColor = color;
375        mSearchAffordanceColorSet = true;
376
377        if (mSearchOrbView != null) {
378            mSearchOrbView.setOrbColor(mSearchAffordanceColor);
379        }
380    }
381
382    /**
383     * Returns the color used to draw the search affordance.
384     * Can be called only after an activity has been attached.
385     */
386    public int getSearchAffordanceColor() {
387        if (getActivity() == null) {
388            throw new IllegalStateException("Activity must be attached");
389        }
390
391        if (mSearchAffordanceColorSet) {
392            return mSearchAffordanceColor;
393        }
394
395        TypedValue outValue = new TypedValue();
396        getActivity().getTheme().resolveAttribute(R.attr.defaultSearchColor, outValue, true);
397        return getResources().getColor(outValue.resourceId);
398    }
399
400    /**
401     * Start headers transition.
402     */
403    public void startHeadersTransition(boolean withHeaders) {
404        if (!mCanShowHeaders) {
405            throw new IllegalStateException("Cannot start headers transition");
406        }
407        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
408            return;
409        }
410        startHeadersTransitionInternal(withHeaders);
411    }
412
413    /**
414     * Returns true if headers transition is currently running.
415     */
416    public boolean isInHeadersTransition() {
417        return mHeadersTransition != null;
418    }
419
420    /**
421     * Returns true if headers is showing.
422     */
423    public boolean isShowingHeaders() {
424        return mShowingHeaders;
425    }
426
427    /**
428     * Set listener for browse fragment transitions.
429     */
430    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
431        mBrowseTransitionListener = listener;
432    }
433
434    private void startHeadersTransitionInternal(boolean withHeaders) {
435        mShowingHeaders = withHeaders;
436        mRowsFragment.onTransitionStart();
437        mHeadersFragment.onTransitionStart();
438        createHeadersTransition();
439        if (mBrowseTransitionListener != null) {
440            mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
441        }
442        sTransitionHelper.runTransition(withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders,
443                mHeadersTransition);
444        if (mHeadersBackStackEnabled) {
445            if (!withHeaders) {
446                getFragmentManager().beginTransaction()
447                        .addToBackStack(mWithHeadersBackStackName).commit();
448            } else {
449                int count = getFragmentManager().getBackStackEntryCount();
450                if (count > 0) {
451                    BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
452                    if (mWithHeadersBackStackName.equals(entry.getName())) {
453                        getFragmentManager().popBackStack();
454                    }
455                }
456            }
457        }
458    }
459
460    private boolean isVerticalScrolling() {
461        // don't run transition
462        return mHeadersFragment.getVerticalGridView().getScrollState()
463                != HorizontalGridView.SCROLL_STATE_IDLE
464                || mRowsFragment.getVerticalGridView().getScrollState()
465                != HorizontalGridView.SCROLL_STATE_IDLE;
466    }
467
468    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
469            new BrowseFrameLayout.OnFocusSearchListener() {
470        @Override
471        public View onFocusSearch(View focused, int direction) {
472            // If headers fragment is disabled, just return null.
473            if (!mCanShowHeaders) return null;
474
475            // if headers is running transition,  focus stays
476            if (isInHeadersTransition()) return focused;
477            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
478            if (direction == View.FOCUS_LEFT) {
479                if (isVerticalScrolling() || mShowingHeaders) {
480                    return focused;
481                }
482                return mHeadersFragment.getVerticalGridView();
483            } else if (direction == View.FOCUS_RIGHT) {
484                if (isVerticalScrolling() || !mShowingHeaders) {
485                    return focused;
486                }
487                return mRowsFragment.getVerticalGridView();
488            } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) {
489                return mShowingHeaders ? mHeadersFragment.getVerticalGridView() :
490                    mRowsFragment.getVerticalGridView();
491
492            } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
493                    && direction == View.FOCUS_UP) {
494                return mSearchOrbView;
495
496            } else {
497                return null;
498            }
499        }
500    };
501
502    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
503            new BrowseFrameLayout.OnChildFocusListener() {
504        @Override
505        public void onRequestChildFocus(View child, View focused) {
506            int childId = child.getId();
507            if (!mCanShowHeaders || isInHeadersTransition()) return;
508            if (childId == R.id.browse_container_dock && mShowingHeaders) {
509                startHeadersTransitionInternal(false);
510            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
511                startHeadersTransitionInternal(true);
512            }
513        }
514    };
515
516    @Override
517    public void onCreate(Bundle savedInstanceState) {
518        super.onCreate(savedInstanceState);
519        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
520        mContainerListMarginLeft = (int) ta.getDimension(
521                R.styleable.LeanbackTheme_browseRowsMarginStart, 0);
522        mContainerListAlignTop = (int) ta.getDimension(
523                R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
524        ta.recycle();
525
526        mHeadersTransitionStartDelay = getResources()
527                .getInteger(R.integer.lb_browse_headers_transition_delay);
528        mHeadersTransitionDuration = getResources()
529                .getInteger(R.integer.lb_browse_headers_transition_duration);
530
531        readArguments(getArguments());
532
533        if (mCanShowHeaders && mHeadersBackStackEnabled) {
534            mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
535            mBackStackChangedListener = new BackStackListener();
536            getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
537            if (!mShowingHeaders) {
538                getFragmentManager().beginTransaction()
539                        .addToBackStack(mWithHeadersBackStackName).commit();
540            }
541        }
542
543    }
544
545    @Override
546    public View onCreateView(LayoutInflater inflater, ViewGroup container,
547            Bundle savedInstanceState) {
548        if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
549            mRowsFragment = new RowsFragment();
550            mHeadersFragment = new HeadersFragment();
551            getChildFragmentManager().beginTransaction()
552                    .replace(R.id.browse_headers_dock, mHeadersFragment)
553                    .replace(R.id.browse_container_dock, mRowsFragment).commit();
554        } else {
555            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
556                    .findFragmentById(R.id.browse_headers_dock);
557            mRowsFragment = (RowsFragment) getChildFragmentManager()
558                    .findFragmentById(R.id.browse_container_dock);
559        }
560
561        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
562
563        mRowsFragment.setAdapter(mAdapter);
564        if (mHeaderPresenterSelector != null) {
565            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
566        }
567        mHeadersFragment.setAdapter(mAdapter);
568
569        mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
570        mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
571        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
572        mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
573
574        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
575
576        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
577        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
578        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
579
580        mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
581        mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
582        mTitleView.setText(mTitle);
583        mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
584        setBadgeViewImage();
585
586        mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
587        mSearchOrbView.setOrbColor(getSearchAffordanceColor());
588        if (mExternalOnSearchClickedListener != null) {
589            mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
590        }
591
592        if (mBrandColorSet) {
593            mHeadersFragment.setBackgroundColor(mBrandColor);
594        }
595
596        mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
597            @Override
598            public void run() {
599                showTitle(true);
600            }
601        });
602        mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
603            @Override
604            public void run() {
605                showTitle(false);
606            }
607        });
608        mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
609            @Override
610            public void run() {
611                showHeaders(true);
612            }
613        });
614        mSceneWithoutHeaders =  sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
615            @Override
616            public void run() {
617                showHeaders(false);
618            }
619        });
620        mTitleUpTransition = sTransitionHelper.createChangeBounds(false);
621        sTransitionHelper.setInterpolator(mTitleUpTransition, new DecelerateInterpolator(4));
622        mTitleDownTransition = sTransitionHelper.createChangeBounds(false);
623        sTransitionHelper.setInterpolator(mTitleDownTransition, new DecelerateInterpolator());
624
625        sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_headers, true);
626        sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_headers, true);
627        sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.container_list, true);
628        sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.container_list, true);
629
630        return root;
631    }
632
633    private void createHeadersTransition() {
634        mHeadersTransition = sTransitionHelper.createTransitionSet(false);
635        sTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true);
636        Object changeBounds = sTransitionHelper.createChangeBounds(false);
637        Object fadeIn = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_IN);
638        Object fadeOut = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_OUT);
639
640        sTransitionHelper.setDuration(fadeOut, mHeadersTransitionDuration);
641        sTransitionHelper.addTransition(mHeadersTransition, fadeOut);
642        if (mShowingHeaders) {
643            sTransitionHelper.setStartDelay(changeBounds, mHeadersTransitionStartDelay);
644        }
645        sTransitionHelper.setDuration(changeBounds, mHeadersTransitionDuration);
646        sTransitionHelper.addTransition(mHeadersTransition, changeBounds);
647        sTransitionHelper.setDuration(fadeIn, mHeadersTransitionDuration);
648        sTransitionHelper.setStartDelay(fadeIn, mHeadersTransitionStartDelay);
649        sTransitionHelper.addTransition(mHeadersTransition, fadeIn);
650
651        sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() {
652            @Override
653            public void onTransitionStart(Object transition) {
654            }
655            @Override
656            public void onTransitionEnd(Object transition) {
657                mHeadersTransition = null;
658                mRowsFragment.onTransitionEnd();
659                mHeadersFragment.onTransitionEnd();
660                if (mShowingHeaders) {
661                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
662                    if (headerGridView != null && !headerGridView.hasFocus()) {
663                        headerGridView.requestFocus();
664                    }
665                } else {
666                    VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView();
667                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
668                        rowsGridView.requestFocus();
669                    }
670                }
671                if (mBrowseTransitionListener != null) {
672                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
673                }
674            }
675        });
676    }
677
678    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
679        mHeaderPresenterSelector = headerPresenterSelector;
680        if (mHeadersFragment != null) {
681            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
682        }
683    }
684
685    private void showTitle(boolean show) {
686        MarginLayoutParams lp = (MarginLayoutParams) mBrowseTitle.getLayoutParams();
687        lp.topMargin = show ? 0 : -mBrowseTitle.getHeight();
688        mBrowseTitle.setLayoutParams(lp);
689    }
690
691    private void showHeaders(boolean show) {
692        if (DEBUG) Log.v(TAG, "showHeaders " + show);
693        mHeadersFragment.setHeadersEnabled(show);
694        MarginLayoutParams lp;
695        View containerList;
696
697        containerList = mRowsFragment.getView();
698        lp = (MarginLayoutParams) containerList.getLayoutParams();
699        lp.leftMargin = show ? mContainerListMarginLeft : 0;
700        containerList.setLayoutParams(lp);
701
702        containerList = mHeadersFragment.getView();
703        lp = (MarginLayoutParams) containerList.getLayoutParams();
704        lp.leftMargin = show ? 0 : -mContainerListMarginLeft;
705        containerList.setLayoutParams(lp);
706
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 OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
723        @Override
724        public void onItemSelected(Object item, Row row) {
725            int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
726            if (DEBUG) Log.v(TAG, "row selected position " + position);
727            onRowSelected(position);
728            if (mExternalOnItemSelectedListener != null) {
729                mExternalOnItemSelectedListener.onItemSelected(item, row);
730            }
731        }
732    };
733
734    private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
735        @Override
736        public void onItemSelected(Object item, Row row) {
737            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
738            if (DEBUG) Log.v(TAG, "header selected position " + position);
739            onRowSelected(position);
740        }
741    };
742
743    private void onRowSelected(int position) {
744        if (position != mSelectedPosition) {
745            mSetSelectionRunnable.mPosition = position;
746            mBrowseFrame.getHandler().post(mSetSelectionRunnable);
747
748            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
749                if (!mShowingTitle) {
750                    sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
751                    mShowingTitle = true;
752                }
753            } else if (mShowingTitle) {
754                sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
755                mShowingTitle = false;
756            }
757        }
758    }
759
760    private class SetSelectionRunnable implements Runnable {
761        int mPosition;
762        @Override
763        public void run() {
764            setSelection(mPosition);
765        }
766    }
767
768    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
769
770    private void setSelection(int position) {
771        if (position != NO_POSITION) {
772            mRowsFragment.setSelectedPosition(position);
773            mHeadersFragment.setSelectedPosition(position);
774        }
775        mSelectedPosition = position;
776    }
777
778    @Override
779    public void onStart() {
780        super.onStart();
781        mHeadersFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
782        mHeadersFragment.setItemAlignment();
783        mRowsFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
784        mRowsFragment.setItemAlignment();
785
786        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
787            mHeadersFragment.getView().requestFocus();
788        } else if ((!mCanShowHeaders || !mShowingHeaders)
789                && mRowsFragment.getView() != null) {
790            mRowsFragment.getView().requestFocus();
791        }
792        if (mCanShowHeaders) {
793            showHeaders(mShowingHeaders);
794        }
795    }
796
797    /**
798     * Enable/disable headers transition on back key support.  This is enabled by default.
799     * BrowseFragment will add a back stack entry when headers are showing.
800     * Headers transition on back key only works for {@link #HEADERS_ENABLED}
801     * or {@link #HEADERS_HIDDEN}.
802     * <p>
803     * NOTE: If app has its own onBackPressed() handling,
804     * app must disable this feature, app may use {@link #startHeadersTransition(boolean)}
805     * and {@link BrowseTransitionListener} in its own back stack handling.
806     */
807    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
808        mHeadersBackStackEnabled = headersBackStackEnabled;
809    }
810
811    /**
812     * Returns true if headers transition on back key support is enabled.
813     */
814    public final boolean isHeadersTransitionOnBackEnabled() {
815        return mHeadersBackStackEnabled;
816    }
817
818    private void readArguments(Bundle args) {
819        if (args == null) {
820            return;
821        }
822        if (args.containsKey(ARG_TITLE)) {
823            setTitle(args.getString(ARG_TITLE));
824        }
825        if (args.containsKey(ARG_HEADERS_STATE)) {
826            setHeadersState(args.getInt(ARG_HEADERS_STATE));
827        }
828    }
829
830    /**
831     * Sets the drawable displayed in the fragment title area.
832     * @param drawable
833     */
834    public void setBadgeDrawable(Drawable drawable) {
835        if (mBadgeDrawable != drawable) {
836            mBadgeDrawable = drawable;
837            setBadgeViewImage();
838        }
839    }
840
841    private void setBadgeViewImage() {
842        if (mBadgeView == null) {
843            return;
844        }
845        mBadgeView.setImageDrawable(mBadgeDrawable);
846        if (mBadgeDrawable != null) {
847            mBadgeView.setVisibility(View.VISIBLE);
848            mTitleView.setVisibility(View.GONE);
849        } else {
850            mBadgeView.setVisibility(View.GONE);
851            mTitleView.setVisibility(View.VISIBLE);
852        }
853    }
854
855    /**
856     * Returns the badge drawable.
857     */
858    public Drawable getBadgeDrawable() {
859        return mBadgeDrawable;
860    }
861
862    /**
863     * Sets a title for the browse fragment.
864     */
865    public void setTitle(String title) {
866        mTitle = title;
867        if (mTitleView != null) {
868            mTitleView.setText(title);
869        }
870    }
871
872    /**
873     * Returns the title for the browse fragment.
874     */
875    public String getTitle() {
876        return mTitle;
877    }
878
879    /**
880     * Sets the state for the headers column in the browse fragment.
881     */
882    public void setHeadersState(int headersState) {
883        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
884            throw new IllegalArgumentException("Invalid headers state: " + headersState);
885        }
886        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
887
888        if (headersState != mHeadersState) {
889            mHeadersState = headersState;
890            switch (headersState) {
891                case HEADERS_ENABLED:
892                    mCanShowHeaders = true;
893                    mShowingHeaders = true;
894                    break;
895                case HEADERS_HIDDEN:
896                    mCanShowHeaders = true;
897                    mShowingHeaders = false;
898                    break;
899                case HEADERS_DISABLED:
900                    mCanShowHeaders = false;
901                    mShowingHeaders = false;
902                    break;
903                default:
904                    Log.w(TAG, "Unknown headers state: " + headersState);
905                    break;
906            }
907            if (mHeadersFragment != null) {
908                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
909            }
910        }
911    }
912
913    /**
914     * Returns the state for the headers column in the browse fragment.
915     */
916    public int getHeadersState() {
917        return mHeadersState;
918    }
919}
920