/* * Copyright 2016 Freelander * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jun.elephant.ui.widget; import android.content.Context; import android.support.annotation.Nullable; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.NestedScrollingChild; import android.support.v4.view.NestedScrollingChildHelper; import android.support.v4.view.VelocityTrackerCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; /** * Created by Jun on 2016/10/20. */ public class NestedWebView extends WebView implements NestedScrollingChild { private static final int INVALID_POINTER = -1; private static String TAG = NestedWebView.class.getSimpleName(); private final int[] mScrollConsumed = new int[2]; private final int[] mScrollOffset = new int[2]; public int direction = 0; public ScrollStateChangedListener.ScrollState position = ScrollStateChangedListener.ScrollState.TOP; int preContentHeight = 0; private int consumedY; private int contentHeight = -1; private float density; private DirectionDetector directionDetector; private NestedScrollingChildHelper mChildHelper; private OnLongClickListener longClickListenerFalse; private OnLongClickListener longClickListenerTrue; private boolean mIsBeingDragged = false; private int mMaximumVelocity; private int mMinimumVelocity; private int mNestedYOffset; private int mLastMotionY; private int mActivePointerId = -1; private VelocityTracker mVelocityTracker; private OnScrollChangeListener onScrollChangeListener; private int originHeight; private ViewGroup parentView; private float preY; private ScrollStateChangedListener scrollStateChangedListener; private WebSettings settings; private int mTouchSlop; private int webviewHeight = -1; public NestedWebView(Context paramContext) { this(paramContext, null); } public NestedWebView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public NestedWebView(Context paramContext, AttributeSet paramAttributeSet, int paramInt) { super(paramContext, paramAttributeSet, paramInt); init(); } private void endTouch() { setJavaScriptEnable(true); this.mIsBeingDragged = false; this.mActivePointerId = -1; recycleVelocityTracker(); stopNestedScroll(); } private void flingWithNestedDispatch(int velocityY) { if (!dispatchNestedPreFling(0.0F, velocityY)) { Log.i(TAG, "dispatchNestedPreFling : velocityY : " + velocityY); dispatchNestedFling(0, velocityY, true); // flingScroll(0,velocityY); } } private void getEmbeddedParent(View paramView) { ViewParent localViewParent = paramView.getParent(); if (localViewParent != null) { if ((localViewParent instanceof ScrollStateChangedListener)) { this.parentView = ((ViewGroup) localViewParent); setScrollStateChangedListener((ScrollStateChangedListener) localViewParent); } else { if ((localViewParent instanceof ViewGroup)) { getEmbeddedParent((ViewGroup) localViewParent); } } } } private void init() { this.mChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); ViewConfiguration configuration = ViewConfiguration.get(getContext()); this.mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); this.mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); this.mTouchSlop = configuration.getScaledTouchSlop(); this.directionDetector = new DirectionDetector(); this.density = getScale(); setOverScrollMode(View.OVER_SCROLL_NEVER); this.settings = getSettings(); // addJavascriptInterface(new JSGetContentHeight(), "InjectedObject"); Log.i(TAG, "max -- min Velocity = " + this.mMaximumVelocity + " -- " + this.mMinimumVelocity + " touchSlop = " + this.mTouchSlop); } private void setJavaScriptEnable(boolean flag) { if (this.settings.getJavaScriptEnabled() != flag) { Log.i(TAG, "setJavaScriptEnable : " + this.settings.getJavaScriptEnabled() + " / " + flag); this.settings.setJavaScriptEnabled(flag); } } private void setScrollStateChangedListener(ScrollStateChangedListener paramc) { this.scrollStateChangedListener = paramc; } @Override public void computeScroll() { if (this.position == ScrollStateChangedListener.ScrollState.MIDDLE) { super.computeScroll(); } } public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return this.mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return this.mChildHelper.dispatchNestedPreFling(velocityX, velocityY); } public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return this.mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return this.mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } public int getWebContentHeight() { return this.contentHeight; } public boolean hasNestedScrollingParent() { return this.mChildHelper.hasNestedScrollingParent(); } @Override public void invalidate() { super.invalidate(); this.contentHeight = ((int) (getContentHeight() * getScale())); if (this.contentHeight != this.preContentHeight) { loadUrl("javascript:window.InjectedObject.getContentHeight(document.getElementsByTagName('div')[0].scrollHeight)"); this.preContentHeight = this.contentHeight; } } public boolean isBeingDragged() { return this.mIsBeingDragged; } public boolean isNestedScrollingEnabled() { return this.mChildHelper.isNestedScrollingEnabled(); } @Override public void setNestedScrollingEnabled(boolean paramBoolean) { this.mChildHelper.setNestedScrollingEnabled(paramBoolean); } protected void onAttachedToWindow() { super.onAttachedToWindow(); getEmbeddedParent(this); } private void setLongClickEnable(boolean longClickable) { if (longClickable) { Log.i(TAG, "111111 setLongClickEnable : " + isLongClickable()); if (!isLongClickable()) { super.setOnLongClickListener(this.longClickListenerFalse); setLongClickable(true); setHapticFeedbackEnabled(true); } return; } Log.i(TAG, "22222 setLongClickEnable : " + isLongClickable()); if (this.longClickListenerTrue == null) { this.longClickListenerTrue = new OnLongClickListener() { public boolean onLongClick(View view) { return true; } }; } super.setOnLongClickListener(this.longClickListenerTrue); setLongClickable(false); setHapticFeedbackEnabled(false); } @Override protected void onScrollChanged(int x, int y, int oldx, int oldy) { super.onScrollChanged(x, y, oldx, oldy); this.consumedY = (y - oldy); Log.i(TAG, "consumedYconsumedYconsumedY====" + consumedY); if (y <= 0) { this.position = ScrollStateChangedListener.ScrollState.TOP; return; } if (null != this.scrollStateChangedListener) { this.scrollStateChangedListener.onChildPositionChange(this.position); } if (this.onScrollChangeListener != null) { this.onScrollChangeListener.onScrollChanged(x, y, oldx, oldy, this.position); } else { // Log.i(TAG,"yy=="+y+" webviewHeight=="+this.webviewHeight+" contentHeight=="+this.contentHeight); if (y + this.webviewHeight >= this.contentHeight) { if (this.contentHeight > 0) { this.position = ScrollStateChangedListener.ScrollState.BOTTOM; } } else { this.position = ScrollStateChangedListener.ScrollState.MIDDLE; } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); this.webviewHeight = h + 1; if (this.contentHeight < 1) { setContentHeight(this.webviewHeight); } } @Override public boolean onTouchEvent(MotionEvent ev) { if (this.position == ScrollStateChangedListener.ScrollState.MIDDLE) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { this.mIsBeingDragged = false; this.mActivePointerId = MotionEventCompat.getPointerId(ev, 0); this.startNestedScroll(2); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { this.endTouch(); break; } } super.onTouchEvent(ev); return true; } final int actionMasked = MotionEventCompat.getActionMasked(ev); initVelocityTrackerIfNotExists(); MotionEvent vtev = MotionEvent.obtain(ev); final int index = MotionEventCompat.getActionIndex(ev); if (actionMasked == MotionEvent.ACTION_DOWN) { mNestedYOffset = 0; } vtev.offsetLocation(0, mNestedYOffset); this.consumedY = 0; this.direction = 0; boolean onTouchEvent = false; switch (actionMasked) { case MotionEvent.ACTION_DOWN: { // Remember where the motion event started onTouchEvent = super.onTouchEvent(ev); mLastMotionY = (int) (ev.getY() + 0.5f); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); this.preY = vtev.getY(); this.mIsBeingDragged = false; startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); break; } case MotionEventCompat.ACTION_POINTER_DOWN: { onTouchEvent = super.onTouchEvent(ev); mLastMotionY = (int) (MotionEventCompat.getY(ev, index) + 0.5f); mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEvent.ACTION_MOVE: final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); if (activePointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); break; } if (!mIsBeingDragged && Math.abs(vtev.getY() - this.preY) > mTouchSlop) { final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } mIsBeingDragged = true; } // if(!mIsBeingDragged){ // setLongClickEnable(true); // } final int y = (int) (MotionEventCompat.getY(ev, activePointerIndex) + 0.5f); Log.i(TAG, "mLastMotionY=====" + mLastMotionY); Log.i(TAG, "YYYYYYY=====" + y); int deltaY = mLastMotionY - y; if (deltaY != 0) { this.direction = this.directionDetector.getDirection(deltaY, true, this.scrollStateChangedListener); } if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { deltaY -= mScrollConsumed[1]; vtev.offsetLocation(0, mScrollOffset[1]); mNestedYOffset += mScrollOffset[1]; } if (mIsBeingDragged) { // setJavaScriptEnable(true); // Scroll to follow the motion event mLastMotionY = y - mScrollOffset[1]; Log.i(TAG, "deltaY===" + deltaY); Log.i(TAG, "this.consumedY===" + this.consumedY); final int unconsumedY = deltaY - this.consumedY; Log.i(TAG, " child consumed = " + this.mScrollConsumed[1] + " un_consumed = " + unconsumedY + " position = " + this.position + " direction = " + this.direction); onTouchEvent = super.onTouchEvent(ev); if (this.position == ScrollStateChangedListener.ScrollState.MIDDLE) { return true; } switch (this.direction) { case 1: { if ((this.position != ScrollStateChangedListener.ScrollState.BOTTOM) && (this.contentHeight != this.webviewHeight)) { scrollBy(0, unconsumedY); break; } Log.i(TAG, "1111111consumedY===" + consumedY + " unconsumedY==" + unconsumedY); if (dispatchNestedScroll(0, this.consumedY, 0, unconsumedY, this.mScrollOffset)) { vtev.offsetLocation(0.0F, this.mScrollOffset[1]); this.mNestedYOffset += this.mScrollOffset[1]; this.mLastMotionY -= this.mScrollOffset[1]; } } break; case 2: if ((this.position == ScrollStateChangedListener.ScrollState.TOP) || (this.contentHeight == this.webviewHeight)) { Log.i(TAG, "2222222consumedY===" + consumedY + " unconsumedY==" + unconsumedY); if (dispatchNestedScroll(0, this.consumedY, 0, unconsumedY, this.mScrollOffset)) { vtev.offsetLocation(0.0F, this.mScrollOffset[1]); this.mNestedYOffset += this.mScrollOffset[1]; this.mLastMotionY -= this.mScrollOffset[1]; } } else { scrollBy(0, unconsumedY); } break; default: break; } } break; case MotionEvent.ACTION_CANCEL: onTouchEvent = super.onTouchEvent(ev); break; case MotionEvent.ACTION_UP: onTouchEvent = super.onTouchEvent(ev); if (mIsBeingDragged) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, mActivePointerId); if ((Math.abs(initialVelocity) > mMinimumVelocity)) { flingWithNestedDispatch(-initialVelocity); } } mActivePointerId = INVALID_POINTER; endTouch(); break; case MotionEventCompat.ACTION_POINTER_UP: onTouchEvent = super.onTouchEvent(ev); onSecondaryPointerUp(ev); mLastMotionY = (int) (MotionEventCompat.getY(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)) + 0.5F); break; } if (mVelocityTracker != null) { mVelocityTracker.addMovement(vtev); } vtev.recycle(); return onTouchEvent; } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose getDirection new // active pointer and adjust accordingly. // TODO: Make this decision more intelligent. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = MotionEventCompat.getPointerId(ev, 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; } } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { if (this.position != ScrollStateChangedListener.ScrollState.MIDDLE) { deltaY = 0; } return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } public void scrollToBottom() { scrollTo(getScrollX(), this.contentHeight - this.webviewHeight); } public void scrollToTop() { scrollTo(getScrollX(), 0); } public void setContentHeight(int contentHeight) { this.contentHeight = contentHeight; Log.i(TAG, "contentHeight = " + contentHeight + " - webviewHeight = " + this.webviewHeight + " = " + (contentHeight - this.webviewHeight)); } public void setOnLongClickListener(OnLongClickListener mOnLongClickListener) { this.longClickListenerFalse = mOnLongClickListener; super.setOnLongClickListener(mOnLongClickListener); } public void setOnScrollChangeListener(OnScrollChangeListener paramOnScrollChangeListener) { this.onScrollChangeListener = paramOnScrollChangeListener; } @Override public void setWebViewClient(WebViewClient paramWebViewClient) { if (!(paramWebViewClient instanceof WebViewClient)) throw new IllegalArgumentException("WebViewClient should be instance of EmbeddedWebView$WebViewClient"); super.setWebViewClient(paramWebViewClient); } @Override public boolean startNestedScroll(int paramInt) { return this.mChildHelper.startNestedScroll(paramInt); } @Override protected void onDetachedFromWindow() { this.mChildHelper.onDetachedFromWindow(); super.onDetachedFromWindow(); } @Override public void stopNestedScroll() { this.mChildHelper.stopNestedScroll(); } public static interface OnScrollChangeListener { public abstract void onScrollChanged(int paramInt1, int paramInt2, int paramInt3, int paramInt4, ScrollStateChangedListener.ScrollState parama); } }