Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
NestedWebView compatible with CoordinatorLayout
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;
}
}
@androuis

This comment has been minimized.

Copy link

androuis commented Nov 2, 2017

Nice work! Why not make it a library? (so that it's easier to be imported)

@glureau-betclic

This comment has been minimized.

Copy link

glureau-betclic commented Oct 17, 2018

Looks like the consumed is never set. Have you any update/news/input on this?

@bsscco

This comment has been minimized.

Copy link

bsscco commented Mar 21, 2019

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.