diff --git a/app/src/main/assets/scrollable_web_content.html b/app/src/main/assets/scrollable_web_content.html index a61ba2b..d068e4d 100644 --- a/app/src/main/assets/scrollable_web_content.html +++ b/app/src/main/assets/scrollable_web_content.html @@ -1,5 +1,12 @@ + + +

diff --git a/app/src/main/java/com/telefonica/nestedscrollwebviewdemo/ScrollingActivity.kt b/app/src/main/java/com/telefonica/nestedscrollwebviewdemo/ScrollingActivity.kt index 32a551e..6089296 100644 --- a/app/src/main/java/com/telefonica/nestedscrollwebviewdemo/ScrollingActivity.kt +++ b/app/src/main/java/com/telefonica/nestedscrollwebviewdemo/ScrollingActivity.kt @@ -26,6 +26,9 @@ class ScrollingActivity : AppCompatActivity() { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } + binding.bottomFab.setOnClickListener { + binding.webView.evaluateJavascript("document.body.classList.toggle('expanded');", null) + } WebView.setWebContentsDebuggingEnabled(true) binding.webView.apply { with(settings) { diff --git a/app/src/main/res/layout/activity_scrolling.xml b/app/src/main/res/layout/activity_scrolling.xml index 9a66515..de6e57b 100644 --- a/app/src/main/res/layout/activity_scrolling.xml +++ b/app/src/main/res/layout/activity_scrolling.xml @@ -56,4 +56,15 @@ app:layout_anchorGravity="bottom|end" app:srcCompat="@android:drawable/ic_dialog_email" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e61d01..41231b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,4 +4,5 @@ Match coordinator bottom behaviour Regular behaviour + toggle width diff --git a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java index f2467a3..25d314e 100644 --- a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java +++ b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java @@ -120,10 +120,23 @@ class (Android Support Library Compat 1.8.0), keeping original code unaltered as @NonNull public EdgeEffect mEdgeGlowBottom; + /** @hide */ + @RestrictTo(LIBRARY) + @VisibleForTesting + @NonNull + public EdgeEffect mEdgeGlowLeft; + + /** @hide */ + @RestrictTo(LIBRARY) + @VisibleForTesting + @NonNull + public EdgeEffect mEdgeGlowRight; + /** * Position of the last motion event. */ private int mLastMotionY; + private int mLastMotionX; /** * True if the user is currently dragging this ScrollView around. This is @@ -152,8 +165,11 @@ class (Android Support Library Compat 1.8.0), keeping original code unaltered as */ private final int[] mScrollOffset = new int[2]; private final int[] mScrollConsumed = new int[2]; + + private int mNestedXOffset; private int mNestedYOffset; + private int mLastScrollerX; private int mLastScrollerY; @@ -169,6 +185,8 @@ class (Android Support Library Compat 1.8.0), keeping original code unaltered as public void initNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs/*, int defStyleAttr*/) { mEdgeGlowTop = EdgeEffectCompat.create(context, attrs); mEdgeGlowBottom = EdgeEffectCompat.create(context, attrs); + mEdgeGlowLeft = EdgeEffectCompat.create(context, attrs); + mEdgeGlowRight = EdgeEffectCompat.create(context, attrs); initScrollView(); @@ -207,15 +225,17 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { final int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedXOffset = 0; mNestedYOffset = 0; /* Not present on NestedScrollView code. We need to reset scroll offset on touch down, as we found while testing that in some cases action was invoked with a non-zero scroll offset, breaking page scroll. */ + mScrollOffset[0] = 0; mScrollOffset[1] = 0; } MotionEvent vtev = MotionEvent.obtain(ev); - vtev.offsetLocation(0, mNestedYOffset); + vtev.offsetLocation(mNestedXOffset, mNestedYOffset); switch (actionMasked) { case MotionEvent.ACTION_DOWN: { @@ -239,9 +259,11 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { } // Remember where the motion event started + mLastMotionX = (int) ev.getX(); mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); - startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL | ViewCompat.SCROLL_AXIS_HORIZONTAL, ViewCompat.TYPE_TOUCH); break; } case MotionEvent.ACTION_MOVE: @@ -252,7 +274,9 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { } final int y = (int) ev.getY(activePointerIndex); + final int x = (int) ev.getX(activePointerIndex); int deltaY = mLastMotionY - y; + int deltaX = mLastMotionX - x; deltaY -= releaseVerticalGlow(deltaY, ev.getX(activePointerIndex)); if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { final ViewParent parent = getParent(); @@ -268,41 +292,61 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { } if (mIsBeingDragged) { // Start with nested pre scrolling - if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, + if (dispatchNestedPreScroll(deltaX, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH)) { + deltaX -= mScrollConsumed[0]; deltaY -= mScrollConsumed[1]; + + mNestedXOffset += mScrollOffset[0]; mNestedYOffset += mScrollOffset[1]; } // Scroll to follow the motion event + mLastMotionX = x - mScrollOffset[0]; mLastMotionY = y - mScrollOffset[1]; + final int oldX = getScrollX(); final int oldY = getScrollY(); - final int range = getScrollRange(); + + final int rangeVertical = getVerticalScrollRange(); + final int rangeHorizontal = getHorizontalScrollRange(); final int overscrollMode = getOverScrollMode(); - boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS - || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + boolean canOverscrollVertically = overscrollMode == OVER_SCROLL_ALWAYS + || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && rangeVertical > 0); + + boolean canOverscrollHorizontally = overscrollMode == OVER_SCROLL_ALWAYS + || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && rangeHorizontal > 0); // Calling overScrollByCompat will call onOverScrolled, which // calls onScrollChanged if applicable. boolean clearVelocityTracker = - overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, + overScrollByCompat(deltaX, deltaY, getScrollX(), getScrollY(), rangeHorizontal, rangeVertical, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH); + final int scrolledDeltaX = getScrollX() - oldX; + final int unconsumedX = deltaX - scrolledDeltaX; + final int scrolledDeltaY = getScrollY() - oldY; final int unconsumedY = deltaY - scrolledDeltaY; + mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; - dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, + dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed); + mLastMotionX -= mScrollOffset[0]; mLastMotionY -= mScrollOffset[1]; + + mNestedXOffset += mScrollOffset[0]; mNestedYOffset += mScrollOffset[1]; - if (canOverscroll) { + if (canOverscrollVertically) { deltaY -= mScrollConsumed[1]; + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { EdgeEffectCompat.onPullDistance(mEdgeGlowTop, (float) -deltaY / getHeight(), @@ -310,7 +354,7 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } - } else if (pulledToY > range) { + } else if (pulledToY > rangeVertical) { EdgeEffectCompat.onPullDistance(mEdgeGlowBottom, (float) deltaY / getHeight(), 1.f - ev.getX(activePointerIndex) / getWidth()); @@ -323,6 +367,32 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { clearVelocityTracker = false; } } + + if (canOverscrollHorizontally) { + deltaX -= mScrollConsumed[0]; + + final int pulledToX = oldX + deltaX; + + if (pulledToX < 0) { + EdgeEffectCompat.onPullDistance(mEdgeGlowLeft, + (float) -deltaX / getWidth(), + ev.getY(activePointerIndex) / getHeight()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > rangeHorizontal) { + EdgeEffectCompat.onPullDistance(mEdgeGlowRight, + (float) deltaX / getWidth(), + 1.f - ev.getY(activePointerIndex) / getHeight()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + if (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished()) { + ViewCompat.postInvalidateOnAnimation(this); + clearVelocityTracker = false; + } + } if (clearVelocityTracker) { // Break our velocity if we hit a scroll barrier. mVelocityTracker.clear(); @@ -332,16 +402,19 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); - int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); - if ((Math.abs(initialVelocity) >= mMinimumVelocity)) { - if (!edgeEffectFling(initialVelocity) - && !dispatchNestedPreFling(0, -initialVelocity)) { - dispatchNestedFling(0, -initialVelocity, true); - fling(-initialVelocity); + + int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId); + int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId); + + if ((Math.abs(initialVelocityY) >= mMinimumVelocity) || Math.abs(initialVelocityX) >= mMinimumVelocity) { + if (!edgeEffectFling(initialVelocityX, initialVelocityY) + && !dispatchNestedPreFling(-initialVelocityX, -initialVelocityY)) { + dispatchNestedFling(-initialVelocityX, -initialVelocityY, true); + fling(-initialVelocityX, -initialVelocityY); } - } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, - getScrollRange())) { - ViewCompat.postInvalidateOnAnimation(this); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getHorizontalScrollRange(), 0, + getVerticalScrollRange())) { + this.postInvalidateOnAnimation(); } mActivePointerId = INVALID_POINTER; endDrag(); @@ -349,9 +422,9 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { case MotionEvent.ACTION_CANCEL: // WebView always return no children if (mIsBeingDragged /* && getChildCount() > 0*/) { - if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, - getScrollRange())) { - ViewCompat.postInvalidateOnAnimation(this); + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getHorizontalScrollRange(), 0, + getVerticalScrollRange())) { + this.postInvalidateOnAnimation(); } } mActivePointerId = INVALID_POINTER; @@ -359,6 +432,7 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { break; case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); + mLastMotionX = (int) ev.getX(index); mLastMotionY = (int) ev.getY(index); mActivePointerId = ev.getPointerId(index); break; @@ -372,6 +446,7 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { break; } /* End of code not present on NestedScrollView code. */ + mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); break; } @@ -390,38 +465,55 @@ public void computeScroll() { } mScroller.computeScrollOffset(); + + final int x = mScroller.getCurrX(); final int y = mScroller.getCurrY(); - int unconsumed = y - mLastScrollerY; + + int unconsumedX = x - mLastScrollerX; + int unconsumedY = y - mLastScrollerY; + + mLastScrollerX = x; mLastScrollerY = y; // Nested Scrolling Pre Pass + mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; - dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, + + dispatchNestedPreScroll(unconsumedX, unconsumedY, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH); - unconsumed -= mScrollConsumed[1]; - final int range = getScrollRange(); + unconsumedX -= mScrollConsumed[0]; + unconsumedY -= mScrollConsumed[1]; + + final int horizontalRange = getHorizontalScrollRange(); + final int verticalRange = getVerticalScrollRange(); - if (unconsumed != 0) { + if (unconsumedY != 0 || unconsumedX != 0) { // Internal Scroll final int oldScrollY = getScrollY(); - overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, range, 0, 0, false); - final int scrolledByMe = getScrollY() - oldScrollY; - unconsumed -= scrolledByMe; + final int oldScrollX = getScrollX(); + overScrollByCompat(unconsumedX, unconsumedY, oldScrollX, oldScrollY, horizontalRange, verticalRange, 0, 0, false); + + final int scrolledByMeX = getScrollX() - oldScrollX; + final int scrolledByMeY = getScrollY() - oldScrollY; + + unconsumedX -= scrolledByMeX; + unconsumedY -= scrolledByMeY; // Nested Scrolling Post Pass + mScrollConsumed[0] = 0; mScrollConsumed[1] = 0; - dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, + dispatchNestedScroll(scrolledByMeX, scrolledByMeY, unconsumedX, unconsumedY, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed); - unconsumed -= mScrollConsumed[1]; + unconsumedY -= mScrollConsumed[1]; } - if (unconsumed != 0) { + if (unconsumedY != 0) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS - || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && verticalRange > 0); if (canOverscroll) { - if (unconsumed < 0) { + if (unconsumedY < 0) { if (mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); } @@ -431,6 +523,26 @@ public void computeScroll() { } } } + } + + if (unconsumedX != 0) { + final int mode = getOverScrollMode(); + final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS + || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && horizontalRange > 0); + if (canOverscroll) { + if (unconsumedX < 0) { + if (mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } + } else { + if (mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + + if (unconsumedX != 0 && unconsumedY != 0) { abortAnimatedScroll(); } @@ -537,7 +649,7 @@ boolean overScrollByCompat(int deltaX, int deltaY, } if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { - mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange()); + mScroller.springBack(newScrollX, newScrollY, 0, getHorizontalScrollRange(), 0, getVerticalScrollRange()); } onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); @@ -545,7 +657,7 @@ boolean overScrollByCompat(int deltaX, int deltaY, return clampedX || clampedY; } - private boolean edgeEffectFling(int velocityY) { + private boolean edgeEffectFling(int velocityX, int velocityY) { boolean consumed = true; if (EdgeEffectCompat.getDistance(mEdgeGlowTop) != 0) { mEdgeGlowTop.onAbsorb(velocityY); @@ -564,25 +676,30 @@ private boolean edgeEffectFling(int velocityY) { * numbers mean that the finger/cursor is moving down the screen, * which means we want to scroll towards the top. */ - public void fling(int velocityY) { - // WebView always returns no children - //if (getChildCount() > 0) { + public void fling(int velocityX, int velocityY) { + if (!canScrollHorizontally(1) && !canScrollHorizontally(-1)) + velocityX = 0; + if (!canScrollVertically(-1) && !canScrollVertically(1)) + velocityY = 0; - mScroller.fling(getScrollX(), getScrollY(), // start - 0, velocityY, // velocities - 0, 0, // x - Integer.MIN_VALUE, Integer.MAX_VALUE, // y - 0, 0); // overscroll - runAnimatedScroll(true); - //} + mScroller.fling( + getScrollX(), getScrollY(), // start + velocityX, velocityY, // velocities + Integer.MIN_VALUE, Integer.MAX_VALUE, // x + Integer.MIN_VALUE, Integer.MAX_VALUE, // y + 0, 0 // overscroll + ); + + runAnimatedScroll(true); } private void runAnimatedScroll(boolean participateInNestedScrolling) { if (participateInNestedScrolling) { - startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL | ViewCompat.SCROLL_AXIS_HORIZONTAL, ViewCompat.TYPE_NON_TOUCH); } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } + mLastScrollerX = getScrollX(); mLastScrollerY = getScrollY(); ViewCompat.postInvalidateOnAnimation(this); } @@ -612,7 +729,10 @@ private void onSecondaryPointerUp(MotionEvent ev) { // active pointer and adjust accordingly. // TODO: Make this decision more intelligent. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + + mLastMotionX = (int) ev.getX(newPointerIndex); mLastMotionY = (int) ev.getY(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); if (mVelocityTracker != null) { mVelocityTracker.clear(); @@ -744,13 +864,17 @@ public boolean overScrollBy( @Override public int getNestedScrollAxes() { - return ViewCompat.SCROLL_AXIS_VERTICAL; + return ViewCompat.SCROLL_AXIS_VERTICAL | SCROLL_AXIS_HORIZONTAL; } - private int getScrollRange() { + private int getVerticalScrollRange() { return computeVerticalScrollRange(); } + private int getHorizontalScrollRange() { + return computeHorizontalScrollRange(); + } + /* Extra functionalities */ public void setCoordinatorBottomMatchingBehaviourEnabled(boolean enabled) { @@ -782,7 +906,10 @@ protected void onScrollChanged(int l, int t, int oldl, int oldt) { @Override public boolean onTouchEvent(MotionEvent event) { - if (!internalScrollDetector.onTouchEvent(event)) { + if (event.getPointerCount() > 1) { + mIsBeingDragged = false; + stopNestedScroll(ViewCompat.TYPE_TOUCH); + } else if (!internalScrollDetector.onTouchEvent(event)) { onNestedTouchEvent(event); } return super.onTouchEvent(event);