Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: NestedScrolling behavior on complicated layouts works #31

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0-alpha1'
classpath 'com.android.tools.build:gradle:2.1.0'
}
}

Expand Down
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Mon Nov 30 13:15:11 EET 2015
#Tue May 03 22:31:47 PDT 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
239 changes: 215 additions & 24 deletions library/src/main/java/com/yalantis/phoenix/PullToRefreshView.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
Expand All @@ -23,7 +27,8 @@

import java.security.InvalidParameterException;

public class PullToRefreshView extends ViewGroup {
public class PullToRefreshView extends ViewGroup implements NestedScrollingParent,
NestedScrollingChild {

private static final int DRAG_MAX_DISTANCE = 120;
private static final float DRAG_RATE = .5f;
Expand Down Expand Up @@ -56,6 +61,16 @@ public class PullToRefreshView extends ViewGroup {
private int mTargetPaddingRight;
private int mTargetPaddingLeft;

// If nested scrolling is enabled, the total amount that needed to be
// consumed by this as the nested scrolling parent is used in place of the
// overscroll determined by MOVE events in the onTouch handler
private float mTotalUnconsumed;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private boolean mNestedScrollInProgress;

public PullToRefreshView(Context context) {
this(context, null);
}
Expand All @@ -78,6 +93,10 @@ public PullToRefreshView(Context context, AttributeSet attrs) {

setWillNotDraw(false);
ViewCompat.setChildrenDrawingOrderEnabled(this, true);

mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}

public void setRefreshStyle(int type) {
Expand Down Expand Up @@ -136,13 +155,14 @@ private void ensureTarget() {

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();

if (!isEnabled() || canChildScrollUp() || mRefreshing) {
final int action = MotionEventCompat.getActionMasked(ev);

if (!isEnabled() || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) {
return false;
}

final int action = MotionEventCompat.getActionMasked(ev);

switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTop(0, true);
Expand All @@ -164,6 +184,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) {
}
final float yDiff = y - mInitialMotionY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialMotionY + mTouchSlop;
mIsBeingDragged = true;
}
break;
Expand All @@ -189,6 +210,11 @@ public boolean onTouchEvent(@NonNull MotionEvent ev) {

final int action = MotionEventCompat.getActionMasked(ev);

if (!isEnabled() || canChildScrollUp() || mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}

switch (action) {
case MotionEvent.ACTION_MOVE: {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
Expand All @@ -198,23 +224,7 @@ public boolean onTouchEvent(@NonNull MotionEvent ev) {

final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = y - mInitialMotionY;
final float scrollTop = yDiff * DRAG_RATE;
mCurrentDragPercent = scrollTop / mTotalDragDistance;
if (mCurrentDragPercent < 0) {
return false;
}
float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
float slingshotDist = mTotalDragDistance;
float tensionSlingshotPercent = Math.max(0,
Math.min(extraOS, slingshotDist * 2) / slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent / 2;
int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);

mBaseRefreshView.setPercent(mCurrentDragPercent, true);
setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
moveAnimation(yDiff);
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN:
Expand All @@ -225,7 +235,7 @@ public boolean onTouchEvent(@NonNull MotionEvent ev) {
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
case MotionEvent.ACTION_CANCEL:
if (mActivePointerId == INVALID_POINTER) {
return false;
}
Expand All @@ -241,7 +251,7 @@ public boolean onTouchEvent(@NonNull MotionEvent ev) {
}
mActivePointerId = INVALID_POINTER;
return false;
}

}

return true;
Expand Down Expand Up @@ -388,7 +398,7 @@ private boolean canChildScrollUp() {
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return mTarget.getScrollY() > 0;
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
Expand Down Expand Up @@ -421,5 +431,186 @@ public interface OnRefreshListener {
void onRefresh();
}

// NestedScrollingParent

@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// Dispatch up to the nested parent
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}

@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
moveAnimation(mTotalUnconsumed);
}

// If a client layout is using a custom start position for the circle
// view, they mean to hide it again before scrolling the child view
// If we get back to mTotalUnconsumed == 0 and there is more to go, hide
// the circle so it isn't exposed if its blocking content is moved
/*
if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
&& Math.abs(dy - consumed[1]) > 0) {
mSomeView.setVisibility(View.GONE);
}
*/

// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}

@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}

@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Finish the spinner for nested scrolling if we ever consumed any
// unconsumed nested scroll
if (mTotalUnconsumed > 0) {
finishAnimation(mTotalUnconsumed);
mTotalUnconsumed = 0;
}

// Dispatch up our nested parent
stopNestedScroll();
}

@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);

// This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
// sometimes between two nested scrolling views, we need a way to be able to know when any
// nested scrolling parent has stopped handling events. We do that by using the
// 'offset in window 'functionality to see if we have been moved from the event.
// This is a decent indication of whether we should take over the event stream or not.
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveAnimation(mTotalUnconsumed);
}
}

private void moveAnimation(float overscrollTop) {
final float scrollTop = overscrollTop * DRAG_RATE;
mCurrentDragPercent = scrollTop / mTotalDragDistance;
if (mCurrentDragPercent < 0) {
return;
}
float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
float slingshotDist = mTotalDragDistance;
float tensionSlingshotPercent = Math.max(0,
Math.min(extraOS, slingshotDist * 2) / slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent / 2;
int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);

mBaseRefreshView.setPercent(mCurrentDragPercent, true);
setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
}

private void finishAnimation(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
setRefreshing(true, true /* notify */);
} else {
// cancel refresh
mRefreshing = false;
animateOffsetToStartPosition();
}
}

// NestedScrollingChild

@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}

@Override
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}

@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}

@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}

@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}

@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}

@Override
public boolean onNestedPreFling(View target, float velocityX,
float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}

@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}

@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -29,6 +30,8 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
ListView listView = (ListView) rootView.findViewById(R.id.list_view);
listView.setAdapter(new SampleAdapter(getActivity(), R.layout.list_item, mSampleList));

ViewCompat.setNestedScrollingEnabled(listView, true);

mPullToRefreshView = (PullToRefreshView) rootView.findViewById(R.id.pull_to_refresh);
mPullToRefreshView.setOnRefreshListener(new PullToRefreshView.OnRefreshListener() {
@Override
Expand Down
Loading