您的位置:首页 > 其它

自定义View系列教程08--滑动冲突的产生及其处理

2016-06-27 15:58 573 查看
自定义View系列教程01--常用工具介绍

自定义View系列教程02--onMeasure源码详尽分析

自定义View系列教程03--onLayout源码详尽分析

自定义View系列教程04--Draw源码分析及其实践

自定义View系列教程05--示例分析

自定义View系列教程06--详解View的Touch事件处理

自定义View系列教程07--详解ViewGroup分发Touch事件

自定义View系列教程08--滑动冲突的产生及其处理

在之前的几篇文章中,我们已经分析了View对于Touch的处理以及ViewGroup对于Touch事件的分发。 

但在开发中时常遇到一个棘手的问题:Touch事件的滑动冲突。比如ListView嵌套ScrollView,ViewPager嵌套ScrollView,ListView嵌套ScrollView时常常发生。

一.常见冲突场景:

场景1:外部滑动方向和内部滑动方向不一致。例如:ViewPaget+Fragment(ListView)或者HorizontalScrollView+listView

场景2:外部滑动方向和内部滑动方向一致。例如:HoriaontalScrollView+ViewPager

场景3:上面两种的嵌套。例如:SlideMenu+ViewPager+ListView



二.滑动冲突的处理规则:

如下图:根据滑动过程中两点之间的坐标,可以得出到底是水平滑动还是竖直滑动。

得到坐标之后可以通过:1.滑动角度    2.滑动的距离差    3.滑动的速度差    或者是从业务上找突破点。



三.滑动冲突的解决方法:

    1.在父View中准确地进行事件分发和拦截【外部拦截法】 
我们可以重写父View中与Touch事件分发相关的方法,比如onInterceptTouchEvent( )。这些方法中摒弃系统默认的流程,结合自身的业务逻辑重写该部分代码,从而使父View放行子View需要的Touch。

      2.子View禁止父View拦截Touch事件 【内部拦截法】

         在分析ViewGroup的dispatchTouchEvent()源码时,我们知道:Touch事件是由父View分发的。如果一个Touch事件是子View需要的,但是被其父View拦截了,子View就无法处理该Touch事件了。在此情形下,子View可以调用requestDisallowInterceptTouchEvent( )禁止父View对Touch的拦截

伪代码:

外部拦截法:

@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;
break;
}
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;

return intercepted;
}
//注释:ACTION_DOWN这个事件,父容器必须返回false,因为一旦父拦截了,后续ACTION_MOVE,ACTION_UP事件都会直接交给父处理,事件无>法传给子元素。
//ACTION_MOVE这个事件,根据需要来决定:父需要拦截返回true,否则返回false。
//ACTION_UP这个事件,必须返回false,因为ACTION_UP事件本身没有太多意义。
//说明:为什么说ACTION_UP必须返回false?
//当返回true时,考虑一种情况,假设事件交给子处理,如果父在ACTION_UP时返回true,就会导致子无法收到ACTION_UP事件,子的Click事件就无法触发。(就出现了问题)
//即使总返回false,也不会出错,因为父容器比较特殊,一旦开始拦截任何一个事件,那么后续事件都会交给它来处理,而ACTION_UP作为最后一个事件,也必定可以传给父容器,即是父容器的onInterceptTouchEvent方法在ACTION_UP返回了false。
内部拦截法:

//内部拦截法
@Override//子类
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;

if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

}

//注释:除了子元素需要做处理以外,父元素也要默认拦截ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。

@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;
return false;
} else {
return true;
}
}
四,示例展示解决滑动冲突的常用方法。

示例效果,请看下图:



它的布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.stay4it.testtouch3.MainActivity">

<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_height="450dp"
android:layout_width="match_parent"
android:scrollbars="horizontal"
android:fillViewport="true">

<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</HorizontalScrollView>

</RelativeLayout>
在该示例中,我们在HorizontalScrollView中嵌入ViewPager浏览图片。由于HorizontalScrollView和ViewPager都可以响应水平滑动,当我们用手指切换图片时发现ViewPager无法正常的滚动。这是因为Touch事件被HorizontalScrollView处理,根本没有传递给ViewPager。

嗯哼,滑动冲突的原因已经清楚了,我们现在来解决这个问题。

第一种解决方法

在ViewPager中禁止HorizontalScrollView拦截Touch事件
package com.stay4it.testtouch3;

import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;
/**
* 原创作者
* 谷哥的小弟
*
* 博客地址
* http://blog.csdn.net/lfdfhl */
public class MainActivity extends AppCompatActivity {
private ViewPager mViewPager;
private ViewPagerAdapter mViewPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init(){
mViewPager = (ViewPager) findViewById(R.id.viewPager);
mViewPagerAdapter=new ViewPagerAdapter(this);
mViewPager.setAdapter(mViewPagerAdapter);
mViewPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mViewPager.getParent().requestDisallowInterceptTouchEvent(true);
return false;
}
});
}
}
在此,请关注第32行代码

mViewPager.getParent().requestDisallowInterceptTouchEvent(true);

此处首先利用 mViewPager.getParent( )得到了ViewPager的父View即HorizontalScrollView;然后再执行requestDisallowInterceptTouchEvent( )方法从而禁止掉父View对于Touch事件的传递。

第二种解决方法

在自定义HorizontalScrollView中合理地处理Touch事件的拦截和分发
package com.stay4it.testtouch4;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.HorizontalScrollView;
/**
* 原创作者
* 谷哥的小弟
*
* 博客地址
* http://blog.csdn.net/lfdfhl */
public class HorizontalScrollViewSubclass extends HorizontalScrollView{
private float LastX;
private float LastY;
private float distanceX;
private float distanceY;
public HorizontalScrollViewSubclass(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
distanceX = 0f;
distanceY = 0f;
LastX = ev.getX();
LastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:

final float currentX = ev.getX();
final float currentY = ev.getY();

distanceX += Math.abs(currentX - LastX);
distanceY += Math.abs(currentY - LastY);

LastX = currentX;
LastY = currentY;

if(distanceX > distanceY){
return false;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
return super.onTouchEvent(ev);
}
}
从这段代码中,我们的主要操作是在自定义HorizontalScrollView中重写了onInterceptTouchEvent( )。 

在该方法中如果判定Touch是水平的滑动:

if(distanceX > distanceY)

那么直接返回false不拦截该Touch。这样Touch事件就可以顺利传递到其子View即ViewPager中,从而正常地切换图片。

既然已经采用了自定义的HorizontalScrollView就不要忘记修改布局文件喔~
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.stay4it.testtouch4.MainActivity">

<com.stay4it.testtouch4.HorizontalScrollViewSubclass
android:id="@+id/horizontalScrollView"
android:layout_height="450dp"
android:layout_width="match_parent"
android:scrollbars="horizontal"
android:fillViewport="true"
>

<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</com.stay4it.testtouch4.HorizontalScrollViewSubclass>

</RelativeLayout>
嗯哼,在此通过一个完整的示例展示了事件滑动冲突的常用解决方式。 

在实际的项目开发中所遇到的滑动冲突可能比这要复杂,但是处理冲突的基本原理和方式是相同的。

至此,自定义View系列教程就全部结束了

多谢大家的指正和鼓励

Thank you very much

原文链接:http://blog.csdn.net/lfdfhl/article/details/51656492
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  View 自定义View