现在很多做信息流的应用都会有文章详情页,构造基本上就是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;
}
}
代码很简单,关键点是两个标识位mIsWebViewOnBottom 和 mIsOthersLayoutShow,由外部动态传入参数来告诉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里而不做任何滑动处理。