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