Android触摸事件详解

Android触摸事件分发流程详解

 
(注:自己总结的一些目前自己分析的Android触摸事件的整个传递流程。待完善)

一、需要了解的几个与触摸事件分发有关的几个方法

 1、我们分两个类型来进行讨论(这里讨论的是几个常用的方法):

      首先我们需要知道ViewGroup继承自View。因此View中的方法ViewGroup也有,但是ViewGroup有View没有的,与触摸事件相关的方法。
      View相关的:dispatchTouchEvent(MotionEvent ev) 方法和onTouchEvent(MotionEvent event)方法、
      ViewGroup相关的:dispatchTouchEvent(MotionEvent ev) 方法、onInterceptTouchEvent(MotionEvent ev)方法、requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法、onTouchEvent(MotionEvent event)方法。

       2、方法的调用顺序

	方法的调用顺序为:dispatchTouchEvent方法 -> onInterceptTouchEvent方法 -> onTouchEvent方法

       3、各个方法的用途已经返回值的含义

dispatchTouchEvent方法:
    这个方法是用来分发事件的,自定义的View中都可以重写该方法,但是一般都会将这个方法中忽略。我们只需要知道,这个方法是触摸事件发生后第一个被调用的就好了。同时,我们在代码中可以通过view.dispatchTouchEvent(event)方法,将当前的触摸事件分发给该view进行处理。这个例子在下边讲解的一个小例子的代码中会有出现,结合代码更容易理解。
onInterceptTouchEvent方法:
    这个方法是独属于ViewGroup的,原因和它的功能有关,我们可以从字面含义看出,这个方法是用来中断事件的传递的,因为只有ViewGroup才有可能将事件传递给子View,因此,这个方法是ViewGroup特有的,如果自定义的View继承自View而不是继承自ViewGroup,就不能重写该方法。
该方法的功能:中断事件的传递。
返回值true时:表示当前的View中断了这个触摸事件的传递,那么这个触摸事件就交给本View处理,不在向子View传递该事件,也	就是触摸事件会交给onTouchEvent方法进行处理。
返回值为false时:表示当前的View没有中断该事件的传递,还会想子View传递改事件,如果子View不进行处理才会将事件返还给子	View的父View,也就是当前的View的onTouchEvent方法进行处理。注意,这个事件传递给子View后也同样的走和本View一	样的逻辑。也就是触摸事件的执行顺序在子View中也是同样的,只有子View没有处理该事件才会返给当前的View,否则,	就由子View处理。
注:onInterceptTouchEvent()方法让父view能够在它的子view之前处理触摸事件。如果我们让onInterceptTouchEvent()返回true,则之前处理触摸事件的子view会收到ACTION_CANCEL事件,并且该点之后的事件会被发送给该父view自身的onTouchEvent()函数,进行常规处理。onInterceptTouchEvent()也可以返回false,这样事件沿view层级分发到目标前,父view可以简单地观察该事件。这里的目标是指,通过onTouchEvent()处理消息事件的view。
requestDisallowInterceptTouchEvent方法:
一个继承自ViewGroup的方法可以调用该方法,例如一个ListView调用该方法lv.requestDisallowInterceptTouchEvent(true);表示当子view不想该父view和祖先view通过onInterceptTouchEvent()截获它的触摸事件时,如果设置的为false表示父View可以通过onInterceptTouchEvent()方法中断触摸事件。
onTouchEvent方法:
该方法是用来处理触摸事件的方法。
返回值为true:表示这个触摸事件被我们当前的View消费了。
返回值为false:表示这个触摸事件没有被我们当前的View消费。即使我们在onTouchEvent方法中写了处理该触摸事件的逻辑,这段逻辑也不会被执行,因为这个触摸事件不是我们消费的。

二、讲解几个处理触摸事件用到的类和方法

1、GestureDetector类的使用:
   该类能够帮助我们处理touch事件,需要我们将event作为参数传入进去。直接上代码:
//这里我们主要为了实现触摸滑动时View跟着滑动的效果。切换页面的动作没有在这里实现,直接写在onTouchEvent中
  GestureDetector gestureDetector = new GestureDetector(context,
                new GestureDetector.SimpleOnGestureListener(){
                    @Override
                    /**
                     *当手指在屏幕上移动时调用这个方法
                     * @param e1 : down事件的MotionEvent
                     * @param e2 : move事件的MotionEvent
                     * @param distanceX : X轴方向移动的距离
                     * @param distanceY : Y轴方向移动的距离
                     *
                     */
                    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                        /**
                        *  移动View,是移动了,表示每次移动某个距离。不是移动到某处
                         * @param distanceX:x轴移动的距离。为正时表示向左移动,为负时向右移动
                         * @param distanceY:y轴移动的距离
                         *
                         */
                        scrollBy((int)distanceX,0);
                        Log.i(TAG, "distanceX :: " + distanceX);

                        /**
                         * 将当前的view移动到某个位置,某个点上去
                         */
//                        scrollTo((int)distanceX,(int)distanceY);


                        return true;//这个返回值没有什么用,是在onTouchEvent方法中调用时的返回值。我们没有接收这个返回值使用
                    }
                });
 gestureDetector.onTouchEvent(event);//调用该类。

2、scrollBy方法和scrollTo方法的区别。
    具体的区别在上边的代码已经给出。两者都能实现移动某个View的效果。
3、view.dispatchTouchEvent(event)方法
   该方法的使用在下边的代码中会出现。这里讲解一下这个方法的作用。该方法作用就是将事件分发给调用它的方法的这个view对象。
4、event.setLocation(event.getX(), event.getY());方法
   该方法的具体使用在下边的代码中也有。它的主要作用是对当前的event对象中的x和y的值进行重新设定,当这个event重新分发给其它的view时,就会按照设定后的值进行处理相关的逻辑。设定后,event这个对象通过event.getX()等方法获取到的值就是经过重新设定后的值了。和原来的event相关的值就不同了。

三、结合代码总结使用的触摸事件分发的具体流程以及如何使用相关的方法

1、介绍一下Demo实现的效果

    该Demo自定义了一个View继承自VIewGroup。实现的效果是向自定义的View中添加三个ListView,每个ListView都有若干个条目,三个ListView平分屏幕的宽度,与屏幕等高。当分别滑动第一个和第三个ListView所在的区域时,第一个和第三个ListView和普通的ListView一样能够分别上下的滑动,当滑动第二个ListView的上半部分区域时第二个ListView也像普通的ListView一样能够上下滑动,但是滑动第二个ListView的下半部分区域时无论上下滑动(滑动的区域在第二个ListView的下半部分)三个ListView都会同时向上滑动或者向下滑动。

2、介绍实现给Demo效果的原理过程

   这个Demo很好的运用了事件分发的整个过程。这里实现的原理非常简单:所有的触摸事件都会中断向其子View处理,都由本View处理。然后对不同的区域将事件分发给对应的子View就行了。

3、直接上代码

com.example.mytoucheventtest.MyViewGroup:
package com.example.mytoucheventtest;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewGroup;

/**
 * 实现的思想:
 * 所有的触摸事件都由本View来处理,因此在onInterceptTouchEvent中默认返回true
 * 在onTouchEvent方法中在根据需求对事件进行进一步的分发。
 * 使用dispatchTouchEvent方法进行分发。
 * @author huangjian
 *
 */
public class MyViewGroup extends ViewGroup {
	public MyViewGroup(Context context) {
		super(context);
	}

	public MyViewGroup(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//		measureChildren(widthMeasureSpec, heightMeasureSpec);
//		for(int i = 0;i<getChildCount();i++){
//			getChildAt(i).measure(getWidth()/3, getHeight()/3);
//		}
		
	}
	@Override
	/**
	 * 指定位置
	 */
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		for(int i = 0;i < getChildCount(); i++){
			int width = getWidth()/3;
			getChildAt(i).layout(i * width, 0, (i+1) * width, getHeight());
		}
			
	}
	
	@Override
	/**
	 * 事件的分发。最先被调用的。
	 * 
	 */
	public boolean dispatchTouchEvent(MotionEvent ev) {
		System.out.println("dispatchTouchEvent::" + ev.getAction());
		return super.dispatchTouchEvent(ev);
	}
	@Override
	/**
	 * 中断事件的。
	 * 返回true 表示中断事件的传递。该事件不会向子view传递,由本view自己接收处理
	 * 返回false 表示不中断事件的传递,该事件还会向下一直传递。
	 * 
	 */
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		System.out.println("onInterceptTouchEvent::" + ev.getAction());
		return true;
	}
	
	@Override
	/**
	 * 触摸事件。
	 * 返回true表示这个事件被我们自己写的方法处理了。
	 * 返回false表示这个事件没有被我们写的方法处理。及时我们处理了事件也不会有效果
	 * 
	 */
	public boolean onTouchEvent(MotionEvent event) {
		super.onTouchEvent(event);
		System.out.println("onTouchEvent::" + event.getAction());
		System.out.println("=============================未调用setLocation前x坐标:" + event.getX());
		
		int width = getWidth() / getChildCount();
		int evX = (int) event.getX();
		//分三个区域进行处理
		if(evX < width){
			System.out.println("第一区域");
			//触摸的是第一个listview.直接将事件分发给第一个listview处理
			/**
			 * event.setLocation(width/2, event.getY());
			 * 自己经过测试的理解:
			 * setLocation方法是为了设置当前的触点的坐标,这个是当将事件分发给ListView时需要的,如果
			 * 事件还是由本View处理就不需要设置,因为坐标值没有变。这里设置的坐标是在ListView中的坐标值
			 * 如果我们不设置,这个坐标x值就直接是MyViewGroup中的坐标,但是ListView的x轴的最大值就是width
			 * 这样,我们触摸第一个ListView是不会出错的,在第一个ListView中可以不设置,
			 * 因为第一个ListView中触点x的值和在MyViewGroup中触摸时x的值是相同的
			 * 但是,第二个和第三个ListView就不行了。我们触摸第二或第三个ListView时在MyViewGroup中x的值肯定大于
			 * width的值,这样,触摸事件分发给ListView后,它的x值还是在MyViewGroup中的x值,超出了ListView的范围,
			 * ListView就处理不了了。
			 * 注意:调用event.setLocation设置新的坐标后,以后的再通过event获取的坐标都是设置的坐标了,不是原来的了。
			 */
			getChildAt(0).dispatchTouchEvent(event);
		}else if(evX >= width && evX < 2*width){
			System.out.println("第二区域");
			//触摸的是第二个listview
			int evY = (int) event.getY();
			//触摸第二个ListView,那么设置该坐标在ListView中的相对坐标时x都是event.getX() - width
			event.setLocation(event.getX() - width, event.getY());
			if(evY > getHeight()/2){
				//当触摸的是下半部分时。事件同时分发给三个ListView处理。
				for(int i = 0;i<getChildCount();i++){
					getChildAt(i).dispatchTouchEvent(event);
				}
			}else{
				//当触摸的是上半部分时。事件只分发给第二个ListView处理
				getChildAt(1).dispatchTouchEvent(event);
			}
			
		}else{
			System.out.println("第三区域");
			//触摸的是第三个listView直接将事件分发给第三个listview处理
			event.setLocation(event.getX() - 2 * width, event.getY());
			getChildAt(2).dispatchTouchEvent(event);
		}
		System.out.println("=====================width:" + width);
		System.out.println("调用setLocation后x坐标:" + event.getX());
		return true;
	}

}

com.example.mytoucheventtest.MainActivity:
package com.example.mytoucheventtest;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;

public class MainActivity extends Activity {

	private MyViewGroup myViewGroup;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		myViewGroup = (MyViewGroup) findViewById(R.id.myviewgroup);
		myViewGroup.requestDisallowInterceptTouchEvent(true);
		
		initData();
	}

	private void initData() {
		for(int i = 0;i<3;i++){
			//加入3个ListView
			ListView lv = new ListView(this);
			lv.requestDisallowInterceptTouchEvent(true);
			lv.setId(i);
			if(i == 0){
				lv.setAdapter(new MyListViewAdapter(this,R.drawable.a));
			}else if(i == 1){
				lv.setAdapter(new MyListViewAdapter(this,R.drawable.b));
			}else{
				lv.setAdapter(new MyListViewAdapter(this,R.drawable.c));
			}
			myViewGroup.addView(lv);
		}
	}
	

}

com.example.mytoucheventtest.MyListViewAdapter:
package com.example.mytoucheventtest;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;

public class MyListViewAdapter extends BaseAdapter{

	private Context context;
	private int resId;

	public MyListViewAdapter(Context context,int resId){
		this.context = context;
		this.resId = resId;
	}
	@Override
	public int getCount() {
		return 30;
	}

	@Override
	public Object getItem(int position) {
		return null;
	}

	@Override
	public long getItemId(int position) {
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		ViewHolder vh;
		if(convertView == null){
			vh = new ViewHolder();
			convertView = View.inflate(context, R.layout.item_listview, null);
			vh.iv = (ImageView) convertView.findViewById(R.id.iv);
			convertView.setTag(vh);
		}else{
			vh = (ViewHolder) convertView.getTag();
		}
		vh.iv.setBackgroundResource(resId);
		return convertView;
	}
	
	class ViewHolder{
		ImageView iv;
	}
	

}

activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <com.example.mytoucheventtest.MyViewGroup
        android:id="@+id/myviewgroup"
        android:padding="0dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

item_listview.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="fill_parent" >

    <ImageView
        android:id="@+id/iv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/a" />

</LinearLayout>

四、一个小知识。

   说明一个小知识。当我们自定义一个View时,我们重新了该View的onTouchEvent方法,同时我们在代码中使用了监听view.setOnTouchListener();。那么这两个方法是谁先执行呢?
   分析这个问题我们直接看View的源码。分析它的dispatchTouchEvent方法。代码如下:
 /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            <span style="color:#000099;">if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }</span>
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

结论:从源码中我们可以清楚的知道,分发事件时会检查该的view有没有设置监听,如果有设置监听就会直接将触摸事件交给设置监听的代码处理,如没有设置监听才会将事件交给onTouchEvent方法来处理。

如果需要项目源码的可以点击下边的链接免费下载:
点击打开链接

                
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值