BrowseFragment.java revision f7a4099b29d7739616dd9d7e466b48dfda4b32f2
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.app.Fragment;
17import android.app.FragmentManager;
18import android.app.FragmentManager.BackStackEntry;
19import android.content.res.TypedArray;
20import android.graphics.Color;
21import android.graphics.Rect;
22import android.os.Bundle;
23import android.support.annotation.ColorInt;
24import android.support.v17.leanback.R;
25import android.support.v17.leanback.transition.TransitionHelper;
26import android.support.v17.leanback.transition.TransitionListener;
27import android.support.v17.leanback.widget.BrowseFrameLayout;
28import android.support.v17.leanback.widget.ListRow;
29import android.support.v17.leanback.widget.ObjectAdapter;
30import android.support.v17.leanback.widget.OnItemViewClickedListener;
31import android.support.v17.leanback.widget.OnItemViewSelectedListener;
32import android.support.v17.leanback.widget.PageRow;
33import android.support.v17.leanback.widget.Presenter;
34import android.support.v17.leanback.widget.PresenterSelector;
35import android.support.v17.leanback.widget.Row;
36import android.support.v17.leanback.widget.RowHeaderPresenter;
37import android.support.v17.leanback.widget.RowPresenter;
38import android.support.v17.leanback.widget.ScaleFrameLayout;
39import android.support.v17.leanback.widget.TitleView;
40import android.support.v17.leanback.widget.VerticalGridView;
41import android.support.v4.view.ViewCompat;
42import android.util.Log;
43import android.view.LayoutInflater;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewGroup.MarginLayoutParams;
47import android.view.ViewTreeObserver;
48
49import static android.support.v7.widget.RecyclerView.NO_POSITION;
50
51/**
52 * A fragment for creating Leanback browse screens. It is composed of a
53 * RowsFragment and a HeadersFragment.
54 * <p>
55 * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
56 * of rows in a vertical list. The elements in this adapter must be subclasses
57 * of {@link Row}.
58 * <p>
59 * The HeadersFragment can be set to be either shown or hidden by default, or
60 * may be disabled entirely. See {@link #setHeadersState} for details.
61 * <p>
62 * By default the BrowseFragment includes support for returning to the headers
63 * when the user presses Back. For Activities that customize {@link
64 * android.app.Activity#onBackPressed()}, you must disable this default Back key support by
65 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
66 * use {@link BrowseFragment.BrowseTransitionListener} and
67 * {@link #startHeadersTransition(boolean)}.
68 * <p>
69 * The recommended theme to use with a BrowseFragment is
70 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}.
71 * </p>
72 */
73public class BrowseFragment extends BaseFragment {
74
75    // BUNDLE attribute for saving header show/hide status when backstack is used:
76    static final String HEADER_STACK_INDEX = "headerStackIndex";
77    // BUNDLE attribute for saving header show/hide status when backstack is not used:
78    static final String HEADER_SHOW = "headerShow";
79
80    final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
81        int mLastEntryCount;
82        int mIndexOfHeadersBackStack;
83
84        BackStackListener() {
85            mLastEntryCount = getFragmentManager().getBackStackEntryCount();
86            mIndexOfHeadersBackStack = -1;
87        }
88
89        void load(Bundle savedInstanceState) {
90            if (savedInstanceState != null) {
91                mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1);
92                mShowingHeaders = mIndexOfHeadersBackStack == -1;
93            } else {
94                if (!mShowingHeaders) {
95                    getFragmentManager().beginTransaction()
96                            .addToBackStack(mWithHeadersBackStackName).commit();
97                }
98            }
99        }
100
101        void save(Bundle outState) {
102            outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack);
103        }
104
105
106        @Override
107        public void onBackStackChanged() {
108            if (getFragmentManager() == null) {
109                Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
110                return;
111            }
112            int count = getFragmentManager().getBackStackEntryCount();
113            // if backstack is growing and last pushed entry is "headers" backstack,
114            // remember the index of the entry.
115            if (count > mLastEntryCount) {
116                BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
117                if (mWithHeadersBackStackName.equals(entry.getName())) {
118                    mIndexOfHeadersBackStack = count - 1;
119                }
120            } else if (count < mLastEntryCount) {
121                // if popped "headers" backstack, initiate the show header transition if needed
122                if (mIndexOfHeadersBackStack >= count) {
123                    mIndexOfHeadersBackStack = -1;
124                    if (!mShowingHeaders) {
125                        startHeadersTransitionInternal(true);
126                    }
127                }
128            }
129            mLastEntryCount = count;
130        }
131    }
132
133    /**
134     * Listener for transitions between browse headers and rows.
135     */
136    public static class BrowseTransitionListener {
137        /**
138         * Callback when headers transition starts.
139         *
140         * @param withHeaders True if the transition will result in headers
141         *        being shown, false otherwise.
142         */
143        public void onHeadersTransitionStart(boolean withHeaders) {
144        }
145        /**
146         * Callback when headers transition stops.
147         *
148         * @param withHeaders True if the transition will result in headers
149         *        being shown, false otherwise.
150         */
151        public void onHeadersTransitionStop(boolean withHeaders) {
152        }
153    }
154
155    private class SetSelectionRunnable implements Runnable {
156        static final int TYPE_INVALID = -1;
157        static final int TYPE_INTERNAL_SYNC = 0;
158        static final int TYPE_USER_REQUEST = 1;
159
160        private int mPosition;
161        private int mType;
162        private boolean mSmooth;
163
164        SetSelectionRunnable() {
165            reset();
166        }
167
168        void post(int position, int type, boolean smooth) {
169            // Posting the set selection, rather than calling it immediately, prevents an issue
170            // with adapter changes.  Example: a row is added before the current selected row;
171            // first the fast lane view updates its selection, then the rows fragment has that
172            // new selection propagated immediately; THEN the rows view processes the same adapter
173            // change and moves the selection again.
174            if (type >= mType) {
175                mPosition = position;
176                mType = type;
177                mSmooth = smooth;
178                mBrowseFrame.removeCallbacks(this);
179                mBrowseFrame.post(this);
180            }
181        }
182
183        @Override
184        public void run() {
185            setSelection(mPosition, mSmooth);
186            reset();
187        }
188
189        private void reset() {
190            mPosition = -1;
191            mType = TYPE_INVALID;
192            mSmooth = false;
193        }
194    }
195
196    /**
197     * Interface that defines the interaction between {@link BrowseFragment} and it's main
198     * content fragment. The key method is {@link AbstractMainFragmentAdapter#getFragment()},
199     * it will be used to get the fragment to be shown in the content section. Clients can
200     * provide any implementation of fragment and customize it's interaction with
201     * {@link BrowseFragment} by overriding the necessary methods.
202     *
203     * <p>
204     * Clients are expected to provide
205     * an instance of {@link MainFragmentAdapterFactory} which will be responsible for providing
206     * implementations of {@link AbstractMainFragmentAdapter} for given content types. Currently
207     * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype
208     * of {@link Row}. We provide an out of the box adapter implementation for any rows other than
209     * {@link PageRow} - {@link RowsFragmentAdapter}.
210     *
211     * <p>
212     * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment
213     * design. Users will have to provide an implementation of {@link AbstractMainFragmentAdapter}
214     * and provide that through {@link MainFragmentAdapterFactory}.
215     * {@link AbstractMainFragmentAdapter} implementation can supply any fragment and override
216     * just those interactions that makes sense.
217     */
218    public static abstract class AbstractMainFragmentAdapter {
219        private boolean mScalingEnabled;
220        public abstract Fragment getFragment();
221
222        /**
223         * Sets an item clicked listener on the fragment.
224         */
225        public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
226        }
227
228        /**
229         * Sets an item selection listener.
230         */
231        public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
232        }
233
234        /**
235         * Selects a Row and perform an optional task on the Row.
236         */
237        public void setSelectedPosition(int rowPosition,
238                                 boolean smooth,
239                                 final Presenter.ViewHolderTask rowHolderTask) {
240        }
241
242        /**
243         * Selects a Row.
244         */
245        public void setSelectedPosition(int rowPosition, boolean smooth) {
246        }
247
248        /**
249         * Returns the selected position.
250         */
251        public int getSelectedPosition() {
252            return 0;
253        }
254
255        /**
256         * Returns whether its scrolling.
257         */
258        public boolean isScrolling() {
259            return false;
260        }
261
262        /**
263         * Set the visibility of titles/hovercard of browse rows.
264         */
265        public void setExpand(boolean expand) {
266        }
267
268        /**
269         * Set the visibility titles/hoverca of browse rows.
270         */
271        public void setAdapter(ObjectAdapter adapter) {
272        }
273
274        /**
275         * For rows that willing to participate entrance transition,  this function
276         * hide views if afterTransition is true,  show views if afterTransition is false.
277         */
278        public void setEntranceTransitionState(boolean state) {
279        }
280
281        /**
282         * Sets the window alignment and also the pivots for scale operation.
283         */
284        public void setAlignment(int windowAlignOffsetFromTop) {
285        }
286
287        /**
288         * Callback indicating transition prepare start.
289         */
290        public boolean onTransitionPrepare() {
291            return false;
292        }
293
294        /**
295         * Callback indicating transition start.
296         */
297        public void onTransitionStart() {
298        }
299
300        /**
301         * Callback indicating transition end.
302         */
303        public void onTransitionEnd() {
304        }
305
306        /**
307         * Returns whether row scaling is enabled.
308         */
309        public boolean isScalingEnabled() {
310            return mScalingEnabled;
311        }
312
313        /**
314         * Sets the row scaling property.
315         */
316        public void setScalingEnabled(boolean scalingEnabled) {
317            this.mScalingEnabled = scalingEnabled;
318        }
319    }
320
321    /**
322     * Factory class for {@link BrowseFragment.AbstractMainFragmentAdapter}. Developers can provide
323     * a custom implementation into {@link BrowseFragment}. {@link BrowseFragment} will use this
324     * factory to create fragments {@link BrowseFragment.AbstractMainFragmentAdapter#getFragment()}
325     * to display on the main content section.
326     */
327    public static abstract class MainFragmentAdapterFactory {
328        private BrowseFragment.AbstractMainFragmentAdapter rowsFragmentAdapter;
329        private BrowseFragment.AbstractMainFragmentAdapter pageFragmentAdapter;
330
331        public BrowseFragment.AbstractMainFragmentAdapter getAdapter(
332                ObjectAdapter adapter, int position) {
333            if (adapter == null || adapter.size() == 0) {
334                return getRowsFragmentAdapter();
335            }
336
337            if (position < 0 || position > adapter.size()) {
338                throw new IllegalArgumentException(
339                        String.format("Invalid position %d requested", position));
340            }
341
342            Object item = adapter.get(position);
343            if (item instanceof PageRow) {
344                if (pageFragmentAdapter == null) {
345                    pageFragmentAdapter = getPageFragmentAdapter();
346                }
347                return pageFragmentAdapter;
348            } else {
349                return getRowsFragmentAdapter();
350            }
351        }
352
353        private BrowseFragment.AbstractMainFragmentAdapter getRowsFragmentAdapter() {
354            if (rowsFragmentAdapter == null) {
355                rowsFragmentAdapter = new RowsFragmentAdapter();
356            }
357            return rowsFragmentAdapter;
358        }
359
360        public abstract BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter();
361    }
362
363    private static final String TAG = "BrowseFragment";
364
365    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
366
367    private static boolean DEBUG = false;
368
369    /** The headers fragment is enabled and shown by default. */
370    public static final int HEADERS_ENABLED = 1;
371
372    /** The headers fragment is enabled and hidden by default. */
373    public static final int HEADERS_HIDDEN = 2;
374
375    /** The headers fragment is disabled and will never be shown. */
376    public static final int HEADERS_DISABLED = 3;
377
378    private MainFragmentAdapterFactory mMainFragmentAdapterFactory
379            = new MainFragmentAdapterFactory() {
380        @Override
381        public BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter() {
382            return null;
383        }
384    };
385
386    private AbstractMainFragmentAdapter mMainFragmentAdapter;
387    private Fragment mMainFragment;
388    private HeadersFragment mHeadersFragment;
389
390    private ObjectAdapter mAdapter;
391
392    private int mHeadersState = HEADERS_ENABLED;
393    private int mBrandColor = Color.TRANSPARENT;
394    private boolean mBrandColorSet;
395
396    private BrowseFrameLayout mBrowseFrame;
397    private ScaleFrameLayout mScaleFrameLayout;
398    private boolean mHeadersBackStackEnabled = true;
399    private String mWithHeadersBackStackName;
400    private boolean mShowingHeaders = true;
401    private boolean mCanShowHeaders = true;
402    private int mContainerListMarginStart;
403    private int mContainerListAlignTop;
404    private boolean mMainFragmentScaleEnabled = true;
405    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
406    private OnItemViewClickedListener mOnItemViewClickedListener;
407    private int mSelectedPosition = 0;
408    private float mScaleFactor;
409
410    private PresenterSelector mHeaderPresenterSelector;
411    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
412
413    // transition related:
414    private Object mSceneWithHeaders;
415    private Object mSceneWithoutHeaders;
416    private Object mSceneAfterEntranceTransition;
417    private Object mHeadersTransition;
418    private BackStackListener mBackStackChangedListener;
419    private BrowseTransitionListener mBrowseTransitionListener;
420
421    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
422    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
423    private static final String ARG_HEADERS_STATE =
424        BrowseFragment.class.getCanonicalName() + ".headersState";
425
426    /**
427     * Creates arguments for a browse fragment.
428     *
429     * @param args The Bundle to place arguments into, or null if the method
430     *        should return a new Bundle.
431     * @param title The title of the BrowseFragment.
432     * @param headersState The initial state of the headers of the
433     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
434     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
435     * @return A Bundle with the given arguments for creating a BrowseFragment.
436     */
437    public static Bundle createArgs(Bundle args, String title, int headersState) {
438        if (args == null) {
439            args = new Bundle();
440        }
441        args.putString(ARG_TITLE, title);
442        args.putInt(ARG_HEADERS_STATE, headersState);
443        return args;
444    }
445
446    /**
447     * Sets the brand color for the browse fragment. The brand color is used as
448     * the primary color for UI elements in the browse fragment. For example,
449     * the background color of the headers fragment uses the brand color.
450     *
451     * @param color The color to use as the brand color of the fragment.
452     */
453    public void setBrandColor(@ColorInt int color) {
454        mBrandColor = color;
455        mBrandColorSet = true;
456
457        if (mHeadersFragment != null) {
458            mHeadersFragment.setBackgroundColor(mBrandColor);
459        }
460    }
461
462    /**
463     * Returns the brand color for the browse fragment.
464     * The default is transparent.
465     */
466    @ColorInt
467    public int getBrandColor() {
468        return mBrandColor;
469    }
470
471    /**
472     * Sets the adapter containing the rows for the fragment.
473     *
474     * <p>The items referenced by the adapter must be be derived from
475     * {@link Row}. These rows will be used by the rows fragment and the headers
476     * fragment (if not disabled) to render the browse rows.
477     *
478     * @param adapter An ObjectAdapter for the browse rows. All items must
479     *        derive from {@link Row}.
480     */
481    public void setAdapter(ObjectAdapter adapter) {
482        mAdapter = adapter;
483        if (mMainFragment != null) {
484            mMainFragmentAdapter.setAdapter(adapter);
485            mHeadersFragment.setAdapter(adapter);
486        }
487    }
488
489    public void setMainFragmentAdapterFactory(MainFragmentAdapterFactory factory) {
490        this.mMainFragmentAdapterFactory = factory;
491    }
492    /**
493     * Returns the adapter containing the rows for the fragment.
494     */
495    public ObjectAdapter getAdapter() {
496        return mAdapter;
497    }
498
499    /**
500     * Sets an item selection listener.
501     */
502    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
503        mExternalOnItemViewSelectedListener = listener;
504    }
505
506    /**
507     * Returns an item selection listener.
508     */
509    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
510        return mExternalOnItemViewSelectedListener;
511    }
512
513    /**
514     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
515     * not been created yet or a different fragment is bound to it.
516     *
517     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
518     */
519    public RowsFragment getRowsFragment() {
520        if (mMainFragment instanceof RowsFragment) {
521            return (RowsFragment) mMainFragment;
522        }
523
524        return null;
525    }
526
527    /**
528     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
529     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
530     */
531    public HeadersFragment getHeadersFragment() {
532        return mHeadersFragment;
533    }
534
535    /**
536     * Sets an item clicked listener on the fragment.
537     * OnItemViewClickedListener will override {@link View.OnClickListener} that
538     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
539     * So in general,  developer should choose one of the listeners but not both.
540     */
541    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
542        mOnItemViewClickedListener = listener;
543        if (mMainFragmentAdapter != null) {
544            mMainFragmentAdapter.setOnItemViewClickedListener(listener);
545        }
546    }
547
548    /**
549     * Returns the item Clicked listener.
550     */
551    public OnItemViewClickedListener getOnItemViewClickedListener() {
552        return mOnItemViewClickedListener;
553    }
554
555    /**
556     * Starts a headers transition.
557     *
558     * <p>This method will begin a transition to either show or hide the
559     * headers, depending on the value of withHeaders. If headers are disabled
560     * for this browse fragment, this method will throw an exception.
561     *
562     * @param withHeaders True if the headers should transition to being shown,
563     *        false if the transition should result in headers being hidden.
564     */
565    public void startHeadersTransition(boolean withHeaders) {
566        if (!mCanShowHeaders) {
567            throw new IllegalStateException("Cannot start headers transition");
568        }
569        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
570            return;
571        }
572        startHeadersTransitionInternal(withHeaders);
573    }
574
575    /**
576     * Returns true if the headers transition is currently running.
577     */
578    public boolean isInHeadersTransition() {
579        return mHeadersTransition != null;
580    }
581
582    /**
583     * Returns true if headers are shown.
584     */
585    public boolean isShowingHeaders() {
586        return mShowingHeaders;
587    }
588
589    /**
590     * Sets a listener for browse fragment transitions.
591     *
592     * @param listener The listener to call when a browse headers transition
593     *        begins or ends.
594     */
595    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
596        mBrowseTransitionListener = listener;
597    }
598
599    /**
600     * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead.
601     *
602     * @param enable true to enable row scaling
603     */
604    public void enableRowScaling(boolean enable) {
605        enableMainFragmentScaling(enable);
606    }
607
608    /**
609     * Enables scaling of main fragment when headers are present. For the page/row fragment,
610     * scaling is enabled only when both this method and
611     * {@link AbstractMainFragmentAdapter#isScalingEnabled()} are enabled.
612     *
613     * @param enable true to enable row scaling
614     */
615    public void enableMainFragmentScaling(boolean enable) {
616        mMainFragmentScaleEnabled = enable;
617    }
618
619    private void startHeadersTransitionInternal(final boolean withHeaders) {
620        if (getFragmentManager().isDestroyed()) {
621            return;
622        }
623        mShowingHeaders = withHeaders;
624        mMainFragmentAdapter.onTransitionPrepare();
625        mMainFragmentAdapter.onTransitionStart();
626        onExpandTransitionStart(!withHeaders, new Runnable() {
627            @Override
628            public void run() {
629                mHeadersFragment.onTransitionPrepare();
630                mHeadersFragment.onTransitionStart();
631                createHeadersTransition();
632                if (mBrowseTransitionListener != null) {
633                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
634                }
635                TransitionHelper.runTransition(
636                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
637                if (mHeadersBackStackEnabled) {
638                    if (!withHeaders) {
639                        getFragmentManager().beginTransaction()
640                                .addToBackStack(mWithHeadersBackStackName).commit();
641                    } else {
642                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
643                        if (index >= 0) {
644                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
645                            getFragmentManager().popBackStackImmediate(entry.getId(),
646                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
647                        }
648                    }
649                }
650            }
651        });
652    }
653
654    boolean isVerticalScrolling() {
655        // don't run transition
656        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
657    }
658
659
660    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
661            new BrowseFrameLayout.OnFocusSearchListener() {
662        @Override
663        public View onFocusSearch(View focused, int direction) {
664            // if headers is running transition,  focus stays
665            if (mCanShowHeaders && isInHeadersTransition()) {
666                return focused;
667            }
668            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
669
670            if (getTitleView() != null && focused != getTitleView() &&
671                    direction == View.FOCUS_UP) {
672                return getTitleView();
673            }
674            if (getTitleView() != null && getTitleView().hasFocus() &&
675                    direction == View.FOCUS_DOWN) {
676                return mCanShowHeaders && mShowingHeaders ?
677                        mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
678            }
679
680            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
681            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
682            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
683            if (mCanShowHeaders && direction == towardStart) {
684                if (isVerticalScrolling() || mShowingHeaders) {
685                    return focused;
686                }
687                return mHeadersFragment.getVerticalGridView();
688            } else if (direction == towardEnd) {
689                if (isVerticalScrolling()) {
690                    return focused;
691                }
692                return mMainFragment.getView();
693            } else {
694                return null;
695            }
696        }
697    };
698
699    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
700            new BrowseFrameLayout.OnChildFocusListener() {
701
702        @Override
703        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
704            if (getChildFragmentManager().isDestroyed()) {
705                return true;
706            }
707            // Make sure not changing focus when requestFocus() is called.
708            if (mCanShowHeaders && mShowingHeaders) {
709                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
710                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
711                    return true;
712                }
713            }
714            if (mMainFragment != null && mMainFragment.getView() != null &&
715                    mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
716                return true;
717            }
718            if (getTitleView() != null &&
719                    getTitleView().requestFocus(direction, previouslyFocusedRect)) {
720                return true;
721            }
722            return false;
723        }
724
725        @Override
726        public void onRequestChildFocus(View child, View focused) {
727            if (getChildFragmentManager().isDestroyed()) {
728                return;
729            }
730            if (!mCanShowHeaders || isInHeadersTransition()) return;
731            int childId = child.getId();
732            if (childId == R.id.browse_container_dock && mShowingHeaders) {
733                startHeadersTransitionInternal(false);
734            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
735                startHeadersTransitionInternal(true);
736            }
737        }
738    };
739
740    @Override
741    public void onSaveInstanceState(Bundle outState) {
742        super.onSaveInstanceState(outState);
743        if (mBackStackChangedListener != null) {
744            mBackStackChangedListener.save(outState);
745        } else {
746            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
747        }
748    }
749
750    @Override
751    public void onCreate(Bundle savedInstanceState) {
752        super.onCreate(savedInstanceState);
753        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
754        mContainerListMarginStart = (int) ta.getDimension(
755                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
756                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
757        mContainerListAlignTop = (int) ta.getDimension(
758                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
759                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
760        ta.recycle();
761
762        readArguments(getArguments());
763
764        if (mCanShowHeaders) {
765            if (mHeadersBackStackEnabled) {
766                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
767                mBackStackChangedListener = new BackStackListener();
768                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
769                mBackStackChangedListener.load(savedInstanceState);
770            } else {
771                if (savedInstanceState != null) {
772                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
773                }
774            }
775        }
776
777        mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
778    }
779
780    @Override
781    public void onDestroy() {
782        if (mBackStackChangedListener != null) {
783            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
784        }
785        super.onDestroy();
786    }
787
788    @Override
789    public View onCreateView(LayoutInflater inflater, ViewGroup container,
790            Bundle savedInstanceState) {
791        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
792            mHeadersFragment = new HeadersFragment();
793            mMainFragmentAdapter = mMainFragmentAdapterFactory.getAdapter(
794                    mAdapter, mSelectedPosition);
795            mMainFragment = mMainFragmentAdapter.getFragment();
796            getChildFragmentManager().beginTransaction()
797                    .replace(R.id.browse_headers_dock, mHeadersFragment)
798                    .replace(R.id.scale_frame, mMainFragment)
799                    .commit();
800        } else {
801            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
802                    .findFragmentById(R.id.scale_frame);
803            mMainFragment = getChildFragmentManager()
804                    .findFragmentById(R.id.browse_container_dock);
805        }
806
807        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
808        if (mHeaderPresenterSelector != null) {
809            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
810        }
811        mHeadersFragment.setAdapter(mAdapter);
812        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
813        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
814
815        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
816
817        setTitleView((TitleView) root.findViewById(R.id.browse_title_group));
818
819        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
820        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
821        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
822
823        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
824        mScaleFrameLayout.setPivotX(0);
825        mScaleFrameLayout.setPivotY(mContainerListAlignTop);
826
827        setupMainFragment();
828
829        if (mBrandColorSet) {
830            mHeadersFragment.setBackgroundColor(mBrandColor);
831        }
832
833        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
834            @Override
835            public void run() {
836                showHeaders(true);
837            }
838        });
839        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
840            @Override
841            public void run() {
842                showHeaders(false);
843            }
844        });
845        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
846            @Override
847            public void run() {
848                setEntranceTransitionEndState();
849            }
850        });
851        return root;
852    }
853
854    private void setupMainFragment() {
855        mMainFragmentAdapter.setAdapter(mAdapter);
856        mMainFragmentAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener);
857        mMainFragmentAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
858    }
859
860    private void createHeadersTransition() {
861        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
862                mShowingHeaders ?
863                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
864
865        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
866            @Override
867            public void onTransitionStart(Object transition) {
868            }
869            @Override
870            public void onTransitionEnd(Object transition) {
871                mHeadersTransition = null;
872                mMainFragmentAdapter.onTransitionEnd();
873                mHeadersFragment.onTransitionEnd();
874                if (mShowingHeaders) {
875                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
876                    if (headerGridView != null && !headerGridView.hasFocus()) {
877                        headerGridView.requestFocus();
878                    }
879                } else {
880                    View rowsGridView = mMainFragment.getView();
881                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
882                        rowsGridView.requestFocus();
883                    }
884                }
885                if (mBrowseTransitionListener != null) {
886                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
887                }
888            }
889        });
890    }
891
892    /**
893     * Sets the {@link PresenterSelector} used to render the row headers.
894     *
895     * @param headerPresenterSelector The PresenterSelector that will determine
896     *        the Presenter for each row header.
897     */
898    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
899        mHeaderPresenterSelector = headerPresenterSelector;
900        if (mHeadersFragment != null) {
901            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
902        }
903    }
904
905    private void setHeadersOnScreen(boolean onScreen) {
906        MarginLayoutParams lp;
907        View containerList;
908        containerList = mHeadersFragment.getView();
909        lp = (MarginLayoutParams) containerList.getLayoutParams();
910        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
911        containerList.setLayoutParams(lp);
912    }
913
914    private void showHeaders(boolean show) {
915        if (DEBUG) Log.v(TAG, "showHeaders " + show);
916        mHeadersFragment.setHeadersEnabled(show);
917        setHeadersOnScreen(show);
918        expandMainFragment(!show);
919    }
920
921    private void expandMainFragment(boolean expand) {
922        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
923        params.leftMargin = !expand ? mContainerListMarginStart : 0;
924        mScaleFrameLayout.setLayoutParams(params);
925        mMainFragmentAdapter.setExpand(expand);
926
927        setMainFragmentAlignment();
928        final float scaleFactor = !expand
929                && mMainFragmentScaleEnabled && mMainFragmentAdapter.isScalingEnabled()
930                ? mScaleFactor : 1;
931        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
932        mScaleFrameLayout.setChildScale(scaleFactor);
933    }
934
935    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
936        new HeadersFragment.OnHeaderClickedListener() {
937            @Override
938            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
939                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
940                    return;
941                }
942                startHeadersTransitionInternal(false);
943                mMainFragment.getView().requestFocus();
944            }
945        };
946
947    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
948        @Override
949        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
950                RowPresenter.ViewHolder rowViewHolder, Row row) {
951            if (mMainFragment == null) {
952                return;
953            }
954
955            int position = ((RowsFragment) mMainFragment)
956                    .getVerticalGridView().getSelectedPosition();
957            if (DEBUG) Log.v(TAG, "row selected position " + position);
958            onRowSelected(position);
959            if (mExternalOnItemViewSelectedListener != null) {
960                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
961                        rowViewHolder, row);
962            }
963        }
964    };
965
966    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
967            new HeadersFragment.OnHeaderViewSelectedListener() {
968        @Override
969        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
970            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
971            if (DEBUG) Log.v(TAG, "header selected position " + position);
972            onRowSelected(position);
973        }
974    };
975
976    private void onRowSelected(int position) {
977        if (position != mSelectedPosition) {
978            mSetSelectionRunnable.post(
979                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
980
981            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
982                showTitle(true);
983            } else {
984                showTitle(false);
985            }
986        }
987    }
988
989    private void setSelection(int position, boolean smooth) {
990        if (position == NO_POSITION) {
991            return;
992        }
993
994        mHeadersFragment.setSelectedPosition(position, smooth);
995        AbstractMainFragmentAdapter newFragmentAdapter = mMainFragmentAdapterFactory.getAdapter(
996                mAdapter, position);
997        if (mMainFragmentAdapter != newFragmentAdapter) {
998            mMainFragmentAdapter = newFragmentAdapter;
999            mMainFragment = mMainFragmentAdapter.getFragment();
1000            swapBrowseContent(mMainFragment);
1001            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
1002            setupMainFragment();
1003            mMainFragmentAdapter.setAlignment(mContainerListAlignTop);
1004        }
1005        mMainFragmentAdapter.setSelectedPosition(position, smooth);
1006        mSelectedPosition = position;
1007    }
1008
1009    private void swapBrowseContent(Fragment fragment) {
1010        getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit();
1011    }
1012
1013    /**
1014     * Sets the selected row position with smooth animation.
1015     */
1016    public void setSelectedPosition(int position) {
1017        setSelectedPosition(position, true);
1018    }
1019
1020    /**
1021     * Gets position of currently selected row.
1022     * @return Position of currently selected row.
1023     */
1024    public int getSelectedPosition() {
1025        return mSelectedPosition;
1026    }
1027
1028    /**
1029     * Sets the selected row position.
1030     */
1031    public void setSelectedPosition(int position, boolean smooth) {
1032        mSetSelectionRunnable.post(
1033                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
1034    }
1035
1036    /**
1037     * Selects a Row and perform an optional task on the Row. For example
1038     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
1039     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
1040     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
1041     * ViewGroup, Bundle)}).
1042     *
1043     * @param rowPosition Which row to select.
1044     * @param smooth True to scroll to the row, false for no animation.
1045     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
1046     * fragment will be collapsed.
1047     */
1048    public void setSelectedPosition(int rowPosition, boolean smooth,
1049            final Presenter.ViewHolderTask rowHolderTask) {
1050        if (mMainFragmentAdapterFactory == null) {
1051            return;
1052        }
1053        if (rowHolderTask != null) {
1054            startHeadersTransition(false);
1055        }
1056        mMainFragmentAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
1057    }
1058
1059    @Override
1060    public void onStart() {
1061        super.onStart();
1062        mHeadersFragment.setAlignment(mContainerListAlignTop);
1063        setMainFragmentAlignment();
1064
1065        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
1066            mHeadersFragment.getView().requestFocus();
1067        } else if ((!mCanShowHeaders || !mShowingHeaders)
1068                && mMainFragment.getView() != null) {
1069            mMainFragment.getView().requestFocus();
1070        }
1071
1072        if (mCanShowHeaders) {
1073            showHeaders(mShowingHeaders);
1074        }
1075
1076        if (isEntranceTransitionEnabled()) {
1077            setEntranceTransitionStartState();
1078        }
1079    }
1080
1081    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
1082        if (expand) {
1083            callback.run();
1084            return;
1085        }
1086        // Run a "pre" layout when we go non-expand, in order to get the initial
1087        // positions of added rows.
1088        new ExpandPreLayout(callback, mMainFragmentAdapter).execute();
1089    }
1090
1091    private void setMainFragmentAlignment() {
1092        int alignOffset = mContainerListAlignTop;
1093        if (mMainFragmentScaleEnabled
1094                && mMainFragmentAdapter.isScalingEnabled()
1095                && mShowingHeaders) {
1096            alignOffset = (int) (alignOffset / mScaleFactor + 0.5f);
1097        }
1098        mMainFragmentAdapter.setAlignment(alignOffset);
1099    }
1100
1101    /**
1102     * Enables/disables headers transition on back key support. This is enabled by
1103     * default. The BrowseFragment will add a back stack entry when headers are
1104     * showing. Running a headers transition when the back key is pressed only
1105     * works when the headers state is {@link #HEADERS_ENABLED} or
1106     * {@link #HEADERS_HIDDEN}.
1107     * <p>
1108     * NOTE: If an Activity has its own onBackPressed() handling, you must
1109     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
1110     * and {@link BrowseTransitionListener} in your own back stack handling.
1111     */
1112    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
1113        mHeadersBackStackEnabled = headersBackStackEnabled;
1114    }
1115
1116    /**
1117     * Returns true if headers transition on back key support is enabled.
1118     */
1119    public final boolean isHeadersTransitionOnBackEnabled() {
1120        return mHeadersBackStackEnabled;
1121    }
1122
1123    private void readArguments(Bundle args) {
1124        if (args == null) {
1125            return;
1126        }
1127        if (args.containsKey(ARG_TITLE)) {
1128            setTitle(args.getString(ARG_TITLE));
1129        }
1130        if (args.containsKey(ARG_HEADERS_STATE)) {
1131            setHeadersState(args.getInt(ARG_HEADERS_STATE));
1132        }
1133    }
1134
1135    /**
1136     * Sets the state for the headers column in the browse fragment. Must be one
1137     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1138     * {@link #HEADERS_DISABLED}.
1139     *
1140     * @param headersState The state of the headers for the browse fragment.
1141     */
1142    public void setHeadersState(int headersState) {
1143        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1144            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1145        }
1146        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1147
1148        if (headersState != mHeadersState) {
1149            mHeadersState = headersState;
1150            switch (headersState) {
1151                case HEADERS_ENABLED:
1152                    mCanShowHeaders = true;
1153                    mShowingHeaders = true;
1154                    break;
1155                case HEADERS_HIDDEN:
1156                    mCanShowHeaders = true;
1157                    mShowingHeaders = false;
1158                    break;
1159                case HEADERS_DISABLED:
1160                    mCanShowHeaders = false;
1161                    mShowingHeaders = false;
1162                    break;
1163                default:
1164                    Log.w(TAG, "Unknown headers state: " + headersState);
1165                    break;
1166            }
1167            if (mHeadersFragment != null) {
1168                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1169            }
1170        }
1171    }
1172
1173    /**
1174     * Returns the state of the headers column in the browse fragment.
1175     */
1176    public int getHeadersState() {
1177        return mHeadersState;
1178    }
1179
1180    @Override
1181    protected Object createEntranceTransition() {
1182        return TransitionHelper.loadTransition(getActivity(),
1183                R.transition.lb_browse_entrance_transition);
1184    }
1185
1186    @Override
1187    protected void runEntranceTransition(Object entranceTransition) {
1188        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
1189    }
1190
1191    @Override
1192    protected void onEntranceTransitionPrepare() {
1193        mHeadersFragment.onTransitionPrepare();
1194        mMainFragmentAdapter.onTransitionPrepare();
1195    }
1196
1197    @Override
1198    protected void onEntranceTransitionStart() {
1199        mHeadersFragment.onTransitionStart();
1200        mMainFragmentAdapter.onTransitionStart();
1201    }
1202
1203    @Override
1204    protected void onEntranceTransitionEnd() {
1205        mMainFragmentAdapter.onTransitionEnd();
1206        mHeadersFragment.onTransitionEnd();
1207    }
1208
1209    void setSearchOrbViewOnScreen(boolean onScreen) {
1210        View searchOrbView = getTitleView().getSearchAffordanceView();
1211        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1212        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1213        searchOrbView.setLayoutParams(lp);
1214    }
1215
1216    void setEntranceTransitionStartState() {
1217        setHeadersOnScreen(false);
1218        setSearchOrbViewOnScreen(false);
1219        mMainFragmentAdapter.setEntranceTransitionState(false);
1220    }
1221
1222    void setEntranceTransitionEndState() {
1223        setHeadersOnScreen(mShowingHeaders);
1224        setSearchOrbViewOnScreen(true);
1225        mMainFragmentAdapter.setEntranceTransitionState(true);
1226    }
1227
1228    private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
1229
1230        private final View mView;
1231        private final Runnable mCallback;
1232        private int mState;
1233        private AbstractMainFragmentAdapter mainFragmentAdapter;
1234
1235        final static int STATE_INIT = 0;
1236        final static int STATE_FIRST_DRAW = 1;
1237        final static int STATE_SECOND_DRAW = 2;
1238
1239        ExpandPreLayout(Runnable callback, AbstractMainFragmentAdapter adapter) {
1240            mView = adapter.getFragment().getView();
1241            mCallback = callback;
1242            mainFragmentAdapter = adapter;
1243        }
1244
1245        void execute() {
1246            mView.getViewTreeObserver().addOnPreDrawListener(this);
1247            mainFragmentAdapter.setExpand(false);
1248            mState = STATE_INIT;
1249        }
1250
1251        @Override
1252        public boolean onPreDraw() {
1253            if (mView == null) {
1254                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1255                return true;
1256            }
1257            if (mState == STATE_INIT) {
1258                mainFragmentAdapter.setExpand(true);
1259                mState = STATE_FIRST_DRAW;
1260            } else if (mState == STATE_FIRST_DRAW) {
1261                mCallback.run();
1262                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1263                mState = STATE_SECOND_DRAW;
1264            }
1265            return false;
1266        }
1267    }
1268}
1269