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

自定义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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值