在上篇文章中已经了解到界面Activity
的绘制完全依赖其加载的视图组件View
,不仅如此,用户的每次触摸操作都可以在界面Activity
内接收并响应,也可以直接传递给其中的某个视图View
响应。那么对于用户的操作,应该如何响应,而同一个操作到底是作用于界面,还是界面中的某一个子视图?针对用户的操作对象所产生的交互方式不同,本文将分别展开介绍。
界面内交互
界面响应
说到界面交互,很容易想到用户在设备屏幕上的触摸操作。可是屏幕那么大要怎么确定用户触摸的位置呢?Android系统定义了一套屏幕坐标规则,该规则不仅适用于当前的屏幕交互,在后文提及的动画绘制及其他屏幕相关操作等都同样适用。该规则将屏幕的左上角作为屏幕坐标的原点,从左上角往右上角延伸的方向作为屏幕坐标的x轴,从左上角往左下角延伸的方向作为屏幕坐标的y轴。
比如针对一款 1024x512 尺寸的TV设备,其左下角的屏幕坐标值为 (0, 512),右下角的屏幕坐标值为 (1024, 512),右上角的屏幕坐标值为 (1024, 0),左上角的屏幕坐标值为 (0, 0)。
对屏幕的触摸位置有了衡量标准,是不是就可以根据不同的位置做触摸操作了呢?说到触摸操作,也需要细化之后单独处理。Android系统将用户操作行为,大致分为三种:按下行为,滑动行为,抬起释放行为。这样系统就可以根据每一个操作行为做单独的响应处理了。
另外,用户的操作对象,除了上文提到的硬件设备屏幕以外,还有硬件设备的按键(包括硬件按键和虚拟按键)。只不过对按键的操作行为只有按下行为和抬起释放行为两种,而且按键的操作不需要用到屏幕坐标相关内容。
基于上文的介绍,可以在界面Activity
中可以分别重写下边三个方法对用户的界面操作交互做出响应。
-
boolean onTouchEvent(MotionEvent event)
在子视图没有处理的情况下,用户对硬件设备屏幕的每一个操作,都会回调一次该方法。
其参数android.view.MotionEvent事件类的实例化对象event。
event.getAction()
方法可以获取当前事件行为,包括MotionEvent.ACTION_DOWN
按下行为、MotionEvent.ACTION_MOVE
滑动行为、MotionEvent.ACTION_UP
抬起释放行为等。
event.getX()
方法获取当前操作的屏幕坐标x轴值。
同理event.getY()
方法获取当前操作的屏幕坐标y轴值。 -
boolean onKeyDown(int keyCode, KeyEvent event)
在子视图没有处理的情况下,用户对硬件设备按键的每一次按下行为,都会回调一次该方法。
参数一int类型的keyCode指定按键类型,一般其值与参数二event.getKeyCode()
相等。
参数二android.view.KeyEvent类的实例化对象event。
event.getAction()
方法同样可以获取当前事件行为,只有KeyEvent.ACTION_DOWN
按下行为和KeyEvent.ACTION_UP
抬起释放行为两个行为值。
event.getKeyCode()
方法可以获取触发当前事件的按键类型,其值包括KeyEvent.KEYCODE_HOME
HOME键,KeyEvent.KEYCODE_POWER
电源键,KEYCODE_VOLUME_UP
音量增加键等。 -
boolean onKeyUp(int keyCode, KeyEvent event)
在子视图没有处理的情况下,用户对硬件设备按键的每一次抬起释放行为,都会回调一次该方法。其两个参数与上述onKeyDown()
中的两个参数类似。
视图响应
相对来说,界面内的视图响应要繁琐一些,而能实现的效果也更多样化。当把视图View
作为用户的操作对象时,仍然可以重写上述界面响应的三个方法,但是系统视图往往也封装了一层更加简单粗暴的响应方法。
在视图中重写界面响应的三个方法后,如果返回的结果为true,则上文界面响应中的三个方法将不会被回调。
对于重写界面响应中的三个方法的方式,有两种代码实现方式。一是针对已定义的视图View
类,通过调用类似 setOnTouchListener (View.OnTouchListener l)
等监听方法,在监听类中实现相关响应方法即可。或者将已定义的视图View
作为父类,重新创建一个自定义视图View
,在新的自定义View
中可以直接重写相关响应方法,而在声明使用原视图View
类的地方,修改为使用新的自定义View
类。通常针对声明为final
的类只能使用方式一实现。
在
View.OnTouchListener
类中实现的onTouch(View v, MotionEvent event)
方法,会优先于视图View
内部的onTouchEvent(MotionEvent event)
被调用,因此如果onTouch(View v, MotionEvent event)
返回结果为true,将不会回调视图View
内部的onTouchEvent(MotionEvent event)
,而且该视图View
内部的其他响应交互类监听(如setOnClickListener(View.OnClickListener l)、setOnLongClickListener (View.OnLongClickListener l)等)也不会被调用。
为什么需要封装一层响应方法呢?用户对视图的操作,往往就是点击(短时间内执行按下行为和抬起释放行为),长按(在执行按下行为后等待一段时间再执行抬起释放行为),拖拽(在执行按下行为后执行一段滑动行为之后再执行抬起释放行为)这些固定操作类型。如果每个视图都要细分用户的操作行为,就会有大量冗余的操作类型判断代码,所以AndroidSDK定义了一系列接口分别对应用户的操作类型。视图如果需要响应某个操作,只需要设置其操作类型接口的实例化对象,并在该对象中实现相关方法即可。而这些接口主要有以下三个。
- View.OnClickListener接口
需要实现onClick(View view)
方法,在该方法内响应响应视图View
被用户点击后的代码逻辑。 - View.OnLongClickListener接口
需要实现onLongClick(View view)
方法,在该方法内响应响应视图View
被用户长按后的代码逻辑。 - View.OnDragListener接口
需要实现onDrag(View v, DragEvent event)
方法,在该方法内响应视图View
被用户拖拽后的代码逻辑。
另外,不同的系统视图也可能有单独设置的响应方法,或者自定义视图也会提供单独的响应方法,例如列表视图中的某一行数据被单独点击后如何响应,这些都要根据具体的视图类查找并使用对应的响应方法,这里不再赘述。
事件传递机制
在上文界面响应的三个方法中,关于他们被回调的时机,有个前提是子视图没有处理,即子视图的界面响应方法返回结果为false。这就涉及到Android系统的事件传递机制了。
我们知道界面Activity
在创建之后会调用setContentView(int layoutId)
加载根视图View
,而根视图里边则可以内嵌一层层的子视图。那么,如果用户将手指触摸到屏幕上,会触发按下行为,该行为作为事件首先传递到根视图中,之后根视图再将该事件传递给子视图,子视图再将该事件传递给子视图的子视图,这样按照加载时的嵌套顺序一层层传递事件,称之为事件分发。
直到该事件传递到最后一层子视图,或者某一层视图不再继续传递该事件,那么该事件将在最后传递到的这层视图中被首先处理。而每层视图在收到传递进来的事件后,都有两条路可以选择,要么将该事件继续传递给子视图,要么自己处理该事件,如果选择第二条路不再继续传递子视图而是自己处理该事件,称之为事件拦截。
一旦某层视图处理了该事件,那么其父层视图将继续处理该事件,之后是父层的父层视图处理该事件,事件被这样一层层处理,直到根视图处理该事件结束,称之为事件处理。
在经历了事件分发和事件处理之后,这样的一个事件传递机制就算完成了。而上文提到的每一个事件,都是如此。
上述过程在代码中的实现,只需要针对事件分发、事件拦截和事件处理分别定义一个可重写的方法即可。能够重写该方法的位置主要是android.app.Acitivty
和android.view.View
中,由于事件拦截只会发生在子视图的传递过程中,在界面中并不需要,所以事件拦截对应的方法只在android.view.GroupView
中重写。
boolean dispatchTouchEvent (MotionEvent event)
当某个事件被分发到该视图时,系统回调视图中的该方法。返回结果表示当前事件是否被处理。boolean onInterceptTouchEvent(MotionEvent event)
当某个事件被分发到该视图后,系统会回调视图中的该方法,根据其返回结果判断是否拦截该事件交由当前视图处理。默认返回结果为false,表示不拦截该事件,将会继续回调子视图的dispatchTouchEvent()
。返回结果为true时,表示拦截该事件,将会回调当前视图的onTouchEvent()
.boolean onTouchEvent (MotionEvent event)
当某个事件轮到该视图被处理时,会在该视图的上述事件分发方法boolean dispatchTouchEvent (MotionEvent event)
中回调到该方法。返回结果表示当前事件是否被处理。
界面间交互
上文介绍了针对一个界面Activity
的交互响应,那么两个界面Activity
之间如何交互呢?这就用到在加载界面一文中启动Activity
所使用的android.content.Intent
意图类了。不同于用户与界面的交互,界面间交互主要是变量数据的共享,所以通过Intent
支持的交互数据类型是有限的。
发送数据界面
在启动一个界面Activity
之前要先创建意图对象,在该意图对象调用putExtras(Bundle bundle)
方法,可以将要发送的数据打包成android.os.Bundle类型的实例存入。
而该Bundle
对象可以存储的数据类型支持包括boolean
、char
、byte
、short
、int
、float
、double
、long
八种基本数据类型,String
类型和实现Parcelable
接口的任意类型,及其[]
数组或ArrayList
数组,和其他一些不常用类型。这些数据都是以key-value键值对的形式保存在Bundle
对象中。对于要保存的不同数据类型,分别调用对应的putT(String key, T value)
系列方法即可以参数一key和参数二value的形式存入,同样可以调用对应的getT(String key)
系列方法取出指定参数一key对应的value数据,这里的T泛指支持的不同数据类型。
另外也可以在创建的意图对象中直接调用putExtra(String key, T value)
系列方法,将要发送的数据直接以key-value键值对的形式存入,同样也可以使用getTExtra(String key)
系列方法取出指定参数一key对应的value数据,这里的T同样泛指Bundle
可支持的不同数据类型。
在打包所有的数据后,就可以在当前界面Activity
中继续调用startActivity(Intent intent)
系列方法启动Intent
意图参数中指定的另一界面Activity
了。
这里的startActivity(Intent)
方法是最简单的启动方法,另外还有startActivity(Intent, Bundle)
在启动时将要发送的数据打包作为参数二传入。
或者startActivityForResult(Intent intent, int requestCode)
在启动时传入一个唯一值作为参数二,以区分启动不同界面的意图,在启动的界面Activity
返回后,系统会调用当前界面Activity
中的onActivityResult(int requestCode, int resultCode, Intent data)
方法,因此可以重写该方法。并根据参数一的唯一性对之前启动的不同界面意图做区分处理。参数二是根据启动界面不同关闭状态所返回的结果值,默认为android.app.Activity.RESULT_CANCELED
,另外也可以为android.app.Activity.RESULT_FIRST_USER
和android.app.Activity.RESULT_OK
,其值需要在启动界面返回时设置。参数三是从启动界面返回的Intent
类型,主要使用其中的Bundle
打包数据类型对象,同样其值可以在启动界面返回时设置。
接收数据界面
作为接收数据的启动界面Activity
,在其绑定上下文环境之后,一般是在onCreate(Bundle savedInstanceState)
方法中,可以使用getIntent()
方法获取传递进来的Intent
意图对象,获取该对象之后自然就可以通过getBExtras()
或一系列getTExtra(String key)
获取到打包的数据,这样在启动界面中就可以使用在启动之前上一个界面Activtiy
中的变量数据了。
而当启动界面Activity
在被用户操作返回时,系统将回调该启动界面的onBackPressed()
方法,之后将该Activity
从栈中移出并销毁。所以可以重写onBackPressed()
方法,在该方法中调用setResult(int resultCode, Intent data)
设置上文提到的返回时参数。
或者在启动界面Activity
代码中也可以主动调用finish()
方法,以关闭当前界面。因此在调用finish()
方法之前先调用setResult(int resultCode, Intent data)
设置返回参数即可。