【问题标题】:WebView doesn't scroll in NestedScrollView with CollapsibleToolBarLayoutWebView 不使用 CollapsibleToolBarLayout 在 NestedScrollView 中滚动
【发布时间】:2019-10-04 20:45:35
【问题描述】:

我目前有这样的布局设置:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:elevation="4dp">
        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="250dp"
            android:fitsSystemWindows="true"
            app:contentScrim="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">
            <ImageView
                android:id="@+id/backdrop"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:fitsSystemWindows="true"
                android:scaleType="centerCrop"
                android:src="@drawable/android_icon"
                app:layout_collapseMode="parallax" />
            <androidx.appcompat.widget.Toolbar
                android:id="@+id/generic_toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:fitsSystemWindows="true"
                android:background="@null"
                app:layout_collapseMode="pin"/>
        </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
    <androidx.core.widget.NestedScrollView
        android:id="@+id/primary_nestedScrollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <WebView
            android:id="@+id/primary_webview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

虽然这在大多数情况下都可以正常工作,但当我想确保其内容填满视口时,webview 的滚动会出现问题。

如果我将 android:fillViewport="true" 属性添加到 NestedScrollView,则视口会正确填满,折叠工具栏仍会折叠,但 webview 本身无法再滚动。

我在一个小演示项目视频中对此进行了演示: https://i.imgur.com/EJevXuz.mp4

(编辑:录音似乎有一些闪烁的伪影。这是一个录音问题,与问题无关)

正如您所见,当 fillViewPort 设置为 false 时,滚动效果很好。尽管正如预期的那样,如果内容(具有深灰色背景)很短/很小,它不会填满可用空间。

当 NestedScrollView 上的 fillViewPort 值设置为 true 时,内容(的背景)会填充可用空间,但似乎禁用了对较大内容的任何滚动(WebView 被“卡在”其顶部滚动位置)即使 web 视图中的滚动条表明有更多内容可用。

我已经尝试了很多方法来让它再次工作(包括在网络上找到的几个 NestedWebView 实现),但到目前为止都无济于事。

如何在此布局设置中再次正确滚动 fillViewPort 选项的 WebView?

【问题讨论】:

    标签: android webview android-collapsingtoolbarlayout android-nestedscrollview coordinator-layout


    【解决方案1】:

    我终于找到了适合我情况的 NestedWebView 实现。

    此实现完全消除了 NestedScrollView 的必要性,允许 webview 根据 wrap_content/match_parent 布局值自动调整其视口,而不是滚动视图的视口处理。

    这是 snachmsm 最近添加的一个实现,作为对a similar question 的回答:

    import android.annotation.SuppressLint;
    import android.content.Context;
    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.ViewParent;
    import android.webkit.WebView;
    import android.widget.OverScroller;
    
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.core.view.NestedScrollingChild3;
    import androidx.core.view.NestedScrollingChildHelper;
    import androidx.core.view.ViewCompat;
    
    /**
     * WebView compatible with CoordinatorLayout by snachmsm
     * The implementation based on NestedScrollView of design library androidx v1.0.1
     */
    public class NestedWebView extends WebView implements NestedScrollingChild3 {
    
        private static final String TAG = "NestedWebView";
        private static final int INVALID_POINTER = -1;
    
        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 OverScroller mScroller;
        private int mMinimumVelocity;
        private int mMaximumVelocity;
        private int mLastScrollerY;
    
        public NestedWebView(Context context) {
            this(context, null);
        }
    
        public NestedWebView(Context context, AttributeSet attrs) {
            this(context, attrs, android.R.attr.webViewStyle);
        }
    
        public NestedWebView(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 = new OverScroller(getContext());
            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)) { // most common
                return true;
            }
    
            switch (action & MotionEvent.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:
                    mLastMotionY = (int) ev.getY();
                    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 MotionEvent.ACTION_POINTER_UP:
                    onSecondaryPointerUp(ev);
                    break;
            }
    
            return mIsBeingDragged;
        }
    
        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            initVelocityTrackerIfNotExists();
    
            MotionEvent vtev = MotionEvent.obtain(ev);
    
            final int actionMasked = ev.getActionMasked();
    
            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()) {
                        abortAnimatedScroll();
                    }
    
                    mLastMotionY = (int) ev.getY();
                    mActivePointerId = ev.getPointerId(0);
                    startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
                    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,
                            ViewCompat.TYPE_TOUCH)) {
                        deltaY -= mScrollConsumed[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 range = getScrollRange();
    
                        // Calling overScrollByCompat will call onOverScrolled, which
                        // calls onScrollChanged if applicable.
                        if (overScrollByCompat(0, deltaY, 0, oldY, 0, range, 0,
                                0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) {
                            mVelocityTracker.clear();
                        }
    
                        final int scrolledDeltaY = getScrollY() - oldY;
                        final int unconsumedY = deltaY - scrolledDeltaY;
    
                        mScrollConsumed[1] = 0;
    
                        dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset,
                                ViewCompat.TYPE_TOUCH, mScrollConsumed);
    
                        mLastMotionY -= mScrollOffset[1];
                        mNestedYOffset += mScrollOffset[1];
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
                            dispatchNestedFling(0, -initialVelocity, true);
                            fling(-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) {
                        if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
                                getScrollRange())) {
                            ViewCompat.postInvalidateOnAnimation(this);
                        }
                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                    break;
                case MotionEvent.ACTION_POINTER_DOWN:
                    final int index = ev.getActionIndex();
                    mLastMotionY = (int) ev.getY(index);
                    mActivePointerId = ev.getPointerId(index);
                    break;
                case MotionEvent.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);
        }
    
        private void abortAnimatedScroll() {
            mScroller.abortAnimation();
            stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
        }
    
        private void endDrag() {
            mIsBeingDragged = false;
    
            recycleVelocityTracker();
            stopNestedScroll();
        }
    
        private void onSecondaryPointerUp(MotionEvent ev) {
            final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
                    >> MotionEvent.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 fling(int velocityY) {
            int height = getHeight();
            mScroller.fling(getScrollX(), getScrollY(), // start
                    0, velocityY, // velocities
                    0, 0, // x
                    Integer.MIN_VALUE, Integer.MAX_VALUE, // y
                    0, height / 2);
            runAnimatedScroll(true);
        }
    
        private void runAnimatedScroll(boolean participateInNestedScrolling) {
            if (participateInNestedScrolling) {
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
            } else {
                stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
            }
            mLastScrollerY = getScrollY();
            ViewCompat.postInvalidateOnAnimation(this);
        }
    
        @Override
        public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
            if (disallowIntercept) {
                recycleVelocityTracker();
            }
            super.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    
        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
        protected boolean overScrollBy(int deltaX, int deltaY,
                                       int scrollX, int scrollY,
                                       int scrollRangeX, int scrollRangeY,
                                       int maxOverScrollX, int maxOverScrollY,
                                       boolean isTouchEvent) {
            // this is causing double scroll call (doubled speed), but this WebView isn't overscrollable
            // all overscrolls are passed to appbar, so commenting this out during drag
            if (!mIsBeingDragged)
                overScrollByCompat(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
                        maxOverScrollX, maxOverScrollY, isTouchEvent);
            // without this call webview won't scroll to top when url change or when user pick input
            // (webview should move a bit making input still in viewport when "adjustResize")
            return true;
        }
    
        int getScrollRange() {
            //Using scroll range of webview instead of childs as NestedScrollView does.
            return computeVerticalScrollRange();
        }
    
        @Override
        public boolean isNestedScrollingEnabled() {
            return mChildHelper.isNestedScrollingEnabled();
        }
    
        @Override
        public void setNestedScrollingEnabled(boolean enabled) {
            mChildHelper.setNestedScrollingEnabled(enabled);
        }
    
        @Override
        public boolean startNestedScroll(int axes, int type) {
            return mChildHelper.startNestedScroll(axes, type);
        }
    
        @Override
        public boolean startNestedScroll(int axes) {
            return startNestedScroll(axes, ViewCompat.TYPE_TOUCH);
        }
    
        @Override
        public void stopNestedScroll(int type) {
            mChildHelper.stopNestedScroll(type);
        }
    
        @Override
        public void stopNestedScroll() {
            stopNestedScroll(ViewCompat.TYPE_TOUCH);
        }
    
        @Override
        public boolean hasNestedScrollingParent(int type) {
            return mChildHelper.hasNestedScrollingParent(type);
        }
    
        @Override
        public boolean hasNestedScrollingParent() {
            return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH);
        }
    
        @Override
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                                            int[] offsetInWindow) {
            return dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, ViewCompat.TYPE_TOUCH);
        }
    
        @Override
        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                                            int[] offsetInWindow, int type) {
            return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, type);
        }
    
        @Override
        public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                                         @Nullable int[] offsetInWindow, int type, @NonNull int[] consumed) {
            mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                    offsetInWindow, type, consumed);
        }
    
        @Override
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
            return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH);
        }
    
    
        @Override
        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
            return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
        }
    
        @Override
        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
            return mChildHelper.dispatchNestedFling(velocityX, velocityY, false);
        }
    
        @Override
        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
            return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
        }
    
        @Override
        public int getNestedScrollAxes() {
            return ViewCompat.SCROLL_AXIS_VERTICAL;
        }
    
        @Override
        public void computeScroll() {
            if (mScroller.isFinished()) {
                return;
            }
    
            mScroller.computeScrollOffset();
            final int y = mScroller.getCurrY();
            int unconsumed = y - mLastScrollerY;
            mLastScrollerY = y;
    
            // Nested Scrolling Pre Pass
            mScrollConsumed[1] = 0;
            dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null,
                    ViewCompat.TYPE_NON_TOUCH);
            unconsumed -= mScrollConsumed[1];
    
    
            if (unconsumed != 0) {
                // Internal Scroll
                final int oldScrollY = getScrollY();
                overScrollByCompat(0, unconsumed, getScrollX(), oldScrollY, 0, getScrollRange(),
                        0, 0, false);
                final int scrolledByMe = getScrollY() - oldScrollY;
                unconsumed -= scrolledByMe;
    
                // Nested Scrolling Post Pass
                mScrollConsumed[1] = 0;
                dispatchNestedScroll(0, 0, 0, unconsumed, mScrollOffset,
                        ViewCompat.TYPE_NON_TOUCH, mScrollConsumed);
                unconsumed -= mScrollConsumed[1];
            }
    
            if (unconsumed != 0) {
                abortAnimatedScroll();
            }
    
            if (!mScroller.isFinished()) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    
        // copied from NestedScrollView exacly as it looks, leaving overscroll related code, maybe future use
        private boolean overScrollByCompat(int deltaX, int deltaY,
                                           int scrollX, int scrollY,
                                           int scrollRangeX, int scrollRangeY,
                                           int maxOverScrollX, int maxOverScrollY,
                                           boolean isTouchEvent) {
            final int overScrollMode = getOverScrollMode();
            final boolean canScrollHorizontal =
                    computeHorizontalScrollRange() > computeHorizontalScrollExtent();
            final boolean canScrollVertical =
                    computeVerticalScrollRange() > computeVerticalScrollExtent();
            final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS
                    || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal);
            final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS
                    || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical);
    
            int newScrollX = scrollX + deltaX;
            if (!overScrollHorizontal) {
                maxOverScrollX = 0;
            }
    
            int newScrollY = scrollY + deltaY;
            if (!overScrollVertical) {
                maxOverScrollY = 0;
            }
    
            // Clamp values if at the limits and record
            final int left = -maxOverScrollX;
            final int right = maxOverScrollX + scrollRangeX;
            final int top = -maxOverScrollY;
            final int bottom = maxOverScrollY + scrollRangeY;
    
            boolean clampedX = false;
            if (newScrollX > right) {
                newScrollX = right;
                clampedX = true;
            } else if (newScrollX < left) {
                newScrollX = left;
                clampedX = true;
            }
    
            boolean clampedY = false;
            if (newScrollY > bottom) {
                newScrollY = bottom;
                clampedY = true;
            } else if (newScrollY < top) {
                newScrollY = top;
                clampedY = true;
            }
    
            if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
                mScroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange());
            }
    
            onOverScrolled(newScrollX, newScrollY, clampedX, clampedY);
    
            return clampedX || clampedY;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-28
      相关资源
      最近更新 更多