Last active
March 1, 2024 01:45
-
-
Save alexmiragall/0c4c7163f7a17938518ce9794c4a5236 to your computer and use it in GitHub Desktop.
NestedWebView compatible with CoordinatorLayout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tuenti.nestedwebscrollview; | |
import android.content.Context; | |
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.VelocityTrackerCompat; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v4.widget.ScrollerCompat; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.ViewConfiguration; | |
import android.view.ViewParent; | |
import android.webkit.WebView; | |
/** | |
* Copyright (c) Tuenti Technologies. All rights reserved. | |
* | |
* WebView compatible with CoordinatorLayout. | |
* The implementation based on NestedScrollView of design library | |
*/ | |
public class NestedScrollWebView extends WebView implements NestedScrollingChild, NestedScrollingParent { | |
private static final int INVALID_POINTER = -1; | |
private static final String TAG = "NestedWebView"; | |
private final int[] mScrollOffset = new int[2]; | |
private final int[] mScrollConsumed = new int[2]; | |
private int mLastMotionY; | |
private NestedScrollingChildHelper mChildHelper; | |
private boolean mIsBeingDragged = false; | |
private VelocityTracker mVelocityTracker; | |
private int mTouchSlop; | |
private int mActivePointerId = INVALID_POINTER; | |
private int mNestedYOffset; | |
private ScrollerCompat mScroller; | |
private int mMinimumVelocity; | |
private int mMaximumVelocity; | |
public NestedScrollWebView(Context context) { | |
this(context, null); | |
} | |
public NestedScrollWebView(Context context, AttributeSet attrs) { | |
this(context, attrs, android.R.attr.webViewStyle); | |
} | |
public NestedScrollWebView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
setOverScrollMode(WebView.OVER_SCROLL_NEVER); | |
initScrollView(); | |
mChildHelper = new NestedScrollingChildHelper(this); | |
setNestedScrollingEnabled(true); | |
} | |
private void initScrollView() { | |
mScroller = ScrollerCompat.create(getContext(), null); | |
final ViewConfiguration configuration = ViewConfiguration.get(getContext()); | |
mTouchSlop = configuration.getScaledTouchSlop(); | |
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); | |
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); | |
} | |
@Override | |
public boolean onInterceptTouchEvent(MotionEvent ev) { | |
final int action = ev.getAction(); | |
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { | |
return true; | |
} | |
switch (action & MotionEventCompat.ACTION_MASK) { | |
case MotionEvent.ACTION_MOVE: { | |
final int activePointerId = mActivePointerId; | |
if (activePointerId == INVALID_POINTER) { | |
break; | |
} | |
final int pointerIndex = ev.findPointerIndex(activePointerId); | |
if (pointerIndex == -1) { | |
Log.e(TAG, "Invalid pointerId=" + activePointerId | |
+ " in onInterceptTouchEvent"); | |
break; | |
} | |
final int y = (int) ev.getY(pointerIndex); | |
final int yDiff = Math.abs(y - mLastMotionY); | |
if (yDiff > mTouchSlop | |
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { | |
mIsBeingDragged = true; | |
mLastMotionY = y; | |
initVelocityTrackerIfNotExists(); | |
mVelocityTracker.addMovement(ev); | |
mNestedYOffset = 0; | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
} | |
break; | |
} | |
case MotionEvent.ACTION_DOWN: { | |
final int y = (int) ev.getY(); | |
mLastMotionY = y; | |
mActivePointerId = ev.getPointerId(0); | |
initOrResetVelocityTracker(); | |
mVelocityTracker.addMovement(ev); | |
mScroller.computeScrollOffset(); | |
mIsBeingDragged = !mScroller.isFinished(); | |
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); | |
break; | |
} | |
case MotionEvent.ACTION_CANCEL: | |
case MotionEvent.ACTION_UP: | |
mIsBeingDragged = false; | |
mActivePointerId = INVALID_POINTER; | |
recycleVelocityTracker(); | |
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
stopNestedScroll(); | |
break; | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
break; | |
} | |
return mIsBeingDragged; | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent ev) { | |
initVelocityTrackerIfNotExists(); | |
MotionEvent vtev = MotionEvent.obtain(ev); | |
final int actionMasked = MotionEventCompat.getActionMasked(ev); | |
if (actionMasked == MotionEvent.ACTION_DOWN) { | |
mNestedYOffset = 0; | |
} | |
vtev.offsetLocation(0, mNestedYOffset); | |
switch (actionMasked) { | |
case MotionEvent.ACTION_DOWN: { | |
if (mIsBeingDragged = !mScroller.isFinished()) { | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
} | |
if (!mScroller.isFinished()) { | |
mScroller.abortAnimation(); | |
} | |
mLastMotionY = (int) ev.getY(); | |
mActivePointerId = ev.getPointerId(0); | |
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); | |
break; | |
} | |
case MotionEvent.ACTION_MOVE: | |
final int activePointerIndex = ev.findPointerIndex(mActivePointerId); | |
if (activePointerIndex == -1) { | |
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); | |
break; | |
} | |
final int y = (int) ev.getY(activePointerIndex); | |
int deltaY = mLastMotionY - y; | |
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { | |
deltaY -= mScrollConsumed[1]; | |
vtev.offsetLocation(0, mScrollOffset[1]); | |
mNestedYOffset += mScrollOffset[1]; | |
} | |
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
mIsBeingDragged = true; | |
if (deltaY > 0) { | |
deltaY -= mTouchSlop; | |
} else { | |
deltaY += mTouchSlop; | |
} | |
} | |
if (mIsBeingDragged) { | |
mLastMotionY = y - mScrollOffset[1]; | |
final int oldY = getScrollY(); | |
final int scrolledDeltaY = getScrollY() - oldY; | |
final int unconsumedY = deltaY - scrolledDeltaY; | |
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { | |
mLastMotionY -= mScrollOffset[1]; | |
vtev.offsetLocation(0, mScrollOffset[1]); | |
mNestedYOffset += mScrollOffset[1]; | |
} | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if (mIsBeingDragged) { | |
final VelocityTracker velocityTracker = mVelocityTracker; | |
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); | |
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, | |
mActivePointerId); | |
if (Math.abs(initialVelocity) > mMinimumVelocity) { | |
flingWithNestedDispatch(-initialVelocity); | |
} else if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, | |
getScrollRange())) { | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
} | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
if (mIsBeingDragged && getChildCount() > 0) { | |
if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, | |
getScrollRange())) { | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
} | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
break; | |
case MotionEventCompat.ACTION_POINTER_DOWN: { | |
final int index = MotionEventCompat.getActionIndex(ev); | |
mLastMotionY = (int) ev.getY(index); | |
mActivePointerId = ev.getPointerId(index); | |
break; | |
} | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); | |
break; | |
} | |
if (mVelocityTracker != null) { | |
mVelocityTracker.addMovement(vtev); | |
} | |
vtev.recycle(); | |
return super.onTouchEvent(ev); | |
} | |
int getScrollRange() { | |
//Using scroll range of webview instead of childs as NestedScrollView does. | |
return computeVerticalScrollRange(); | |
} | |
private void endDrag() { | |
mIsBeingDragged = false; | |
recycleVelocityTracker(); | |
stopNestedScroll(); | |
} | |
private void onSecondaryPointerUp(MotionEvent ev) { | |
final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) | |
>> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; | |
final int pointerId = ev.getPointerId(pointerIndex); | |
if (pointerId == mActivePointerId) { | |
final int newPointerIndex = pointerIndex == 0 ? 1 : 0; | |
mLastMotionY = (int) ev.getY(newPointerIndex); | |
mActivePointerId = ev.getPointerId(newPointerIndex); | |
if (mVelocityTracker != null) { | |
mVelocityTracker.clear(); | |
} | |
} | |
} | |
private void initOrResetVelocityTracker() { | |
if (mVelocityTracker == null) { | |
mVelocityTracker = VelocityTracker.obtain(); | |
} else { | |
mVelocityTracker.clear(); | |
} | |
} | |
private void initVelocityTrackerIfNotExists() { | |
if (mVelocityTracker == null) { | |
mVelocityTracker = VelocityTracker.obtain(); | |
} | |
} | |
private void recycleVelocityTracker() { | |
if (mVelocityTracker != null) { | |
mVelocityTracker.recycle(); | |
mVelocityTracker = null; | |
} | |
} | |
private void flingWithNestedDispatch(int velocityY) { | |
final int scrollY = getScrollY(); | |
final boolean canFling = (scrollY > 0 || velocityY > 0) | |
&& (scrollY < getScrollRange() || velocityY < 0); | |
if (!dispatchNestedPreFling(0, velocityY)) { | |
dispatchNestedFling(0, velocityY, canFling); | |
if (canFling) { | |
fling(velocityY); | |
} | |
} | |
} | |
public void fling(int velocityY) { | |
if (getChildCount() > 0) { | |
int height = getHeight() - getPaddingBottom() - getPaddingTop(); | |
int bottom = getChildAt(0).getHeight(); | |
mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, | |
Math.max(0, bottom - height), 0, height / 2); | |
ViewCompat.postInvalidateOnAnimation(this); | |
} | |
} | |
@Override | |
public boolean isNestedScrollingEnabled() { | |
return mChildHelper.isNestedScrollingEnabled(); | |
} | |
@Override | |
public void setNestedScrollingEnabled(boolean enabled) { | |
mChildHelper.setNestedScrollingEnabled(enabled); | |
} | |
@Override | |
public boolean startNestedScroll(int axes) { | |
return mChildHelper.startNestedScroll(axes); | |
} | |
@Override | |
public void stopNestedScroll() { | |
mChildHelper.stopNestedScroll(); | |
} | |
@Override | |
public boolean hasNestedScrollingParent() { | |
return mChildHelper.hasNestedScrollingParent(); | |
} | |
@Override | |
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, | |
int[] offsetInWindow) { | |
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); | |
} | |
@Override | |
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { | |
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); | |
} | |
@Override | |
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { | |
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); | |
} | |
@Override | |
public boolean dispatchNestedPreFling(float velocityX, float velocityY) { | |
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); | |
} | |
@Override | |
public int getNestedScrollAxes() { | |
return ViewCompat.SCROLL_AXIS_NONE; | |
} | |
} |
Thank you!
how to use ?
Hey, thanks for the work, this is the closest one to a smooth scroll experience though in my case fling is not working when scrolling down, it works while scrolling up, do you have any idea of the reason?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Looks like the consumed is never set. Have you any update/news/input on this?