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

1. Touch事件的传递:

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

 

当我们点击子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的点击监听和触摸监听:
 1 ******.setOnClickListener(new View.OnClickListener() {
 2             
 3             @Override
 4             public void onClick(View v) {
 5                 // TODO Auto-generated method stub
 6                 Log.i(tag, "testLinelayout---onClick...");
 7             }
 8         });
 9 
10         *******.setOnTouchListener(new View.OnTouchListener() {
11             
12             @Override
13             public boolean onTouch(View v, MotionEvent event) {
14                 // TODO Auto-generated method stub
15     
16                 return false;
17             }
18         });

我们简称为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里,有两个回调函数 :

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

     ViewGroup里,有三个回调函数 :

1 public boolean dispatchTouchEvent(MotionEvent ev);  
2 public boolean onInterceptTouchEvent(MotionEvent ev);  
3 public boolean onTouchEvent(MotionEvent ev);

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

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

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

 

     触摸事件是一连串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]

 

                               图1. ACTION_DOWN都没被消费

 

 

                              图2-1.ACTION_DOWN被View消费了

  

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

 
                                  图3.后续的被拦截了
 
 
                              图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拦截点击事件

 1 public boolean onTouch(View v, MotionEvent event) {
 2      ViewGroup viewGroup = (ViewGroup) v.getParent();
 3      switch (event.getAction()) {
 4      case MotionEvent.ACTION_MOVE:
 5          viewGroup.requestDisallowInterceptTouchEvent(true);
 6          break;
 7      case MotionEvent.ACTION_UP:
 8      case MotionEvent.ACTION_CANCEL:
 9          viewGroup .requestDisallowInterceptTouchEvent(false);
10          break;
11      }
12 }

 

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

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

 

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

 1 <com.example.pinterestlistview.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:id="@+id/mll"
 6     tools:context=".MainActivity" >
 7 
 8     <ListView
 9         android:id="@+id/lv2"
10         android:scrollbars="none"
11         android:layout_width="0dp"
12         android:layout_height="wrap_content"
13         android:layout_weight="1" />
14 
15     <ListView
16         android:id="@+id/lv1"
17         android:scrollbars="none"
18         android:layout_width="0dp"
19         android:layout_height="wrap_content"
20         android:layout_weight="1" />
21 
22     <ListView
23         android:id="@+id/lv3"
24         android:scrollbars="none"
25         android:layout_width="0dp"
26         android:layout_height="wrap_content"
27         android:layout_weight="1" />
28 
29 </com.example.pinterestlistview.MyLinearLayout>

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

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

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

 

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

  1 package com.example.pinterestlistview;
  2 
  3 import android.content.Context;
  4 import android.util.AttributeSet;
  5 import android.view.MotionEvent;
  6 import android.view.View;
  7 import android.widget.LinearLayout;
  8 
  9 public class MyLinearLayout extends LinearLayout {
 10 
 11 
 12     public MyLinearLayout(Context context, AttributeSet attrs) {
 13         super(context, attrs);
 14     }
 15 
 16     @Override
 17     public boolean onInterceptTouchEvent(MotionEvent ev) {
 18         return true;
 19     }
 20 
 21     /*
 22      * 事件传递机制:
 23      *  1. view执行dispatchTouchEvent方法,开始分发事件 ;    
 24      *  2. 执行onInterceptTouchEvent 判断是否是中断事件 ;
 25      *  3. 执行onTouchEvent方法,去处理事件
 26      * 
 27      */
 28     
 29     
 30     /**
 31      * 分发事件的方法,最早执行
 32      */
 33     @Override
 34     public boolean dispatchTouchEvent(MotionEvent ev) {
 35         return super.dispatchTouchEvent(ev);
 36     }
 37     
 38     @Override
 39     public boolean onTouchEvent(MotionEvent event) {
 40         
 41         //这里getChildCount()是3,那么width是屏幕宽度的1/3
 42         int width=getWidth()/getChildCount();
 43         int height = getHeight();
 44         //count=getChildCount()=3
 45         int count=getChildCount();
 46         
 47         float eventX = event.getX();
 48         
 49         if (eventX<width){    // 滑动左边的 listView
 50             event.setLocation(width/2, event.getY());//告诉左边的listView,事件触发点在左边的listview的x坐标的一半处(y坐标随意滑动)
 51             float eventY = event.getY();
 52             if (eventY < height / 2) {
 53                 event.setLocation(width / 2, event.getY());
 54                 getChildAt(0).dispatchTouchEvent(event);
 55                 getChildAt(2).dispatchTouchEvent(event);
 56                 
 57                 System.out.println("左边的listview的上半部分:"+eventY);
 58                 return true;
 59             } else if (eventY > height / 2) {
 60                 event.setLocation(width / 2, event.getY());
 61                 try {
 62                     getChildAt(0).dispatchTouchEvent(event);
 63                 } catch (Exception e) {
 64                     e.printStackTrace();
 65                 }
 66                 
 67                 System.out.println("左边的listview的下半部分:"+eventY);
 68                 return true;
 69             }
 70             return true;
 71             
 72         } else if (eventX > width && eventX < 2 * width) { //滑动中间的 listView  
 73             float eventY = event.getY();
 74             if (eventY < height / 2) {//滑动中间listView上半部分(0 < eventY < height /2)
 75                 event.setLocation(width / 2, event.getY());//告诉中间的listView,事件触发点在中间listview的x坐标的一半处(y坐标随意滑动)
 76                 for (int i = 0; i < count; i++) {
 77                     View child = getChildAt(i);
 78                     try {
 79                         child.dispatchTouchEvent(event);
 80                     } catch (Exception e) {
 81                         e.printStackTrace();
 82                     }    
 83                 }
 84                 System.out.println("中间listview的上半部分:"+eventY);
 85                 return true;
 86             } else if (eventY > height / 2) {//滑动中间listView下半部分(height / 2 < eventY < height)
 87                 event.setLocation(width / 2, event.getY());//告诉中间的listView,事件触发点在中间listview的x坐标的一半处(y坐标随意滑动)
 88                 try {
 89                     getChildAt(1).dispatchTouchEvent(event);
 90                 } catch (Exception e) {
 91                     e.printStackTrace();
 92                 }
 93                 System.out.println("中间listview的下半部分:"+eventY);
 94                 return true;
 95                 
 96             }
 97         }else if (eventX>2*width){// 滑动右边的 listView
 98             //event.setLocation(width/2, event.getY());//告诉右边的listView,事件触发点在右边listview的x坐标的一半处(y坐标随意滑动)
 99             //getChildAt(2).dispatchTouchEvent(event);
100             float eventY = event.getY();
101             if (eventY < height / 2) {
102                 event.setLocation(width / 2, event.getY());
103                 getChildAt(0).dispatchTouchEvent(event);
104                 getChildAt(2).dispatchTouchEvent(event);
105                 
106                 System.out.println("右边listview的上半部分:"+eventY);
107                 return true;
108             } else if (eventY > height / 2) {
109                 event.setLocation(width / 2, event.getY());
110                 try {
111                     getChildAt(2).dispatchTouchEvent(event);
112                 } catch (Exception e) {
113                     e.printStackTrace();
114                 }
115                 
116                 System.out.println("右边listview的下半部分:"+eventY);
117                 return true;
118             }
119             
120             
121             return true;
122         }
123         
124         return true;
125     }
126     
127 }

 

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

 1 package com.example.pinterestlistview;
 2 
 3 import android.app.Activity;
 4 import android.os.Bundle;
 5 import android.view.View;
 6 import android.view.ViewGroup;
 7 import android.widget.BaseAdapter;
 8 import android.widget.ImageView;
 9 import android.widget.ListView;
10 
11 public class MainActivity extends Activity {
12 
13     private ListView lv1;
14     private ListView lv2;
15     private ListView lv3;
16 
17     @Override
18     protected void onCreate(Bundle savedInstanceState) {
19         super.onCreate(savedInstanceState);
20         setContentView(R.layout.activity_main);
21 
22         lv1 = (ListView) findViewById(R.id.lv1);
23         lv2 = (ListView) findViewById(R.id.lv2);
24         lv3 = (ListView) findViewById(R.id.lv3);
25 
26         try {
27             lv1.setAdapter(new MyAdapter1());
28             lv2.setAdapter(new MyAdapter1());
29             lv3.setAdapter(new MyAdapter1());
30         } catch (Exception e) {
31             e.printStackTrace();
32         }
33 
34     }
35 
36     private int ids[] = new int[] { R.drawable.default1, R.drawable.girl1,
37             R.drawable.girl2, R.drawable.girl3 };
38 
39     class MyAdapter1 extends BaseAdapter {
40 
41         @Override
42         public int getCount() {
43             return 3000;
44         }
45 
46         @Override
47         public Object getItem(int position) {
48             return position;
49         }
50 
51         @Override
52         public long getItemId(int position) {
53             return 0;
54         }
55 
56         @Override
57         public View getView(int position, View convertView, ViewGroup parent) {
58 
59             ImageView iv = (ImageView) View.inflate(getApplicationContext(),
60                     R.layout.lv_item, null);
61             int resId = (int) (Math.random() * 4);
62             iv.setImageResource(ids[resId]);
63             return iv;
64         }
65     }
66 }

 

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

 

 

动态gif效果图,如下:

posted on 2015-10-04 16:57  鸿钧老祖  阅读(318)  评论(0编辑  收藏  举报

导航