Created
March 29, 2017 14:42
-
-
Save cab404/f909d298bfd5f4c609efae44b8f76392 to your computer and use it in GitHub Desktop.
Makes getting overscroll value from ListViews and other scrollables easy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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