Android ScrollView with custom overscroll and overfling behavior.

Yuriy Skul
4 min readJul 29, 2020

--

Part 1: current article.
Part 2: custom Glowing EdgeEffect in Android RecyclerView.
Part 3: custom Distort EdgeEffect in Android ScrollView.

We regularly have to deal with scrolling content. Usually we use RecyclerView or ScrollView or NestedScrollView but what if we want to add custom EdgeEffect instead of standard one or create fancy overscroll/overfling behavior?

OnlyRecyclerView has setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) method…
What about overscroll and overfling distance? All these classes use
OverScroller class but still by default ScrollView has private mOverscrollDistance and mOverflingDistance which are initialized with zero value. NestedScrollView does not have it at all.

For current story I have picked up NestedScrollView as a starter. Because its methods and fields are private and there is no sense to extend this class let’s just create same class that is fully replica of NestedScrollView:

Starter code is just a simple copy-paste replica of NestedScrollView.
Let’s add any content to this layout as example textView with very long text.

Add overscroll and overfling.

First we have to add two field variables for overscrolling and overfling:

private int mOverscrollDistance;
private int mOverflingDistance;

And let’s init these variables with 1/6 of screen height inside onSizeChanged():

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

mOverscrollDistance = h / 2;
mOverflingDistance = h / 2;
...

And now we have to update inside onTouchEvent() method in case MotionEvent.ACTION_MOVE: the place where overScrollByCompat(...) is called.
Pass to it mOverscrollDistance as maxOverScrollY param:

Every time when fling happens postInvalidateOnAnimation(this) causes computeScroll() method to be invoked.
Fix inside computeScroll() method by passing to overScrollByCompat(...) mOverflingDistance value as maxOverScrollY parameter:

Now If we perform drag or fling overscrolling works fine only during first phase but during second one (springBack) it does not working.

Fix springBack phase:

If user overflings — fling starts springback phase so
we have to fix fling() method (by analogy to ScrollView.fling() ):

But overscroll is still not working sometimes.

Let’s compare ScrollView.computeScroll() with NestedScrollView.computeScroll:
Both methods have same logic but NestedScrollView computes unconsumed local variable next way :

final int y = mScroller.getCurrY();
int unconsumed = y - mLastScrollerY;
mLastScrollerY = y;

And ScrollView:

uses for it:mScroller.getCurrY()- mScrollY

That is the key: replace whole method with ScrollView.fling() implementation or just compute unconsumed like this:

int unconsumed = y - getScrollY();

Or it can be fixed next way. The problem is in mLastScrollerY variable. It has to store previous scroller position but during overScroll if user drags to the max distance runAnimatedScroll() is not being called and mLastScrollerY has wrong value. So third way to fix it just to update it directly from onTouchEvent() in case of MotionEvent.ACTION_UP check for overscroll and update mLastScrollerY :

case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker
.getYVelocity(mActivePointerId);
if (getScrollY() < 0 || getScrollY() > getScrollRange()) {
mLastScrollerY = getScrollY();
}
...

Remove edge effect.

Now let’s get rid of EdgeEffect : just comment out edgeEffect related logic from draw() method or modify all places where ensureGlows() is called with new flag so it should not run edge effect related code but do not change overScrollMode flag to OVER_SCROLL_NEVER because we actually have overscroll and use this flag in other places.

And last thing:

Fix auto springBack during over-dragging over the mOverscrollDistance.

I don’t wan’t springBack to be invoked automatically during dragging but only when I have taken away my finger it should happen:

Modify overScrollByCompat(): never call springBack() while dragging.

Now it looks fine:

Polishing overscroll.

We could add another background color to the overscrolled bottom and top rectangles this way:

@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (getScrollY() < 0) {
canvas.drawRect(0, getScrollY(), getWidth(), 0,
mOverScrollPaint);
}
...
}

And even draw elevation lines, but it can be done much more easier with the help of CardView. Just wrap TextView with CardView , add elevation, corner radius and dark background to it.

Creativity

I have added hand pulling rope from here previously rotated by 90 degree without pixel resizing. So, this is a bit of a rush job. But this is just an idea, I just would like to point out that there are so many UI little things we even never notice which can be used as a good place for creativity and originality.

Conclusion

I had to dig through whole ScrollView to compare with NestedScrollView to fix couple of bugs. I had to modify whole sdk class for this and it is still buggy. And I am not sure about current solution and have not tested nested scrolling by the way. Hey, what about open-close principe? There is no way to change overscroller, its duration as example and behavior or replace EdgeEffect variables with custom implementation; and even overscroll and overfling distance have no setters in ScrollView and NestedScrollView does not support it at all… Unexpected for me…

But any librarian class/source code can be the best tutorial by itself for any customization idea!

--

--