MotionEvent分析及ImageView缩放实现
这个类在各种View和用户的手势操作之间的交互存在很大的自定义空间。要理解清楚这个类的一些特性和意义,对自定义的新型控件很有帮助
先翻译一下开发者文档的描述
Overview
Motion events describe movements in terms of an action code and a set of axis values. The action code specifies the state change that occurred such as a pointer going down or up. The axis values describe the position and other movement properties.
运动事件描述了关于一个动作代码和一组轴值的动作。动作代码描述的是状态改变的发生,例如一个触点按下或者抬起。轴值描述的是位置和其他运动属性。
For example, when the user first touches the screen, the system delivers a touch event to the appropriate View
with the action code ACTION_DOWN
and a set of axis values that include the X and Y coordinates of the touch and information about the pressure, size and orientation of the contact area.
例如,当用户第一次按下屏幕,系统将带有动作代码为ACTION_DOWN和一族含有x、y坐标和压力、大小、方向轴值的触摸时间传递给适当的View。
Some devices can report multiple movement traces at the same time. Multi-touch screens emit one movement trace for each finger. The individual fingers or other objects that generate movement traces are referred to as pointers. Motion events contain information about all of the pointers that are currently active even if some of them have not moved since the last event was delivered.
一些设备能够同时反馈多个运动。多点触控屏能够为每一根手指生成单独的运动轨迹。单独的手指或者其他能够产生运动轨迹的对象都被叫做pointers。运动事件包含了所有这些当前活动的pointers即使其中一些并没有移动。
The number of pointers only ever changes by one as individual pointers go up and down, except when the gesture is canceled.
当pointer按下或者抬起的时候pointers的数量每次只会以1为单位进行变化
Each pointer has a unique id that is assigned when it first goes down (indicated by ACTION_DOWN
or ACTION_POINTER_DOWN
). A pointer id remains valid until the pointer eventually goes up (indicated by ACTION_UP
or ACTION_POINTER_UP
) or when the gesture is canceled (indicated by ACTION_CANCEL
).
当按下时,每个pointer都会有一个独立的id。这个id会一直保持直到抬起或者姿势被取消。
The MotionEvent class provides many methods to query the position and other properties of pointers, such as getX(int)
, getY(int)
, getAxisValue(int)
,getPointerId(int)
, getToolType(int)
, and many others. Most of these methods accept the pointer index as a parameter rather than the pointer id. The pointer index of each pointer in the event ranges from 0 to one less than the value returned by getPointerCount()
.
这些方法中的大多数都以pointer的index作为参数而不是pointer的id。各个pointer的index在事件中从0变到getPointerCount()-1.
The order in which individual pointers appear within a motion event is undefined. Thus the pointer index of a pointer can change from one event to the next but the pointer id of a pointer is guaranteed to remain constant as long as the pointer remains active. Use the getPointerId(int)
method to obtain the pointer id of a pointer to track it across all subsequent motion events in a gesture. Then for successive motion events, use the findPointerIndex(int)
method to obtain the pointer index for a given pointer id in that motion event.
pointer的index从一个事件到另一个事件的时候会变化,但是其id会保持不变。调用getPointerId就能够获取到id了。有了这个ID之后我们就能够在整个运动的过程中一直监控这个pointer了,findPointerIndex就可以通过id去得到index了,也就是说index和id是有办法互换的。
Mouse and stylus buttons can be retrieved using getButtonState()
. It is a good idea to check the button state while handling ACTION_DOWN
as part of a touch event. The application may choose to perform some different action if the touch event starts due to a secondary button click, such as presenting a context menu.
这类触摸的功能都是以一种状态机设计存在,针对两根手指进行缩放操作
Event的getAction()的操作可以检测到pointer1、pointer2的按下和抬起动作事件。
那我们针对这个状态机需要设计的状态有:DONE(初始状态或者说是完成状态),POINTER_1_DOWN_2_UP(pointer1按下,pointer2抬起),POINTER_1_UP_2_DOWN(pointer1抬起,pointer2按下),ZOOMING(放大状态).一共四个状态
状态机就是要在每一种状态下,收到一个新的事件时状态如何变迁。但写程序的时候是反过来,收到某种事件,然后根据不同的状态做变迁
然后在状态变迁之后,根据当前的状态来做实际的图片的显示操作或者是一些变量状态的更新。
针对图像缩放的原理,最基本的就是以两根手指触点的中心作为缩放的原点;然后以两根手指的初始距离作为基准,在移动过程中实时计算两根手指之间的距离,用这个实时距离和基准的比例作为缩放的一个scale幅度
public class MyImageView extends ImageView implements View.OnTouchListener { private static final String TAG = "MyImageView"; private static final int DONE = 0; private static final int POINTER_1_DOWN_2_UP = 1; private static final int POINTER_1_UP_2_DOWN = 2; private static final int ZOOMING = 3; private float pointer_1_x; private float pointer_1_y; private float pointer_2_x; private float pointer_2_y; private float pointer_center_x; private float pointer_center_y; private int focus_state = 0; private int current_state = DONE; private float init_distance = 0; private float current_distance = 0; private Matrix matrix = new Matrix(); private Matrix currentMatrix = new Matrix(); private Matrix originalMatrix = new Matrix(); public MyImageView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub this.setOnTouchListener(this); } public MyImageView(Context context) { super(context); this.setOnTouchListener(this); } private float distance(float x1, float y1, float x2, float y2) { return (float) Math.sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)); } @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub int cnt = event.getPointerCount(); Log.d("MyImageView","pointer cnt is:"+cnt+" action is:"+event.getAction()); //起来的时候切换缩放的原点;按下的时候,记录位置以及两个pointer之间的距离 switch(event.getAction()) { case MotionEvent.ACTION_DOWN: //状态变迁过程done->pointer_1_down //这个action_down在整个过程中只会出现一次 current_state = POINTER_1_DOWN_2_UP; focus_state = 1; //记录这个pointer的坐标位置 pointer_1_x = event.getX(event.getPointerCount()-1); pointer_1_y = event.getY(event.getPointerCount()-1); currentMatrix.set(this.getImageMatrix()); originalMatrix.set(this.getImageMatrix()); Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"x_1:"+pointer_1_x+";y_1:"+pointer_1_y); break; case MotionEvent.ACTION_POINTER_1_DOWN: if(current_state == POINTER_1_UP_2_DOWN ) { current_state = ZOOMING; pointer_1_x = event.getX(event.getPointerCount()-1); pointer_1_y = event.getY(event.getPointerCount()-1); pointer_center_x = (pointer_1_x + pointer_2_x)/2; pointer_center_y = (pointer_1_y + pointer_2_y)/2; init_distance = distance(pointer_1_x,pointer_1_y,pointer_2_x,pointer_2_y); Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"init distance is:"+init_distance); } break; case MotionEvent.ACTION_POINTER_1_UP: if(current_state == ZOOMING) { focus_state = 2; current_state = POINTER_1_UP_2_DOWN; } else if(current_state == POINTER_1_DOWN_2_UP) { focus_state = 0; current_state = DONE; this.setImageMatrix(originalMatrix); } init_distance = 0; Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"init distance is:"+init_distance); break; case MotionEvent.ACTION_POINTER_2_DOWN: if(current_state == POINTER_1_DOWN_2_UP) { current_state = ZOOMING; pointer_2_x = event.getX(event.getPointerCount()-1); pointer_2_y = event.getY(event.getPointerCount()-1); pointer_center_x = (pointer_1_x + pointer_2_x)/2; pointer_center_y = (pointer_1_y + pointer_2_y)/2; init_distance = distance(pointer_1_x,pointer_1_y,pointer_2_x,pointer_2_y); Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"init distance is:"+init_distance); } break; case MotionEvent.ACTION_POINTER_2_UP: if(current_state == ZOOMING) { focus_state = 1; current_state = POINTER_1_DOWN_2_UP; } else if(current_state == POINTER_1_UP_2_DOWN) { focus_state = 0; current_state = DONE; this.setImageMatrix(originalMatrix); } init_distance = 0; Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"init distance is:"+init_distance); break; case MotionEvent.ACTION_MOVE: if(current_state == ZOOMING) { //1st update the 2 pointer coordinate if(focus_state == 1) { pointer_1_x = event.getX(0); pointer_1_y = event.getY(0);; pointer_2_x = event.getX(1);; pointer_2_y = event.getY(1);; } else if(focus_state == 2) { pointer_1_x = event.getX(1); pointer_1_y = event.getY(1); pointer_2_x = event.getX(0); pointer_2_y = event.getY(0); } //2nd count the current distance current_distance = distance(pointer_1_x,pointer_1_y,pointer_2_x,pointer_2_y); float scale = current_distance/init_distance; Log.d(TAG,"current state is:"+current_state); Log.d(TAG,"current distance is:"+current_distance); Log.d("Scale","scale is :"+scale); matrix.set(currentMatrix); matrix.postScale(scale, scale,pointer_center_x,pointer_center_y); this.setImageMatrix(matrix); } break; } return true; } }
总结一下这类view的扩展:
1.要增加触摸交互的方式,核心是要在原有的基础上新增view的OnTouchListener类,中间常用的就是状态机的设计,下拉刷新的实现也同理是实现了listview的onTouchListener与此同时设计了4个状态的状态机
2.针对这种后台数据并不复杂的view,就没有必要考虑那种adapter的操作,但是那种列表性质的view,在触摸操作的过程中还需要考虑对后台数据的更改,比如下拉刷新中就要在出于refreshing的时候增加adapter中的数据