Android 手势识别类 ( 二 ) GestureDetector 源码浅析
前言:Android 关于手势的操作提供两种形式:一种是针对用户手指在屏幕上划出的动作而进行移动的检测,这些手势的检测通过android提供的监听器来实现;另一种是用 户手指在屏幕上滑动而形成一定的不规则的几何图形(即为多个持续触摸事件在屏幕形成特定的形状);本文主要是针对第二种手势的绘制原理进行浅析,我们姑且 称它为输入法手势; 一.输入法手势 在Android源码中,谷歌提供了相关的手势库源码,供给开发者丰富多彩的接口调用实现;这些提供相关接口的类所在的源码路径为frameworks/base/core/java/android/gesture; 如下图gesture文件中的相关类: 绘制手势需要一个视图界面平台,而这个视图界面平台由GestureOverlayView这个类来实现,该类继承FrameLayout容器视图类。所以,当我们在手机屏幕上画手势时,GestureOverlayView主要负责显示和处理手指在屏幕上滑动所形成的手势。 以下举一个简单的Demo来说明如何通过GestureOverlayView实现在屏幕上绘制手势; 1). main.xml文件代码如下: [html] view plain copy <?xmlversion="1.0"encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <android.gesture.GestureOverlayView android:id="@+id/gesture" android:layout_width="fill_parent" android:layout_height="fill_parent" > </android.gesture.GestureOverlayView> </LinearLayout> 很简单,添加一个android.gesture.GestureOverlayView的布局组件; 2). 加载布局文件和实现手势绘制的Actitivty代码如下: [java] view plain copy packagecom.stevenhu.hu.dgt; importandroid.app.Activity; importandroid.gesture.Gesture; importandroid.gesture.GestureOverlayView; importandroid.gesture.GestureOverlayView.OnGesturePerformedListener; importandroid.gesture.GestureOverlayView.OnGesturingListener; importandroid.os.Bundle; importandroid.widget.Toast; publicclassDrawGestureTestextendsActivityimplementsOnGesturePerformedListener,OnGesturingListener { privateGestureOverlayViewmDrawGestureView; /**Calledwhentheactivityisfirstcreated.*/ @Override publicvoidonCreate(BundlesavedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mDrawGestureView=(GestureOverlayView)findViewById(R.id.gesture); //设置手势可多笔画绘制,默认情况为单笔画绘制 mDrawGestureView.setGestureStrokeType(GestureOverlayView.GESTURE_STROKE_TYPE_MULTIPLE); //设置手势的颜色(蓝色) mDrawGestureView.setGestureColor(gestureColor(R.color.gestureColor)); //设置还没未能形成手势绘制是的颜色(红色) mDrawGestureView.setUncertainGestureColor(gestureColor(R.color.ungestureColor)); //设置手势的粗细 mDrawGestureView.setGestureStrokeWidth(4); /*手势绘制完成后淡出屏幕的时间间隔,即绘制完手指离开屏幕后相隔多长时间手势从屏幕上消失; *可以理解为手势绘制完成手指离开屏幕后到调用onGesturePerformed的时间间隔 *默认值为420毫秒,这里设置为2秒 */ mDrawGestureView.setFadeOffset(2000); //绑定监听器 mDrawGestureView.addOnGesturePerformedListener(this); mDrawGestureView.addOnGesturingListener(this); } //手势绘制完成时调用 @Override publicvoidonGesturePerformed(GestureOverlayViewoverlay,Gesturegesture) { //TODOAuto-generatedmethodstub showMessage("手势绘制完成"); } privateintgestureColor(intresId) { returngetResources().getColor(resId); } privatevoidshowMessage(Strings) { Toast.makeText(this,s,Toast.LENGTH_SHORT).show(); } //结束正在绘制手势时调用(手势绘制完成时一般是先调用它在调用onGesturePerformed) @Override publicvoidonGesturingEnded(GestureOverlayViewoverlay) { //TODOAuto-generatedmethodstub showMessage("结束正在绘制手势"); } //正在绘制手势时调用 @Override publicvoidonGesturingStarted(GestureOverlayViewoverlay) { //TODOAuto-generatedmethodstub showMessage("正在绘制手势"); } @Override protectedvoidonDestroy() { //TODOAuto-generatedmethodstub super.onDestroy(); //移除绑定的监听器 mDrawGestureView.removeOnGesturePerformedListener(this); mDrawGestureView.removeOnGesturingListener(this); } } 示例代码下载链接地址:http://download.csdn.net/detail/stevenhu_223/5789777 通过上面的Demo可知,要想实现绘制和监听操作手势,GestureOverlayView是必不可少的,GestureOverlayView为何方神圣 ,它是如何实现手势的绘制和监听操作的,接下来将对它进行浅析。 二.GestureOverlayView类浅析 其实手势的绘制原理和前篇<<Android中Path类的lineTo方法和quadTo方法画线的区别>>中绘制轨迹线的原理差不多,只不过在GestureOverlayView中的处理相对比较复杂; GestureOverlayView继承FrameLayout,所以它也是ViewGroup类型(继承 View),GestureOverlayView重写View的dispatchTouchEvent方法。所以,我们手指在屏幕上触摸滑动时,会调用 GestureOverlayView的dispatchTouchEvent方法;代码如下: [java] view plain copy publicclassGestureOverlayViewextendsFrameLayout{ ... @Override publicbooleandispatchTouchEvent(MotionEventevent){ if(isEnabled()){ finalbooleancancelDispatch=(mIsGesturing||(mCurrentGesture!=null&& mCurrentGesture.getStrokesCount()>0&&mPreviousWasGesturing))&& mInterceptEvents; processEvent(event); if(cancelDispatch){ event.setAction(MotionEvent.ACTION_CANCEL); } super.dispatchTouchEvent(event); returntrue; } returnsuper.dispatchTouchEvent(event); } ... } isEnabled()得到当前视图的enable状态,若当前视图的enable状态为true,则继续执行processEvent(event),传入参数为对应的滑动事件。 ---->我们接着继续跟踪processEvent方法,代码如下: [java] view plain copy ... privatebooleanprocessEvent(MotionEventevent){ switch(event.getAction()){ caseMotionEvent.ACTION_DOWN: touchDown(event); invalidate(); returntrue; caseMotionEvent.ACTION_MOVE: if(mIsListeningForGestures){ Rectrect=touchMove(event); if(rect!=null){ invalidate(rect); } returntrue; } break; caseMotionEvent.ACTION_UP: if(mIsListeningForGestures){ touchUp(event,false); invalidate(); returntrue; } break; caseMotionEvent.ACTION_CANCEL: if(mIsListeningForGestures){ touchUp(event,true); invalidate(); returntrue; } } returnfalse; } ... 在processEvent方法中会根据用户手指对屏幕操作的MotionEvent进行处理: 1). 当MotionEvent事件为ACTION_DOWN时,调用touchDown(MotionEvent event)方法; 2).当MotionEvent事件为ACTION_MOVE,且mIsListeningForGestures为true时(执行touchDown时赋值为true),调用touchMove(MotionEvent event)方法; 3).当MotionEvent事件为ACTION_UP,且mIsListeningForGestures为true时,调用touchUp(MotionEvent event, boolean cancel)方法;mIsListeningForGestures在执行touchUp时赋值为false; 4).当MotionEvent事件为ACTION_CANCEL,且mIsListeningForGestures为true时,调用touchUp(MotionEvent event, boolean cancel)方法; 接下来逐步分析以上分发处理MotionEvent事件的各个函数的实现: ---->touchDown(MotionEvent event),当用户手指点下屏幕时调用该方法,码如下: [java] view plain copy ... privatevoidtouchDown(MotionEventevent){ mIsListeningForGestures=true; floatx=event.getX(); floaty=event.getY(); mX=x; mY=y; mTotalLength=0; mIsGesturing=false; if(mGestureStrokeType==GESTURE_STROKE_TYPE_SINGLE||mResetGesture){ if(mHandleGestureActions)setCurrentColor(mUncertainGestureColor); mResetGesture=false; mCurrentGesture=null; mPath.rewind(); }elseif(mCurrentGesture==null||mCurrentGesture.getStrokesCount()==0){ if(mHandleGestureActions)setCurrentColor(mUncertainGestureColor); } //ifthereisfadingoutgoingon,stopit. //如果手势已正在淡出,则停止它 if(mFadingHasStarted){ cancelClearAnimation(); }elseif(mIsFadingOut){ setPaintAlpha(255); mIsFadingOut=false; mFadingHasStarted=false; removeCallbacks(mFadingOut); } if(mCurrentGesture==null){ mCurrentGesture=newGesture(); } mStrokeBuffer.add(newGesturePoint(x,y,event.getEventTime())); mPath.moveTo(x,y); //mInvalidateExtraBorder值由设置手势画笔粗细值决定 finalintborder=mInvalidateExtraBorder; mInvalidRect.set((int)x-border,(int)y-border,(int)x+border,(int)y+border); mCurveEndX=x; mCurveEndY=y; //passtheeventtohandlers finalArrayList<OnGestureListener>listeners=mOnGestureListeners; finalintcount=listeners.size(); for(inti=0;i<count;i++){ listeners.get(i).onGestureStarted(this,event); } } ... 在touchDown中,实现处理当用户手指在点下屏幕时的一些操作,这些操作包括: 1). 获取用户手指点下屏幕时所在的坐标值x,y,同时将它们分别赋值给全局变量mX,mY;mTotalLength变量代表绘制手势的总长度,在调用 touchDown时,手势还没绘制,所以mTotalLength为0;mIsGesturing描述是否正在绘制手势,为false表示不是正在绘制 手势; 2). 根据一些条件判断,设置画笔颜色,处理手势画笔的相关状态,以及创建Gesture对象等。 3). 将1)得到的x,y坐标值和event.getEventTime()的值作为GesturePoint构造函数的实参创建GesturePoint对象,并将该对象添加进mStrokeBuffer数组集合。 4). 将1)得到的x,y坐标值作为mPath画笔路径的初始点。 5). 遍历存放OnGestureListener的集合listeners,调用实现OnGestureListener接口的onGestureStarted()方法; ---->touchMove(MotionEvent event),当用户手指在屏幕上滑动时调用该方法,码如下: [java] view plain copy ... privateRecttouchMove(MotionEventevent){ //更新区域 RectareaToRefresh=null; finalfloatx=event.getX(); finalfloaty=event.getY(); finalfloatpreviousX=mX; finalfloatpreviousY=mY; finalfloatdx=Math.abs(x-previousX); finalfloatdy=Math.abs(y-previousY); //手势在屏幕滑动的两点之间的距离大于GestureStroke.TOUCH_TOLERANCE的值,则显示手势的绘制 if(dx>=GestureStroke.TOUCH_TOLERANCE||dy>=GestureStroke.TOUCH_TOLERANCE){ areaToRefresh=mInvalidRect; //startwiththecurveend finalintborder=mInvalidateExtraBorder; areaToRefresh.set((int)mCurveEndX-border,(int)mCurveEndY-border, (int)mCurveEndX+border,(int)mCurveEndY+border); //设置贝塞尔曲线的操作点为起点和终点的一半 floatcX=mCurveEndX=(x+previousX)/2; floatcY=mCurveEndY=(y+previousY)/2; //二次贝塞尔,实现平滑曲线;previousX,previousY为操作点,cX,cY为终点 mPath.quadTo(previousX,previousY,cX,cY); //unionwiththecontrolpointofthenewcurve /*areaToRefresh矩形扩大了border(宽和高扩大了两倍border), *border值由设置手势画笔粗细值决定 */ areaToRefresh.union((int)previousX-border,(int)previousY-border, (int)previousX+border,(int)previousY+border); //unionwiththeendpointofthenewcurve areaToRefresh.union((int)cX-border,(int)cY-border, (int)cX+border,(int)cY+border); //第二次执行时,第一次结束调用的坐标值将作为第二次调用的初始坐标值 mX=x; mY=y; mStrokeBuffer.add(newGesturePoint(x,y,event.getEventTime())); //当调用addOnGesturePerformedListener添加手势完成调用的监听器时,mHandleGestureActions为true; if(mHandleGestureActions&&!mIsGesturing){ mTotalLength+=(float)Math.sqrt(dx*dx+dy*dy); if(mTotalLength>mGestureStrokeLengthThreshold){ finalOrientedBoundingBoxbox= GestureUtils.computeOrientedBoundingBox(mStrokeBuffer); floatangle=Math.abs(box.orientation); if(angle>90){ angle=180-angle; } /*这个条件成立时,说明所手势绘制已经在进行 */ if(box.squareness>mGestureStrokeSquarenessTreshold|| (mOrientation==ORIENTATION_VERTICAL? angle<mGestureStrokeAngleThreshold: angle>mGestureStrokeAngleThreshold)){ mIsGesturing=true; //手势尚未形成的显示颜色 setCurrentColor(mCertainGestureColor); finalArrayList<OnGesturingListener>listeners=mOnGesturingListeners; intcount=listeners.size(); for(inti=0;i<count;i++){ listeners.get(i).onGesturingStarted(this); } } } } //passtheeventtohandlers finalArrayList<OnGestureListener>listeners=mOnGestureListeners; finalintcount=listeners.size(); for(inti=0;i<count;i++){ listeners.get(i).onGesture(this,event); } } returnareaToRefresh; } ... touchMove方法中主要有以下功能的实现: 1). touchMove方法返回值类型为Rect(定义一个矩形区域),若返回值不会空,则调用invalidate(Rectrect)刷新; 2). 得到当前的手指滑动所在屏幕位置的x,y坐标值,将x,y值与调用touchDown()时得到的x,y值相减后取绝对值,得到偏移量dx,dy; 3). dx或dy大于指定的GestureStroke.TOUCH_TOLERANCE时(默认值为3),执行画笔绘制手势的实现流程代码。 4). mPath画笔路径调用quadTo()方法执行贝塞尔曲线计算,实现得到平滑曲线。 5). areaToRefresh矩形区域负责根据手势绘制控制点和结束点的位置不断更新,画出手势画笔轨迹(每次调用touchMove()时,areaToRefresh逐点更新从而汇成一定轨迹的几何图形,即手势的雏形)。 6). 将第二步得到x,y坐标值和event.getEventTime()的值作为GesturePoint构造函数的实参创建GesturePoint对 象,并将该对象添加进mStrokeBuffer数组集合。(保存用户在屏幕上绘制形成手势的相关信息) 7). 当调用GestureOverlayView的addOnGesturePerformedListener方法添加监听器 OnGesturePerformedListener时,mHandleGestureActions为true,这时候会执行计算移动所得的这些点集 的最小边界框,然后根据这个最小边界框进行一些条件判断,进而设置mIsGesturering为true,以及设置手势尚未形成绘制手势的显示颜色。 8). touchMove()的最后,遍历存放OnGestureListener接口的集合listeners,调用实现OnGestureListener接口的onGesture方法。 ---->touchUp(MotionEvent event, boolean cancel),当用户手指离开屏幕或MotionEvent 事件取消时调用该方法,码如下: [java] view plain copy ... privatevoidtouchUp(MotionEventevent,booleancancel){ mIsListeningForGestures=false; //Agesturewasn'tstartedorwascancelled if(mCurrentGesture!=null){ //addthestroketothecurrentgesture /*将之前调用touchDonw和touchMove收集得到GesturePoint的组成的数组集合mStrokeBuffer, *做为GestureStroke构造函数的实参创建GestureStroke对象, *然后将GestureStroke对象通过调用addStroke方法添加到mCurrentGesture中 */ mCurrentGesture.addStroke(newGestureStroke(mStrokeBuffer)); if(!cancel){ //passtheeventtohandlers finalArrayList<OnGestureListener>listeners=mOnGestureListeners; intcount=listeners.size(); for(inti=0;i<count;i++){ listeners.get(i).onGestureEnded(this,event); } /*当调用addOnGesturePerformedListener方法时,mHandleGestureActions为true; *mFadeEnabled默认值为true,可通过setFadeEnabled函数设值 */ clear(mHandleGestureActions&&mFadeEnabled,mHandleGestureActions&&mIsGesturing, false); }else{ cancelGesture(event); } }else{ cancelGesture(event); } mStrokeBuffer.clear(); mPreviousWasGesturing=mIsGesturing; mIsGesturing=false; finalArrayList<OnGesturingListener>listeners=mOnGesturingListeners; intcount=listeners.size(); for(inti=0;i<count;i++){ listeners.get(i).onGesturingEnded(this); } } ... touchUp方法中主要有以下功能的实现: 1). 首先将mIsListeningForGesture赋值为false; 2). 判断当前是否存在mCurrentGesture(Gesture类型),该变量在执行touchDown方法时创建Gesture对象赋值的,也可以通 过调用setGesture方法赋值;(mCurrentGesture描述的就是当前用户绘制形成的整个手势) 3). 若mCurrentGesture不为空,则将之前调用touchDonw和touchMove收集得到的GesturePoint组成的数组集合 mStrokeBuffer做为GestureStroke构造函数的实参,创建GestureStroke对象。然后将GestureStroke对象 通过调用addStroke方法添加到mCurrentGesture中; 4). 若touchUp方法的第二个参数为false(即执行ACTION_UP事件时),则遍历存放OnGestureListener的集合,调用实现该接 口的onGestureEnded()方法。接着调用clear方法,实现将当前绘制形成的手势清除(即手势淡出屏幕;手指离开屏幕时到手势淡出屏幕,这 期间是有时间间隔的,且这个时间间隔也是可以设置); 5). 若touchUp()方法的第二个参数为true(即执行ACTION_CANCEL事件时),调用cancelGesture()方法。在该方法中:首 先遍历存放OnGestureListener的集合,调用实现该接口的onGestureCancelled()方法,接着调用clear()方法实现 回收mCurrentGesture对象、清除画笔等淡出屏幕处理; ---->上面4)中,当touchUp方法的cancel参数为false时,通过调用clear(boolean animated, boolean fireActionPerformed, boolean immediate)处理手势淡出屏幕,我们来看看这个方法的实现,代码如下: [java] view plain copy ... privatevoidclear(booleananimated,booleanfireActionPerformed,booleanimmediate){ setPaintAlpha(255); removeCallbacks(mFadingOut); mResetGesture=false; mFadingOut.fireActionPerformed=fireActionPerformed; mFadingOut.resetMultipleStrokes=false; if(animated&&mCurrentGesture!=null){//调用addOnGesturePerformedListener时animated为true mFadingAlpha=1.0f; mIsFadingOut=true; mFadingHasStarted=false; /*mFadeOffset定义手势淡出屏幕的时间间隔, *默认值420,可通过setFadeOffset函数设置 */ mFadingStart=AnimationUtils.currentAnimationTimeMillis()+mFadeOffset; postDelayed(mFadingOut,mFadeOffset); }else{ mFadingAlpha=1.0f; mIsFadingOut=false; mFadingHasStarted=false; if(immediate){ mCurrentGesture=null; mPath.rewind(); invalidate(); }elseif(fireActionPerformed){ postDelayed(mFadingOut,mFadeOffset); }elseif(mGestureStrokeType==GESTURE_STROKE_TYPE_MULTIPLE){ mFadingOut.resetMultipleStrokes=true; postDelayed(mFadingOut,mFadeOffset); }else{ mCurrentGesture=null; mPath.rewind(); invalidate(); } } } ... 通过上面的代码,我们知道,在clear函数中,会通过传入的实参来决定如何去进一步处理手势的淡出,有两种处理方式: 1. 调用mPath.rewind(),将绘制手势的重置清除,然后调用invalidate(); 2. 调用postDelayed(mFadingOut, mFadeOffset),到主线程中处理,mFadeOffset就是决定手势淡出屏幕的时间间隔; 我们针对第二种在主线程中处理的方式继续跟踪解析代码,mFadingOut是FadeOutRunnable对象,FadeOutRunnable继承Runnable类,该类的实现代码如下: [java] view plain copy ... /*处理手势淡出; *手势淡出的条件: *1.前面一次画完手势,且画完的同时没有调用onGesturePerformed, *则当用户再次画手势时,前面画出的保留在屏幕上的手势将淡出; *2.当画完手势,且添加OnGesturePerformedListener监听器时, *在完成手势,调用onGesturePerformed时,将手势轨迹画笔淡出 */ privateclassFadeOutRunnableimplementsRunnable{ //调用addOnGesturePerformedListener时为true; booleanfireActionPerformed; //手势设置为多笔画绘制时为true; booleanresetMultipleStrokes; publicvoidrun(){ if(mIsFadingOut){//fireActionPerformed为true且mCurrentGesture不为空是成立 finallongnow=AnimationUtils.currentAnimationTimeMillis(); finallongduration=now-mFadingStart; //mFadeDuration默认值为150 if(duration>mFadeDuration){ if(fireActionPerformed){ //调用onGesturePerformed方法 fireOnGesturePerformed(); } mPreviousWasGesturing=false; mIsFadingOut=false; mFadingHasStarted=false; mPath.rewind(); mCurrentGesture=null; setPaintAlpha(255); }else{ mFadingHasStarted=true; floatinterpolatedTime=Math.max(0.0f, Math.min(1.0f,duration/(float)mFadeDuration)); mFadingAlpha=1.0f-mInterpolator.getInterpolation(interpolatedTime); setPaintAlpha((int)(255*mFadingAlpha)); //FADE_ANIMATION_RATE默认值为16 postDelayed(this,FADE_ANIMATION_RATE); } }elseif(resetMultipleStrokes){//fireActionPerformed为false且手势为多笔画绘制时成立 mResetGesture=true; }else{ //调用实现监听器OnGesturePerformedListener的onGesturePerformed方法 fireOnGesturePerformed(); mFadingHasStarted=false; mPath.rewind(); mCurrentGesture=null; mPreviousWasGesturing=false; setPaintAlpha(255); } invalidate(); } } ... 值得注意的是,在主线程中处理手势淡出屏幕,当我们绑定了监听器OnGesturePerformedListener,手势淡出屏幕时会调用fireOnGesturePerformed方法,该方法实现遍历存放OnGesturePerformedListener的集合actionListeners,进而调用实现OnGesturePerformedListener接口的函数onGesturePerformed,代码如下: [java] view plain copy ... privatevoidfireOnGesturePerformed(){ finalArrayList<OnGesturePerformedListener>actionListeners=mOnGesturePerformedListeners; finalintcount=actionListeners.size(); for(inti=0;i<count;i++){ actionListeners.get(i).onGesturePerformed(GestureOverlayView.this,mCurrentGesture); } } ... 最后,有一点值得注意, 当我们手指在触摸屏上滑动时,在processEvent方法中,每次执行完touchDown、touchMove方法后都会调用 invalidate()、invalidate(rect)进行不断的刷新,那么这时候就调用draw方法将用户在触摸屏上绘制的手势轨迹显示出来,代 码如下: [java] view plain copy ... @Override publicvoiddraw(Canvascanvas){ super.draw(canvas); if(mCurrentGesture!=null&&mGestureVisible){ canvas.drawPath(mPath,mGesturePaint); } } ... 至此,关于实现手势绘制的视图平台类GestureOverlayView的浅析就结束了! 借鉴:http://blog.csdn.net/stevenhu_223/article/details/9394491 本文转自 一点点征服 博客园博客,原文链接:http://www.cnblogs.com/ldq2016/p/5290933.html,如需转载请自行联系原作者