package com.boardgamegeek.ui.widget;
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.
* <p>
* WebView compatible with CoordinatorLayout.
* The implementation based on NestedScrollView of design library
* From: https://gist.github.com/alexmiragall/0c4c7163f7a17938518ce9794c4a5236
*/
public class NestedScrollWebView extends WebView implements NestedScrollingChild, NestedScrollingParent {
private static final int INVALID_POINTER = -1;
private static final String TAG = "NestedWebView";
private final int[] scrollOffset = new int[2];
private final int[] scrollConsumed = new int[2];
private int lastMotionY;
private final NestedScrollingChildHelper childHelper;
private boolean isBeingDragged = false;
private VelocityTracker velocityTracker;
private int touchSlop;
private int activePointerId = INVALID_POINTER;
private int nestedYOffset;
private ScrollerCompat scroller;
private int minimumVelocity;
private int maximumVelocity;
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();
childHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
private void initScrollView() {
scroller = ScrollerCompat.create(getContext(), null);
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
touchSlop = configuration.getScaledTouchSlop();
minimumVelocity = configuration.getScaledMinimumFlingVelocity();
maximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (isBeingDragged)) {
return true;
}
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int activePointerId = this.activePointerId;
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 - lastMotionY);
if (yDiff > touchSlop
&& (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) {
isBeingDragged = true;
lastMotionY = y;
initVelocityTrackerIfNotExists();
velocityTracker.addMovement(ev);
nestedYOffset = 0;
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
lastMotionY = (int) ev.getY();
activePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
velocityTracker.addMovement(ev);
scroller.computeScrollOffset();
isBeingDragged = !scroller.isFinished();
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isBeingDragged = false;
activePointerId = INVALID_POINTER;
recycleVelocityTracker();
if (scroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
stopNestedScroll();
break;
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
}
return isBeingDragged;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
initVelocityTrackerIfNotExists();
MotionEvent motionEvent = MotionEvent.obtain(ev);
final int actionMasked = MotionEventCompat.getActionMasked(ev);
if (actionMasked == MotionEvent.ACTION_DOWN) {
nestedYOffset = 0;
}
motionEvent.offsetLocation(0, nestedYOffset);
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
if (isBeingDragged = !scroller.isFinished()) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
lastMotionY = (int) ev.getY();
activePointerId = ev.getPointerId(0);
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
break;
}
case MotionEvent.ACTION_MOVE:
final int activePointerIndex = ev.findPointerIndex(activePointerId);
if (activePointerIndex == -1) {
Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onTouchEvent");
break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = lastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, scrollConsumed, scrollOffset)) {
deltaY -= scrollConsumed[1];
motionEvent.offsetLocation(0, scrollOffset[1]);
nestedYOffset += scrollOffset[1];
}
if (!isBeingDragged && Math.abs(deltaY) > touchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
isBeingDragged = true;
if (deltaY > 0) {
deltaY -= touchSlop;
} else {
deltaY += touchSlop;
}
}
if (isBeingDragged) {
lastMotionY = y - scrollOffset[1];
final int oldY = getScrollY();
final int scrolledDeltaY = getScrollY() - oldY;
final int unconsumedY = deltaY - scrolledDeltaY;
if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, scrollOffset)) {
lastMotionY -= scrollOffset[1];
motionEvent.offsetLocation(0, scrollOffset[1]);
nestedYOffset += scrollOffset[1];
}
}
break;
case MotionEvent.ACTION_UP:
if (isBeingDragged) {
final VelocityTracker velocityTracker = this.velocityTracker;
velocityTracker.computeCurrentVelocity(1000, maximumVelocity);
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
activePointerId);
if (Math.abs(initialVelocity) > minimumVelocity) {
flingWithNestedDispatch(-initialVelocity);
} else if (scroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
activePointerId = INVALID_POINTER;
endDrag();
break;
case MotionEvent.ACTION_CANCEL:
if (isBeingDragged && getChildCount() > 0) {
if (scroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
getScrollRange())) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
activePointerId = INVALID_POINTER;
endDrag();
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
lastMotionY = (int) ev.getY(index);
activePointerId = ev.getPointerId(index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
lastMotionY = (int) ev.getY(ev.findPointerIndex(activePointerId));
break;
}
if (velocityTracker != null) {
velocityTracker.addMovement(motionEvent);
}
motionEvent.recycle();
return super.onTouchEvent(ev);
}
int getScrollRange() {
//Using scroll range of webView instead of child as NestedScrollView does.
return computeVerticalScrollRange();
}
private void endDrag() {
isBeingDragged = 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 == activePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
lastMotionY = (int) ev.getY(newPointerIndex);
activePointerId = ev.getPointerId(newPointerIndex);
if (velocityTracker != null) {
velocityTracker.clear();
}
}
}
private void initOrResetVelocityTracker() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
} else {
velocityTracker.clear();
}
}
private void initVelocityTrackerIfNotExists() {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
}
private void recycleVelocityTracker() {
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = 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();
scroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height / 2);
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public boolean isNestedScrollingEnabled() {
return childHelper.isNestedScrollingEnabled();
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
childHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean startNestedScroll(int axes) {
return childHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
childHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return childHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
int[] offsetInWindow) {
return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return childHelper.dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public int getNestedScrollAxes() {
return ViewCompat.SCROLL_AXIS_NONE;
}
}