ScrollView嵌套WebView与原生控件组合的一些问题

现在很多做信息流的应用都会有文章详情页,构造基本上就是ScrollView嵌套WebView,然后WebView下面组合一个原生的ListView(RecyclerView)来实现评论列表或者相关推荐等。
大致的xml布局如下,并注意:
1、这里最外层有一父布局container,等会儿有用;
2、WebView一定要是wrap高度,否则会滑不动。

<FrameLayout
    android:id="@+id/test_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
	<TestScrollView
        android:id="@+id/test_scroll_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <TestWebView
                android:id="@+id/test_web_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
            <LinearLayout
                android:id="@+id/test_others_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
                ...可以在这里嵌套各种list等等
            </LinearLayout>
        </LinearLayout>
    </TestScrollView>
</FrameLayout>

我们知道由于WebView本身是可滑动的,ScrollView也可滑动,自然就有了滑动冲突。
其实你不处理滑动冲突大部分情况下也没问题,WebView会一次性全部加载并填充高度,然后滑动事件只在ScrollView上,WebView接收不到任何触摸事件(被父布局ScrollView拦截),表面上手感和视觉效果差异不大,下面组合的原生控件也能正常展示。
但这仅仅是对于静态HTML页面,你换作加载动态页面,问题就来了,比如一些网页图片都是懒加载的,必须依赖向下滚动才会继续加载,这时候你就会发现上面的做法加载不出来图片,因为WebView没吃到滑动事件。(这里有我之前遇到的坑供大家参考:ScrollView(或NestedScrollView)嵌套WebView只加载图片无文字的巨坑
所以还是老老实实处理滑动冲突吧。

问题梳理

总结一下上面遇到的问题,我们需要解决3个事情:
1、当浏览网页时,滑动事件要交给WebView,不能给ScrollView;
2、网页滑到底部时,滑动事件要交给ScrollView,这样才能继续滑出来底部的原生layout(变为可见状态);
3、从底部layout滑回WebView时(此时底部layout变为不可见),滑到事件重新交给WebView。

解决方案

采用外部拦截法,所以我们需要自己实现一个ScrollView控件:

public class TestScrollView extends NestedScrollView {
    private boolean mIsWebViewOnBottom;
    private boolean mIsOthersLayoutShow;
    private float mDownY;
    public TestScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                float dy = y - mDownY;
                if (dy < 0) { // 手指向上滑
                    if (!mIsWebViewOnBottom)
                        return false; // 网页未到底,不拦截事件
                } else { // 手指向下滑
                    if (!mIsOthersLayoutShow)
                        return false; // 底部原生layout完全隐藏,不拦截事件
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    public void setIsWebViewOnBottom(boolean onBottom) {
        this.mIsWebViewOnBottom = onBottom;
    }
    public void setIsOthersLayoutShow(boolean isOthersLayoutShow) {
        this.mIsOthersLayoutShow = isOthersLayoutShow;
    }
}

代码很简单,关键点是两个标识位mIsWebViewOnBottommIsOthersLayoutShow,由外部动态传入参数来告诉ScrollView内部的WebView是否滑到底部了,或者OtherLayout(即底部原生控件的layout)是否露出来了。
滑动冲突解决完毕,接下来就是外部如何控制了,首先需要监听到WebView中网页内容滑到底部,这个可以用重写onOverScrolled来实现(网上流传了很多通过网页的ContentHeight来计算是否到底部,实测并不好用,尤其是有图文的网页,图片加载填充后整个网页高度会变化,问题很多):

public class TestWebView extends WebView {
    public interface OnOverScrollListener {
        void onOverScrolled(TestWebView v, boolean onBottom);
    }
    private OnOverScrollListener mOnOverScrollListener;
    public TestWebView(Context context) {
        this(context, null);
    }
    ... // 省略无关代码
    public void setOnOverScrollListener(OnOverScrollListener listener) {
        this.mOnOverScrollListener = listener;
    }
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
        if (mOnOverScrollListener != null) {
            // clampedY=true的前提下,scrollY=0时表示滑动到顶部,scrollY!=0时表示到底部
            mOnOverScrollListener.onOverScrolled(this, scrollY != 0 && clampedY);
        }
    }
}

然后在Activity或者Fragment中对相关控件进行监听传值(这段代码可以在页面初始化时进行):

private void initView() {
    ...
    // 监听网页是否滑到底
    mWebView.setOnOverScrollListener(new TestWebView.OnOverScrollListener() {
        @Override
        public void onOverScrolled(TestWebView v, boolean onBottom) {
            mScrollView.setIsWebViewOnBottom(onBottom);
        }
    });
    // 没有这段代码的话WebView会滑不动
    mWebView.post(() -> {
        if (mWebView != null) {
            // WebView设置固定高度,避免各种嵌套问题
            ViewGroup.LayoutParams lp = mWebView.getLayoutParams();
            // 注意这里的mContainer就是文章开头讲那个最外层父布局
            lp.height = mContainer.getHeight();
            mWebView.setLayoutParams(lp);
        }
    });
}

最后就是监听底部layout是否显示了,通过ScrollView的OnScrollChangeListener即可实现:

mScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
    @Override
    public void onScrollChange(NestedScrollView nestedScrollView, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        mScrollView.setIsOthersLayoutShow(isViewShowReally(mOthersLayout));
    }
});

这里的isViewShowReally是一个检测View是否真正可见于屏幕内的方法:

/**
 * 判断View是否部分或完全可见
 * @return 一点可见即返回true,完全隐藏时才返回false
 */
boolean isViewShowReally(View view) {
    if (view != null && view.getVisibility() == View.VISIBLE) {
        return view.getGlobalVisibleRect(new Rect());
    }
    return false;
}

OK啦大功告成!
后话:
其实我观察了很多大厂的信息流App都是类似这样实现的,只是他们做了更多的优化,比如在网页滚动到底部时自动下移一段距离以展示出底部原生控件,或者在完全到底前的几十个像素预先转交事件,让滑动的拦截切换显得更自然。当然也有可能是触摸事件处理得更精妙,这便涉及更多细节了,但大致思想和我这个差不多。总之他们绝不会直接把WebView嵌套在ScrollView里而不做任何滑动处理。

源码

https://github.com/ysy950803/ScrollWebView

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值