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