Skip to content

Instantly share code, notes, and snippets.

@cab404
Created March 29, 2017 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cab404/f909d298bfd5f4c609efae44b8f76392 to your computer and use it in GitHub Desktop.
Save cab404/f909d298bfd5f4c609efae44b8f76392 to your computer and use it in GitHub Desktop.
Makes getting overscroll value from ListViews and other scrollables easy
package ru.mos.portal.android.design;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.AbsListView;
import android.widget.FrameLayout;
import java.util.ArrayList;
import java.util.List;
/**
* Makes getting overscroll value from ListViews and other scrollables easy
* Just add it as parent of whatever scrollable you have.
* @author me@cab404.ru
*/
public class BlockingOverscrollView extends FrameLayout {
private static final String TAG = "BlockingOverscrollView";
private final static int
STATE_IDLE = 0,
STATE_DETECTING = 1,
STATE_PULLING = 2,
STATE_SETTLING = 3;
private View mTarget;
private float mOverscrollDirection;
private int mState;
private float mTouchSlop;
private float mDragStart;
private float mPullHeight = 500;
private float mSettlingSpeed = 0.7f;
private float mSettlingThreshold = 0.01f;
private float mProgress = 0f;
public interface DragListener {
void onDrag(float amount);
}
private List<DragListener> dragListeners = new ArrayList<>();
public void addDragListener(DragListener listener) {
dragListeners.add(listener);
}
public void removeDragListener(DragListener listener) {
dragListeners.remove(listener);
}
public BlockingOverscrollView(@NonNull Context context) {
super(context, null);
}
public BlockingOverscrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Log.d(TAG, "BlockingOverscrollView: " + mTouchSlop);
mPullHeight *= context.getResources().getDisplayMetrics().density;
}
void ensureTarget() {
mTarget = getChildAt(0);
}
private boolean canScrollUp() {
return ViewCompat.canScrollVertically(mTarget, -1);
}
private boolean canScrollDown() {
return ViewCompat.canScrollVertically(mTarget, 1);
}
void onProgressChange() {
if (Math.abs(mProgress) < mSettlingThreshold)
mProgress = 0;
for (DragListener listener : dragListeners) {
listener.onDrag(mProgress);
}
}
void calculateProgress(float unconsumedY) {
mProgress = unconsumedY / mPullHeight;
if (Math.abs(mProgress) > 1f)
mProgress = Math.signum(mProgress);
float overkill = Math.abs(unconsumedY) - mPullHeight;
if (overkill > 0)
mDragStart -= overkill * Math.signum(mProgress);
onProgressChange();
}
void drag(float y) {
final float deltaY = mDragStart - y;
final float deltaYSignum = Math.signum(deltaY);
switch (mState) {
case STATE_DETECTING:
if (mOverscrollDirection != 0 && Math.abs(deltaY) > mTouchSlop)
if (deltaYSignum == mOverscrollDirection) {
mDragStart = y;
mState = STATE_PULLING;
}
break;
case STATE_PULLING:
if (deltaYSignum == mOverscrollDirection)
calculateProgress(deltaY);
break;
case STATE_IDLE:
mDragStart = y;
mState = STATE_DETECTING;
break;
}
}
private void onFinishDrag() {
if (Math.abs(mProgress) < mSettlingThreshold)
mState = STATE_IDLE;
else {
mState = STATE_SETTLING;
postSettler();
}
}
// Copied from SwipeRefreshLayout
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// if this is a List < L or another view that doesn't support nested
// scrolling, ignore this request so that the vertical scroll event
// isn't stolen
if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
|| (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
// Nope.
} else {
super.requestDisallowInterceptTouchEvent(b);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = MotionEventCompat.getActionMasked(ev);
if (mState == STATE_SETTLING) {
// Intercepting before client does something bad
return true;
}
if (action == MotionEvent.ACTION_DOWN) onFinishDrag();
if (action == MotionEvent.ACTION_MOVE) {
if (!canScrollUp())
mOverscrollDirection = -1;
else if (!canScrollDown())
mOverscrollDirection = 1;
else
mOverscrollDirection = 0;
drag(ev.getY());
}
return mState == STATE_PULLING;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
if (mState == STATE_SETTLING) {
// Intercepting before client does something bad
return true;
}
switch (mState) {
case STATE_PULLING:
if (action == MotionEvent.ACTION_MOVE) {
drag(ev.getY());
}
break;
}
if (action == MotionEvent.ACTION_UP) onFinishDrag();
return true;
}
Runnable settler = new Runnable() {
@Override
public void run() {
mProgress *= mSettlingSpeed;
onProgressChange();
if (Math.abs(mProgress) > mSettlingThreshold)
postSettler();
else
mState = STATE_IDLE;
}
};
public void postSettler() {
ViewCompat.postOnAnimation(this, settler);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment