最近一个项目需要给应用初始界面上的动态按钮添加在不同状态的变换效果,如点击(俗一点也可称为按压)后实现背景图的 更换或者图标的缩放等效果。由于按钮点击的时间有长有短,所以采用OnTouchListener监听器对点击事件进行监听,并利用对应的onTouch(View v, MotionEvent event)方法来实现按钮图标的变换效果(背景图更换或者图标缩放)。 但是项目中除了利用Touch事件来处理按钮基本的变换外,还需要响应LongClick或者Click事件来为用户做进一步的响应,即Touch和Click事件分别完成不同的任务。
那么问题来了,表面上看Touch、LongClick及Click这三个事件的关系很普通(均可由用户点击组件触发),在一般的应用中对它们中的个别进行监听也不太会遇到什么奇怪的现象。但是深入研究与测试之后,会发现当它们一起用的时候,有太多地方需要注意,否则很容易出错。下面就来看看有哪些平时不太注意却可能出现意外的地方。
读书期间一直用VC++,现在转为Android,个人觉得在对于点击事件的监听即响应的实现这个点上很多语言间非常相似,即解决问题的思路是相通的。
本文的测试案例是用Android进行实现,先看下面两段代码。
1 <LinearLayout 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:gravity="center" 6 tools:context="com.example.eventtest.MainActivity" > 7 8 <ImageView android:id="@+id/imageView" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:src="@drawable/ic_launcher" /> 12 13 </LinearLayout>
由于只是观察Touch、LongClick及Click三者之间对点击事件的响应关系,所以整个界面布局中仅仅放置了一个ImageView组件。
1 package com.example.eventtest; 2 3 import android.support.v7.app.ActionBarActivity; 4 import android.os.Bundle; 5 import android.util.Log; 6 import android.view.Menu; 7 import android.view.MenuItem; 8 import android.view.MotionEvent; 9 import android.view.View; 10 import android.widget.ImageView; 11 12 13 public class MainActivity extends ActionBarActivity { 14 15 private ImageView imageView = null; 16 private String TAG = "MainActivity"; 17 @Override 18 protected void onCreate(Bundle savedInstanceState) { 19 super.onCreate(savedInstanceState); 20 setContentView(R.layout.activity_main); 21 22 imageView = (ImageView)findViewById(R.id.imageView); 23 imageView.setOnTouchListener(mOnTouchListener); 24 imageView.setOnLongClickListener(mOnLongClickListener); 25 imageView.setOnClickListener(mOnClickListener); 26 } 27 28 View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { 29 30 @Override 31 public boolean onTouch(View arg0, MotionEvent event) { 32 // TODO Auto-generated method stub 33 if(event.getAction() == MotionEvent.ACTION_DOWN){ 34 Log.d(TAG, "touch down"); 35 return false; //1 FALSE 36 } 37 else if(event.getAction() == MotionEvent.touch move){ 38 Log.d(TAG, "touch move"); 39 return false; //2 FALSE 40 } 41 else if(event.getAction() == MotionEvent.ACTION_UP){ 42 Log.d(TAG, "touch up"); 43 return false; //3 FALSE 44 } 45 return false; 46 } 47 }; 48 49 50 View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() { 51 52 @Override 53 public boolean onLongClick(View arg0) { 54 // TODO Auto-generated method stub 55 Log.d(TAG, "long click"); 56 return false; //4 FALSE 57 } 58 }; 59 60 View.OnClickListener mOnClickListener = new View.OnClickListener() { 61 62 @Override 63 public void onClick(View arg0) { 64 // TODO Auto-generated method stub 65 Log.d(TAG, "click"); //5 NULL 66 } 67 }; 68 69 @Override 70 public boolean onCreateOptionsMenu(Menu menu) { 71 // Inflate the menu; this adds items to the action bar if it is present. 72 getMenuInflater().inflate(R.menu.main, menu); 73 return true; 74 } 75 76 @Override 77 public boolean onOptionsItemSelected(MenuItem item) { 78 // Handle action bar item clicks here. The action bar will 79 // automatically handle clicks on the Home/Up button, so long 80 // as you specify a parent activity in AndroidManifest.xml. 81 int id = item.getItemId(); 82 if (id == R.id.action_settings) { 83 return true; 84 } 85 return super.onOptionsItemSelected(item); 86 } 87 }
从上面给出的Java代码可以看出:
1、分别为点击ImageView组件后的Touch、LongClick及Click事件设置了OnTouchListener、OnLongClickListener及OnClickListener监听器,并且在对应的响应方法onTouch()、onLongClick()及onClick()中进行了日志输出函数的调用与返回值的设定。其中返回值的设定是本文的关键,某个响应方法返回的是true还是false,对于后续的响应方法的影响非常大,后面会慢慢地进行分析。
2、为了观察组件点击后的一系列响应情况,利用Log类的d(Tag, msg)方法来输出运行日志,由于系统本身在应用运行时会输出很多我们不必要查看的信息,所以最好在logcat中设置一个日志信息输出过滤器,名称要和程序中的TAG字串相同。如此处的TAG为主类名字串“MainActicity”,那么Filter的名称也要取为“MainActicity”,设置完之后保存,然后在logcat中选择debug选项(上述的d()方法对应debug,e()对应error(),还有几个类别感兴趣的朋友可以自己研究),那窗口中就会 只 输出我们设定的打印信息了。如下图中的输出结果,看着简单、舒服。
3、说明,java代码行35、39、43、56及65的注释是方法返回值的说明,由于onClick()方法无返回值,所以用NULL表示。在测试中会对各种方法的返回值进行改变,形成不同的组合,然后通过输出的日志信息观察它们的响应情况。由于Android中对点击事件的响应顺序为touch down-touch move-long click-touch up-touch move-click(当然,不是每次点击均会产生所有的事件,这只是完整的流程描述),所以描述返回值组合时也是按照这个顺序。如上述代码中方法的返回值组合为false-false-false-false-null,而且对组件的短按、长按、移开(短按+移开或长按+移开,算两种不同的状态)(移开表示最终没有触发Click事件,但有可能触发LongClick事件)这四种点击状态都进行了测试,对于是否执行点击状态用yes/no表示,那么上面代码运行后组件的长按+移开的完整组合(返回值和操作标记)就是false-false-false-false-null-no-yes-yes。
说了这么多,所谓有图有真相,不给出结果怎么说得清呢。如果表述有不恰当或者内容有缺陷的地方还希望朋友您能够指出,谢谢!
按照上面的返回值组合(各方法均返回false),四种点击状态的打印日志信息如下:
1、短按(组合:false-false-false-false-null-yes-no-no)
短按其实专业点说是手指(或者触笔,有些手机和平板会配备)在应用界面的组件上轻触,结果为touch down-touch up-click,有时候输出结果会是touch down-touch move-touch up-click(产生touch move事件但还至于到long click事件)。
2、长按(组合:false-false-false-false-null- no - yes -no)
这里就产生了long click事件,输出为结果为touch down-touch move-long click-touch move-touch up-click,其实touch move事件的产生与否、个数和手指按压与抬起的速度有关,不必深究(后面还会提到不必深究的原因)。
3、短按移开(组合:false-false-false-false-null-yes-no-yes)
由于短按不会产生long click事件,而按压组件并最终移开后不会产生click事件,所以结果为touch down-touch move-touch up。
4、长按移开(组合:false-false-false-false-null-no-yes-yes)
到这里,就不要多解释了,日志输出结果为touch down-touch move-long click-touch move-touch up。
上面的测试结果相信大部分人都预想到了,是不是觉得挺简单的呢?
但是得注意一个细节:上述测试结果是在所有事件响应方法的返回值均为false(onClick()除外,为void,后面会用null表示)的情况下得到的。 这意味着什么,如果各方法的返回值不全是false又会怎么样呢?下面先从全局层面来简单解释一下Android对于点击事件的响应流程吧。
之前提到过,点击事件的一般响应流程为touch down-touch move-long click-touch move-touch up-click。这是整体响应情况的描述,前提就是像上面程序中设置的那样——返回值均为false。从顺序的角度出发,如果touch down事件对应的模块在执行完自身的实现后返回false,即它还想把当前的点击状态继续向上(上层类,一般为父类)传递,而不是扼杀在自己的摇篮里;按住一段时间后,touch move事件触发;随后long click事件产生,返回false,继续交给上层处理;这中间还会有touch move事件产生;当手指抬起时(不是移开),touch up事件触发,返回false;最终到了click事件,本次点击事件结束。
还有,组件按压并移开,是不会触发click事件的,如果按压时间够久,会有long ckick事件发生。
好了,简单的测试与描述就到这里。下面该挑战复杂的返回值组合了,希望大家还保持着清醒的头脑,之后的描述不会那么啰嗦,组合值及输出日志将以表格形式进行简洁明了的展现。代码中进行的相应改变也不再给出,感兴趣的朋友自己实现一下吧。
1、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
FALSE | BOTH | FALSE | FALSE | NULL | YES | NO | NO | down-move-up-click |
FALSE | BOTH | FALSE | FALSE | NULL | NO | YES | NO | down-move...-long click-move..-up-click |
FALSE | BOTH | FALSE | FALSE | NULL | YES | NO | YES | down-move...-up |
FALSE | BOTH | FALSE | FALSE | NULL | NO | YES | YES | down-move...-long click-move...-up |
上面描述过的测试状态的返回值组合和输出日志信息也重新以表格形式给出,touch down简化为down,其他类似,move...表示多个touch move事件的输出信息。
2、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
FALSE | BOTH | FALSE | TRUE | NULL | YES | NO | NO | down-move-up-click |
FALSE | BOTH | FALSE | TRUE | NULL | NO | YES | NO | down-move...-long click-move..-up |
FALSE | BOTH | FALSE | TRUE | NULL | YES | NO | YES | down-move...-up |
FALSE | BOTH | FALSE | TRUE | NULL | NO | YES | YES | down-move...-long click-move...-up |
发现不同之处了吗?将onLongClick()方法返回值设置为true,相应地,组件长按并抬起后,click事件并没有触发。即点击事件到touch up事件 后 就不在往上传递 触发消息 了。到此,可以解释一下touch move事件了,可以看到将其对应的响应模块的返回值设置为true或者false,对执行结果不会产生任何影响,它只需要完成自己的任务就好,其他的模块和它并没有多大的关系。注意,表格中标红的部分是以第1种测试状态的组合和输出做为基础的,下同。
3、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
TRUE | BOTH | FALSE | FALSE | NULL | YES | NO | NO | down-move-up |
TRUE | BOTH | FALSE | FALSE | NULL | NO | YES | NO | down-move...-up |
TRUE | BOTH | FALSE | FALSE | NULL | YES | NO | YES | down-move...-up |
TRUE | BOTH | FALSE | FALSE | NULL | NO | YES | YES | down-move...-up |
将touch down事件的返回值设置为true,long click和click事件居然都没有触发,结合点击事件的响应流程就容易理解了。即touch down返回true之后,表示不再需要上层类的协助(这里指long click和click事件)。没有特殊的情况下,touch down-move-up是组件touch事件的一个完整流程。当然,后面会出现特殊的情况。
4、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
FALSE | BOTH | TRUE | BOTH | NULL | YES | NO | NO | down-move-up-long click |
FALSE | BOTH | TRUE | BOTH | NULL | NO | YES | NO | down-move...-long lick-move...-up |
FALSE | BOTH | TRUE | BOTH | NULL | YES | NO | YES | down-move...-up |
FALSE | BOTH | TRUE | BOTH | NULL | NO | YES | YES | down-move...-long lick-move...-up |
将touch up事件的返回值设置为true, 组件轻触并抬起时, 出现了奇怪的现象: 触发了long click事件,并且 long click事件处理完之后,不管返回值是false还是true,均没有再出现touch move和touch up事件。
其实,结合点击事件的流程,在touch up事件返回true之后,click事件不会触发很容易理解,但为什么还会产生long click呢?这是目前没有搞懂的一个疑问!!!
5、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
FALSE | BOTH | BOTH | 无 | 无 | YES | NO | NO | down |
FALSE | BOTH | BOTH | 无 | 无 | NO | YES | NO | down |
FALSE | BOTH | BOTH | 无 | 无 | YES | NO | YES | down |
FALSE | BOTH | BOTH | 无 | 无 | NO | YES | YES | down |
这里主要是对组件ImageView监听器的设置进行了改变,“无”表示没有设置对应的监听器,即将java代码的24、25行注释掉。从响应结果可以看出,在touch down事件返回false时, 会希望 上层类的long click或者click事件来处理,而此时组件 只设置了OnTouchListener监听器,所以会一直等待,永远不会触发touch move和touch up事件了。
6、
touch down | touch move | touch up | long click | click | 轻触 | 长按 | 按住移出 | 响应结果 |
TRUE | BOTH | BOTH | 无 | 无 | YES | NO | NO | down-move-up |
TRUE | BOTH | BOTH | 无 | 无 | NO | YES | NO | down-move...-up |
TRUE | BOTH | BOTH | 无 | 无 | YES | NO | YES | down-move...-up |
TRUE | BOTH | BOTH | 无 | 无 | NO | YES | YES | down-move...-up |
有了以上的基础,这种情况就很好理解了其实和第3种测试状态是类似的(甚至相同)。在touch down事件返回true后,不管有没有设置long click和click事件的监听器,都不会再触发了,而touch move-up事件正常了。
本篇文章通过对Touch、Long Click及Click事件的响应方法设置各种不同返回值组合,测试组件点击状态的响应情况。对于带问好的标题,可以确定的是:点击组件后产生什么事件,做怎样的实现,是由编程者进行控制。
遗留问题,即上述测试第4种情况:轻触组件,在touch up事件返回true之后,为什么会触发long click?触发之后,为什么没有touch move和touch up事件的产生?希望知道缘由的朋友指点,欢迎大家一起讨论,学习!