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