有了前面储存知识的储备,接下来我们就来解决这个在开发中常遇到的问题滑动冲突。
1.1常用的滑动冲突的场景
场景1 内外滑动方向不一致
场景2 内外滑动一致
场景3 上面两种情况的嵌套
- 先说场景1,一般主流应用都是采用ViewPager和Fragment搭配,通过左右滑动来切换页面,一般页面里又有Listview进行上下滑动。本来这种情况下会出现滑动冲突,但是ViewPager内部已经处理了这个问题,所以我们无须关注这个问题。但是如果不是使用ViewPager而是使用ScrollView,那就必须手动处理滑动冲突了,否则会造成的后果就是内外两层只能有一层能够滑动,这是因为两者之间的滑动事件有冲突。当然除了这种情况,外部上下滑动,内部左右滑动也一样,但是他们都属于第一种场景冲突。
- 再说场景2,这种情况就稍微复杂一点,内外两层都在同一个方向可以滑动的时候,系统无法知道用户到底想让那一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动会有点顿卡。在实际开发中就比如ScrollView嵌套这Listview。
- 最后说场景3,场景3是场景1和场景2两种情况的嵌套,因为场景3的滑动冲突看起来更加复杂。比如有一个导航条,每一个导航里面有ViewPager和Fargmeng。虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动冲突的叠加,因此只需要分别处理内层和中间层,中间层和外层的滑动冲突即可。
本质上来说,这三种滑动冲突的场景的复杂度其实都相同,因为他们的区别仅仅是滑动策略的不同,接下来讲解一下处理方法,它们几个是通用的。
1.2 滑动冲突的处理规则
对于场景1,假设外层是左右滑动,内层是上下滑动,那么当我们在左右滑动的时候,让外层对事件进行拦截,当我们上下滑动的时候,让内层对事件进行拦截。这里我们就要去分别用户到底是上下滑动呢?还是只有滑动?其实很简单,我们可以根据滑动过程中两个点之间的坐标判断,我们可以去计算出水平滑动的速度与竖直滑动的速度去分别是上下还是左右滑动。我们可以去计算水平滑动的长度和竖直滑动的长度去比较是上下还是左右滑动。我们可以根据两点之间的直线与水平成的夹角去判断是上下还是左右滑动。
对于场景2,比较特别我们无法通过速度,夹角,水平竖直长度去判断,我们只能通过业务逻辑去判断,当某种状态时需要响应外部的view,某种状态是需要响应内部的view。场景3也一样需要根据业务逻辑这个突破口去判断。
1.3滑动冲突的方式
这里我们会一一讲解各各场景对于的处理办法。首先我们先来将场景1的处理办法,因为场景是是最简单也是最典型的,因为它的滑动规则比较简单,不管多么复杂的滑动冲突,它们之间的区别仅仅就是滑动规则的不同。我们抛开滑动规则不说,我们需要找到一种能够不具体依赖滑动规则的一个方法去解决场景1的滑动冲突。
上面说过,针对场景1的滑动冲突,我们可以根据滑动的距离差来判断。这个距离差就是滑动规则。如果用ViewPager去实现场景1,我们不需要手动解决,因为ViewPager已经为我们去解决该问题。因此我们不采用ViewPager,然而在实际的滑动过程中,去获取滑动角度也是相当简单的,那么到底怎么去分发这个事件呢?根据之前的分发机制我们可以得出两种方法,1.外部拦截2.内部拦截
1外部拦截法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器点击事件的判断){
intercept = true;
}else{
intercept = false;
}
break;
}
mlastx = x;
mlasty = y;
return intercept;
}
只需要在ACTION_MOVE里填写需要拦截时的条件即可,ACTION_DOWN必须要false,因为父容器一旦拦截那么后面的ACTION_MOVR ACTION_UP都会交给父容器去处理,ACTION_UP也必须返回false,因为其本身没有太多意义。还有就是如果父容器拦截了ACTION_UP,那么子view的onclick事件就无法触发,因为具有回调onclick的performClick方法在ACTION_UP里面。
2 内部拦截法
内部拦截法就是让父容器不去拦截事件,让子view去判断是否拦截,如果子View需要去处理事件就是事件进行拦截,如果不去出来就不是拦截事件,把事件交给父容器去处理。为此我们需要去重写子view的diapatchTouchEvent和配合使用requstDisallowInterceptTouchEvent。内部拦截法相对于外部拦截法稍显复杂。
首先我们先重写子View的diapatchTouchEvent
@Override
protected boolean dispatchHoverEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
if (父容器点击事件的判断) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
mlastx = x;
mlasty = y;
return super.dispatchHoverEvent(event);
}
getParent().requesDisallowInterceptTouchEvent(true)只要声明一次就够了,我们要在ACTION_MOVE中声明呢?因为一次滑动操作中最新出发的事件是点击也就是ACTION_DOWN。然后根据移动的状态去判断是否要消耗事件,若不消耗则requesDisallowInterceptTouchEvent(false),让父容器去拦截该事件。我们重写子View的dispatchTouchEvent远远不够的,因为父容器的ACTION_MOVE会重置
FLAG_DISALLOW_INTERCEPT标识,之前也讲过了,所以我们还要去重写父容器的onInterceptTouchEvent对ACTION_MOVE事件不去拦截。
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
下面我们来通过一个实例分别来介绍,我们来实现一个类似ViewPager中嵌套这ListView的效果,为此我们写了一个类,类似于ViewPager的HorizontalScrollViewEx的类,这个控件的具体实现后面过再讲,这里只讲滑动冲突。我们还需要定义一个类似于水平的LinearLayout的东西,只不过它可以水平滑动,这样一来他内部有上下滑动的ListView,自身又是左右滑动的,一个典型的场景1就出来了。
现对Demo1Activity进行初始化
public class Demo1Activity extends AppCompatActivity {
String TAG = "DEMO1";
private HorizontalScrollViewEx mListContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo1);
Log.d(TAG, "onCreate");
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = (HorizontalScrollViewEx) findViewById(R.id.container);
final int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
final int screenHeight = MyUtils.getScreenMetrics(this).heightPixels;
for (int i = 0; i < 3; i++) {
ViewGroup layout = (ViewGroup) inflater.inflate(
R.layout.content_layout, mListContainer, false);
layout.getLayoutParams().width = screenWidth;
TextView textView = (TextView) layout.findViewById(R.id.title);
textView.setText("page " + (i + 1));
layout.setBackgroundColor(Color.rgb(255 / (i + 1), 255 / (i + 1), 0));
createList(layout);
mListContainer.addView(layout);
}
}
private void createList(ViewGroup layout) {
ListView listView = (ListView) layout.findViewById(R.id.list);
ArrayList<String> datas = new ArrayList<String>();
for (int i = 0; i < 50; i++) {
datas.add("name " + i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
R.layout.content_list_item, R.id.name, datas);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Toast.makeText(Demo1Activity.this, "click item",
Toast.LENGTH_SHORT).show();
}
});
}
}
很简单我们创建了三个ListView添加到HorizontalScrollViewEx容器中,首先我们先用外部拦截法来处理滑动冲突的问题。
我们来重写onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
* if (!mScroller.isFinished()) {*
mScroller.abortAnimation();//这句话是为了优化滑动体验
intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {//判断水平和竖直的滑动距离那个大
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
Log.d(TAG, "intercepted=" + intercepted);
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
这代码和之前的伪代码没什么区别只是改了一个父容器对拦截事件的判断。但有一种特殊情况,假设当用户正在左右滑动的时候,此时滑动还没结束用户又向下滑动,那要怎么办呢?*包裹的判断就是为了该目的,当华东还没结束的时候,父容器会对事件进行拦截。
内部拦截法也一样,我们要去重写Listview的diapatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "dx:" + deltaX + " dy:" + deltaY);
if (Math.abs(deltaX) > Math.abs(deltaY)) {
mHorizontalScrollViewEx2.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
然后还需要修改HorizontalScrollViewEx的onInterceptTouchEvent,修改后的类暂且叫HorizontalScrollViewEx2
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
return true;
}
return false;
} else {
return true;
}
}
这样内部拦截法就完成了,相对于外部拦截代码量会相对要多,但是萝卜青菜各有所爱!
前面说过了,只要我们根据场景1的情况来得出通用的解决方案,那么对于场景2和场景3来说我们只需要修改相关滑动规则和逻辑就可以。那么接下来我们来演示如用利用场景1得到的解决方案来解决更复杂的滑动冲突。这里只详细分析场景2的滑动冲突,对于场景3中的叠加的滑动冲突,由于它可以拆解成单一的滑动冲突,所以其解决思想和场景1、2中的解决思想一致,就不再对场景3详细分析。