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