/* * 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 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
*
* 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;
}
}
}
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}.
*