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