BrowseFragment.java revision a9f6062bd2dd02b3de253b57c69302893bf1f2e3
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
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    /**
308     * Factory class for {@link BrowseFragment.AbstractMainFragmentAdapter}. Developers can provide
309     * a custom implementation into {@link BrowseFragment}. {@link BrowseFragment} will use this
310     * factory to create fragments {@link BrowseFragment.AbstractMainFragmentAdapter#getFragment()}
311     * to display on the main content section.
312     */
313    public static abstract class MainFragmentAdapterFactory {
314        private BrowseFragment.AbstractMainFragmentAdapter rowsFragmentAdapter;
315        private BrowseFragment.AbstractMainFragmentAdapter pageFragmentAdapter;
316
317        public BrowseFragment.AbstractMainFragmentAdapter getAdapter(
318                ObjectAdapter adapter, int position) {
319            if (adapter == null || adapter.size() == 0) {
320                return getRowsFragmentAdapter();
321            }
322
323            if (position < 0 || position > adapter.size()) {
324                throw new IllegalArgumentException(
325                        String.format("Invalid position %d requested", position));
326            }
327
328            Object item = adapter.get(position);
329            if (item instanceof PageRow) {
330                if (pageFragmentAdapter == null) {
331                    pageFragmentAdapter = getPageFragmentAdapter();
332                }
333                return pageFragmentAdapter;
334            } else {
335                return getRowsFragmentAdapter();
336            }
337        }
338
339        private BrowseFragment.AbstractMainFragmentAdapter getRowsFragmentAdapter() {
340            if (rowsFragmentAdapter == null) {
341                rowsFragmentAdapter = new RowsFragmentAdapter();
342            }
343            return rowsFragmentAdapter;
344        }
345
346        public abstract BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter();
347    }
348
349    private static final String TAG = "BrowseFragment";
350
351    private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
352
353    private static boolean DEBUG = false;
354
355    /** The headers fragment is enabled and shown by default. */
356    public static final int HEADERS_ENABLED = 1;
357
358    /** The headers fragment is enabled and hidden by default. */
359    public static final int HEADERS_HIDDEN = 2;
360
361    /** The headers fragment is disabled and will never be shown. */
362    public static final int HEADERS_DISABLED = 3;
363
364    private MainFragmentAdapterFactory mMainFragmentAdapterFactory
365            = new MainFragmentAdapterFactory() {
366        @Override
367        public BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter() {
368            return null;
369        }
370    };
371
372    private AbstractMainFragmentAdapter mMainFragmentAdapter;
373    private Fragment mMainFragment;
374    private HeadersFragment mHeadersFragment;
375
376    private ObjectAdapter mAdapter;
377
378    private int mHeadersState = HEADERS_ENABLED;
379    private int mBrandColor = Color.TRANSPARENT;
380    private boolean mBrandColorSet;
381
382    private BrowseFrameLayout mBrowseFrame;
383    private ScaleFrameLayout mScaleFrameLayout;
384    private boolean mHeadersBackStackEnabled = true;
385    private String mWithHeadersBackStackName;
386    private boolean mShowingHeaders = true;
387    private boolean mCanShowHeaders = true;
388    private int mContainerListMarginStart;
389    private int mContainerListAlignTop;
390    private boolean mRowScaleEnabled = true;
391    private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
392    private OnItemViewClickedListener mOnItemViewClickedListener;
393    private int mSelectedPosition = 0;
394    private float mRowScaleFactor;
395
396    private PresenterSelector mHeaderPresenterSelector;
397    private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
398
399    // transition related:
400    private Object mSceneWithHeaders;
401    private Object mSceneWithoutHeaders;
402    private Object mSceneAfterEntranceTransition;
403    private Object mHeadersTransition;
404    private BackStackListener mBackStackChangedListener;
405    private BrowseTransitionListener mBrowseTransitionListener;
406
407    private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
408    private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
409    private static final String ARG_HEADERS_STATE =
410        BrowseFragment.class.getCanonicalName() + ".headersState";
411
412    /**
413     * Creates arguments for a browse fragment.
414     *
415     * @param args The Bundle to place arguments into, or null if the method
416     *        should return a new Bundle.
417     * @param title The title of the BrowseFragment.
418     * @param headersState The initial state of the headers of the
419     *        BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
420     *        #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
421     * @return A Bundle with the given arguments for creating a BrowseFragment.
422     */
423    public static Bundle createArgs(Bundle args, String title, int headersState) {
424        if (args == null) {
425            args = new Bundle();
426        }
427        args.putString(ARG_TITLE, title);
428        args.putInt(ARG_HEADERS_STATE, headersState);
429        return args;
430    }
431
432    /**
433     * Sets the brand color for the browse fragment. The brand color is used as
434     * the primary color for UI elements in the browse fragment. For example,
435     * the background color of the headers fragment uses the brand color.
436     *
437     * @param color The color to use as the brand color of the fragment.
438     */
439    public void setBrandColor(@ColorInt int color) {
440        mBrandColor = color;
441        mBrandColorSet = true;
442
443        if (mHeadersFragment != null) {
444            mHeadersFragment.setBackgroundColor(mBrandColor);
445        }
446    }
447
448    /**
449     * Returns the brand color for the browse fragment.
450     * The default is transparent.
451     */
452    @ColorInt
453    public int getBrandColor() {
454        return mBrandColor;
455    }
456
457    /**
458     * Sets the adapter containing the rows for the fragment.
459     *
460     * <p>The items referenced by the adapter must be be derived from
461     * {@link Row}. These rows will be used by the rows fragment and the headers
462     * fragment (if not disabled) to render the browse rows.
463     *
464     * @param adapter An ObjectAdapter for the browse rows. All items must
465     *        derive from {@link Row}.
466     */
467    public void setAdapter(ObjectAdapter adapter) {
468        mAdapter = adapter;
469        if (mMainFragment != null) {
470            mMainFragmentAdapter.setAdapter(adapter);
471            mHeadersFragment.setAdapter(adapter);
472        }
473    }
474
475    public void setMainFragmentAdapterFactory(MainFragmentAdapterFactory factory) {
476        this.mMainFragmentAdapterFactory = factory;
477    }
478    /**
479     * Returns the adapter containing the rows for the fragment.
480     */
481    public ObjectAdapter getAdapter() {
482        return mAdapter;
483    }
484
485    /**
486     * Sets an item selection listener.
487     */
488    public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
489        mExternalOnItemViewSelectedListener = listener;
490    }
491
492    /**
493     * Returns an item selection listener.
494     */
495    public OnItemViewSelectedListener getOnItemViewSelectedListener() {
496        return mExternalOnItemViewSelectedListener;
497    }
498
499    /**
500     * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has
501     * not been created yet or a different fragment is bound to it.
502     *
503     * @return RowsFragment if it's bound to BrowseFragment or null otherwise.
504     */
505    public RowsFragment getRowsFragment() {
506        if (mMainFragment instanceof RowsFragment) {
507            return (RowsFragment) mMainFragment;
508        }
509
510        return null;
511    }
512
513    /**
514     * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet.
515     * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet.
516     */
517    public HeadersFragment getHeadersFragment() {
518        return mHeadersFragment;
519    }
520
521    /**
522     * Sets an item clicked listener on the fragment.
523     * OnItemViewClickedListener will override {@link View.OnClickListener} that
524     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
525     * So in general,  developer should choose one of the listeners but not both.
526     */
527    public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
528        mOnItemViewClickedListener = listener;
529        if (mMainFragmentAdapter != null) {
530            mMainFragmentAdapter.setOnItemViewClickedListener(listener);
531        }
532    }
533
534    /**
535     * Returns the item Clicked listener.
536     */
537    public OnItemViewClickedListener getOnItemViewClickedListener() {
538        return mOnItemViewClickedListener;
539    }
540
541    /**
542     * Starts a headers transition.
543     *
544     * <p>This method will begin a transition to either show or hide the
545     * headers, depending on the value of withHeaders. If headers are disabled
546     * for this browse fragment, this method will throw an exception.
547     *
548     * @param withHeaders True if the headers should transition to being shown,
549     *        false if the transition should result in headers being hidden.
550     */
551    public void startHeadersTransition(boolean withHeaders) {
552        if (!mCanShowHeaders) {
553            throw new IllegalStateException("Cannot start headers transition");
554        }
555        if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
556            return;
557        }
558        startHeadersTransitionInternal(withHeaders);
559    }
560
561    /**
562     * Returns true if the headers transition is currently running.
563     */
564    public boolean isInHeadersTransition() {
565        return mHeadersTransition != null;
566    }
567
568    /**
569     * Returns true if headers are shown.
570     */
571    public boolean isShowingHeaders() {
572        return mShowingHeaders;
573    }
574
575    /**
576     * Sets a listener for browse fragment transitions.
577     *
578     * @param listener The listener to call when a browse headers transition
579     *        begins or ends.
580     */
581    public void setBrowseTransitionListener(BrowseTransitionListener listener) {
582        mBrowseTransitionListener = listener;
583    }
584
585    /**
586     * Enables scaling of rows when headers are present.
587     * By default enabled to increase density.
588     *
589     * @param enable true to enable row scaling
590     */
591    public void enableRowScaling(boolean enable) {
592        mRowScaleEnabled = enable;
593    }
594
595    private void startHeadersTransitionInternal(final boolean withHeaders) {
596        if (getFragmentManager().isDestroyed()) {
597            return;
598        }
599        mShowingHeaders = withHeaders;
600        mMainFragmentAdapter.onTransitionPrepare();
601        mMainFragmentAdapter.onTransitionStart();
602        onExpandTransitionStart(!withHeaders, new Runnable() {
603            @Override
604            public void run() {
605                mHeadersFragment.onTransitionPrepare();
606                mHeadersFragment.onTransitionStart();
607                createHeadersTransition();
608                if (mBrowseTransitionListener != null) {
609                    mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
610                }
611                TransitionHelper.runTransition(
612                        withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition);
613                if (mHeadersBackStackEnabled) {
614                    if (!withHeaders) {
615                        getFragmentManager().beginTransaction()
616                                .addToBackStack(mWithHeadersBackStackName).commit();
617                    } else {
618                        int index = mBackStackChangedListener.mIndexOfHeadersBackStack;
619                        if (index >= 0) {
620                            BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index);
621                            getFragmentManager().popBackStackImmediate(entry.getId(),
622                                    FragmentManager.POP_BACK_STACK_INCLUSIVE);
623                        }
624                    }
625                }
626            }
627        });
628    }
629
630    boolean isVerticalScrolling() {
631        // don't run transition
632        return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling();
633    }
634
635
636    private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
637            new BrowseFrameLayout.OnFocusSearchListener() {
638        @Override
639        public View onFocusSearch(View focused, int direction) {
640            // if headers is running transition,  focus stays
641            if (mCanShowHeaders && isInHeadersTransition()) {
642                return focused;
643            }
644            if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
645
646            if (getTitleView() != null && focused != getTitleView() &&
647                    direction == View.FOCUS_UP) {
648                return getTitleView();
649            }
650            if (getTitleView() != null && getTitleView().hasFocus() &&
651                    direction == View.FOCUS_DOWN) {
652                return mCanShowHeaders && mShowingHeaders ?
653                        mHeadersFragment.getVerticalGridView() : mMainFragment.getView();
654            }
655
656            boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL;
657            int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
658            int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT;
659            if (mCanShowHeaders && direction == towardStart) {
660                if (isVerticalScrolling() || mShowingHeaders) {
661                    return focused;
662                }
663                return mHeadersFragment.getVerticalGridView();
664            } else if (direction == towardEnd) {
665                if (isVerticalScrolling()) {
666                    return focused;
667                }
668                return mMainFragment.getView();
669            } else {
670                return null;
671            }
672        }
673    };
674
675    private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
676            new BrowseFrameLayout.OnChildFocusListener() {
677
678        @Override
679        public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
680            if (getChildFragmentManager().isDestroyed()) {
681                return true;
682            }
683            // Make sure not changing focus when requestFocus() is called.
684            if (mCanShowHeaders && mShowingHeaders) {
685                if (mHeadersFragment != null && mHeadersFragment.getView() != null &&
686                        mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
687                    return true;
688                }
689            }
690            if (mMainFragment != null && mMainFragment.getView() != null &&
691                    mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
692                return true;
693            }
694            if (getTitleView() != null &&
695                    getTitleView().requestFocus(direction, previouslyFocusedRect)) {
696                return true;
697            }
698            return false;
699        }
700
701        @Override
702        public void onRequestChildFocus(View child, View focused) {
703            if (getChildFragmentManager().isDestroyed()) {
704                return;
705            }
706            if (!mCanShowHeaders || isInHeadersTransition()) return;
707            int childId = child.getId();
708            if (childId == R.id.browse_container_dock && mShowingHeaders) {
709                startHeadersTransitionInternal(false);
710            } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
711                startHeadersTransitionInternal(true);
712            }
713        }
714    };
715
716    @Override
717    public void onSaveInstanceState(Bundle outState) {
718        super.onSaveInstanceState(outState);
719        if (mBackStackChangedListener != null) {
720            mBackStackChangedListener.save(outState);
721        } else {
722            outState.putBoolean(HEADER_SHOW, mShowingHeaders);
723        }
724    }
725
726    @Override
727    public void onCreate(Bundle savedInstanceState) {
728        super.onCreate(savedInstanceState);
729        TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
730        mContainerListMarginStart = (int) ta.getDimension(
731                R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources()
732                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start));
733        mContainerListAlignTop = (int) ta.getDimension(
734                R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources()
735                .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top));
736        ta.recycle();
737
738        readArguments(getArguments());
739
740        if (mCanShowHeaders) {
741            if (mHeadersBackStackEnabled) {
742                mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
743                mBackStackChangedListener = new BackStackListener();
744                getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
745                mBackStackChangedListener.load(savedInstanceState);
746            } else {
747                if (savedInstanceState != null) {
748                    mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW);
749                }
750            }
751        }
752
753        mRowScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1);
754    }
755
756    @Override
757    public void onDestroy() {
758        if (mBackStackChangedListener != null) {
759            getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
760        }
761        super.onDestroy();
762    }
763
764    @Override
765    public View onCreateView(LayoutInflater inflater, ViewGroup container,
766            Bundle savedInstanceState) {
767        if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) {
768            mHeadersFragment = new HeadersFragment();
769            mMainFragmentAdapter = mMainFragmentAdapterFactory.getAdapter(
770                    mAdapter, mSelectedPosition);
771            mMainFragment = mMainFragmentAdapter.getFragment();
772            getChildFragmentManager().beginTransaction()
773                    .replace(R.id.browse_headers_dock, mHeadersFragment)
774                    .replace(R.id.scale_frame, mMainFragment)
775                    .commit();
776        } else {
777            mHeadersFragment = (HeadersFragment) getChildFragmentManager()
778                    .findFragmentById(R.id.scale_frame);
779            mMainFragment = getChildFragmentManager()
780                    .findFragmentById(R.id.browse_container_dock);
781        }
782
783        mHeadersFragment.setHeadersGone(!mCanShowHeaders);
784        if (mHeaderPresenterSelector != null) {
785            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
786        }
787        mHeadersFragment.setAdapter(mAdapter);
788        mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener);
789        mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
790
791        View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
792
793        setTitleView((TitleView) root.findViewById(R.id.browse_title_group));
794
795        mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
796        mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
797        mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
798
799        mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame);
800        mScaleFrameLayout.setPivotX(0);
801        mScaleFrameLayout.setPivotY(mContainerListAlignTop);
802
803        setupMainFragment();
804
805        if (mBrandColorSet) {
806            mHeadersFragment.setBackgroundColor(mBrandColor);
807        }
808
809        mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
810            @Override
811            public void run() {
812                showHeaders(true);
813            }
814        });
815        mSceneWithoutHeaders =  TransitionHelper.createScene(mBrowseFrame, new Runnable() {
816            @Override
817            public void run() {
818                showHeaders(false);
819            }
820        });
821        mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() {
822            @Override
823            public void run() {
824                setEntranceTransitionEndState();
825            }
826        });
827        return root;
828    }
829
830    private void setupMainFragment() {
831        mMainFragmentAdapter.setAdapter(mAdapter);
832        mMainFragmentAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener);
833        mMainFragmentAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener);
834    }
835
836    private void createHeadersTransition() {
837        mHeadersTransition = TransitionHelper.loadTransition(getActivity(),
838                mShowingHeaders ?
839                R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out);
840
841        TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() {
842            @Override
843            public void onTransitionStart(Object transition) {
844            }
845            @Override
846            public void onTransitionEnd(Object transition) {
847                mHeadersTransition = null;
848                mMainFragmentAdapter.onTransitionEnd();
849                mHeadersFragment.onTransitionEnd();
850                if (mShowingHeaders) {
851                    VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
852                    if (headerGridView != null && !headerGridView.hasFocus()) {
853                        headerGridView.requestFocus();
854                    }
855                } else {
856                    View rowsGridView = mMainFragment.getView();
857                    if (rowsGridView != null && !rowsGridView.hasFocus()) {
858                        rowsGridView.requestFocus();
859                    }
860                }
861                if (mBrowseTransitionListener != null) {
862                    mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
863                }
864            }
865        });
866    }
867
868    /**
869     * Sets the {@link PresenterSelector} used to render the row headers.
870     *
871     * @param headerPresenterSelector The PresenterSelector that will determine
872     *        the Presenter for each row header.
873     */
874    public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
875        mHeaderPresenterSelector = headerPresenterSelector;
876        if (mHeadersFragment != null) {
877            mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
878        }
879    }
880
881    private void setHeadersOnScreen(boolean onScreen) {
882        MarginLayoutParams lp;
883        View containerList;
884        containerList = mHeadersFragment.getView();
885        lp = (MarginLayoutParams) containerList.getLayoutParams();
886        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
887        containerList.setLayoutParams(lp);
888    }
889
890    private void showHeaders(boolean show) {
891        if (DEBUG) Log.v(TAG, "showHeaders " + show);
892        mHeadersFragment.setHeadersEnabled(show);
893        setHeadersOnScreen(show);
894        expandMainFragment(!show);
895    }
896
897    private void expandMainFragment(boolean expand) {
898        MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams();
899        params.leftMargin = !expand ? mContainerListMarginStart : 0;
900        mScaleFrameLayout.setLayoutParams(params);
901        mMainFragmentAdapter.setExpand(expand);
902
903        setMainFragmentAlignment();
904        final float scaleFactor = !expand ? mRowScaleFactor : 1;
905        mScaleFrameLayout.setLayoutScaleY(scaleFactor);
906        mScaleFrameLayout.setChildScale(scaleFactor);
907    }
908
909    private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
910        new HeadersFragment.OnHeaderClickedListener() {
911            @Override
912            public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
913                if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
914                    return;
915                }
916                startHeadersTransitionInternal(false);
917                mMainFragment.getView().requestFocus();
918            }
919        };
920
921    private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
922        @Override
923        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
924                RowPresenter.ViewHolder rowViewHolder, Row row) {
925            if (mMainFragment == null) {
926                return;
927            }
928
929            int position = ((RowsFragment) mMainFragment)
930                    .getVerticalGridView().getSelectedPosition();
931            if (DEBUG) Log.v(TAG, "row selected position " + position);
932            onRowSelected(position);
933            if (mExternalOnItemViewSelectedListener != null) {
934                mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
935                        rowViewHolder, row);
936            }
937        }
938    };
939
940    private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener =
941            new HeadersFragment.OnHeaderViewSelectedListener() {
942        @Override
943        public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) {
944            int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
945            if (DEBUG) Log.v(TAG, "header selected position " + position);
946            onRowSelected(position);
947        }
948    };
949
950    private void onRowSelected(int position) {
951        if (position != mSelectedPosition) {
952            mSetSelectionRunnable.post(
953                    position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true);
954
955            if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
956                showTitle(true);
957            } else {
958                showTitle(false);
959            }
960        }
961    }
962
963    private void setSelection(int position, boolean smooth) {
964        if (position == NO_POSITION) {
965            return;
966        }
967
968        mHeadersFragment.setSelectedPosition(position, smooth);
969        AbstractMainFragmentAdapter newFragmentAdapter = mMainFragmentAdapterFactory.getAdapter(
970                mAdapter, position);
971        if (mMainFragmentAdapter != newFragmentAdapter) {
972            mMainFragmentAdapter = newFragmentAdapter;
973            mMainFragment = mMainFragmentAdapter.getFragment();
974            swapBrowseContent(mMainFragment);
975            expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
976            setupMainFragment();
977            mMainFragmentAdapter.setAlignment(mContainerListAlignTop);
978        }
979        mMainFragmentAdapter.setSelectedPosition(position, smooth);
980        mSelectedPosition = position;
981    }
982
983    private void swapBrowseContent(Fragment fragment) {
984        getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit();
985    }
986
987    /**
988     * Sets the selected row position with smooth animation.
989     */
990    public void setSelectedPosition(int position) {
991        setSelectedPosition(position, true);
992    }
993
994    /**
995     * Gets position of currently selected row.
996     * @return Position of currently selected row.
997     */
998    public int getSelectedPosition() {
999        return mSelectedPosition;
1000    }
1001
1002    /**
1003     * Sets the selected row position.
1004     */
1005    public void setSelectedPosition(int position, boolean smooth) {
1006        mSetSelectionRunnable.post(
1007                position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth);
1008    }
1009
1010    /**
1011     * Selects a Row and perform an optional task on the Row. For example
1012     * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code>
1013     * scrolls to 11th row and selects 6th item on that row.  The method will be ignored if
1014     * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater,
1015     * ViewGroup, Bundle)}).
1016     *
1017     * @param rowPosition Which row to select.
1018     * @param smooth True to scroll to the row, false for no animation.
1019     * @param rowHolderTask Optional task to perform on the Row.  When the task is not null, headers
1020     * fragment will be collapsed.
1021     */
1022    public void setSelectedPosition(int rowPosition, boolean smooth,
1023            final Presenter.ViewHolderTask rowHolderTask) {
1024        if (mMainFragmentAdapterFactory == null) {
1025            return;
1026        }
1027        if (rowHolderTask != null) {
1028            startHeadersTransition(false);
1029        }
1030        mMainFragmentAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask);
1031    }
1032
1033    @Override
1034    public void onStart() {
1035        super.onStart();
1036        mHeadersFragment.setAlignment(mContainerListAlignTop);
1037        setMainFragmentAlignment();
1038
1039        if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
1040            mHeadersFragment.getView().requestFocus();
1041        } else if ((!mCanShowHeaders || !mShowingHeaders)
1042                && mMainFragment.getView() != null) {
1043            mMainFragment.getView().requestFocus();
1044        }
1045
1046        if (mCanShowHeaders) {
1047            showHeaders(mShowingHeaders);
1048        }
1049
1050        if (isEntranceTransitionEnabled()) {
1051            setEntranceTransitionStartState();
1052        }
1053    }
1054
1055    private void onExpandTransitionStart(boolean expand, final Runnable callback) {
1056        if (expand) {
1057            callback.run();
1058            return;
1059        }
1060        // Run a "pre" layout when we go non-expand, in order to get the initial
1061        // positions of added rows.
1062        new ExpandPreLayout(callback, mMainFragmentAdapter).execute();
1063    }
1064
1065    private void setMainFragmentAlignment() {
1066        int alignOffset = mContainerListAlignTop;
1067        if (mRowScaleEnabled && mShowingHeaders) {
1068            alignOffset = (int) (alignOffset / mRowScaleFactor + 0.5f);
1069        }
1070        mMainFragmentAdapter.setAlignment(alignOffset);
1071    }
1072
1073    /**
1074     * Enables/disables headers transition on back key support. This is enabled by
1075     * default. The BrowseFragment will add a back stack entry when headers are
1076     * showing. Running a headers transition when the back key is pressed only
1077     * works when the headers state is {@link #HEADERS_ENABLED} or
1078     * {@link #HEADERS_HIDDEN}.
1079     * <p>
1080     * NOTE: If an Activity has its own onBackPressed() handling, you must
1081     * disable this feature. You may use {@link #startHeadersTransition(boolean)}
1082     * and {@link BrowseTransitionListener} in your own back stack handling.
1083     */
1084    public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
1085        mHeadersBackStackEnabled = headersBackStackEnabled;
1086    }
1087
1088    /**
1089     * Returns true if headers transition on back key support is enabled.
1090     */
1091    public final boolean isHeadersTransitionOnBackEnabled() {
1092        return mHeadersBackStackEnabled;
1093    }
1094
1095    private void readArguments(Bundle args) {
1096        if (args == null) {
1097            return;
1098        }
1099        if (args.containsKey(ARG_TITLE)) {
1100            setTitle(args.getString(ARG_TITLE));
1101        }
1102        if (args.containsKey(ARG_HEADERS_STATE)) {
1103            setHeadersState(args.getInt(ARG_HEADERS_STATE));
1104        }
1105    }
1106
1107    /**
1108     * Sets the state for the headers column in the browse fragment. Must be one
1109     * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
1110     * {@link #HEADERS_DISABLED}.
1111     *
1112     * @param headersState The state of the headers for the browse fragment.
1113     */
1114    public void setHeadersState(int headersState) {
1115        if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
1116            throw new IllegalArgumentException("Invalid headers state: " + headersState);
1117        }
1118        if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
1119
1120        if (headersState != mHeadersState) {
1121            mHeadersState = headersState;
1122            switch (headersState) {
1123                case HEADERS_ENABLED:
1124                    mCanShowHeaders = true;
1125                    mShowingHeaders = true;
1126                    break;
1127                case HEADERS_HIDDEN:
1128                    mCanShowHeaders = true;
1129                    mShowingHeaders = false;
1130                    break;
1131                case HEADERS_DISABLED:
1132                    mCanShowHeaders = false;
1133                    mShowingHeaders = false;
1134                    break;
1135                default:
1136                    Log.w(TAG, "Unknown headers state: " + headersState);
1137                    break;
1138            }
1139            if (mHeadersFragment != null) {
1140                mHeadersFragment.setHeadersGone(!mCanShowHeaders);
1141            }
1142        }
1143    }
1144
1145    /**
1146     * Returns the state of the headers column in the browse fragment.
1147     */
1148    public int getHeadersState() {
1149        return mHeadersState;
1150    }
1151
1152    @Override
1153    protected Object createEntranceTransition() {
1154        return TransitionHelper.loadTransition(getActivity(),
1155                R.transition.lb_browse_entrance_transition);
1156    }
1157
1158    @Override
1159    protected void runEntranceTransition(Object entranceTransition) {
1160        TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition);
1161    }
1162
1163    @Override
1164    protected void onEntranceTransitionPrepare() {
1165        mHeadersFragment.onTransitionPrepare();
1166        mMainFragmentAdapter.onTransitionPrepare();
1167    }
1168
1169    @Override
1170    protected void onEntranceTransitionStart() {
1171        mHeadersFragment.onTransitionStart();
1172        mMainFragmentAdapter.onTransitionStart();
1173    }
1174
1175    @Override
1176    protected void onEntranceTransitionEnd() {
1177        mMainFragmentAdapter.onTransitionEnd();
1178        mHeadersFragment.onTransitionEnd();
1179    }
1180
1181    void setSearchOrbViewOnScreen(boolean onScreen) {
1182        View searchOrbView = getTitleView().getSearchAffordanceView();
1183        MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams();
1184        lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart);
1185        searchOrbView.setLayoutParams(lp);
1186    }
1187
1188    void setEntranceTransitionStartState() {
1189        setHeadersOnScreen(false);
1190        setSearchOrbViewOnScreen(false);
1191        mMainFragmentAdapter.setEntranceTransitionState(false);
1192    }
1193
1194    void setEntranceTransitionEndState() {
1195        setHeadersOnScreen(mShowingHeaders);
1196        setSearchOrbViewOnScreen(true);
1197        mMainFragmentAdapter.setEntranceTransitionState(true);
1198    }
1199
1200    private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener {
1201
1202        private final View mView;
1203        private final Runnable mCallback;
1204        private int mState;
1205        private AbstractMainFragmentAdapter mainFragmentAdapter;
1206
1207        final static int STATE_INIT = 0;
1208        final static int STATE_FIRST_DRAW = 1;
1209        final static int STATE_SECOND_DRAW = 2;
1210
1211        ExpandPreLayout(Runnable callback, AbstractMainFragmentAdapter adapter) {
1212            mView = adapter.getFragment().getView();
1213            mCallback = callback;
1214            mainFragmentAdapter = adapter;
1215        }
1216
1217        void execute() {
1218            mView.getViewTreeObserver().addOnPreDrawListener(this);
1219            mainFragmentAdapter.setExpand(false);
1220            mState = STATE_INIT;
1221        }
1222
1223        @Override
1224        public boolean onPreDraw() {
1225            if (mView == null) {
1226                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1227                return true;
1228            }
1229            if (mState == STATE_INIT) {
1230                mainFragmentAdapter.setExpand(true);
1231                mState = STATE_FIRST_DRAW;
1232            } else if (mState == STATE_FIRST_DRAW) {
1233                mCallback.run();
1234                mView.getViewTreeObserver().removeOnPreDrawListener(this);
1235                mState = STATE_SECOND_DRAW;
1236            }
1237            return false;
1238        }
1239    }
1240}
1241