You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android:WebView嵌套SwipeRefreshLayout的滚动与缩放异常求助

解决SwipeRefreshLayout与WebView嵌套滚动冲突及缩放后滚动回滚问题

问题概述

  • SwipeRefreshLayout嵌套WebView时,WebView向下滚动后尝试向上滚动,会触发SwipeRefreshLayout的刷新动作,而非WebView内部滚动
  • 使用第三方NestedScrollWebView解决上述滚动冲突后,WebView页面放大后向右滚动,会自动回滚至初始左侧位置

方案一:原生自定义WebView实现嵌套滚动(无第三方依赖)

通过自定义WebView实现NestedScrollingChild3接口,精确控制滚动事件传递逻辑,同时解决两个核心问题。

代码实现

class FixedNestedScrollWebView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = android.R.attr.webViewStyle
) : WebView(context, attrs, defStyleAttr), NestedScrollingChild3 {

    private val scrollConsumed = IntArray(2)
    private val offsetInWindow = IntArray(2)
    private var nestedScrollHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this)

    init {
        isNestedScrollingEnabled = true
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var handled = super.onTouchEvent(event)
        val action = event.actionMasked

        when (action) {
            MotionEvent.ACTION_DOWN -> {
                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL or ViewCompat.SCROLL_AXIS_HORIZONTAL, NestedScrollingChildHelper.TYPE_TOUCH)
                offsetInWindow[0] = 0
                offsetInWindow[1] = 0
            }
            MotionEvent.ACTION_MOVE -> {
                val scrollX = scrollX
                val scrollY = scrollY
                val deltaX = event.x.toInt() - offsetInWindow[0]
                val deltaY = event.y.toInt() - offsetInWindow[1]

                val consumed = IntArray(2)
                if (dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, NestedScrollingChildHelper.TYPE_TOUCH)) {
                    val remainingX = deltaX - consumed[0]
                    val remainingY = deltaY - consumed[1]
                    if (remainingX != 0 || remainingY != 0) {
                        handled = super.onTouchEvent(event) || performScroll(remainingX, remainingY)
                    }
                } else {
                    handled = super.onTouchEvent(event)
                }
                offsetInWindow[0] += deltaX - consumed[0]
                offsetInWindow[1] += deltaY - consumed[1]
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                stopNestedScroll(NestedScrollingChildHelper.TYPE_TOUCH)
            }
        }
        return handled
    }

    private fun performScroll(deltaX: Int, deltaY: Int): Boolean {
        val oldScrollX = scrollX
        val oldScrollY = scrollY
        scrollBy(-deltaX, -deltaY)
        return scrollX != oldScrollX || scrollY != oldScrollY
    }

    // 实现NestedScrollingChild3接口的必要方法
    override fun setNestedScrollingEnabled(enabled: Boolean) {
        nestedScrollHelper.isNestedScrollingEnabled = enabled
    }

    override fun isNestedScrollingEnabled(): Boolean {
        return nestedScrollHelper.isNestedScrollingEnabled
    }

    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        return nestedScrollHelper.startNestedScroll(axes, type)
    }

    override fun startNestedScroll(axes: Int): Boolean {
        return nestedScrollHelper.startNestedScroll(axes)
    }

    override fun stopNestedScroll(type: Int) {
        nestedScrollHelper.stopNestedScroll(type)
    }

    override fun stopNestedScroll() {
        nestedScrollHelper.stopNestedScroll()
    }

    override fun hasNestedScrollingParent(type: Int): Boolean {
        return nestedScrollHelper.hasNestedScrollingParent(type)
    }

    override fun hasNestedScrollingParent(): Boolean {
        return nestedScrollHelper.hasNestedScrollingParent()
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int) {
        nestedScrollHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type)
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?) {
        nestedScrollHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
        // 仅当WebView无法向上滚动时,才允许父布局处理垂直滚动
        val canScrollUp = scrollY > 0 || canScrollVertically(-1)
        val shouldDispatchVertical = !canScrollUp && dy > 0
        
        // 水平滚动完全由WebView自行处理
        if (dx != 0) {
            consumed?.let {
                it[0] = 0
                it[1] = if (shouldDispatchVertical) dy else 0
            }
            return shouldDispatchVertical
        }
        return shouldDispatchVertical && super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean {
        return nestedScrollHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
    }

    override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean {
        return nestedScrollHelper.dispatchNestedFling(velocityX, velocityY, consumed)
    }

    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
        return nestedScrollHelper.dispatchNestedPreFling(velocityX, velocityY)
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int, consumed: IntArray?) {
        nestedScrollHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed)
    }
}

使用方式

在布局文件中替换原生WebView即可:

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.yourpackage.FixedNestedScrollWebView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/webView"/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

方案二:修改第三方NestedScrollWebView解决缩放回滚问题

如果已依赖目标仓库,只需修改核心滚动判断逻辑,区分水平/垂直滚动事件:

修改核心代码

找到原仓库NestedScrollWebView中的dispatchNestedPreScroll方法,替换为:

@Override
public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
    // 仅当WebView无法向上滚动时,允许父布局处理垂直滚动
    boolean canScrollUp = getScrollY() > 0 || canScrollVertically(-1);
    boolean shouldDispatchVertical = !canScrollUp && dy > 0;
    
    // 水平滚动完全由WebView处理,不交给父布局
    if (dx != 0) {
        if (consumed != null) {
            consumed[0] = 0;
            consumed[1] = shouldDispatchVertical ? dy : 0;
        }
        return shouldDispatchVertical;
    }
    
    return shouldDispatchVertical && super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
}

说明

原仓库问题在于未区分水平/垂直滚动,导致缩放后WebView的水平滚动被干扰。修改后水平滚动完全由WebView处理,垂直滚动仅在WebView滚动到顶部时才交给SwipeRefreshLayout,同时解决两个问题。


内容的提问来源于stack exchange,提问作者Rahul Mishra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 00:47:30