From 9ab3c93d07be8c5e17ad5ff542a1cb1d2d285df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=B6ffken?= Date: Fri, 6 Nov 2026 10:28:58 +0100 Subject: [PATCH 1/5] Allow horizontal scroll --- .../NestedScrollWebView.java | 153 +++++++++++++----- 1 file changed, 112 insertions(+), 41 deletions(-) diff --git a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java index f2467a3..74c0879 100644 --- a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java +++ b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java @@ -124,6 +124,7 @@ class (Android Support Library Compat 1.8.0), keeping original code unaltered as * 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 +153,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; @@ -207,15 +211,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,7 +245,9 @@ 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); break; @@ -252,7 +260,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 +278,60 @@ 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(); + + // TODO check for horizontal boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS - || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && rangeVertical > 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]; + // TODO EdgeEffect for X if (canOverscroll) { deltaY -= mScrollConsumed[1]; + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { EdgeEffectCompat.onPullDistance(mEdgeGlowTop, (float) -deltaY / getHeight(), @@ -310,7 +339,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()); @@ -332,16 +361,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 +381,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 +391,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 +405,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 +424,57 @@ 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) { + // TODO add overscroll glow for X + + 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()); } @@ -537,7 +590,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 +598,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,17 +617,27 @@ 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; + if (Math.abs(velocityX) > Math.abs(velocityY)) { + mScroller.fling(getScrollX(), getScrollY(), // start + velocityX, velocityY, // velocities + Integer.MIN_VALUE, Integer.MAX_VALUE, // x + 0, 0, // y + 0, 0); // overscroll + } else { mScroller.fling(getScrollX(), getScrollY(), // start - 0, velocityY, // velocities + velocityX, velocityY, // velocities 0, 0, // x Integer.MIN_VALUE, Integer.MAX_VALUE, // y 0, 0); // overscroll - runAnimatedScroll(true); - //} + } + + runAnimatedScroll(true); } private void runAnimatedScroll(boolean participateInNestedScrolling) { @@ -583,6 +646,7 @@ private void runAnimatedScroll(boolean participateInNestedScrolling) { } else { stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); } + mLastScrollerX = getScrollX(); mLastScrollerY = getScrollY(); ViewCompat.postInvalidateOnAnimation(this); } @@ -612,7 +676,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(); @@ -747,10 +814,14 @@ public int getNestedScrollAxes() { return ViewCompat.SCROLL_AXIS_VERTICAL; } - private int getScrollRange() { + private int getVerticalScrollRange() { return computeVerticalScrollRange(); } + private int getHorizontalScrollRange() { + return computeHorizontalScrollRange(); + } + /* Extra functionalities */ public void setCoordinatorBottomMatchingBehaviourEnabled(boolean enabled) { From 4fa06a4a891b9c5a0cc54730277df9007ecddfd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=B6ffken?= Date: Fri, 6 Nov 2026 11:57:16 +0100 Subject: [PATCH 2/5] Add horizontal EdgeEffects --- .../NestedScrollWebView.java | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java index 74c0879..f273a8a 100644 --- a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java +++ b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java @@ -120,6 +120,18 @@ 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. */ @@ -173,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(); @@ -249,7 +263,7 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { 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: @@ -298,10 +312,12 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { final int rangeHorizontal = getHorizontalScrollRange(); final int overscrollMode = getOverScrollMode(); - // TODO check for horizontal - boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS + 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 = @@ -326,8 +342,7 @@ public void onNestedTouchEvent(@NonNull MotionEvent ev) { mNestedXOffset += mScrollOffset[0]; mNestedYOffset += mScrollOffset[1]; - // TODO EdgeEffect for X - if (canOverscroll) { + if (canOverscrollVertically) { deltaY -= mScrollConsumed[1]; final int pulledToY = oldY + deltaY; @@ -352,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(); @@ -467,8 +508,6 @@ public void computeScroll() { unconsumedY -= mScrollConsumed[1]; } - // TODO add overscroll glow for X - if (unconsumedY != 0) { final int mode = getOverScrollMode(); final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS @@ -487,6 +526,23 @@ public void computeScroll() { abortAnimatedScroll(); } + 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()); + } + } + } + abortAnimatedScroll(); + } if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } else { @@ -642,7 +698,7 @@ public void fling(int velocityX, int velocityY) { 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); } @@ -811,7 +867,7 @@ public boolean overScrollBy( @Override public int getNestedScrollAxes() { - return ViewCompat.SCROLL_AXIS_VERTICAL; + return ViewCompat.SCROLL_AXIS_VERTICAL | SCROLL_AXIS_HORIZONTAL; } private int getVerticalScrollRange() { From 4f7bf9fe9b62271c49e6cb7585096b5d2c20dffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=B6ffken?= Date: Fri, 6 Nov 2026 12:06:13 +0100 Subject: [PATCH 3/5] Allow 2D fling --- .../NestedScrollWebView.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java index f273a8a..16db843 100644 --- a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java +++ b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java @@ -523,7 +523,6 @@ public void computeScroll() { } } } - abortAnimatedScroll(); } if (unconsumedX != 0) { @@ -541,8 +540,12 @@ public void computeScroll() { } } } + } + + if (unconsumedX != 0 && unconsumedY != 0) { abortAnimatedScroll(); } + if (!mScroller.isFinished()) { ViewCompat.postInvalidateOnAnimation(this); } else { @@ -679,19 +682,13 @@ public void fling(int velocityX, int velocityY) { if (!canScrollVertically(-1) && !canScrollVertically(1)) velocityY = 0; - if (Math.abs(velocityX) > Math.abs(velocityY)) { - mScroller.fling(getScrollX(), getScrollY(), // start - velocityX, velocityY, // velocities - Integer.MIN_VALUE, Integer.MAX_VALUE, // x - 0, 0, // y - 0, 0); // overscroll - } else { - mScroller.fling(getScrollX(), getScrollY(), // start - velocityX, velocityY, // velocities - 0, 0, // x - Integer.MIN_VALUE, Integer.MAX_VALUE, // y - 0, 0); // overscroll - } + 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); } From 13a6aad845c373ba5943ea795d43776453dab4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=B6ffken?= Date: Fri, 6 Nov 2026 12:55:53 +0100 Subject: [PATCH 4/5] Add option to test horizontal scrolling --- app/src/main/assets/scrollable_web_content.html | 7 +++++++ .../nestedscrollwebviewdemo/ScrollingActivity.kt | 3 +++ app/src/main/res/layout/activity_scrolling.xml | 11 +++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 22 insertions(+) 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 From 89f699a452cf7c293fd0706193feb1ce9300eec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=B6ffken?= Date: Fri, 6 Nov 2026 13:10:51 +0100 Subject: [PATCH 5/5] Allow pinch to zoom --- .../telefonica/nestedscrollwebview/NestedScrollWebView.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java index 16db843..25d314e 100644 --- a/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java +++ b/nestedscrollwebview/src/main/java/com/telefonica/nestedscrollwebview/NestedScrollWebView.java @@ -906,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);