BrowseFragment: headers transition back press support.
Added a default support for "headers transition on back pressed".
Also provided BrowseTransitionListener and startHeadersTransition() for
the rare case if app has its own onBackPressed() handling (e.g. PlayStore).
b/13628758
Change-Id: I8c70171d0353d6b212fec9e9b0e4739b9f0d2138
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index e30bfff..0413d5f 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -82,7 +82,7 @@
<item name="android:scaleType">centerInside</item>
</style>
- <!-- HeadersFragment (fast lane) -->
+ <!-- HeadersFragment -->
<style name="Widget.Leanback.Headers" />
<!-- RowsFragment -->
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index fecd7db..c25a824 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -27,7 +27,10 @@
import android.util.Log;
import android.util.SparseIntArray;
import android.util.TypedValue;
+import android.app.Activity;
import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -47,84 +50,14 @@
/**
* Wrapper fragment for leanback browse screens. Composed of a
* RowsFragment and a HeadersFragment.
- *
+ * <p>
+ * The fragment comes with default back key support to show headers.
+ * For app customized {@link Activity#onBackPressed()}, app must disable
+ * BrowseFragment's default back key support by calling
+ * {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and use
+ * {@link BrowseFragment.BrowseTransitionListener} and {@link #startHeadersTransition(boolean)}.
*/
public class BrowseFragment extends Fragment {
- private static final String TAG = "BrowseFragment";
- private static boolean DEBUG = false;
-
- /** The fastlane navigation panel is enabled and shown by default. */
- public static final int HEADERS_ENABLED = 1;
-
- /** The fastlane navigation panel is enabled and hidden by default. */
- public static final int HEADERS_HIDDEN = 2;
-
- /** The fastlane navigation panel is disabled and will never be shown. */
- public static final int HEADERS_DISABLED = 3;
-
- private static final float SLIDE_DISTANCE_FACTOR = 2;
-
- private RowsFragment mRowsFragment;
- private HeadersFragment mHeadersFragment;
-
- private ObjectAdapter mAdapter;
-
- private Params mParams;
- private int mBrandColor = Color.TRANSPARENT;
- private boolean mBrandColorSet;
-
- private BrowseFrameLayout mBrowseFrame;
- private ImageView mBadgeView;
- private TextView mTitleView;
- private ViewGroup mBrowseTitle;
- private SearchOrbView mSearchOrbView;
- private boolean mShowingTitle = true;
- private boolean mShowingHeaders = true;
- private boolean mCanShowHeaders = true;
- private int mContainerListMarginLeft;
- private int mContainerListAlignTop;
- private int mSearchAffordanceColor;
- private boolean mSearchAffordanceColorSet;
- private OnItemSelectedListener mExternalOnItemSelectedListener;
- private OnClickListener mExternalOnSearchClickedListener;
- private OnItemClickedListener mOnItemClickedListener;
- private int mSelectedPosition = -1;
-
- private PresenterSelector mHeaderPresenterSelector;
-
- // transition related:
- private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
- private int mReparentHeaderId = View.generateViewId();
- private Object mSceneWithTitle;
- private Object mSceneWithoutTitle;
- private Object mSceneWithHeaders;
- private Object mSceneWithoutHeaders;
- private Object mTitleTransition;
- private Object mHeadersTransition;
- private int mHeadersTransitionStartDelay;
- private int mHeadersTransitionDuration;
-
- private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
- private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
- private static final String ARG_HEADERS_STATE =
- BrowseFragment.class.getCanonicalName() + ".headersState";
-
- /**
- * @param args Bundle to use for the arguments, if null a new Bundle will be created.
- */
- public static Bundle createArgs(Bundle args, String title, String badgeUri) {
- return createArgs(args, title, badgeUri, HEADERS_ENABLED);
- }
-
- public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) {
- if (args == null) {
- args = new Bundle();
- }
- args.putString(ARG_TITLE, title);
- args.putString(ARG_BADGE_URI, badgeUri);
- args.putInt(ARG_HEADERS_STATE, headersState);
- return args;
- }
public static class Params {
private String mTitle;
@@ -180,6 +113,140 @@
}
}
+ final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
+ int mLastEntryCount;
+ int mIndexOfHeadersBackStack;
+
+ BackStackListener() {
+ reset();
+ }
+
+ void reset() {
+ mLastEntryCount = getFragmentManager().getBackStackEntryCount();
+ mIndexOfHeadersBackStack = -1;
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ 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 (!mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ }
+ mLastEntryCount = count;
+ }
+ }
+
+ /**
+ * Listener for browse transitions.
+ */
+ public static class BrowseTransitionListener {
+ /**
+ * Callback when headers transition starts.
+ */
+ public void onHeadersTransitionStart(boolean withHeaders) {
+ }
+ /**
+ * Callback when headers transition stops.
+ */
+ public void onHeadersTransitionStop(boolean withHeaders) {
+ }
+ }
+
+ 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 static final float SLIDE_DISTANCE_FACTOR = 2;
+
+ private RowsFragment mRowsFragment;
+ private HeadersFragment mHeadersFragment;
+
+ private ObjectAdapter mAdapter;
+
+ private Params mParams;
+ private int mBrandColor = Color.TRANSPARENT;
+ private boolean mBrandColorSet;
+
+ private BrowseFrameLayout mBrowseFrame;
+ private ImageView mBadgeView;
+ private TextView mTitleView;
+ private ViewGroup mBrowseTitle;
+ private SearchOrbView mSearchOrbView;
+ private boolean mShowingTitle = true;
+ private boolean mHeadersBackStackEnabled = true;
+ private String mWithHeadersBackStackName;
+ private boolean mShowingHeaders = true;
+ private boolean mCanShowHeaders = true;
+ private int mContainerListMarginLeft;
+ private int mContainerListAlignTop;
+ private int mSearchAffordanceColor;
+ private boolean mSearchAffordanceColorSet;
+ private OnItemSelectedListener mExternalOnItemSelectedListener;
+ private OnClickListener mExternalOnSearchClickedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+ private int mSelectedPosition = -1;
+
+ private PresenterSelector mHeaderPresenterSelector;
+
+ // transition related:
+ private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
+ private int mReparentHeaderId = View.generateViewId();
+ private Object mSceneWithTitle;
+ private Object mSceneWithoutTitle;
+ private Object mSceneWithHeaders;
+ private Object mSceneWithoutHeaders;
+ private Object mTitleTransition;
+ private Object mHeadersTransition;
+ private int mHeadersTransitionStartDelay;
+ private int mHeadersTransitionDuration;
+ private BackStackListener mBackStackChangedListener;
+ private BrowseTransitionListener mBrowseTransitionListener;
+
+ private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
+ private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
+ private static final String ARG_HEADERS_STATE =
+ BrowseFragment.class.getCanonicalName() + ".headersState";
+
+ /**
+ * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ */
+ public static Bundle createArgs(Bundle args, String title, String badgeUri) {
+ return createArgs(args, title, badgeUri, HEADERS_ENABLED);
+ }
+
+ public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_TITLE, title);
+ args.putString(ARG_BADGE_URI, badgeUri);
+ args.putInt(ARG_HEADERS_STATE, headersState);
+ return args;
+ }
+
/**
* Set browse parameters.
*/
@@ -309,10 +376,64 @@
return getResources().getColor(outValue.resourceId);
}
- private void onHeadersTransitionStart(boolean withHeaders) {
+ /**
+ * Start headers transition.
+ */
+ public void startHeadersTransition(boolean withHeaders) {
+ if (!mCanShowHeaders) {
+ throw new IllegalStateException("Cannot start headers transition");
+ }
+ if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
+ return;
+ }
+ startHeadersTransitionInternal(withHeaders);
+ }
+
+ /**
+ * Returns true if headers transition is currently running.
+ */
+ public boolean isInHeadersTransition() {
+ return mHeadersTransition != null;
+ }
+
+ /**
+ * Returns true if headers is showing.
+ */
+ public boolean isShowingHeaders() {
+ return mShowingHeaders;
+ }
+
+ /**
+ * Set listener for browse fragment transitions.
+ */
+ public void setBrowseTransitionListener(BrowseTransitionListener listener) {
+ mBrowseTransitionListener = listener;
+ }
+
+ private void startHeadersTransitionInternal(boolean withHeaders) {
+ mShowingHeaders = withHeaders;
mRowsFragment.onTransitionStart();
mHeadersFragment.onTransitionStart();
- createHeadersTransition(withHeaders);
+ createHeadersTransition();
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
+ }
+ sTransitionHelper.runTransition(withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders,
+ mHeadersTransition);
+ if (mHeadersBackStackEnabled) {
+ if (!withHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ } else {
+ int count = getFragmentManager().getBackStackEntryCount();
+ if (count > 0) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
+ if (mWithHeadersBackStackName.equals(entry.getName())) {
+ getFragmentManager().popBackStack();
+ }
+ }
+ }
+ }
}
private boolean isVerticalScrolling() {
@@ -327,11 +448,11 @@
new BrowseFrameLayout.OnFocusSearchListener() {
@Override
public View onFocusSearch(View focused, int direction) {
- // If fastlane is disabled, just return null.
+ // If headers fragment is disabled, just return null.
if (!mCanShowHeaders) return null;
- // if fast lane is running transition, focus stays
- if (mHeadersTransition != null) return focused;
+ // if headers is running transition, focus stays
+ if (isInHeadersTransition()) return focused;
if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
if (direction == View.FOCUS_LEFT) {
if (isVerticalScrolling() || mShowingHeaders) {
@@ -362,16 +483,11 @@
@Override
public void onRequestChildFocus(View child, View focused) {
int childId = child.getId();
- if (mHeadersTransition != null) return;
+ if (!mCanShowHeaders || isInHeadersTransition()) return;
if (childId == R.id.browse_container_dock && mShowingHeaders) {
- mShowingHeaders = false;
- onHeadersTransitionStart(false);
- sTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(false);
} else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
- mShowingHeaders = true;
- //mHeadersFragment.getView().setAlpha(1f);
- onHeadersTransitionStart(true);
- sTransitionHelper.runTransition(mSceneWithHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(true);
}
}
};
@@ -476,7 +592,7 @@
return root;
}
- private void createHeadersTransition(boolean withHeaders) {
+ private void createHeadersTransition() {
mHeadersTransition = sTransitionHelper.createTransitionSet(false);
sTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true);
Object changeBounds = sTransitionHelper.createChangeBounds(true);
@@ -485,12 +601,12 @@
sTransitionHelper.exclude(fadeIn, mReparentHeaderId, true);
sTransitionHelper.exclude(fadeOut, mReparentHeaderId, true);
- if (!withHeaders) {
+ if (!mShowingHeaders) {
sTransitionHelper.setChangeBoundsDefaultStartDelay(changeBounds,
mHeadersTransitionStartDelay);
}
sTransitionHelper.setChangeBoundsStartDelay(changeBounds, mReparentHeaderId,
- withHeaders ? mHeadersTransitionStartDelay : 0);
+ mShowingHeaders ? mHeadersTransitionStartDelay : 0);
final int selectedPosition = mSelectedPosition;
Object slide = sTransitionHelper.createSlide(new SlideCallback() {
@@ -514,7 +630,7 @@
});
sTransitionHelper.exclude(slide, mReparentHeaderId, true);
sTransitionHelper.setDuration(slide, mHeadersTransitionDuration);
- if (withHeaders) {
+ if (mShowingHeaders) {
sTransitionHelper.setStartDelay(slide, mHeadersTransitionStartDelay);
}
sTransitionHelper.addTransition(mHeadersTransition, slide);
@@ -547,6 +663,9 @@
rowsGridView.requestFocus();
}
}
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
+ }
}
});
}
@@ -585,14 +704,10 @@
new HeadersFragment.OnHeaderClickedListener() {
@Override
public void onHeaderClicked() {
- if (!mCanShowHeaders || !mShowingHeaders) return;
-
- if (mHeadersTransition != null) {
+ if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
return;
}
- mShowingHeaders = false;
- onHeadersTransitionStart(false);
- sTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(false);
mRowsFragment.getVerticalGridView().requestFocus();
}
};
@@ -686,6 +801,48 @@
mRowsFragment.getView().requestFocus();
}
showHeaders(mCanShowHeaders && mShowingHeaders);
+ if (mCanShowHeaders && mHeadersBackStackEnabled) {
+ mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
+ if (mBackStackChangedListener == null) {
+ mBackStackChangedListener = new BackStackListener();
+ } else {
+ mBackStackChangedListener.reset();
+ }
+ getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
+ if (!mShowingHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mBackStackChangedListener != null) {
+ getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
+ }
+ super.onStop();
+ }
+
+ /**
+ * Enable/disable headers transition on back key support. This is enabled by default.
+ * BrowseFragment will add a back stack entry when headers are showing.
+ * Headers transition on back key only works for {@link #HEADERS_ENABLED}
+ * or {@link #HEADERS_HIDDEN}.
+ * <p>
+ * NOTE: If app has its own onBackPressed() handling,
+ * app must disable this feature, app may use {@link #startHeadersTransition(boolean)}
+ * and {@link BrowseTransitionListener} in its 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) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
index 946c69d..dce91d0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
@@ -21,7 +21,7 @@
* Called when the a row or a new item becomes selected. The concept of current selection
* is different than focus. Row or item can be selected even they don't have focus.
* Having the concept of selection will allow developer to switch background to selected
- * item or selected row when user selects rows outside row UI (e.g. a fast lane next to
+ * item or selected row when user selects rows outside row UI (e.g. headers left of
* rows).
* <p>
* For a none {@link ListRow} case, parameter item is always null. Event is fired when