/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package android.support.v17.leanback.app; import static android.support.v7.widget.RecyclerView.NO_POSITION; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentManager.BackStackEntry; import android.app.FragmentTransaction; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; import android.support.annotation.ColorInt; import android.support.v17.leanback.R; import android.support.v17.leanback.transition.TransitionHelper; import android.support.v17.leanback.transition.TransitionListener; import android.support.v17.leanback.widget.BrowseFrameLayout; import android.support.v17.leanback.widget.InvisibleRowPresenter; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ObjectAdapter; import android.support.v17.leanback.widget.OnItemViewClickedListener; import android.support.v17.leanback.widget.OnItemViewSelectedListener; import android.support.v17.leanback.widget.PageRow; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowHeaderPresenter; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.ScaleFrameLayout; import android.support.v17.leanback.widget.TitleViewAdapter; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewTreeObserver; import java.util.HashMap; import java.util.Map; /** * A fragment for creating Leanback browse screens. It is composed of a * RowsFragment and a HeadersFragment. *

* A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set * of rows in a vertical list. The elements in this adapter must be subclasses * of {@link Row}. *

* The HeadersFragment can be set to be either shown or hidden by default, or * may be disabled entirely. See {@link #setHeadersState} for details. *

* By default the BrowseFragment includes support for returning to the headers * when the user presses Back. For Activities that customize {@link * android.app.Activity#onBackPressed()}, you must disable this default Back key support by * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and * use {@link BrowseFragment.BrowseTransitionListener} and * {@link #startHeadersTransition(boolean)}. *

* The recommended theme to use with a BrowseFragment is * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}. *

*/ public class BrowseFragment extends BaseFragment { // BUNDLE attribute for saving header show/hide status when backstack is used: static final String HEADER_STACK_INDEX = "headerStackIndex"; // BUNDLE attribute for saving header show/hide status when backstack is not used: static final String HEADER_SHOW = "headerShow"; private static final String IS_PAGE_ROW = "isPageRow"; private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition"; final class BackStackListener implements FragmentManager.OnBackStackChangedListener { int mLastEntryCount; int mIndexOfHeadersBackStack; BackStackListener() { mLastEntryCount = getFragmentManager().getBackStackEntryCount(); mIndexOfHeadersBackStack = -1; } void load(Bundle savedInstanceState) { if (savedInstanceState != null) { mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1); mShowingHeaders = mIndexOfHeadersBackStack == -1; } else { if (!mShowingHeaders) { getFragmentManager().beginTransaction() .addToBackStack(mWithHeadersBackStackName).commit(); } } } void save(Bundle outState) { outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack); } @Override public void onBackStackChanged() { if (getFragmentManager() == null) { Log.w(TAG, "getFragmentManager() is null, stack:", new Exception()); return; } int count = getFragmentManager().getBackStackEntryCount(); // if backstack is growing and last pushed entry is "headers" backstack, // remember the index of the entry. if (count > mLastEntryCount) { BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1); if (mWithHeadersBackStackName.equals(entry.getName())) { mIndexOfHeadersBackStack = count - 1; } } else if (count < mLastEntryCount) { // if popped "headers" backstack, initiate the show header transition if needed if (mIndexOfHeadersBackStack >= count) { if (!isHeadersDataReady()) { // if main fragment was restored first before BrowseFragment's adapater gets // restored: dont start header transition, but add the entry back. getFragmentManager().beginTransaction() .addToBackStack(mWithHeadersBackStackName).commit(); return; } mIndexOfHeadersBackStack = -1; if (!mShowingHeaders) { startHeadersTransitionInternal(true); } } } mLastEntryCount = count; } } /** * Listener for transitions between browse headers and rows. */ public static class BrowseTransitionListener { /** * Callback when headers transition starts. * * @param withHeaders True if the transition will result in headers * being shown, false otherwise. */ public void onHeadersTransitionStart(boolean withHeaders) { } /** * Callback when headers transition stops. * * @param withHeaders True if the transition will result in headers * being shown, false otherwise. */ public void onHeadersTransitionStop(boolean withHeaders) { } } private class SetSelectionRunnable implements Runnable { static final int TYPE_INVALID = -1; static final int TYPE_INTERNAL_SYNC = 0; static final int TYPE_USER_REQUEST = 1; private int mPosition; private int mType; private boolean mSmooth; SetSelectionRunnable() { reset(); } void post(int position, int type, boolean smooth) { // Posting the set selection, rather than calling it immediately, prevents an issue // with adapter changes. Example: a row is added before the current selected row; // first the fast lane view updates its selection, then the rows fragment has that // new selection propagated immediately; THEN the rows view processes the same adapter // change and moves the selection again. if (type >= mType) { mPosition = position; mType = type; mSmooth = smooth; mBrowseFrame.removeCallbacks(this); mBrowseFrame.post(this); } } @Override public void run() { setSelection(mPosition, mSmooth); reset(); } private void reset() { mPosition = -1; mType = TYPE_INVALID; mSmooth = false; } } /** * Possible set of actions that {@link BrowseFragment} exposes to clients. Custom * fragments can interact with {@link BrowseFragment} using this interface. */ public interface FragmentHost { /** * Fragments are required to invoke this callback once their view is created * inside {@link Fragment#onViewCreated} method. {@link BrowseFragment} starts the entrance * animation only after receiving this callback. Failure to invoke this method * will lead to fragment not showing up. * * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment. */ void notifyViewCreated(MainFragmentAdapter fragmentAdapter); /** * Fragments mapped to {@link PageRow} are required to invoke this callback once their data * is created for transition, the entrance animation only after receiving this callback. * Failure to invoke this method will lead to fragment not showing up. * * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment. */ void notifyDataReady(MainFragmentAdapter fragmentAdapter); /** * Show or hide title view in {@link BrowseFragment} for fragments mapped to * {@link PageRow}. Otherwise the request is ignored, in that case BrowseFragment is fully * in control of showing/hiding title view. *

* When HeadersFragment is visible, BrowseFragment will hide search affordance view if * there are other focusable rows above currently focused row. * * @param show Boolean indicating whether or not to show the title view. */ void showTitleView(boolean show); } /** * Default implementation of {@link FragmentHost} that is used only by * {@link BrowseFragment}. */ private final class FragmentHostImpl implements FragmentHost { boolean mShowTitleView = true; boolean mDataReady = false; @Override public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) { performPendingStates(); } @Override public void notifyDataReady(MainFragmentAdapter fragmentAdapter) { mDataReady = true; // If fragment host is not the currently active fragment (in BrowseFragment), then // ignore the request. if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) { return; } // We only honor showTitle request for PageRows. if (!mIsPageRow) { return; } performPendingStates(); } @Override public void showTitleView(boolean show) { mShowTitleView = show; // If fragment host is not the currently active fragment (in BrowseFragment), then // ignore the request. if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) { return; } // We only honor showTitle request for PageRows. if (!mIsPageRow) { return; } updateTitleViewVisibility(); } } /** * Interface that defines the interaction between {@link BrowseFragment} and it's main * content fragment. The key method is {@link MainFragmentAdapter#getFragment()}, * it will be used to get the fragment to be shown in the content section. Clients can * provide any implementation of fragment and customize it's interaction with * {@link BrowseFragment} by overriding the necessary methods. * *

* Clients are expected to provide * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing * implementations of {@link MainFragmentAdapter} for given content types. Currently * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype * of {@link Row}. We provide an out of the box adapter implementation for any rows other than * {@link PageRow} - {@link android.support.v17.leanback.app.RowsFragment.MainFragmentAdapter}. * *

* {@link PageRow} is intended to give full flexibility to developers in terms of Fragment * design. Users will have to provide an implementation of {@link MainFragmentAdapter} * and provide that through {@link MainFragmentAdapterRegistry}. * {@link MainFragmentAdapter} implementation can supply any fragment and override * just those interactions that makes sense. */ public static class MainFragmentAdapter { private boolean mScalingEnabled; private final T mFragment; private FragmentHostImpl mFragmentHost; public MainFragmentAdapter(T fragment) { this.mFragment = fragment; } public final T getFragment() { return mFragment; } /** * Returns whether its scrolling. */ public boolean isScrolling() { return false; } /** * Set the visibility of titles/hovercard of browse rows. */ public void setExpand(boolean expand) { } /** * For rows that willing to participate entrance transition, this function * hide views if afterTransition is true, show views if afterTransition is false. */ public void setEntranceTransitionState(boolean state) { } /** * Sets the window alignment and also the pivots for scale operation. */ public void setAlignment(int windowAlignOffsetFromTop) { } /** * Callback indicating transition prepare start. */ public boolean onTransitionPrepare() { return false; } /** * Callback indicating transition start. */ public void onTransitionStart() { } /** * Callback indicating transition end. */ public void onTransitionEnd() { } /** * Returns whether row scaling is enabled. */ public boolean isScalingEnabled() { return mScalingEnabled; } /** * Sets the row scaling property. */ public void setScalingEnabled(boolean scalingEnabled) { this.mScalingEnabled = scalingEnabled; } /** * Returns the current host interface so that main fragment can interact with * {@link BrowseFragment}. */ public final FragmentHost getFragmentHost() { return mFragmentHost; } void setFragmentHost(FragmentHostImpl fragmentHost) { this.mFragmentHost = fragmentHost; } } /** * Interface to be implemented by all fragments for providing an instance of * {@link MainFragmentAdapter}. Both {@link RowsFragment} and custom fragment provided * against {@link PageRow} will need to implement this interface. */ public interface MainFragmentAdapterProvider { /** * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseFragment} * would use to communicate with the target fragment. */ MainFragmentAdapter getMainFragmentAdapter(); } /** * Interface to be implemented by {@link RowsFragment} and it's subclasses for providing * an instance of {@link MainFragmentRowsAdapter}. */ public interface MainFragmentRowsAdapterProvider { /** * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseFragment} * would use to communicate with the target fragment. */ MainFragmentRowsAdapter getMainFragmentRowsAdapter(); } /** * This is used to pass information to {@link RowsFragment} or its subclasses. * {@link BrowseFragment} uses this interface to pass row based interaction events to * the target fragment. */ public static class MainFragmentRowsAdapter { private final T mFragment; public MainFragmentRowsAdapter(T fragment) { if (fragment == null) { throw new IllegalArgumentException("Fragment can't be null"); } this.mFragment = fragment; } public final T getFragment() { return mFragment; } /** * Set the visibility titles/hover of browse rows. */ public void setAdapter(ObjectAdapter adapter) { } /** * Sets an item clicked listener on the fragment. */ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { } /** * Sets an item selection listener. */ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { } /** * Selects a Row and perform an optional task on the Row. */ public void setSelectedPosition(int rowPosition, boolean smooth, final Presenter.ViewHolderTask rowHolderTask) { } /** * Selects a Row. */ public void setSelectedPosition(int rowPosition, boolean smooth) { } /** * Returns the selected position. */ public int getSelectedPosition() { return 0; } } private boolean createMainFragment(ObjectAdapter adapter, int position) { Object item = null; if (adapter == null || adapter.size() == 0) { return false; } else { if (position < 0) { position = 0; } else if (position >= adapter.size()) { throw new IllegalArgumentException( String.format("Invalid position %d requested", position)); } item = adapter.get(position); } mSelectedPosition = position; boolean oldIsPageRow = mIsPageRow; mIsPageRow = item instanceof PageRow; boolean swap; if (mMainFragment == null) { swap = true; } else { if (oldIsPageRow) { swap = true; } else { swap = mIsPageRow; } } if (swap) { mMainFragment = mMainFragmentAdapterRegistry.createFragment(item); if (!(mMainFragment instanceof MainFragmentAdapterProvider)) { throw new IllegalArgumentException( "Fragment must implement MainFragmentAdapterProvider"); } mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) .getMainFragmentAdapter(); mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); if (!mIsPageRow) { if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment) .getMainFragmentRowsAdapter(); } else { mMainFragmentRowsAdapter = null; } mIsPageRow = mMainFragmentRowsAdapter == null; } else { mMainFragmentRowsAdapter = null; } } return swap; } /** * Factory class responsible for creating fragment given the current item. {@link ListRow} * should returns {@link RowsFragment} or it's subclass whereas {@link PageRow} * can return any fragment class. */ public abstract static class FragmentFactory { public abstract T createFragment(Object row); } /** * FragmentFactory implementation for {@link ListRow}. */ public static class ListRowFragmentFactory extends FragmentFactory { @Override public RowsFragment createFragment(Object row) { return new RowsFragment(); } } /** * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}. * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for * handling {@link ListRow}. Developers can override that and also if they want to * use custom fragment, they can register a custom {@link FragmentFactory} * against {@link PageRow}. */ public final static class MainFragmentAdapterRegistry { private final Map mItemToFragmentFactoryMapping = new HashMap(); private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory(); public MainFragmentAdapterRegistry() { registerFragment(ListRow.class, sDefaultFragmentFactory); } public void registerFragment(Class rowClass, FragmentFactory factory) { mItemToFragmentFactoryMapping.put(rowClass, factory); } public Fragment createFragment(Object item) { if (item == null) { throw new IllegalArgumentException("Item can't be null"); } FragmentFactory fragmentFactory = mItemToFragmentFactoryMapping.get(item.getClass()); if (fragmentFactory == null && !(item instanceof PageRow)) { fragmentFactory = sDefaultFragmentFactory; } return fragmentFactory.createFragment(item); } } private static final String TAG = "BrowseFragment"; private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; private static boolean DEBUG = false; /** The headers fragment is enabled and shown by default. */ public static final int HEADERS_ENABLED = 1; /** The headers fragment is enabled and hidden by default. */ public static final int HEADERS_HIDDEN = 2; /** The headers fragment is disabled and will never be shown. */ public static final int HEADERS_DISABLED = 3; private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry = new MainFragmentAdapterRegistry(); private MainFragmentAdapter mMainFragmentAdapter; private Fragment mMainFragment; private HeadersFragment mHeadersFragment; private MainFragmentRowsAdapter mMainFragmentRowsAdapter; private ObjectAdapter mAdapter; private PresenterSelector mAdapterPresenter; private PresenterSelector mWrappingPresenterSelector; private int mHeadersState = HEADERS_ENABLED; private int mBrandColor = Color.TRANSPARENT; private boolean mBrandColorSet; private BrowseFrameLayout mBrowseFrame; private ScaleFrameLayout mScaleFrameLayout; private boolean mHeadersBackStackEnabled = true; private String mWithHeadersBackStackName; private boolean mShowingHeaders = true; private boolean mCanShowHeaders = true; private int mContainerListMarginStart; private int mContainerListAlignTop; private boolean mMainFragmentScaleEnabled = true; private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; private OnItemViewClickedListener mOnItemViewClickedListener; private int mSelectedPosition = -1; private float mScaleFactor; private boolean mIsPageRow; private PresenterSelector mHeaderPresenterSelector; private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); // transition related: private Object mSceneWithHeaders; private Object mSceneWithoutHeaders; private Object mSceneAfterEntranceTransition; private Object mHeadersTransition; private BackStackListener mBackStackChangedListener; private BrowseTransitionListener mBrowseTransitionListener; private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title"; private static final String ARG_HEADERS_STATE = BrowseFragment.class.getCanonicalName() + ".headersState"; /** * Creates arguments for a browse fragment. * * @param args The Bundle to place arguments into, or null if the method * should return a new Bundle. * @param title The title of the BrowseFragment. * @param headersState The initial state of the headers of the * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. * @return A Bundle with the given arguments for creating a BrowseFragment. */ public static Bundle createArgs(Bundle args, String title, int headersState) { if (args == null) { args = new Bundle(); } args.putString(ARG_TITLE, title); args.putInt(ARG_HEADERS_STATE, headersState); return args; } /** * Sets the brand color for the browse fragment. The brand color is used as * the primary color for UI elements in the browse fragment. For example, * the background color of the headers fragment uses the brand color. * * @param color The color to use as the brand color of the fragment. */ public void setBrandColor(@ColorInt int color) { mBrandColor = color; mBrandColorSet = true; if (mHeadersFragment != null) { mHeadersFragment.setBackgroundColor(mBrandColor); } } /** * Returns the brand color for the browse fragment. * The default is transparent. */ @ColorInt public int getBrandColor() { return mBrandColor; } /** * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow * DividerRow and PageRow. */ private void createAndSetWrapperPresenter() { final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector(); if (adapterPresenter == null) { throw new IllegalArgumentException("Adapter.getPresenterSelector() is null"); } if (adapterPresenter == mAdapterPresenter) { return; } mAdapterPresenter = adapterPresenter; Presenter[] presenters = adapterPresenter.getPresenters(); final Presenter invisibleRowPresenter = new InvisibleRowPresenter(); final Presenter[] allPresenters = new Presenter[presenters.length + 1]; System.arraycopy(allPresenters, 0, presenters, 0, presenters.length); allPresenters[allPresenters.length - 1] = invisibleRowPresenter; mAdapter.setPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object item) { Row row = (Row) item; if (row.isRenderedAsRowView()) { return adapterPresenter.getPresenter(item); } else { return invisibleRowPresenter; } } @Override public Presenter[] getPresenters() { return allPresenters; } }); } /** * Sets the adapter containing the rows for the fragment. * *

The items referenced by the adapter must be be derived from * {@link Row}. These rows will be used by the rows fragment and the headers * fragment (if not disabled) to render the browse rows. * * @param adapter An ObjectAdapter for the browse rows. All items must * derive from {@link Row}. */ public void setAdapter(ObjectAdapter adapter) { mAdapter = adapter; createAndSetWrapperPresenter(); if (getView() == null) { return; } replaceMainFragment(mSelectedPosition); if (adapter != null) { if (mMainFragmentRowsAdapter != null) { mMainFragmentRowsAdapter.setAdapter(adapter); } mHeadersFragment.setAdapter(adapter); } } public final MainFragmentAdapterRegistry getMainFragmentRegistry() { return mMainFragmentAdapterRegistry; } /** * Returns the adapter containing the rows for the fragment. */ public ObjectAdapter getAdapter() { return mAdapter; } /** * Sets an item selection listener. */ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { mExternalOnItemViewSelectedListener = listener; } /** * Returns an item selection listener. */ public OnItemViewSelectedListener getOnItemViewSelectedListener() { return mExternalOnItemViewSelectedListener; } /** * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has * not been created yet or a different fragment is bound to it. * * @return RowsFragment if it's bound to BrowseFragment or null otherwise. */ public RowsFragment getRowsFragment() { if (mMainFragment instanceof RowsFragment) { return (RowsFragment) mMainFragment; } return null; } /** * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet. * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet. */ public HeadersFragment getHeadersFragment() { return mHeadersFragment; } /** * Sets an item clicked listener on the fragment. * OnItemViewClickedListener will override {@link View.OnClickListener} that * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. * So in general, developer should choose one of the listeners but not both. */ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { mOnItemViewClickedListener = listener; if (mMainFragmentRowsAdapter != null) { mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener); } } /** * Returns the item Clicked listener. */ public OnItemViewClickedListener getOnItemViewClickedListener() { return mOnItemViewClickedListener; } /** * Starts a headers transition. * *

This method will begin a transition to either show or hide the * headers, depending on the value of withHeaders. If headers are disabled * for this browse fragment, this method will throw an exception. * * @param withHeaders True if the headers should transition to being shown, * false if the transition should result in headers being hidden. */ public void startHeadersTransition(boolean withHeaders) { if (!mCanShowHeaders) { throw new IllegalStateException("Cannot start headers transition"); } if (isInHeadersTransition() || mShowingHeaders == withHeaders) { return; } startHeadersTransitionInternal(withHeaders); } /** * Returns true if the headers transition is currently running. */ public boolean isInHeadersTransition() { return mHeadersTransition != null; } /** * Returns true if headers are shown. */ public boolean isShowingHeaders() { return mShowingHeaders; } /** * Sets a listener for browse fragment transitions. * * @param listener The listener to call when a browse headers transition * begins or ends. */ public void setBrowseTransitionListener(BrowseTransitionListener listener) { mBrowseTransitionListener = listener; } /** * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead. * * @param enable true to enable row scaling */ @Deprecated public void enableRowScaling(boolean enable) { enableMainFragmentScaling(enable); } /** * Enables scaling of main fragment when headers are present. For the page/row fragment, * scaling is enabled only when both this method and * {@link MainFragmentAdapter#isScalingEnabled()} are enabled. * * @param enable true to enable row scaling */ public void enableMainFragmentScaling(boolean enable) { mMainFragmentScaleEnabled = enable; } private void startHeadersTransitionInternal(final boolean withHeaders) { if (getFragmentManager().isDestroyed()) { return; } if (!isHeadersDataReady()) { return; } mShowingHeaders = withHeaders; mMainFragmentAdapter.onTransitionPrepare(); mMainFragmentAdapter.onTransitionStart(); onExpandTransitionStart(!withHeaders, new Runnable() { @Override public void run() { mHeadersFragment.onTransitionPrepare(); mHeadersFragment.onTransitionStart(); createHeadersTransition(); if (mBrowseTransitionListener != null) { mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); } TransitionHelper.runTransition( withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition); if (mHeadersBackStackEnabled) { if (!withHeaders) { getFragmentManager().beginTransaction() .addToBackStack(mWithHeadersBackStackName).commit(); } else { int index = mBackStackChangedListener.mIndexOfHeadersBackStack; if (index >= 0) { BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); getFragmentManager().popBackStackImmediate(entry.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE); } } } } }); } boolean isVerticalScrolling() { // don't run transition return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling(); } private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = new BrowseFrameLayout.OnFocusSearchListener() { @Override public View onFocusSearch(View focused, int direction) { // if headers is running transition, focus stays if (mCanShowHeaders && isInHeadersTransition()) { return focused; } if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); if (getTitleView() != null && focused != getTitleView() && direction == View.FOCUS_UP) { return getTitleView(); } if (getTitleView() != null && getTitleView().hasFocus() && direction == View.FOCUS_DOWN) { return mCanShowHeaders && mShowingHeaders ? mHeadersFragment.getVerticalGridView() : mMainFragment.getView(); } boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; if (mCanShowHeaders && direction == towardStart) { if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) { return focused; } return mHeadersFragment.getVerticalGridView(); } else if (direction == towardEnd) { if (isVerticalScrolling()) { return focused; } return mMainFragment.getView(); } else if (direction == View.FOCUS_DOWN && mShowingHeaders) { // disable focus_down moving into PageFragment. return focused; } else { return null; } } }; private final boolean isHeadersDataReady() { return mAdapter != null && mAdapter.size() != 0; } private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = new BrowseFrameLayout.OnChildFocusListener() { @Override public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { if (getChildFragmentManager().isDestroyed()) { return true; } // Make sure not changing focus when requestFocus() is called. if (mCanShowHeaders && mShowingHeaders) { if (mHeadersFragment != null && mHeadersFragment.getView() != null && mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { return true; } } if (mMainFragment != null && mMainFragment.getView() != null && mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { return true; } if (getTitleView() != null && getTitleView().requestFocus(direction, previouslyFocusedRect)) { return true; } return false; } @Override public void onRequestChildFocus(View child, View focused) { if (getChildFragmentManager().isDestroyed()) { return; } if (!mCanShowHeaders || isInHeadersTransition()) return; int childId = child.getId(); if (childId == R.id.browse_container_dock && mShowingHeaders) { startHeadersTransitionInternal(false); } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { startHeadersTransitionInternal(true); } } }; @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition); outState.putBoolean(IS_PAGE_ROW, mIsPageRow); if (mBackStackChangedListener != null) { mBackStackChangedListener.save(outState); } else { outState.putBoolean(HEADER_SHOW, mShowingHeaders); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); mContainerListMarginStart = (int) ta.getDimension( R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); mContainerListAlignTop = (int) ta.getDimension( R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); ta.recycle(); readArguments(getArguments()); if (mCanShowHeaders) { if (mHeadersBackStackEnabled) { mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; mBackStackChangedListener = new BackStackListener(); getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); mBackStackChangedListener.load(savedInstanceState); } else { if (savedInstanceState != null) { mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); } } } mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); } @Override public void onDestroyView() { mMainFragmentRowsAdapter = null; mMainFragmentAdapter = null; mMainFragment = null; mHeadersFragment = null; super.onDestroyView(); } @Override public void onDestroy() { if (mBackStackChangedListener != null) { getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); } super.onDestroy(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { mHeadersFragment = new HeadersFragment(); createMainFragment(mAdapter, mSelectedPosition); FragmentTransaction ft = getChildFragmentManager().beginTransaction() .replace(R.id.browse_headers_dock, mHeadersFragment); if (mMainFragment != null) { ft.replace(R.id.scale_frame, mMainFragment); } else { // Empty adapter used to guard against lazy adapter loading. When this // fragment is instantiated, mAdapter might not have the data or might not // have been set. In either of those cases mFragmentAdapter will be null. // This way we can maintain the invariant that mMainFragmentAdapter is never // null and it avoids doing null checks all over the code. mMainFragmentAdapter = new MainFragmentAdapter(null); mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); } ft.commit(); } else { mHeadersFragment = (HeadersFragment) getChildFragmentManager() .findFragmentById(R.id.browse_headers_dock); mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame); mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) .getMainFragmentAdapter(); mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); mIsPageRow = savedInstanceState != null ? savedInstanceState.getBoolean(IS_PAGE_ROW, false) : false; mSelectedPosition = savedInstanceState != null ? savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0; if (!mIsPageRow) { if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment) .getMainFragmentRowsAdapter(); } else { mMainFragmentRowsAdapter = null; } } else { mMainFragmentRowsAdapter = null; } } mHeadersFragment.setHeadersGone(!mCanShowHeaders); if (mHeaderPresenterSelector != null) { mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); } mHeadersFragment.setAdapter(mAdapter); mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); getProgressBarManager().setRootView((ViewGroup)root); mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); installTitleView(inflater, mBrowseFrame, savedInstanceState); mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); mScaleFrameLayout.setPivotX(0); mScaleFrameLayout.setPivotY(mContainerListAlignTop); setupMainFragment(); if (mBrandColorSet) { mHeadersFragment.setBackgroundColor(mBrandColor); } mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { @Override public void run() { showHeaders(true); } }); mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { @Override public void run() { showHeaders(false); } }); mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { @Override public void run() { setEntranceTransitionEndState(); } }); return root; } private void setupMainFragment() { if (mMainFragmentRowsAdapter != null) { mMainFragmentRowsAdapter.setAdapter(mAdapter); mMainFragmentRowsAdapter.setOnItemViewSelectedListener( new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); } } @Override boolean isReadyForPrepareEntranceTransition() { return mMainFragment != null && mMainFragment.getView() != null; } @Override boolean isReadyForStartEntranceTransition() { return mMainFragment != null && mMainFragment.getView() != null && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady); } private void createHeadersTransition() { mHeadersTransition = TransitionHelper.loadTransition(getActivity(), mShowingHeaders ? R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { @Override public void onTransitionStart(Object transition) { } @Override public void onTransitionEnd(Object transition) { mHeadersTransition = null; if (mMainFragmentAdapter != null) { mMainFragmentAdapter.onTransitionEnd(); if (!mShowingHeaders && mMainFragment != null) { View mainFragmentView = mMainFragment.getView(); if (mainFragmentView != null && !mainFragmentView.hasFocus()) { mainFragmentView.requestFocus(); } } } if (mHeadersFragment != null) { mHeadersFragment.onTransitionEnd(); if (mShowingHeaders) { VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); if (headerGridView != null && !headerGridView.hasFocus()) { headerGridView.requestFocus(); } } } // Animate TitleView once header animation is complete. updateTitleViewVisibility(); if (mBrowseTransitionListener != null) { mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); } } }); } void updateTitleViewVisibility() { if (!mShowingHeaders) { boolean showTitleView; if (mIsPageRow && mMainFragmentAdapter != null) { // page fragment case: showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView; } else { // regular row view case: showTitleView = isFirstRowWithContent(mSelectedPosition); } if (showTitleView) { showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE); } else { showTitle(false); } } else { // when HeaderFragment is showing, showBranding and showSearch are slightly different boolean showBranding; boolean showSearch; if (mIsPageRow && mMainFragmentAdapter != null) { showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView; } else { showBranding = isFirstRowWithContent(mSelectedPosition); } showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition); int flags = 0; if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE; if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE; if (flags != 0) { showTitle(flags); } else { showTitle(false); } } } boolean isFirstRowWithContentOrPageRow(int rowPosition) { if (mAdapter == null || mAdapter.size() == 0) { return true; } for (int i = 0; i < mAdapter.size(); i++) { final Row row = (Row) mAdapter.get(i); if (row.isRenderedAsRowView() || row instanceof PageRow) { return rowPosition == i; } } return true; } boolean isFirstRowWithContent(int rowPosition) { if (mAdapter == null || mAdapter.size() == 0) { return true; } for (int i = 0; i < mAdapter.size(); i++) { final Row row = (Row) mAdapter.get(i); if (row.isRenderedAsRowView()) { return rowPosition == i; } } return true; } /** * Sets the {@link PresenterSelector} used to render the row headers. * * @param headerPresenterSelector The PresenterSelector that will determine * the Presenter for each row header. */ public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { mHeaderPresenterSelector = headerPresenterSelector; if (mHeadersFragment != null) { mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); } } private void setHeadersOnScreen(boolean onScreen) { MarginLayoutParams lp; View containerList; containerList = mHeadersFragment.getView(); lp = (MarginLayoutParams) containerList.getLayoutParams(); lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); containerList.setLayoutParams(lp); } private void showHeaders(boolean show) { if (DEBUG) Log.v(TAG, "showHeaders " + show); mHeadersFragment.setHeadersEnabled(show); setHeadersOnScreen(show); expandMainFragment(!show); } private void expandMainFragment(boolean expand) { MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); params.setMarginStart(!expand ? mContainerListMarginStart : 0); mScaleFrameLayout.setLayoutParams(params); mMainFragmentAdapter.setExpand(expand); setMainFragmentAlignment(); final float scaleFactor = !expand && mMainFragmentScaleEnabled && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1; mScaleFrameLayout.setLayoutScaleY(scaleFactor); mScaleFrameLayout.setChildScale(scaleFactor); } private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = new HeadersFragment.OnHeaderClickedListener() { @Override public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { return; } startHeadersTransitionInternal(false); mMainFragment.getView().requestFocus(); } }; class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener { MainFragmentRowsAdapter mMainFragmentRowsAdapter; public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) { mMainFragmentRowsAdapter = fragmentRowsAdapter; } @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { int position = mMainFragmentRowsAdapter.getSelectedPosition(); if (DEBUG) Log.v(TAG, "row selected position " + position); onRowSelected(position); if (mExternalOnItemViewSelectedListener != null) { mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, rowViewHolder, row); } } }; private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = new HeadersFragment.OnHeaderViewSelectedListener() { @Override public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { int position = mHeadersFragment.getSelectedPosition(); if (DEBUG) Log.v(TAG, "header selected position " + position); onRowSelected(position); } }; private void onRowSelected(int position) { if (position != mSelectedPosition) { mSetSelectionRunnable.post( position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); } } private void setSelection(int position, boolean smooth) { if (position == NO_POSITION) { return; } mHeadersFragment.setSelectedPosition(position, smooth); replaceMainFragment(position); if (mMainFragmentRowsAdapter != null) { mMainFragmentRowsAdapter.setSelectedPosition(position, smooth); } mSelectedPosition = position; updateTitleViewVisibility(); } private void replaceMainFragment(int position) { if (createMainFragment(mAdapter, position)) { swapToMainFragment(); expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); setupMainFragment(); performPendingStates(); } } private void swapToMainFragment() { final VerticalGridView gridView = mHeadersFragment.getVerticalGridView(); if (isShowingHeaders() && gridView != null && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { // if user is scrolling HeadersFragment, swap to empty fragment and wait scrolling // finishes. getChildFragmentManager().beginTransaction() .replace(R.id.scale_frame, new Fragment()).commit(); gridView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { gridView.removeOnScrollListener(this); FragmentManager fm = getChildFragmentManager(); Fragment currentFragment = fm.findFragmentById(R.id.scale_frame); if (currentFragment != mMainFragment) { fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit(); } } } }); } else { // Otherwise swap immediately getChildFragmentManager().beginTransaction() .replace(R.id.scale_frame, mMainFragment).commit(); } } /** * Sets the selected row position with smooth animation. */ public void setSelectedPosition(int position) { setSelectedPosition(position, true); } /** * Gets position of currently selected row. * @return Position of currently selected row. */ public int getSelectedPosition() { return mSelectedPosition; } /** * Sets the selected row position. */ public void setSelectedPosition(int position, boolean smooth) { mSetSelectionRunnable.post( position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); } /** * Selects a Row and perform an optional task on the Row. For example * setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5)) * scrolls to 11th row and selects 6th item on that row. The method will be ignored if * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, * ViewGroup, Bundle)}). * * @param rowPosition Which row to select. * @param smooth True to scroll to the row, false for no animation. * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers * fragment will be collapsed. */ public void setSelectedPosition(int rowPosition, boolean smooth, final Presenter.ViewHolderTask rowHolderTask) { if (mMainFragmentAdapterRegistry == null) { return; } if (rowHolderTask != null) { startHeadersTransition(false); } if (mMainFragmentRowsAdapter != null) { mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); } } @Override public void onStart() { super.onStart(); mHeadersFragment.setAlignment(mContainerListAlignTop); setMainFragmentAlignment(); if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { mHeadersFragment.getView().requestFocus(); } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment.getView() != null) { mMainFragment.getView().requestFocus(); } if (mCanShowHeaders) { showHeaders(mShowingHeaders); } if (isEntranceTransitionEnabled()) { setEntranceTransitionStartState(); } } private void onExpandTransitionStart(boolean expand, final Runnable callback) { if (expand) { callback.run(); return; } // Run a "pre" layout when we go non-expand, in order to get the initial // positions of added rows. new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute(); } private void setMainFragmentAlignment() { int alignOffset = mContainerListAlignTop; if (mMainFragmentScaleEnabled && mMainFragmentAdapter.isScalingEnabled() && mShowingHeaders) { alignOffset = (int) (alignOffset / mScaleFactor + 0.5f); } mMainFragmentAdapter.setAlignment(alignOffset); } /** * Enables/disables headers transition on back key support. This is enabled by * default. The BrowseFragment will add a back stack entry when headers are * showing. Running a headers transition when the back key is pressed only * works when the headers state is {@link #HEADERS_ENABLED} or * {@link #HEADERS_HIDDEN}. *

* NOTE: If an Activity has its own onBackPressed() handling, you must * disable this feature. You may use {@link #startHeadersTransition(boolean)} * and {@link BrowseTransitionListener} in your own back stack handling. */ public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { mHeadersBackStackEnabled = headersBackStackEnabled; } /** * Returns true if headers transition on back key support is enabled. */ public final boolean isHeadersTransitionOnBackEnabled() { return mHeadersBackStackEnabled; } private void readArguments(Bundle args) { if (args == null) { return; } if (args.containsKey(ARG_TITLE)) { setTitle(args.getString(ARG_TITLE)); } if (args.containsKey(ARG_HEADERS_STATE)) { setHeadersState(args.getInt(ARG_HEADERS_STATE)); } } /** * Sets the state for the headers column in the browse fragment. Must be one * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or * {@link #HEADERS_DISABLED}. * * @param headersState The state of the headers for the browse fragment. */ public void setHeadersState(int headersState) { if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { throw new IllegalArgumentException("Invalid headers state: " + headersState); } if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); if (headersState != mHeadersState) { mHeadersState = headersState; switch (headersState) { case HEADERS_ENABLED: mCanShowHeaders = true; mShowingHeaders = true; break; case HEADERS_HIDDEN: mCanShowHeaders = true; mShowingHeaders = false; break; case HEADERS_DISABLED: mCanShowHeaders = false; mShowingHeaders = false; break; default: Log.w(TAG, "Unknown headers state: " + headersState); break; } if (mHeadersFragment != null) { mHeadersFragment.setHeadersGone(!mCanShowHeaders); } } } /** * Returns the state of the headers column in the browse fragment. */ public int getHeadersState() { return mHeadersState; } @Override protected Object createEntranceTransition() { return TransitionHelper.loadTransition(getActivity(), R.transition.lb_browse_entrance_transition); } @Override protected void runEntranceTransition(Object entranceTransition) { TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); } @Override protected void onEntranceTransitionPrepare() { mHeadersFragment.onTransitionPrepare(); // setEntranceTransitionStartState() might be called when mMainFragment is null, // make sure it is called. mMainFragmentAdapter.setEntranceTransitionState(false); mMainFragmentAdapter.onTransitionPrepare(); } @Override protected void onEntranceTransitionStart() { mHeadersFragment.onTransitionStart(); mMainFragmentAdapter.onTransitionStart(); } @Override protected void onEntranceTransitionEnd() { if (mMainFragmentAdapter != null) { mMainFragmentAdapter.onTransitionEnd(); } if (mHeadersFragment != null) { mHeadersFragment.onTransitionEnd(); } } void setSearchOrbViewOnScreen(boolean onScreen) { View searchOrbView = getTitleViewAdapter().getSearchAffordanceView(); if (searchOrbView != null) { MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); searchOrbView.setLayoutParams(lp); } } void setEntranceTransitionStartState() { setHeadersOnScreen(false); setSearchOrbViewOnScreen(false); mMainFragmentAdapter.setEntranceTransitionState(false); } void setEntranceTransitionEndState() { setHeadersOnScreen(mShowingHeaders); setSearchOrbViewOnScreen(true); mMainFragmentAdapter.setEntranceTransitionState(true); } private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { private final View mView; private final Runnable mCallback; private int mState; private MainFragmentAdapter mainFragmentAdapter; final static int STATE_INIT = 0; final static int STATE_FIRST_DRAW = 1; final static int STATE_SECOND_DRAW = 2; ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) { mView = view; mCallback = callback; mainFragmentAdapter = adapter; } void execute() { mView.getViewTreeObserver().addOnPreDrawListener(this); mainFragmentAdapter.setExpand(false); mState = STATE_INIT; } @Override public boolean onPreDraw() { if (getView() == null || getActivity() == null) { mView.getViewTreeObserver().removeOnPreDrawListener(this); return true; } if (mState == STATE_INIT) { mainFragmentAdapter.setExpand(true); mState = STATE_FIRST_DRAW; } else if (mState == STATE_FIRST_DRAW) { mCallback.run(); mView.getViewTreeObserver().removeOnPreDrawListener(this); mState = STATE_SECOND_DRAW; } return false; } } }