diff options
author | Adam Powell <adamp@google.com> | 2014-05-01 10:23:33 -0700 |
---|---|---|
committer | Adam Powell <adamp@google.com> | 2014-05-01 12:45:52 -0700 |
commit | b36e4f944fe28ce68182f9ec91e5341866b49084 (patch) | |
tree | 86bc9f18445924099235cba6d5f25b6f62f9d755 /core | |
parent | b6ea9db449cef8455c239c81f9f89190b28f0f85 (diff) | |
download | frameworks_base-b36e4f944fe28ce68182f9ec91e5341866b49084.zip frameworks_base-b36e4f944fe28ce68182f9ec91e5341866b49084.tar.gz frameworks_base-b36e4f944fe28ce68182f9ec91e5341866b49084.tar.bz2 |
Add support for hiding action bars on scroll.
Also tweak the nested scrolling API around nested flings and fix a bug
where recursive nested scrolling would stop prematurely.
Change-Id: I561226db878b2493970440a6af3e2332c56a1913
Diffstat (limited to 'core')
-rw-r--r-- | core/java/android/app/ActionBar.java | 60 | ||||
-rw-r--r-- | core/java/android/view/View.java | 106 | ||||
-rw-r--r-- | core/java/android/view/ViewGroup.java | 3 | ||||
-rw-r--r-- | core/java/android/view/ViewParent.java | 15 | ||||
-rw-r--r-- | core/java/android/view/ViewRootImpl.java | 2 | ||||
-rw-r--r-- | core/java/android/widget/ScrollView.java | 27 | ||||
-rw-r--r-- | core/java/com/android/internal/app/WindowDecorActionBar.java | 83 | ||||
-rw-r--r-- | core/java/com/android/internal/widget/ActionBarOverlayLayout.java | 282 | ||||
-rw-r--r-- | core/res/res/values/attrs.xml | 3 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 1 |
10 files changed, 481 insertions, 101 deletions
diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index 04f62e3..3c3df01 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -932,6 +932,66 @@ public abstract class ActionBar { */ public void setHomeActionContentDescription(int resId) { } + /** + * Enable hiding the action bar on content scroll. + * + * <p>If enabled, the action bar will scroll out of sight along with a + * {@link View#setNestedScrollingEnabled(boolean) nested scrolling child} view's content. + * The action bar must be in {@link Window#FEATURE_ACTION_BAR_OVERLAY overlay mode} + * to enable hiding on content scroll.</p> + * + * <p>When partially scrolled off screen the action bar is considered + * {@link #hide() hidden}. A call to {@link #show() show} will cause it to return to full view. + * </p> + * @param hideOnContentScroll true to enable hiding on content scroll. + */ + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll) { + throw new UnsupportedOperationException("Hide on content scroll is not supported in " + + "this action bar configuration."); + } + } + + /** + * Return whether the action bar is configured to scroll out of sight along with + * a {@link View#setNestedScrollingEnabled(boolean) nested scrolling child}. + * + * @return true if hide-on-content-scroll is enabled + * @see #setHideOnContentScrollEnabled(boolean) + */ + public boolean isHideOnContentScrollEnabled() { + return false; + } + + /** + * Return the current vertical offset of the action bar. + * + * <p>The action bar's current hide offset is the distance that the action bar is currently + * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's + * current measured {@link #getHeight() height} (fully invisible).</p> + * + * @return The action bar's offset toward its fully hidden state in pixels + */ + public int getHideOffset() { + return 0; + } + + /** + * Set the current hide offset of the action bar. + * + * <p>The action bar's current hide offset is the distance that the action bar is currently + * scrolled offscreen in pixels. The valid range is 0 (fully visible) to the action bar's + * current measured {@link #getHeight() height} (fully invisible).</p> + * + * @param offset The action bar's offset toward its fully hidden state in pixels. + */ + public void setHideOffset(int offset) { + if (offset != 0) { + throw new UnsupportedOperationException("Setting an explicit action bar hide offset " + + "is not supported in this action bar configuration."); + } + } + /** @hide */ public void setDefaultDisplayHomeAsUpEnabled(boolean enabled) { } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 85e3b3d..6afff4d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -17989,7 +17989,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * <p>If this property is set to true the view will be permitted to initiate nested * scrolling operations with a compatible parent view in the current hierarchy. If this - * view does not implement nested scrolling this will have no effect.</p> + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.</p> * * @param enabled true to enable nested scrolling, false to disable * @@ -17999,6 +18001,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (enabled) { mPrivateFlags3 |= PFLAG3_NESTED_SCROLLING_ENABLED; } else { + stopNestedScroll(); mPrivateFlags3 &= ~PFLAG3_NESTED_SCROLLING_ENABLED; } } @@ -18138,23 +18141,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback, public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { - int startX = 0; - int startY = 0; - if (offsetInWindow != null) { - getLocationInWindow(offsetInWindow); - startX = offsetInWindow[0]; - startY = offsetInWindow[1]; - } + if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } - mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed, - dxUnconsumed, dyUnconsumed); + mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); - if (offsetInWindow != null) { - getLocationInWindow(offsetInWindow); - offsetInWindow[0] -= startX; - offsetInWindow[1] -= startY; + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return true; + } else if (offsetInWindow != null) { + // No motion, no dispatch. Keep offsetInWindow up to date. + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; } - return true; } return false; } @@ -18180,30 +18189,35 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { - int startX = 0; - int startY = 0; - if (offsetInWindow != null) { - getLocationInWindow(offsetInWindow); - startX = offsetInWindow[0]; - startY = offsetInWindow[1]; - } - - if (consumed == null) { - if (mTempNestedScrollConsumed == null) { - mTempNestedScrollConsumed = new int[2]; + if (dx != 0 || dy != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; } - consumed = mTempNestedScrollConsumed; - } - consumed[0] = 0; - consumed[1] = 0; - mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); - if (offsetInWindow != null) { - getLocationInWindow(offsetInWindow); - offsetInWindow[0] -= startX; - offsetInWindow[1] -= startY; + if (consumed == null) { + if (mTempNestedScrollConsumed == null) { + mTempNestedScrollConsumed = new int[2]; + } + consumed = mTempNestedScrollConsumed; + } + consumed[0] = 0; + consumed[1] = 0; + mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed); + + if (offsetInWindow != null) { + getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return consumed[0] != 0 || consumed[1] != 0; + } else if (offsetInWindow != null) { + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; } - return consumed[0] != 0 || consumed[1] != 0; } return false; } @@ -18211,18 +18225,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * Dispatch a fling to a nested scrolling parent. * - * <p>If a nested scrolling child view would normally fling but it is at the edge of its - * own content it should use this method to delegate the fling to its nested scrolling parent. - * The view implementation can use a {@link VelocityTracker} to obtain the velocity values - * to pass.</p> + * <p>This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.</p> + * + * <p>If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.</p> * * @param velocityX Horizontal fling velocity in pixels per second * @param velocityY Vertical fling velocity in pixels per second - * @return true if the nested scrolling parent consumed the fling + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling */ - public boolean dispatchNestedFling(float velocityX, float velocityY) { + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { - return mNestedScrollingParent.onNestedFling(this, velocityX, velocityY); + return mNestedScrollingParent.onNestedFling(this, velocityX, velocityY, consumed); } return false; } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 8865ab4..43bc0b6 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -2342,7 +2342,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; - stopNestedScroll(); } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } @@ -5914,7 +5913,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @inheritDoc */ @Override - public boolean onNestedFling(View target, float velocityX, float velocityY) { + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 3cd6449..588b9cd 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -512,14 +512,21 @@ public interface ViewParent { /** * Request a fling from a nested scroll. * + * <p>This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.</p> + * * <p>If a nested scrolling child view would normally fling but it is at the edge of - * its own content, it can delegate the fling to its nested scrolling parent instead. - * This method allows the parent to optionally consume the fling.</p> + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.</p> * * @param target View that initiated the nested scroll * @param velocityX Horizontal velocity in pixels per second. * @param velocityY Vertical velocity in pixels per second - * @return true if this parent consumed the fling + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling */ - public boolean onNestedFling(View target, float velocityX, float velocityY); + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 14e422c..ea52924 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6131,7 +6131,7 @@ public final class ViewRootImpl implements ViewParent, } @Override - public boolean onNestedFling(View target, float velocityX, float velocityY) { + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return false; } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 7e8f6b4..3e46f68 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -1565,10 +1565,10 @@ public class ScrollView extends FrameLayout { } private void flingWithNestedDispatch(int velocityY) { - if (mScrollY == 0 && velocityY < 0 || - mScrollY == getScrollRange() && velocityY > 0) { - dispatchNestedFling(0, velocityY); - } else { + final boolean canFling = (mScrollY > 0 || velocityY > 0) && + (mScrollY < getScrollRange() || velocityY < 0); + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { fling(velocityY); } } @@ -1627,6 +1627,12 @@ public class ScrollView extends FrameLayout { return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; } + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + super.onNestedScrollAccepted(child, target, axes); + startNestedScroll(SCROLL_AXIS_VERTICAL); + } + /** * @inheritDoc */ @@ -1638,16 +1644,23 @@ public class ScrollView extends FrameLayout { @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + final int oldScrollY = mScrollY; scrollBy(0, dyUnconsumed); + final int myConsumed = mScrollY - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); } /** * @inheritDoc */ @Override - public boolean onNestedFling(View target, float velocityX, float velocityY) { - flingWithNestedDispatch((int) velocityY); - return true; + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; } @Override diff --git a/core/java/com/android/internal/app/WindowDecorActionBar.java b/core/java/com/android/internal/app/WindowDecorActionBar.java index 131f828..66548f0 100644 --- a/core/java/com/android/internal/app/WindowDecorActionBar.java +++ b/core/java/com/android/internal/app/WindowDecorActionBar.java @@ -17,7 +17,9 @@ package com.android.internal.app; import android.animation.ValueAnimator; +import android.content.res.TypedArray; import android.view.ViewParent; +import com.android.internal.R; import com.android.internal.view.ActionBarPolicy; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; @@ -41,7 +43,6 @@ import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Handler; import android.util.TypedValue; import android.view.ActionMode; import android.view.ContextThemeWrapper; @@ -57,7 +58,6 @@ import android.widget.SpinnerAdapter; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Map; /** * WindowDecorActionBar is the ActionBar implementation used @@ -66,7 +66,8 @@ import java.util.Map; * across both the ActionBarView at the top of the screen and * a horizontal LinearLayout at the bottom which is normally hidden. */ -public class WindowDecorActionBar extends ActionBar { +public class WindowDecorActionBar extends ActionBar implements + ActionBarOverlayLayout.ActionBarVisibilityCallback { private static final String TAG = "WindowDecorActionBar"; private Context mContext; @@ -116,6 +117,7 @@ public class WindowDecorActionBar extends ActionBar { private Animator mCurrentShowAnim; private boolean mShowHideAnimationEnabled; + boolean mHideOnContentScroll; final AnimatorListener mHideListener = new AnimatorListenerAdapter() { @Override @@ -132,7 +134,7 @@ public class WindowDecorActionBar extends ActionBar { mCurrentShowAnim = null; completeDeferredDestroyActionMode(); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } }; @@ -183,7 +185,7 @@ public class WindowDecorActionBar extends ActionBar { mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById( com.android.internal.R.id.action_bar_overlay_layout); if (mOverlayLayout != null) { - mOverlayLayout.setActionBar(this); + mOverlayLayout.setActionBarVisibilityCallback(this); } mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); mContextView = (ActionBarContextView) decor.findViewById( @@ -213,6 +215,14 @@ public class WindowDecorActionBar extends ActionBar { ActionBarPolicy abp = ActionBarPolicy.get(mContext); setHomeButtonEnabled(abp.enableHomeButtonByDefault() || homeAsUp); setHasEmbeddedTabs(abp.hasEmbeddedTabs()); + + final TypedArray a = mContext.obtainStyledAttributes(null, + com.android.internal.R.styleable.ActionBar, + com.android.internal.R.attr.actionBarStyle, 0); + if (a.getBoolean(R.styleable.ActionBar_hideOnContentScroll, false)) { + setHideOnContentScrollEnabled(true); + } + a.recycle(); } public void onConfigurationChanged(Configuration newConfig) { @@ -234,17 +244,14 @@ public class WindowDecorActionBar extends ActionBar { if (isInTabMode) { mTabScrollView.setVisibility(View.VISIBLE); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } else { mTabScrollView.setVisibility(View.GONE); } } mActionView.setCollapsable(!mHasEmbeddedTabs && isInTabMode); - } - - public boolean hasNonEmbeddedTabs() { - return !mHasEmbeddedTabs && getNavigationMode() == NAVIGATION_MODE_TABS; + mOverlayLayout.setHasNonEmbeddedTabs(!mHasEmbeddedTabs && isInTabMode); } private void ensureTabsExist() { @@ -279,7 +286,7 @@ public class WindowDecorActionBar extends ActionBar { } } - public void setWindowVisibility(int visibility) { + public void onWindowVisibilityChanged(int visibility) { mCurWindowVisibility = visibility; } @@ -453,6 +460,7 @@ public class WindowDecorActionBar extends ActionBar { mActionMode.finish(); } + mOverlayLayout.setHideOnContentScrollEnabled(false); mContextView.killMode(); ActionModeImpl mode = new ActionModeImpl(callback); if (mode.dispatchOnCreate()) { @@ -464,7 +472,7 @@ public class WindowDecorActionBar extends ActionBar { if (mSplitView.getVisibility() != View.VISIBLE) { mSplitView.setVisibility(View.VISIBLE); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } } @@ -652,6 +660,35 @@ public class WindowDecorActionBar extends ActionBar { } } + @Override + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll && !mOverlayLayout.isInOverlayMode()) { + throw new IllegalStateException("Action bar must be in overlay mode " + + "(Window.FEATURE_OVERLAY_ACTION_BAR) to enable hide on content scroll"); + } + mHideOnContentScroll = hideOnContentScroll; + mOverlayLayout.setHideOnContentScrollEnabled(hideOnContentScroll); + } + + @Override + public boolean isHideOnContentScrollEnabled() { + return mOverlayLayout.isHideOnContentScrollEnabled(); + } + + @Override + public int getHideOffset() { + return mOverlayLayout.getActionBarHideOffset(); + } + + @Override + public void setHideOffset(int offset) { + if (offset != 0 && !mOverlayLayout.isInOverlayMode()) { + throw new IllegalStateException("Action bar must be in overlay mode " + + "(Window.FEATURE_OVERLAY_ACTION_BAR) to set a non-zero hide offset"); + } + mOverlayLayout.setActionBarHideOffset(offset); + } + private static boolean checkShowingFlags(boolean hiddenByApp, boolean hiddenBySystem, boolean showingForMode) { if (showingForMode) { @@ -737,7 +774,7 @@ public class WindowDecorActionBar extends ActionBar { mShowListener.onAnimationEnd(null); } if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } @@ -781,11 +818,7 @@ public class WindowDecorActionBar extends ActionBar { } public boolean isShowing() { - return mNowShowing; - } - - public boolean isSystemShowing() { - return !mHiddenBySystem; + return mNowShowing && getHideOffset() < getHeight(); } void animateToMode(boolean toActionMode) { @@ -844,6 +877,18 @@ public class WindowDecorActionBar extends ActionBar { mActionView.setHomeActionContentDescription(resId); } + @Override + public void onContentScrollStarted() { + if (mCurrentShowAnim != null) { + mCurrentShowAnim.cancel(); + mCurrentShowAnim = null; + } + } + + @Override + public void onContentScrollStopped() { + } + /** * @hide */ @@ -894,6 +939,7 @@ public class WindowDecorActionBar extends ActionBar { // Clear out the context mode views after the animation finishes mContextView.closeMode(); mActionView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mOverlayLayout.setHideOnContentScrollEnabled(mHideOnContentScroll); mActionMode = null; } @@ -1204,6 +1250,7 @@ public class WindowDecorActionBar extends ActionBar { break; } mActionView.setCollapsable(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); + mOverlayLayout.setHasNonEmbeddedTabs(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); } @Override diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index c9dff1a..01bee0c 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -16,18 +16,22 @@ package com.android.internal.widget; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.view.ViewGroup; -import android.view.WindowInsets; -import com.android.internal.app.WindowDecorActionBar; - +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Canvas; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.util.AttributeSet; +import android.util.IntProperty; +import android.util.Property; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewPropertyAnimator; +import android.view.WindowInsets; +import android.widget.OverScroller; /** * Special layout for the containing of an overlay action bar (and its @@ -38,7 +42,7 @@ public class ActionBarOverlayLayout extends ViewGroup { private static final String TAG = "ActionBarOverlayLayout"; private int mActionBarHeight; - private WindowDecorActionBar mActionBar; + //private WindowDecorActionBar mActionBar; private int mWindowVisibility = View.VISIBLE; // The main UI elements that we handle the layout of. @@ -54,6 +58,10 @@ public class ActionBarOverlayLayout extends ViewGroup { private boolean mIgnoreWindowContentOverlay; private boolean mOverlayMode; + private boolean mHasNonEmbeddedTabs; + private boolean mHideOnContentScroll; + private boolean mAnimatingForFling; + private int mHideOnContentScrollReference; private int mLastSystemUiVisibility; private final Rect mBaseContentInsets = new Rect(); private final Rect mLastBaseContentInsets = new Rect(); @@ -62,6 +70,84 @@ public class ActionBarOverlayLayout extends ViewGroup { private final Rect mInnerInsets = new Rect(); private final Rect mLastInnerInsets = new Rect(); + private ActionBarVisibilityCallback mActionBarVisibilityCallback; + + private final int ACTION_BAR_ANIMATE_DELAY = 600; // ms + + private OverScroller mFlingEstimator; + + private ViewPropertyAnimator mCurrentActionBarTopAnimator; + private ViewPropertyAnimator mCurrentActionBarBottomAnimator; + + private final Animator.AnimatorListener mTopAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentActionBarTopAnimator = null; + mAnimatingForFling = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentActionBarTopAnimator = null; + mAnimatingForFling = false; + } + }; + + private final Animator.AnimatorListener mBottomAnimatorListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCurrentActionBarBottomAnimator = null; + mAnimatingForFling = false; + } + + @Override + public void onAnimationCancel(Animator animation) { + mCurrentActionBarBottomAnimator = null; + mAnimatingForFling = false; + } + }; + + private final Runnable mRemoveActionBarHideOffset = new Runnable() { + public void run() { + haltActionBarHideOffsetAnimations(); + mCurrentActionBarTopAnimator = mActionBarTop.animate().translationY(0) + .setListener(mTopAnimatorListener); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + mCurrentActionBarBottomAnimator = mActionBarBottom.animate().translationY(0) + .setListener(mBottomAnimatorListener); + } + } + }; + + private final Runnable mAddActionBarHideOffset = new Runnable() { + public void run() { + haltActionBarHideOffsetAnimations(); + mCurrentActionBarTopAnimator = mActionBarTop.animate() + .translationY(-mActionBarTop.getHeight()) + .setListener(mTopAnimatorListener); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + mCurrentActionBarBottomAnimator = mActionBarBottom.animate() + .translationY(mActionBarBottom.getHeight()) + .setListener(mBottomAnimatorListener); + } + } + }; + + public static final Property<ActionBarOverlayLayout, Integer> ACTION_BAR_HIDE_OFFSET = + new IntProperty<ActionBarOverlayLayout>("actionBarHideOffset") { + + @Override + public void setValue(ActionBarOverlayLayout object, int value) { + object.setActionBarHideOffset(value); + } + + @Override + public Integer get(ActionBarOverlayLayout object) { + return object.getActionBarHideOffset(); + } + }; + static final int[] ATTRS = new int [] { com.android.internal.R.attr.actionBarSize, com.android.internal.R.attr.windowContentOverlay @@ -86,14 +172,22 @@ public class ActionBarOverlayLayout extends ViewGroup { mIgnoreWindowContentOverlay = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT; + + mFlingEstimator = new OverScroller(context); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + haltActionBarHideOffsetAnimations(); } - public void setActionBar(WindowDecorActionBar impl) { - mActionBar = impl; + public void setActionBarVisibilityCallback(ActionBarVisibilityCallback cb) { + mActionBarVisibilityCallback = cb; if (getWindowToken() != null) { // This is being initialized after being added to a window; // make sure to update all state now. - mActionBar.setWindowVisibility(mWindowVisibility); + mActionBarVisibilityCallback.onWindowVisibilityChanged(mWindowVisibility); if (mLastSystemUiVisibility != 0) { int newVis = mLastSystemUiVisibility; onWindowSystemUiVisibilityChanged(newVis); @@ -114,6 +208,14 @@ public class ActionBarOverlayLayout extends ViewGroup { Build.VERSION_CODES.KITKAT; } + public boolean isInOverlayMode() { + return mOverlayMode; + } + + public void setHasNonEmbeddedTabs(boolean hasNonEmbeddedTabs) { + mHasNonEmbeddedTabs = hasNonEmbeddedTabs; + } + public void setShowingForActionMode(boolean showing) { if (showing) { // Here's a fun hack: if the status bar is currently being hidden, @@ -140,19 +242,18 @@ public class ActionBarOverlayLayout extends ViewGroup { pullChildren(); final int diff = mLastSystemUiVisibility ^ visible; mLastSystemUiVisibility = visible; - final boolean barVisible = (visible&SYSTEM_UI_FLAG_FULLSCREEN) == 0; - final boolean wasVisible = mActionBar != null ? mActionBar.isSystemShowing() : true; - final boolean stable = (visible&SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0; - if (mActionBar != null) { + final boolean barVisible = (visible & SYSTEM_UI_FLAG_FULLSCREEN) == 0; + final boolean stable = (visible & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0; + if (mActionBarVisibilityCallback != null) { // We want the bar to be visible if it is not being hidden, // or the app has not turned on a stable UI mode (meaning they // are performing explicit layout around the action bar). - mActionBar.enableContentAnimations(!stable); - if (barVisible || !stable) mActionBar.showForSystem(); - else mActionBar.hideForSystem(); + mActionBarVisibilityCallback.enableContentAnimations(!stable); + if (barVisible || !stable) mActionBarVisibilityCallback.showForSystem(); + else mActionBarVisibilityCallback.hideForSystem(); } - if ((diff&SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { - if (mActionBar != null) { + if ((diff & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + if (mActionBarVisibilityCallback != null) { requestApplyInsets(); } } @@ -162,8 +263,8 @@ public class ActionBarOverlayLayout extends ViewGroup { protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); mWindowVisibility = visibility; - if (mActionBar != null) { - mActionBar.setWindowVisibility(visibility); + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onWindowVisibilityChanged(visibility); } } @@ -279,8 +380,8 @@ public class ActionBarOverlayLayout extends ViewGroup { // This is the standard space needed for the action bar. For stable measurement, // we can't depend on the size currently reported by it -- this must remain constant. topInset = mActionBarHeight; - if (mActionBar != null && mActionBar.hasNonEmbeddedTabs()) { - View tabs = mActionBarTop.getTabContainer(); + if (mHasNonEmbeddedTabs) { + final View tabs = mActionBarTop.getTabContainer(); if (tabs != null) { // If tabs are not embedded, increase space on top to account for them. topInset += mActionBarHeight; @@ -395,16 +496,138 @@ public class ActionBarOverlayLayout extends ViewGroup { return false; } + @Override + public boolean onStartNestedScroll(View child, View target, int axes) { + if ((axes & SCROLL_AXIS_VERTICAL) == 0 || mActionBarTop.getVisibility() != VISIBLE) { + return false; + } + return mHideOnContentScroll; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + super.onNestedScrollAccepted(child, target, axes); + mHideOnContentScrollReference = getActionBarHideOffset(); + haltActionBarHideOffsetAnimations(); + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onContentScrollStarted(); + } + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + mHideOnContentScrollReference += dyConsumed; + setActionBarHideOffset(mHideOnContentScrollReference); + } + + @Override + public void onStopNestedScroll(View target) { + super.onStopNestedScroll(target); + if (mHideOnContentScroll && !mAnimatingForFling) { + if (mHideOnContentScrollReference <= mActionBarTop.getHeight()) { + postRemoveActionBarHideOffset(); + } else { + postAddActionBarHideOffset(); + } + } + if (mActionBarVisibilityCallback != null) { + mActionBarVisibilityCallback.onContentScrollStopped(); + } + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!mHideOnContentScroll || !consumed) { + return false; + } + if (shouldHideActionBarOnFling(velocityX, velocityY)) { + addActionBarHideOffset(); + } else { + removeActionBarHideOffset(); + } + mAnimatingForFling = true; + return true; + } + void pullChildren() { if (mContent == null) { mContent = findViewById(com.android.internal.R.id.content); - mActionBarTop = (ActionBarContainer)findViewById( + mActionBarTop = (ActionBarContainer) findViewById( com.android.internal.R.id.action_bar_container); mActionBarView = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); mActionBarBottom = findViewById(com.android.internal.R.id.split_action_bar); } } + public void setHideOnContentScrollEnabled(boolean hideOnContentScroll) { + if (hideOnContentScroll != mHideOnContentScroll) { + mHideOnContentScroll = hideOnContentScroll; + if (!hideOnContentScroll) { + stopNestedScroll(); + haltActionBarHideOffsetAnimations(); + setActionBarHideOffset(0); + } + } + } + + public boolean isHideOnContentScrollEnabled() { + return mHideOnContentScroll; + } + + public int getActionBarHideOffset() { + return -((int) mActionBarTop.getTranslationY()); + } + + public void setActionBarHideOffset(int offset) { + haltActionBarHideOffsetAnimations(); + final int topHeight = mActionBarTop.getHeight(); + offset = Math.max(0, Math.min(offset, topHeight)); + mActionBarTop.setTranslationY(-offset); + if (mActionBarBottom != null && mActionBarBottom.getVisibility() != GONE) { + // Match the hide offset proportionally for a split bar + final float fOffset = (float) offset / topHeight; + final int bOffset = (int) (mActionBarBottom.getHeight() * fOffset); + mActionBarBottom.setTranslationY(bOffset); + } + } + + private void haltActionBarHideOffsetAnimations() { + removeCallbacks(mRemoveActionBarHideOffset); + removeCallbacks(mAddActionBarHideOffset); + if (mCurrentActionBarTopAnimator != null) { + mCurrentActionBarTopAnimator.cancel(); + } + if (mCurrentActionBarBottomAnimator != null) { + mCurrentActionBarBottomAnimator.cancel(); + } + } + + private void postRemoveActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + postDelayed(mRemoveActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY); + } + + private void postAddActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + postDelayed(mAddActionBarHideOffset, ACTION_BAR_ANIMATE_DELAY); + } + + private void removeActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + mRemoveActionBarHideOffset.run(); + } + + private void addActionBarHideOffset() { + haltActionBarHideOffsetAnimations(); + mAddActionBarHideOffset.run(); + } + + private boolean shouldHideActionBarOnFling(float velocityX, float velocityY) { + mFlingEstimator.fling(0, 0, 0, (int) velocityY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); + final int finalY = mFlingEstimator.getFinalY(); + return finalY > mActionBarTop.getHeight(); + } public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { @@ -423,4 +646,13 @@ public class ActionBarOverlayLayout extends ViewGroup { super(source); } } + + public interface ActionBarVisibilityCallback { + void onWindowVisibilityChanged(int visibility); + void showForSystem(); + void hideForSystem(); + void enableContentAnimations(boolean enable); + void onContentScrollStarted(); + void onContentScrollStopped(); + } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index efc1b55..e07ebd4 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -966,7 +966,6 @@ <attr name="colorButtonPressed" format="color" /> <attr name="colorButtonNormalColored" format="color" /> <attr name="colorButtonPressedColored" format="color" /> - </declare-styleable> <!-- **************************************************************** --> @@ -6348,6 +6347,8 @@ <!-- Specifies padding that should be applied to the left and right sides of system-provided items in the bar. --> <attr name="itemPadding" format="dimension" /> + <!-- Set true to hide the action bar on a vertical nested scroll of content. --> + <attr name="hideOnContentScroll" format="boolean" /> </declare-styleable> <declare-styleable name="ActionMode"> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 5ccb05b..d4b3f0d 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2166,6 +2166,7 @@ <public type="attr" name="elevation" /> <public type="attr" name="excludeId" /> <public type="attr" name="excludeClass" /> + <public type="attr" name="hideOnContentScroll" /> <public-padding type="dimen" name="l_resource_pad" end="0x01050010" /> |