自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

时间:2021-07-26 08:25:06

1. Touch事件的传递:

  图解Touch事件的传递,如下:

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

当我们点击子View 02内部的Button控件时候,我们就触发了Touch事件。

• 这个Touch事件首先传递给了*父View,于是这个*父View开始遍历自己的子view(父View 01 和 父View 02 是*父View的子View),

判断这个Touch点击事件是在 父View 01上面 还是在 父View 02上面,判断知道在父 View 02上面。

• 父View 02再次遍历自己的子View(子View 01 和 子View 02 是父View 02的子View),判断得知这个Touch点击事件是在子View 02上面。

• 子View 02判断再次遍历自己的子View,判断得知这个Touch点击事件是在Button上面。

2. 深入研究android的事件传递机制:

(1)View的dispatchTouchEvent 和 onTouchEvent:

     探讨Android事件传递机制前,明确android的两大基础控件类型:View和ViewGroup。View即普通的控件,没有子布局的,如Button、TextView. ViewGroup继承自View,表示可以有子控件,如Linearlayout、Listview这些。而事件即MotionEvent,最重要的有3个:

• MotionEvent.ACTION_DOWN      按下View,是所有事件的开始
• MotionEvent.ACTION_MOVE       滑动事件
• MotionEvent.ACTION_UP            与down对应,表示抬起
另外,明确事件传递机制的最终目的都是为了触发执行View的点击监听和触摸监听:
 ******.setOnClickListener(new View.OnClickListener() {

             @Override
public void onClick(View v) {
// TODO Auto-generated method stub
Log.i(tag, "testLinelayout---onClick...");
}
}); *******.setOnTouchListener(new View.OnTouchListener() { @Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub return false;
}
});

我们简称为onClick监听和onTouch监听,一般程序会注册这两个监听。从上面可以看到,onTouch监听里默认return false。不要小看了这个return false,后面可以看到它有大用。

(2)Android中的dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()

     dispatchTouchEvent:此方法一般用于处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。通常会调用super.dispatchTouchEvent(ev),事件向下分发。这样就会继续调用onInterceptTouchEvent,再由onInterceptTouchEvent决定事件的流向。

     onInterceptTouchEvent:它是ViewGroup提供的方法,默认返回false,返回true表示拦截。

                               如返回值为true,事件会传递到自己的onTouchEvent();

             如返回值为false,事件传递到下一个view的dispatchTouchEvent();

     onTouchEvent:它是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回true,表示消费了这个事件。               

             返回值为true,事件由自己处理消耗,后续动作序列让其处理;

                 返回值为false,自己不消耗事件了,向上返回让其他的父view的onTouchEvent接受处理;

  

     View里,有两个回调函数 :

 public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

     ViewGroup里,有三个回调函数 :

 public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

     在Activity里,有两个回调函数 :

 public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

      Android中默认情况下事件传递是由最终的view的接收到,传递过程是从父布局到子布局,也就是从Activity到ViewGroup到View的过程,默认情况,ViewGroup起到的是透传作用。Android中事件传递过程(按箭头方向)如下图,图片来自[qiushuiqifei],谢谢[qiushuiqifei]整理。

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

     触摸事件是一连串ACTION_DOWN,ACTION_MOVE..MOVE…MOVE、最后ACTION_UP,触摸事件还有ACTION_CANCEL事件。事件都是从ACTION_DOWN开始的,Activity的dispatchTouchEvent()首先接收到ACTION_DOWN,执行super.dispatchTouchEvent(ev),事件向下分发。

     dispatchTouchEvent()用于事件分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回值受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消费当前事件。

     dispatchTouchEvent()返回true,表示当前View(ViewGroup)消费这个事件,后续事件(ACTION_MOVE、ACTION_UP)会再传递;如果返回false,表示当前View(ViewGroup)不消费这个事件,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。

   dispatchTouchEvent()作用是将touch事件向下传递直到遇到被触发的目标view如果返回true表示当前view就是目标view,事件停止向下分发。否则返回false表示当前view不是目标view需要继续向下分发寻找目标view。这个方法也可以被重载,手动分配事件。

下面的几张图参考自[eoe]

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

   图1. ACTION_DOWN都没被消费

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

图2-1.ACTION_DOWN被View消费了

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

图2-2. 后续ACTION_MOVE和UP在不被拦截的情况下都会去找VIEW

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

                                  图3.后续的被拦截了
自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)
                              图4 ACTION_DOWN一开始就被拦截

android中的Touch事件都是从ACTION_DOWN开始的:

单手指操作:ACTION_DOWN---ACTION_MOVE----ACTION_UP

多手指操作:ACTION_DOWN---ACTION_POINTER_DOWN---ACTION_MOVE--ACTION_POINTER_UP---ACTION_UP.

(3)关于ViewGroup中requestDisallowInterceptTouchEvent的用法:

void  requestDisallowInterceptTouchEvent(boolean  disallowIntercept):

这个方法的入参一个boolean 变量,用来表示是否需要调用onInterceptTouchEvent来判断是否拦截

该标记如果为True,就如它的字面意思一样---不允许调用onInterceptTouchEvent(),结果就是,所有的父类方法都不会进行拦截,而把事件传递给子View. 该方法属于ViewGroup ,并且是个递归方法,也就是说一旦调用后,所有父类的disallowIntercept都会设置成True。即当前View的所有父类View,都不会调用自身的onInterceptTouchEvent()进行拦截。

该标记如果为False,就如它的字面意思一样---允许调用onInterceptTouchEvent(),结果就是,父类可以拦截事件。

requestDisallowInterceptTouchEvent 是ViewGroup类中的一个公用方法,参数是一个boolean值,官方介绍如下:

Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent).

This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.

android系统中,一次点击事件是从父view传递到子view中,每一层的view可以决定是否拦截并处理点击事件或者传递到下一层,如果子view不处理点击事件,则该事件会传递会父view,由父view去决定是否处理该点击事件。在子view可以通过设置此方法去告诉父view不要拦截并处理点击事件,父view应该接受这个请求直到此次点击事件结束。

实际的应用中,可以在子view的ontouch事件中注入父ViewGroup的实例,并调用requestDisallowInterceptTouchEvent去阻止父view拦截点击事件

 public boolean onTouch(View v, MotionEvent event) {
ViewGroup viewGroup = (ViewGroup) v.getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
viewGroup.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
viewGroup .requestDisallowInterceptTouchEvent(false);
break;
}
}

3. 通过一个" 瀑布流 "例解析Touch事件的传递机制:

(1)工程一览图,如下:

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

(2)首先我们看看我们的主布局文件activity_main.xml,如下:

 <com.example.pinterestlistview.MyLinearLayout 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"
android:id="@+id/mll"
tools:context=".MainActivity" > <ListView
android:id="@+id/lv2"
android:scrollbars="none"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" /> <ListView
android:id="@+id/lv1"
android:scrollbars="none"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" /> <ListView
android:id="@+id/lv3"
android:scrollbars="none"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" /> </com.example.pinterestlistview.MyLinearLayout>

其中每一个ListView的Item子项目的布局文件lv_item.xml,如下:

 <ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/default1" />

这个lv_item.xml布局效果如下:

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

(3)自定义ViewGroup---MyLinearLayout,如下:

 package com.example.pinterestlistview;

 import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout; public class MyLinearLayout extends LinearLayout { public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
} @Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
} /*
* 事件传递机制:
* 1. view执行dispatchTouchEvent方法,开始分发事件 ;
* 2. 执行onInterceptTouchEvent 判断是否是中断事件 ;
* 3. 执行onTouchEvent方法,去处理事件
*
*/ /**
* 分发事件的方法,最早执行
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
} @Override
public boolean onTouchEvent(MotionEvent event) { //这里getChildCount()是3,那么width是屏幕宽度的1/3
int width=getWidth()/getChildCount();
int height = getHeight();
//count=getChildCount()=3
int count=getChildCount(); float eventX = event.getX(); if (eventX<width){ // 滑动左边的 listView
event.setLocation(width/2, event.getY());//告诉左边的listView,事件触发点在左边的listview的x坐标的一半处(y坐标随意滑动)
float eventY = event.getY();
if (eventY < height / 2) {
event.setLocation(width / 2, event.getY());
getChildAt(0).dispatchTouchEvent(event);
getChildAt(2).dispatchTouchEvent(event); System.out.println("左边的listview的上半部分:"+eventY);
return true;
} else if (eventY > height / 2) {
event.setLocation(width / 2, event.getY());
try {
getChildAt(0).dispatchTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
} System.out.println("左边的listview的下半部分:"+eventY);
return true;
}
return true; } else if (eventX > width && eventX < 2 * width) { //滑动中间的 listView
float eventY = event.getY();
if (eventY < height / 2) {//滑动中间listView上半部分(0 < eventY < height /2)
event.setLocation(width / 2, event.getY());//告诉中间的listView,事件触发点在中间listview的x坐标的一半处(y坐标随意滑动)
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
try {
child.dispatchTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("中间listview的上半部分:"+eventY);
return true;
} else if (eventY > height / 2) {//滑动中间listView下半部分(height / 2 < eventY < height)
event.setLocation(width / 2, event.getY());//告诉中间的listView,事件触发点在中间listview的x坐标的一半处(y坐标随意滑动)
try {
getChildAt(1).dispatchTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("中间listview的下半部分:"+eventY);
return true; }
}else if (eventX>2*width){// 滑动右边的 listView
//event.setLocation(width/2, event.getY());//告诉右边的listView,事件触发点在右边listview的x坐标的一半处(y坐标随意滑动)
//getChildAt(2).dispatchTouchEvent(event);
float eventY = event.getY();
if (eventY < height / 2) {
event.setLocation(width / 2, event.getY());
getChildAt(0).dispatchTouchEvent(event);
getChildAt(2).dispatchTouchEvent(event); System.out.println("右边listview的上半部分:"+eventY);
return true;
} else if (eventY > height / 2) {
event.setLocation(width / 2, event.getY());
try {
getChildAt(2).dispatchTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
} System.out.println("右边listview的下半部分:"+eventY);
return true;
} return true;
} return true;
} }

(4)回到了MainActivity.java,如下:

 package com.example.pinterestlistview;

 import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView; public class MainActivity extends Activity { private ListView lv1;
private ListView lv2;
private ListView lv3; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); lv1 = (ListView) findViewById(R.id.lv1);
lv2 = (ListView) findViewById(R.id.lv2);
lv3 = (ListView) findViewById(R.id.lv3); try {
lv1.setAdapter(new MyAdapter1());
lv2.setAdapter(new MyAdapter1());
lv3.setAdapter(new MyAdapter1());
} catch (Exception e) {
e.printStackTrace();
} } private int ids[] = new int[] { R.drawable.default1, R.drawable.girl1,
R.drawable.girl2, R.drawable.girl3 }; class MyAdapter1 extends BaseAdapter { @Override
public int getCount() {
return 3000;
} @Override
public Object getItem(int position) {
return position;
} @Override
public long getItemId(int position) {
return 0;
} @Override
public View getView(int position, View convertView, ViewGroup parent) { ImageView iv = (ImageView) View.inflate(getApplicationContext(),
R.layout.lv_item, null);
int resId = (int) (Math.random() * 4);
iv.setImageResource(ids[resId]);
return iv;
}
}
}

(5)布署项目到模拟器上,效果如下:

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)

动态gif效果图,如下:

自定义控件(视图)2期笔记10:自定义视图之View事件分发机制("瀑布流"的案例)