diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 642f0fa48c5e6..9e184184b0bfd 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Interpolator; import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; @@ -514,7 +515,8 @@ import java.util.WeakHashMap; * The framework provides basic support for views that wish to internally * scroll their content. This includes keeping track of the X and Y scroll * offset as well as mechanisms for drawing scrollbars. See - * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)} for more details. + * {@link #scrollBy(int, int)}, {@link #scrollTo(int, int)}, and + * {@link #awakenScrollBars()} for more details. *

* * @@ -571,6 +573,8 @@ import java.util.WeakHashMap; * @attr ref android.R.styleable#View_scrollbarSize * @attr ref android.R.styleable#View_scrollbarStyle * @attr ref android.R.styleable#View_scrollbars + * @attr ref android.R.styleable#View_scrollbarDefaultDelayBeforeFade + * @attr ref android.R.styleable#View_scrollbarFadeDuration * @attr ref android.R.styleable#View_scrollbarTrackHorizontal * @attr ref android.R.styleable#View_scrollbarThumbHorizontal * @attr ref android.R.styleable#View_scrollbarThumbVertical @@ -2214,12 +2218,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility protected void initializeScrollbars(TypedArray a) { initScrollCache(); - if (mScrollCache.scrollBar == null) { - mScrollCache.scrollBar = new ScrollBarDrawable(); - } - final ScrollabilityCache scrollabilityCache = mScrollCache; + + if (scrollabilityCache.scrollBar == null) { + scrollabilityCache.scrollBar = new ScrollBarDrawable(); + } + + final boolean fadeScrollbars = a.getBoolean(R.styleable.View_fadeScrollbars, false); + if (!fadeScrollbars) { + scrollabilityCache.state = ScrollabilityCache.ON; + } + scrollabilityCache.fadeScrollBars = fadeScrollbars; + + + scrollabilityCache.scrollBarFadeDuration = a.getInt( + R.styleable.View_scrollbarFadeDuration, ViewConfiguration + .getScrollBarFadeDuration()); + scrollabilityCache.scrollBarDefaultDelayBeforeFade = a.getInt( + R.styleable.View_scrollbarDefaultDelayBeforeFade, + ViewConfiguration.getScrollDefaultDelay()); + + scrollabilityCache.scrollBarSize = a.getDimensionPixelSize( com.android.internal.R.styleable.View_scrollbarSize, ViewConfiguration.get(mContext).getScaledScrollBarSize()); @@ -2263,7 +2283,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ private void initScrollCache() { if (mScrollCache == null) { - mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext)); + mScrollCache = new ScrollabilityCache(ViewConfiguration.get(mContext), this); } } @@ -4671,7 +4691,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mScrollX = x; mScrollY = y; onScrollChanged(mScrollX, mScrollY, oldX, oldY); - invalidate(); + if (!awakenScrollBars()) { + invalidate(); + } } } @@ -4686,6 +4708,120 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility scrollTo(mScrollX + x, mScrollY + y); } + /** + *

Trigger the scrollbars to draw. When invoked this method starts an + * animation to fade the scrollbars out after a default delay. If a subclass + * provides animated scrolling, the start delay should equal the duration + * of the scrolling animation.

+ * + *

The animation starts only if at least one of the scrollbars is + * enabled, as specified by {@link #isHorizontalScrollBarEnabled()} and + * {@link #isVerticalScrollBarEnabled()}. When the animation is started, + * this method returns true, and false otherwise. If the animation is + * started, this method calls {@link #invalidate()}; in that case the + * caller should not call {@link #invalidate()}.

+ * + *

This method should be invoked every time a subclass directly updates + * the scroll parameters. (See {@link #mScrollX} and {@link #mScrollY})

+ * + *

This method is automatically invoked by {@link #scrollBy(int, int)} + * and {@link #scrollTo(int, int)}.

+ * + * @return true if the animation is played, false otherwise + * + * @see #awakenScrollBars(int) + * @see #mScrollX + * @see #mScrollY + * @see #scrollBy(int, int) + * @see #scrollTo(int, int) + * @see #isHorizontalScrollBarEnabled() + * @see #isVerticalScrollBarEnabled() + * @see #setHorizontalScrollBarEnabled(boolean) + * @see #setVerticalScrollBarEnabled(boolean) + */ + protected boolean awakenScrollBars() { + return mScrollCache != null && + awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade); + } + + /** + *

+ * Trigger the scrollbars to draw. When invoked this method starts an + * animation to fade the scrollbars out after a fixed delay. If a subclass + * provides animated scrolling, the start delay should equal the duration of + * the scrolling animation. + *

+ * + *

+ * The animation starts only if at least one of the scrollbars is enabled, + * as specified by {@link #isHorizontalScrollBarEnabled()} and + * {@link #isVerticalScrollBarEnabled()}. When the animation is started, + * this method returns true, and false otherwise. If the animation is + * started, this method calls {@link #invalidate()}; in that case the caller + * should not call {@link #invalidate()}. + *

+ * + *

+ * This method should be invoked everytime a subclass directly updates the + * scroll parameters. (See {@link #mScrollX} and {@link #mScrollY}) + *

+ * + * @param startDelay the delay, in milliseconds, after which the animation + * should start; when the delay is 0, the animation starts + * immediately + * @return true if the animation is played, false otherwise + * + * @see #mScrollX + * @see #mScrollY + * @see #scrollBy(int, int) + * @see #scrollTo(int, int) + * @see #isHorizontalScrollBarEnabled() + * @see #isVerticalScrollBarEnabled() + * @see #setHorizontalScrollBarEnabled(boolean) + * @see #setVerticalScrollBarEnabled(boolean) + */ + protected boolean awakenScrollBars(int startDelay) { + final ScrollabilityCache scrollCache = mScrollCache; + + if (scrollCache == null || !scrollCache.fadeScrollBars) { + return false; + } + + if (scrollCache.scrollBar == null) { + scrollCache.scrollBar = new ScrollBarDrawable(); + } + + if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) { + + // Invalidate to show the scrollbars + invalidate(); + + if (scrollCache.state == ScrollabilityCache.OFF) { + // FIXME: this is copied from WindowManagerService. + // We should get this value from the system when it + // is possible to do so. + final int KEY_REPEAT_FIRST_DELAY = 750; + startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay); + } + + // Tell mScrollCache when we should start fading. This may + // extend the fade start time if one was already scheduled + long fadeStartTime = SystemClock.uptimeMillis() + startDelay; + scrollCache.fadeStartTime = fadeStartTime; + scrollCache.state = ScrollabilityCache.ON; + + // Schedule our fader to run, unscheduling any old ones first + if (mAttachInfo != null) { + mAttachInfo.mHandler.removeCallbacks(scrollCache); + mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime); + } + + return true; + } + + return false; + } + /** * Mark the the area defined by dirty as needing to be drawn. If the view is * visible, {@link #onDraw} will be called at some point in the future. @@ -5344,11 +5480,49 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * scrollbars are painted only if they have been awakened first.

* * @param canvas the canvas on which to draw the scrollbars + * + * @see #awakenScrollBars(int) */ private void onDrawScrollBars(Canvas canvas) { // scrollbars are drawn only when the animation is running final ScrollabilityCache cache = mScrollCache; if (cache != null) { + + int state = cache.state; + + if (state == ScrollabilityCache.OFF) { + return; + } + + boolean invalidate = false; + + if (state == ScrollabilityCache.FADING) { + // We're fading -- get our fade interpolation + if (cache.interpolatorValues == null) { + cache.interpolatorValues = new float[1]; + } + + float[] values = cache.interpolatorValues; + + // Stops the animation if we're done + if (cache.scrollBarInterpolator.timeToValues(values) == + Interpolator.Result.FREEZE_END) { + cache.state = ScrollabilityCache.OFF; + } else { + cache.scrollBar.setAlpha(Math.round(values[0])); + } + + // This will make the scroll bars inval themselves after + // drawing. We only want this when we're fading so that + // we prevent excessive redraws + invalidate = true; + } else { + // We're just on -- but we may have been fading before so + // reset alpha + cache.scrollBar.setAlpha(255); + } + + final int viewFlags = mViewFlags; final boolean drawHorizontalScrollBar = @@ -5371,19 +5545,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final int scrollY = mScrollY; final int inside = (viewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0; + int left, top, right, bottom; + if (drawHorizontalScrollBar) { - scrollBar.setParameters( - computeHorizontalScrollRange(), + scrollBar.setParameters(computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(), false); - final int top = scrollY + height - size - (mUserPaddingBottom & inside); final int verticalScrollBarGap = drawVerticalScrollBar ? - getVerticalScrollbarWidth() : 0; - onDrawHorizontalScrollBar(canvas, scrollBar, - scrollX + (mPaddingLeft & inside), - top, - scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap, - top + size); + getVerticalScrollbarWidth() : 0; + top = scrollY + height - size - (mUserPaddingBottom & inside); + left = scrollX + (mPaddingLeft & inside); + right = scrollX + width - (mUserPaddingRight & inside) - verticalScrollBarGap; + bottom = top + size; + onDrawHorizontalScrollBar(canvas, scrollBar, left, top, right, bottom); + if (invalidate) { + invalidate(left, top, right, bottom); + } } if (drawVerticalScrollBar) { @@ -5391,12 +5568,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility computeVerticalScrollOffset(), computeVerticalScrollExtent(), true); // TODO: Deal with RTL languages to position scrollbar on left - final int left = scrollX + width - size - (mUserPaddingRight & inside); - onDrawVerticalScrollBar(canvas, scrollBar, - left, - scrollY + (mPaddingTop & inside), - left + size, - scrollY + height - (mUserPaddingBottom & inside)); + left = scrollX + width - size - (mUserPaddingRight & inside); + top = scrollY + (mPaddingTop & inside); + right = left + size; + bottom = scrollY + height - (mUserPaddingBottom & inside); + onDrawVerticalScrollBar(canvas, scrollBar, left, top, right, bottom); + if (invalidate) { + invalidate(left, top, right, bottom); + } } } } @@ -5424,7 +5603,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @see #computeHorizontalScrollExtent() * @see #computeHorizontalScrollOffset() * @see android.widget.ScrollBarDrawable - * @hide + * @hide */ protected void onDrawHorizontalScrollBar(Canvas canvas, Drawable scrollBar, @@ -8731,21 +8910,62 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * is supported. This avoids keeping too many unused fields in most * instances of View.

*/ - private static class ScrollabilityCache { + private static class ScrollabilityCache implements Runnable { + + /** + * Scrollbars are not visible + */ + public static final int OFF = 0; + + /** + * Scrollbars are visible + */ + public static final int ON = 1; + + /** + * Scrollbars are fading away + */ + public static final int FADING = 2; + + public boolean fadeScrollBars; + public int fadingEdgeLength; + public int scrollBarDefaultDelayBeforeFade; + public int scrollBarFadeDuration; public int scrollBarSize; public ScrollBarDrawable scrollBar; + public float[] interpolatorValues; + public View host; public final Paint paint; public final Matrix matrix; public Shader shader; + public final Interpolator scrollBarInterpolator = new Interpolator(1, 2); + + private final float[] mOpaque = {255.0f}; + private final float[] mTransparent = {0.0f}; + + /** + * When fading should start. This time moves into the future every time + * a new scroll happens. Measured based on SystemClock.uptimeMillis() + */ + public long fadeStartTime; + + + /** + * The current state of the scrollbars: ON, OFF, or FADING + */ + public int state = OFF; + private int mLastColor; - public ScrollabilityCache(ViewConfiguration configuration) { + public ScrollabilityCache(ViewConfiguration configuration, View host) { fadingEdgeLength = configuration.getScaledFadingEdgeLength(); scrollBarSize = configuration.getScaledScrollBarSize(); + scrollBarDefaultDelayBeforeFade = configuration.getScrollDefaultDelay(); + scrollBarFadeDuration = configuration.getScrollBarFadeDuration(); paint = new Paint(); matrix = new Matrix(); @@ -8755,6 +8975,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); + this.host = host; } public void setFadeColor(int color) { @@ -8770,5 +8991,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility paint.setXfermode(null); } } + + public void run() { + long now = SystemClock.uptimeMillis(); + if (now >= fadeStartTime) { + + // the animation fades the scrollbars out by changing + // the opacity (alpha) from fully opaque to fully + // transparent + int nextFrame = (int) now; + int framesCount = 0; + + Interpolator interpolator = scrollBarInterpolator; + + // Start opaque + interpolator.setKeyFrame(framesCount++, nextFrame, mOpaque); + + // End transparent + nextFrame += scrollBarFadeDuration; + interpolator.setKeyFrame(framesCount, nextFrame, mTransparent); + + state = FADING; + + // Kick off the fade animation + host.invalidate(); + } + } + } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 377646332c973..993048f5ecf27 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -30,6 +30,16 @@ public class ViewConfiguration { */ private static final int SCROLL_BAR_SIZE = 10; + /** + * Duration of the fade when scrollbars fade away in milliseconds + */ + private static final int SCROLL_BAR_FADE_DURATION = 250; + + /** + * Default delay before the scrollbars fade in milliseconds + */ + private static final int SCROLL_BAR_DEFAULT_DELAY = 300; + /** * Defines the length of the fading edges in pixels */ @@ -220,6 +230,20 @@ public class ViewConfiguration { return mScrollbarSize; } + /** + * @return Duration of the fade when scrollbars fade away in milliseconds + */ + public static int getScrollBarFadeDuration() { + return SCROLL_BAR_FADE_DURATION; + } + + /** + * @return Default delay before the scrollbars fade in milliseconds + */ + public static int getScrollDefaultDelay() { + return SCROLL_BAR_DEFAULT_DELAY; + } + /** * @return the length of the fading edges in pixels * diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 0b848484a47c9..603f67ad29f07 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -2480,6 +2480,7 @@ public class WebView extends AbsoluteLayout // Log.d(LOGTAG, "startScroll: " + dx + " " + dy); mScroller.startScroll(mScrollX, mScrollY, dx, dy, animationDuration > 0 ? animationDuration : computeDuration(dx, dy)); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { abortAnimation(); // just in case @@ -4326,6 +4327,7 @@ public class WebView extends AbsoluteLayout // resume the webcore update. final int time = mScroller.getDuration(); mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time); + awakenScrollBars(time); invalidate(); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index b242b583ae9ae..165794aec3d4f 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -2444,7 +2444,9 @@ public abstract class AbsListView extends AdapterView implements Te if (spaceAbove >= absIncrementalDeltaY && spaceBelow >= absIncrementalDeltaY) { hideSelector(); offsetChildrenTopAndBottom(incrementalDeltaY); - invalidate(); + if (!awakenScrollBars()) { + invalidate(); + } mMotionViewNewTop = mMotionViewOriginalTop + deltaY; } else { final int firstPosition = mFirstPosition; @@ -2527,6 +2529,7 @@ public abstract class AbsListView extends AdapterView implements Te mBlockLayoutRequests = false; invokeOnItemScrollListener(); + awakenScrollBars(); } } diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 33e83c3eb7927..ce07f7a111b9d 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1340,8 +1340,23 @@ public class GridView extends AbsListView { */ @Override void setSelectionInt(int position) { + int previousSelectedPosition = mNextSelectedPosition; + setNextSelectedPositionInt(position); layoutChildren(); + + final int next = mStackFromBottom ? mItemCount - 1 - mNextSelectedPosition : + mNextSelectedPosition; + final int previous = mStackFromBottom ? mItemCount - 1 + - previousSelectedPosition : previousSelectedPosition; + + final int nextRow = next / mNumColumns; + final int previousRow = previous / mNumColumns; + + if (nextRow != previousRow) { + awakenScrollBars(); + } + } @Override @@ -1471,6 +1486,7 @@ public class GridView extends AbsListView { if (nextPage >= 0) { setSelectionInt(nextPage); invokeOnItemScrollListener(); + awakenScrollBars(); return true; } @@ -1497,6 +1513,10 @@ public class GridView extends AbsListView { invokeOnItemScrollListener(); moved = true; } + + if (moved) { + awakenScrollBars(); + } return moved; } @@ -1563,6 +1583,10 @@ public class GridView extends AbsListView { invokeOnItemScrollListener(); } + if (moved) { + awakenScrollBars(); + } + return moved; } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 46e514c8b44e9..52f56a7550ae9 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -829,6 +829,7 @@ public class HorizontalScrollView extends FrameLayout { long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; if (duration > ANIMATED_SCROLL_GAP) { mScroller.startScroll(mScrollX, mScrollY, dx, dy); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { if (!mScroller.isFinished()) { @@ -1172,6 +1173,7 @@ public class HorizontalScrollView extends FrameLayout { mScrollViewMovedFocus = false; } + awakenScrollBars(mScroller.getDuration()); invalidate(); } } diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 41c926726bcab..37208b3d0b09c 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -1818,13 +1818,29 @@ public class ListView extends AbsListView { /** * Makes the item at the supplied position selected. - * + * * @param position the position of the item to select */ @Override void setSelectionInt(int position) { setNextSelectedPositionInt(position); + boolean awakeScrollbars = false; + + final int selectedPosition = mSelectedPosition; + + if (selectedPosition >= 0) { + if (position == selectedPosition - 1) { + awakeScrollbars = true; + } else if (position == selectedPosition + 1) { + awakeScrollbars = true; + } + } + layoutChildren(); + + if (awakeScrollbars) { + awakenScrollBars(); + } } /** @@ -2084,7 +2100,9 @@ public class ListView extends AbsListView { setSelectionInt(position); invokeOnItemScrollListener(); - invalidate(); + if (!awakenScrollBars()) { + invalidate(); + } return true; } @@ -2125,7 +2143,8 @@ public class ListView extends AbsListView { } } - if (moved) { + if (moved && !awakenScrollBars()) { + awakenScrollBars(); invalidate(); } @@ -2270,7 +2289,9 @@ public class ListView extends AbsListView { positionSelector(selectedView); mSelectedTop = selectedView.getTop(); } - invalidate(); + if (!awakenScrollBars()) { + invalidate(); + } invokeOnItemScrollListener(); return true; } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 31c78146c98e1..24d97a5b98b5e 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -832,6 +832,7 @@ public class ScrollView extends FrameLayout { long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; if (duration > ANIMATED_SCROLL_GAP) { mScroller.startScroll(mScrollX, mScrollY, dx, dy); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { if (!mScroller.isFinished()) { @@ -1175,6 +1176,7 @@ public class ScrollView extends FrameLayout { mScrollViewMovedFocus = false; } + awakenScrollBars(mScroller.getDuration()); invalidate(); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3c61c1c4e61ee..10f86f27e1663 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -5572,6 +5572,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (duration > ANIMATED_SCROLL_GAP) { mScroller.startScroll(mScrollX, mScrollY, dx, dy); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { if (!mScroller.isFinished()) { diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index f5b8ab672228e..6f109df15dbec 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -1162,6 +1162,12 @@ set, else it will be false. --> + + + + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 257e0f25f3a00..97fb80a1dedab 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1172,7 +1172,10 @@ - + + + + diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index c0ca21bbfa34b..5ddbce27ca18f 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -126,6 +126,8 @@ ?android:attr/textAppearanceInverse + 250 + 300 10dip @android:drawable/scrollbar_handle_horizontal @android:drawable/scrollbar_handle_vertical