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);