Viewpager与webview滑动冲突的解决方案
场景描述
最近在接触h5与android混合开发时遇到一个问题,在一个activity使用ViewPager+Fragment结构,某个Fragment包含了一个webview。而在这个webview展示的h5里有一个横屏轮播的元素,此时当我们横向滑动的时候,大多数情况下是ViewPager在滑动(这里说是大多数情况是不考虑网页可以横向滑动的特殊情况)。因此我们需要判断并处理事件。
如下图所示,蓝色线条中间的部分是一个轮播。而整个首页的结构是一个Viewpager。
Paste_Image.png
问题分析
其实h5的轮播基本都会支持手滑动事件(指JS控制轮播的滑动),我们要做的就是判断什么时候,touch事件由h5来处理,什么时候由ViewPager来处理,这里就不再讲述android的事件机制,有兴趣的同学可以去老衲之前发布的文章中去找。这里只用到了其中一个知识点,即child如何影响parent的事件处理。所涉及到的方法就是
//当result为true的时候,child会阻止parent获取touch事件,反之则不会影响。
requestDisallowInterceptTouchEvent(result);
而这个方法参数result的值是true还是false,就是今天要踩得坑之一。
Paste_Image.png主要思路
-
首先我们需要确定的是,需要重写谁的onTouch方法,webview还是viewpager,当然是webview,通过webview来主动控制viewpager的事件获取权限。
-
接下来,为了判断,我们还需要轮播控件的坐标和范围,这个可以有h5和JS来实现
-
范围有了。接下来就是要真正进行touch的坐标判断了。
踩坑1之Java与JS的之间相互调用的顺序
关于android内的java方法与html内的js方法相互调用请大家自行百度。这里要提醒大家的是,当JS与java在进行交互的时候,他们并不是同步执行的。举个栗子
public void doCheck() {
String call = "javascript:getViewPagerInfo()";
webview.loadUrl(call);
}
当通过上述方法调用JS的getViewPagerInfo方法时,而JS的getViewPagerInfo方法内部又调用了java的下列方法时。
@JavascriptInterface
public void getH5ViewPagerInfo(int x ,int y , int width , int height){
mPagerDesc = new PagerDesc(y,x,x+width,y+height , 0);
}
假如我们需要按顺序执行如下两个方法
doCheck();
showToast();
当执行完doCheck的loadurl方法之后,他会去执行showToast,不会等JS回调java的getH5ViewPagerInfo方法执行完再执行。
踩坑2之JS滑动
刚开始接到这个需求的时候,会想的太多,导致刚开始考虑h5的时候顺带把网页的滑动也计算进去了。这个是没有必要的。见踩坑3内的代码,可以兼容滑动的情况。
踩坑3之h5获取轮播控件的坐标与宽高
没啥技术亮点,直接看代码
//获取轮播控件的宽高以及相对于原点的位置
function getViewPagerInfo() {
var width = img.clientWidth;
var height = img.clientHeight;
var elem = getElementRect(img);
//调用android代码
window.controller.getH5ViewPagerInfo(elem.x,elem.y,width,height);
}
//获取元素的坐标
function getElementRect(e){
var box = e.getBoundingClientRect();
var x = box.left;
var y = box.top;
console.log("x::" + x);
console.log("y::" + y);
return {x:x , y: y};
}
踩坑4之轮播宽高坐标的获取时机
因为h5页面的高度是不确定的。很有可能是可以上下滑动的。所以我们轮播的区域也是会变化的,而且!!!轮播的区域可能不止一个,这个需要注意。轮播区域的获取时机有两个,一个是刚加载h5页面的时候,另外一个就是滑动的时候,在js代码里写
window.onload = function(){
...
}
window.onscroll = function(){
...
}
踩坑6之h5与android坐标系的转换
该需求最大的坑就在于h5与android坐标系的换算,h5的坐标系与android的坐标系的不同在于
- h5的坐标系以webview左上角的点为准,而android得坐标系以屏幕左上角的点为准。因此,这里要将通知栏的高度计算进去。
- h5的坐标系采用的是css的像素,而android是采用的设备的像素值。这两个像素需要进行换算,换算的规则也很简单,与设备的像素密度相关。
假设我们现在拿到了轮播的坐标,以及宽高,并且通过js回传给了java,,此时,我们就可以进行最重要的touch事件的判断了。
首先我们定义一个内部类用来封装轮播的宽高和坐标
class PagerDesc {
private int top;
private int left;
private int right;
private int bottom;
public PagerDesc(int top, int left , int right ,int bottom ) {
this.top = top;
this.bottom = bottom;
}
}
考虑到目前多数的轮播都是横向充满全屏,因此这里我们只考虑touch事件在y轴上的坐标。
接下来,需要通过js将上述类创建所需要的数据回传给java用来创建对象。
private PagerDesc mPagerDesc;
@JavascriptInterface
public void getH5ViewPagerInfo(int x ,int y , int width , int height){
mPagerDesc = new PagerDesc(y,x,x+width,y+height);
}
最关键的一步,我们要在webview的onTouchListener进行如下处理。
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
//获取y轴坐标
float y = motionEvent.getRawY();
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
getHTMLPosition();
if (null != mPagerDesc) {
int top = mPagerDesc.top;
int bottom = top + (mPagerDesc.bottom - mPagerDesc.top);
//将css像素转换为android设备像素并考虑通知栏高度
top = (int) (top * metric.density) + height
bottom = (int) (bottom * metric.density) + height
//如果触摸点的坐标在轮播区域内,则由webview来处理事件,否则由viewpager来处理
if (y > top && y < bottom) {
webview.requestDisallowInterceptTouchEvent(true);
} else {
webview.requestDisallowInterceptTouchEvent(false);
}
}
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
}
至此,轮播和ViewPager的滑动冲突及解决方案已经介绍完了。这个问题考察的点还是挺多的,需要开发者有一定的JS基础,需要懂得JS与java的相互调用,以及深入理解touch事件的传递及拦截机制。当然解决的过程就是踩坑与提高的过程,希望本文能给遇到该问题的小伙伴一个思路。