From af246cce82391db7a5ff80fec502470915f44d69 Mon Sep 17 00:00:00 2001 From: dmytroKarataiev Date: Tue, 3 May 2016 23:17:28 -0700 Subject: [PATCH 1/2] feat: NestedScrolling behavior on complicated layouts works --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- .../yalantis/phoenix/PullToRefreshView.java | 239 ++++++++++++++++-- .../phoenix/sample/ListViewFragment.java | 3 + .../res/layout/activity_pull_to_refresh.xml | 47 +++- .../main/res/layout/fragment_list_view.xml | 15 +- .../res/layout/fragment_recycler_view.xml | 15 +- .../src/main/res/layout/nested_activity.xml | 104 ++++++++ sample/src/main/res/values/dimens.xml | 1 + 9 files changed, 372 insertions(+), 58 deletions(-) create mode 100644 sample/src/main/res/layout/nested_activity.xml diff --git a/build.gradle b/build.gradle index c63c5b9..e979bd0 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 137ca20..e21db57 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/library/src/main/java/com/yalantis/phoenix/PullToRefreshView.java b/library/src/main/java/com/yalantis/phoenix/PullToRefreshView.java index 91dba70..acea222 100644 --- a/library/src/main/java/com/yalantis/phoenix/PullToRefreshView.java +++ b/library/src/main/java/com/yalantis/phoenix/PullToRefreshView.java @@ -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; @@ -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; @@ -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); } @@ -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) { @@ -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); @@ -164,6 +184,7 @@ public boolean onInterceptTouchEvent(MotionEvent ev) { } final float yDiff = y - mInitialMotionY; if (yDiff > mTouchSlop && !mIsBeingDragged) { + mInitialMotionY = mInitialMotionY + mTouchSlop; mIsBeingDragged = true; } break; @@ -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); @@ -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: @@ -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; } @@ -241,7 +251,7 @@ public boolean onTouchEvent(@NonNull MotionEvent ev) { } mActivePointerId = INVALID_POINTER; return false; - } + } return true; @@ -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); @@ -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); + } } diff --git a/sample/src/main/java/com/yalantis/phoenix/sample/ListViewFragment.java b/sample/src/main/java/com/yalantis/phoenix/sample/ListViewFragment.java index 9b76277..e68bafe 100644 --- a/sample/src/main/java/com/yalantis/phoenix/sample/ListViewFragment.java +++ b/sample/src/main/java/com/yalantis/phoenix/sample/ListViewFragment.java @@ -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; @@ -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 diff --git a/sample/src/main/res/layout/activity_pull_to_refresh.xml b/sample/src/main/res/layout/activity_pull_to_refresh.xml index 3035688..e310c55 100644 --- a/sample/src/main/res/layout/activity_pull_to_refresh.xml +++ b/sample/src/main/res/layout/activity_pull_to_refresh.xml @@ -1,23 +1,44 @@ - + - + android:layout_height="@dimen/appbar_height" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> - + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_list_view.xml b/sample/src/main/res/layout/fragment_list_view.xml index e830e7c..821d43a 100644 --- a/sample/src/main/res/layout/fragment_list_view.xml +++ b/sample/src/main/res/layout/fragment_list_view.xml @@ -1,11 +1,9 @@ - - - + - diff --git a/sample/src/main/res/layout/fragment_recycler_view.xml b/sample/src/main/res/layout/fragment_recycler_view.xml index cd9c85a..1e31525 100644 --- a/sample/src/main/res/layout/fragment_recycler_view.xml +++ b/sample/src/main/res/layout/fragment_recycler_view.xml @@ -1,11 +1,9 @@ - - - + - diff --git a/sample/src/main/res/layout/nested_activity.xml b/sample/src/main/res/layout/nested_activity.xml new file mode 100644 index 0000000..d953cdc --- /dev/null +++ b/sample/src/main/res/layout/nested_activity.xml @@ -0,0 +1,104 @@ + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml index 82806c6..64bb47e 100644 --- a/sample/src/main/res/values/dimens.xml +++ b/sample/src/main/res/values/dimens.xml @@ -1,4 +1,5 @@ 222dp + 112dp \ No newline at end of file From ec92454aab03d726d5ef3a058285d0580f96d681 Mon Sep 17 00:00:00 2001 From: dmytroKarataiev Date: Tue, 3 May 2016 23:26:56 -0700 Subject: [PATCH 2/2] delete: unnecessary file --- .../src/main/res/layout/nested_activity.xml | 104 ------------------ 1 file changed, 104 deletions(-) delete mode 100644 sample/src/main/res/layout/nested_activity.xml diff --git a/sample/src/main/res/layout/nested_activity.xml b/sample/src/main/res/layout/nested_activity.xml deleted file mode 100644 index d953cdc..0000000 --- a/sample/src/main/res/layout/nested_activity.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - \ No newline at end of file