Android手势源码浅析-----手势绘制(GestureOverlayView)
前言:Android关于手势的操作提供两种形式:一种是针对用户手指在屏幕上划出的动作而进行移动的检测,这些手势的检测通过android提供的监听器来实现;另一种是用户手指在屏幕上滑动而形成一定的不规则的几何图形(即为多个持续触摸事件在屏幕形成特定的形状);本文主要是针对第二种手势的绘制原理进行浅析,我们姑且称它为输入法手势;
一. 输入法手势
在Android源码中,谷歌提供了相关的手势库源码,供给开发者丰富多彩的接口调用实现;这些提供相关接口的类所在的源码路径为frameworks/base/core/java/android/gesture;
如下图gesture文件中的相关类:
绘制手势需要一个视图界面平台,而这个视图界面平台由GestureOverlayView这个类来实现,该类继承FrameLayout容器视图类。所以,当我们在手机屏幕上画手势时,GestureOverlayView主要负责显示和处理手指在屏幕上滑动所形成的手势。
以下举一个简单的Demo来说明如何通过GestureOverlayView实现在屏幕上绘制手势;
1). main.xml文件代码如下:
- xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns: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代码如下:
- package com.stevenhu.hu.dgt;
-
- import android.app.Activity;
- import android.gesture.Gesture;
- import android.gesture.GestureOverlayView;
- import android.gesture.GestureOverlayView.OnGesturePerformedListener;
- import android.gesture.GestureOverlayView.OnGesturingListener;
- import android.os.Bundle;
- import android.widget.Toast;
-
- public class DrawGestureTest extends Activity implements OnGesturePerformedListener, OnGesturingListener
- {
-
- private GestureOverlayView mDrawGestureView;
-
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- 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);
-
-
-
-
- mDrawGestureView.setFadeOffset(2000);
-
-
- mDrawGestureView.addOnGesturePerformedListener(this);
- mDrawGestureView.addOnGesturingListener(this);
- }
-
-
- @Override
- public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture)
- {
-
- showMessage("手势绘制完成");
- }
-
- private int gestureColor(int resId)
- {
- return getResources().getColor(resId);
- }
-
- private void showMessage(String s)
- {
- Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
- }
-
-
- @Override
- public void onGesturingEnded(GestureOverlayView overlay)
- {
-
- showMessage("结束正在绘制手势");
- }
-
-
- @Override
- public void onGesturingStarted(GestureOverlayView overlay)
- {
-
- showMessage("正在绘制手势");
- }
-
- @Override
- protected void onDestroy()
- {
-
- 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方法;代码如下:
- public class GestureOverlayView extends FrameLayout {
- ...
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- if (isEnabled()) {
- final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null &&
- mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) &&
- mInterceptEvents;
-
- processEvent(event);
-
- if (cancelDispatch) {
- event.setAction(MotionEvent.ACTION_CANCEL);
- }
-
- super.dispatchTouchEvent(event);
-
- return true;
- }
-
- return super.dispatchTouchEvent(event);
- }
- ...
- }
isEnabled()得到当前视图的enable状态,若当前视图的enable状态为true,则继续执行processEvent(event),传入参数为对应的滑动事件。
----> 我们接着继续跟踪processEvent方法,代码如下:
- ...
- private boolean processEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- touchDown(event);
- invalidate();
- return true;
- case MotionEvent.ACTION_MOVE:
- if (mIsListeningForGestures) {
- Rect rect = touchMove(event);
- if (rect != null) {
- invalidate(rect);
- }
- return true;
- }
- break;
- case MotionEvent.ACTION_UP:
- if (mIsListeningForGestures) {
- touchUp(event, false);
- invalidate();
- return true;
- }
- break;
- case MotionEvent.ACTION_CANCEL:
- if (mIsListeningForGestures) {
- touchUp(event, true);
- invalidate();
- return true;
- }
- }
-
- return false;
- }
- ...
在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),当用户手指点下屏幕时调用该方法,码如下:
- ...
- private void touchDown(MotionEvent event) {
- mIsListeningForGestures = true;
-
- float x = event.getX();
- float y = 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();
- } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) {
- if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
- }
-
-
-
- if (mFadingHasStarted) {
- cancelClearAnimation();
- } else if (mIsFadingOut) {
- setPaintAlpha(255);
- mIsFadingOut = false;
- mFadingHasStarted = false;
- removeCallbacks(mFadingOut);
- }
-
- if (mCurrentGesture == null) {
- mCurrentGesture = new Gesture();
- }
-
- mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
- mPath.moveTo(x, y);
-
-
- final int border = mInvalidateExtraBorder;
- mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
-
- mCurveEndX = x;
- mCurveEndY = y;
-
-
- final ArrayList listeners = mOnGestureListeners;
- final int count = listeners.size();
- for (int i = 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),当用户手指在屏幕上滑动时调用该方法,码如下:
- ...
- private Rect touchMove(MotionEvent event) {
-
- Rect areaToRefresh = null;
-
- final float x = event.getX();
- final float y = event.getY();
-
- final float previousX = mX;
- final float previousY = mY;
-
- final float dx = Math.abs(x - previousX);
- final float dy = Math.abs(y - previousY);
-
-
- if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
- areaToRefresh = mInvalidRect;
-
-
- final int border = mInvalidateExtraBorder;
- areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border,
- (int) mCurveEndX + border, (int) mCurveEndY + border);
-
-
- float cX = mCurveEndX = (x + previousX) / 2;
- float cY = mCurveEndY = (y + previousY) / 2;
-
-
- mPath.quadTo(previousX, previousY, cX, cY);
-
-
-
-
-
- areaToRefresh.union((int) previousX - border, (int) previousY - border,
- (int) previousX + border, (int) previousY + border);
-
-
- areaToRefresh.union((int) cX - border, (int) cY - border,
- (int) cX + border, (int) cY + border);
-
-
- mX = x;
- mY = y;
-
- mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
-
-
- if (mHandleGestureActions && !mIsGesturing) {
- mTotalLength += (float) Math.sqrt(dx * dx + dy * dy);
-
- if (mTotalLength > mGestureStrokeLengthThreshold) {
- final OrientedBoundingBox box =
- GestureUtils.computeOrientedBoundingBox(mStrokeBuffer);
-
- float angle = 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);
-
- final ArrayList listeners = mOnGesturingListeners;
- int count = listeners.size();
- for (int i = 0; i < count; i++) {
- listeners.get(i).onGesturingStarted(this);
- }
- }
- }
- }
-
-
- final ArrayList listeners = mOnGestureListeners;
- final int count = listeners.size();
- for (int i = 0; i < count; i++) {
- listeners.get(i).onGesture(this, event);
- }
- }
-
- return areaToRefresh;
- }
- ...
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 事件取消时调用该方法,码如下:
- ...
- private void touchUp(MotionEvent event, boolean cancel) {
- mIsListeningForGestures = false;
-
-
- if (mCurrentGesture != null) {
-
-
-
-
-
- mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
-
- if (!cancel) {
-
- final ArrayList listeners = mOnGestureListeners;
- int count = listeners.size();
- for (int i = 0; i < count; i++) {
- listeners.get(i).onGestureEnded(this, event);
- }
-
-
-
-
- clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing,
- false);
- } else {
- cancelGesture(event);
-
- }
- } else {
- cancelGesture(event);
- }
-
- mStrokeBuffer.clear();
- mPreviousWasGesturing = mIsGesturing;
- mIsGesturing = false;
-
- final ArrayList listeners = mOnGesturingListeners;
- int count = listeners.size();
- for (int i = 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)处理手势淡出屏幕,我们来看看这个方法的实现,代码如下:
- ...
- private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) {
- setPaintAlpha(255);
- removeCallbacks(mFadingOut);
- mResetGesture = false;
- mFadingOut.fireActionPerformed = fireActionPerformed;
- mFadingOut.resetMultipleStrokes = false;
-
- if (animated && mCurrentGesture != null) {
- mFadingAlpha = 1.0f;
- mIsFadingOut = true;
- mFadingHasStarted = false;
-
-
-
- mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset;
-
- postDelayed(mFadingOut, mFadeOffset);
- } else {
- mFadingAlpha = 1.0f;
- mIsFadingOut = false;
- mFadingHasStarted = false;
-
- if (immediate) {
- mCurrentGesture = null;
- mPath.rewind();
- invalidate();
- } else if (fireActionPerformed) {
- postDelayed(mFadingOut, mFadeOffset);
- } else if (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类,该类的实现代码如下:
- ...
-
-
-
-
-
-
-
- private class FadeOutRunnable implements Runnable {
-
- boolean fireActionPerformed;
-
- boolean resetMultipleStrokes;
-
- public void run() {
- if (mIsFadingOut) {
- final long now = AnimationUtils.currentAnimationTimeMillis();
- final long duration = now - mFadingStart;
-
-
- if (duration > mFadeDuration) {
- if (fireActionPerformed) {
-
- fireOnGesturePerformed();
- }
-
- mPreviousWasGesturing = false;
- mIsFadingOut = false;
- mFadingHasStarted = false;
- mPath.rewind();
- mCurrentGesture = null;
- setPaintAlpha(255);
- } else {
- mFadingHasStarted = true;
- float interpolatedTime = Math.max(0.0f,
- Math.min(1.0f, duration / (float) mFadeDuration));
- mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime);
- setPaintAlpha((int) (255 * mFadingAlpha));
-
- postDelayed(this, FADE_ANIMATION_RATE);
- }
- } else if (resetMultipleStrokes) {
- mResetGesture = true;
- } else {
-
- fireOnGesturePerformed();
-
- mFadingHasStarted = false;
- mPath.rewind();
- mCurrentGesture = null;
- mPreviousWasGesturing = false;
- setPaintAlpha(255);
- }
-
- invalidate();
- }
- }
- ...
值得注意的是,在主线程中处理手势淡出屏幕,当我们绑定了监听器OnGesturePerformedListener,手势淡出屏幕时会调用fireOnGesturePerformed方法,该方法实现遍历存放OnGesturePerformedListener的集合actionListeners,进而调用实现OnGesturePerformedListener接口的函数onGesturePerformed,代码如下:
- ...
- private void fireOnGesturePerformed() {
- final ArrayList actionListeners = mOnGesturePerformedListeners;
- final int count = actionListeners.size();
- for (int i = 0; i < count; i++) {
- actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture);
- }
- }
- ...
最后,有一点值得注意,当我们手指在触摸屏上滑动时,在processEvent方法中,每次执行完touchDown、touchMove方法后都会调用invalidate()、invalidate(rect)进行不断的刷新,那么这时候就调用draw方法将用户在触摸屏上绘制的手势轨迹显示出来,代码如下:
- ...
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- if (mCurrentGesture != null && mGestureVisible) {
- canvas.drawPath(mPath, mGesturePaint);
- }
- }
- ...
至此,关于实现手势绘制的视图平台类GestureOverlayView的浅析就结束了!
Android手势源码浅析------手势的形成(Gesture)
前言:上篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势。所以,用户绘制出的一个完整的手势是需要一定的代码机制来完成存储以及必要时加载取出的;那么,在源码中Gesture这个类就是用来描述完整的手势的。一个Gesture就是用户手指在触摸屏上绘制形成的不规则几何图形(A gesture is a hand-drawn shape on a touch screen);
一. Gesture的组成
通过前篇文章<<Android手势源码浅析-----手势绘制(GestureOverlayView)>>的介绍,我们知道,当我们在GestureOverlayView上绘制手势时,形成的不规则几何图形是由多数个点形成的,这些点都有其对应的在屏幕上的坐标值和时间戳(event.getEventTime());那么,这些点是如何组成Gesture的呢?针对这个问题,通过对Android手势源码的浅析来寻求答案;
下图总体上大概描述了Gesture的形成结构,如下:
从上图描述的类关系中,可以知道:
1. 触摸屏上的点首先是通过GesturePoint这个类来描述的,GesturePoint封装点的x,y轴值和时间戳。
2. GesturePoint中封装的点的信息会在GestureStroke类中被拆解处理,点对应的x,y值被拆解存放在GestureStroke的float类型数组成员points中,而点对应的时间戳则放在long类型成员数组timestamps中。
3. GestureStroke表示一个手势行程(用户手指点下屏幕到手势离开屏幕绘制出的轨迹就是一个手势行程)。一个完整的手势由一个或多个手势行程组成(单笔画或多笔画绘制手势)
4. Gesture由单个或多个GestureStroke组成,Gesture类中的mStrokeBuffer成员为ArrayList类型集合,存放的是GestureStroke;
二. Gesture的形成过程:
当我们在GestureOverlayView上绘制手势时,会调用GestureOverlayView的touchDown、touchMove、touchUp方法,然后将通过这个三个方法捕获到的形成手势的多数个点组成Gesture。如下代码:
- public class GestureOverlayView extends FrameLayout {
- ...
- private void touchDown(MotionEvent event) {
- ...
- mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
- ...
- }
-
- private Rect touchMove(MotionEvent event) {
- ...
- mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
- ...
- }
-
- private void touchUp(MotionEvent event, boolean cancel) {
- ...
- mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
- ...
- }
- ...
- }
---->通过上面的代码可知,当用户正在绘制手势时,会调用touchDown、touchMove,执行mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())),实现将点的x、y、event.getEventTime() 值作为GesturePoint的构造函数的实参创建GesturePoint对象,然后将得到的GesturePoint添加到mStrokeBuffer集合中(mStrokeBuffer为ArrayList类型);
GesturePoint的源代码如下:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- package android.gesture;
-
- import java.io.DataInputStream;
- import java.io.IOException;
-
-
-
-
-
-
- public class GesturePoint {
- public final float x;
- public final float y;
-
- public final long timestamp;
-
- public GesturePoint(float x, float y, long t) {
- this.x = x;
- this.y = y;
- timestamp = t;
- }
-
-
- static GesturePoint deserialize(DataInputStream in) throws IOException {
-
- final float x = in.readFloat();
- final float y = in.readFloat();
-
- final long timeStamp = in.readLong();
- return new GesturePoint(x, y, timeStamp);
- }
-
- @Override
- public Object clone() {
- return new GesturePoint(x, y, timestamp);
- }
- }
通过源码可知,在GesturePoint的构造函数中,将传进来的点的各个信息值分别赋值给自身的成员变量x、y、timestamp;所以GesturePoint描述的就是组成完成手势中的一个点元素;而GestureOverlayView中的mStrokeBuffer集合保存着组成手势的多数个点
---->紧接着,当用户完成手势绘制手指离开屏幕时,会调用touchUp,执行 mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)),实现将之前绘制手势得到的mStrokeBuffer集合作为GestureStroke构造函数的实参创建GestureStroke对象,然后将GestureStroke对象作为mCurrentGesture(Gesture对象)的方法addStroke的实参,实现将GestureStroke添加到Gesture中;
GesturePoint的部分源代码如下:
-
-
-
-
-
- public class GestureStroke {
- ...
- public final float length;
- public final float[] points;
- private final long[] timestamps;
-
-
-
-
-
-
- public GestureStroke(ArrayList points) {
- final int count = points.size();
- final float[] tmpPoints = new float[count * 2];
- final long[] times = new long[count];
-
- RectF bx = null;
- float len = 0;
- int index = 0;
-
- for (int i = 0; i < count; i++) {
- final GesturePoint p = points.get(i);
- tmpPoints[i * 2] = p.x;
- tmpPoints[i * 2 + 1] = p.y;
- times[index] = p.timestamp;
-
- if (bx == null) {
- bx = new RectF();
- bx.top = p.y;
- bx.left = p.x;
- bx.right = p.x;
- bx.bottom = p.y;
- len = 0;
- } else {
-
- len += Math.sqrt(Math.pow(p.x - tmpPoints[(i - 1) * 2], 2)
- + Math.pow(p.y - tmpPoints[(i -1 ) * 2 + 1], 2));
-
- bx.union(p.x, p.y);
- }
- index++;
- }
-
- timestamps = times;
- this.points = tmpPoints;
- boundingBox = bx;
- length = len;
- }
- ...
- }
通过上面的代码可知,当我们创建GestureStroke的对象时,会执行GestureStroke的构造函数。而在GestureStroke的构造函数中,实现将传进来的mStrokeBuffer集合中封存的多个点进行遍历拆解出来,然后分别赋值给GestureStroke的数组成员变量points,timestamps,同时也根据点的坐标值计算出手势行程的长度length;
---->接着,将创建得到的GestureStroke对象通过调用Gesture的addStroke方法添加到Gesture类的mStrokes中,Gesture的addStroke方法源码实现如下:
-
-
-
-
-
-
-
-
-
- public class Gesture implements Parcelable {
- ...
- private final ArrayList mStrokes = new ArrayList();
- ...
-
-
-
-
-
- public void addStroke(GestureStroke stroke) {
- mStrokes.add(stroke);
- ...
- }
- ...
- ...
- }
所以,在Gesture的成员mStrokes中存放着是用户在触摸屏上绘制形成的当前手势相关信息。在Gesture中会根据得到的mStrokes中这些信息去进行一些重要的处理,如将其序列化存储(serialize)、手势转化成bitmap显示(toBitmap)、还原手势的绘制路径(toPath)等;
最后,针对手势组成类之间的关系进行一个小结:
1). GesturePoint: 描述用户手指在屏幕位置的一个定时点,封装用户手指在屏幕上的点坐标值以及时间戳,时间戳由event.getEventTime()决定。
2). GestureStroke:描述用户手指在屏幕上滑动到手指离开屏幕时所产生的轨迹线(由多个时间序列点形成),一个GestureStroke由多个GesturePoint组成。
3). Gesture:实现Parcelable接口,描述用户完成绘制的完整手势,一个Gesture由单个或多个GestureStroke组成。手势绘制可通过GestureOverlayView.setGestureStrokeType(inttype)来设置单笔和多笔画。
Android手势源码浅析-----手势的保存和加载(GestureLibrary)
前言:在《Android手势源码浅析------手势的形成(Gesture)》文章中,介绍了手势Gesture的形成。那么,有的时候,用户绘制的手势是需要保存的,以便用户需要时加载出来进行相关的手势识别处理;接下来将结合一个Demo重点介绍源码中手势的保存和加载流程机制;
一. 关于手势保存和加载的Demo
手势保存概要:
1. 在绘制完手势后,需要将手势存入手势库中,手势最终会被解析存放在指定路径创建的文件中。
2. 一般是GestureOverlayView添加实现监听器OnGesturePerformedListener,当绘制完手势时,会调用监听器的onGesturePerformed(GestureOverlayViewoverlay, Gesture gesture);
3. onGesturePerformed方法的第二个参数geture(Gesture对象)就代表用户绘制完成后形成的手势;
4. 将得到的Gesture对象通过调用GestureLibrary的addGesture方法存入手势库创建的指定文件中;
以下举一个简单的Demo来说明第三方应用开发实现手势的保存和加载:
主类代码如下:
- package com.stevenhu.hu.dgt;
-
- import java.io.File;
-
- import android.app.Activity;
- import android.app.AlertDialog;
- import android.app.AlertDialog.Builder;
- import android.content.DialogInterface;
- import android.content.DialogInterface.OnClickListener;
- import android.content.Intent;
- import android.gesture.Gesture;
- import android.gesture.GestureLibraries;
- import android.gesture.GestureLibrary;
- import android.gesture.GestureOverlayView;
- import android.gesture.GestureOverlayView.OnGesturePerformedListener;
- import android.graphics.Bitmap;
- import android.os.Bundle;
- import android.os.Environment;
- import android.view.Menu;
- import android.view.MenuItem;
- import android.view.MenuItem.OnMenuItemClickListener;
- import android.view.View;
- import android.widget.EditText;
- import android.widget.ImageView;
- import android.widget.Toast;
-
- public class DrawGestureTest extends Activity implements OnGesturePerformedListener
- {
-
- private GestureOverlayView mDrawGestureView;
- private static GestureLibrary sStore;
-
-
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- 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);
-
-
-
-
- mDrawGestureView.setFadeOffset(500);
-
-
- mDrawGestureView.addOnGesturePerformedListener(this);
-
- createStore();
- }
-
- private void createStore()
- {
- File mStoreFile = null;
-
-
-
- if (mStoreFile == null && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
- {
- mStoreFile = new File(Environment.getExternalStorageDirectory(), "mygesture");
- }
-
- if (sStore == null)
- {
-
-
-
-
-
-
-
- sStore = GestureLibraries.fromFile(mStoreFile);
- }
- testLoad();
- }
-
-
- private void testLoad()
- {
- if (sStore.load())
- {
- showMessage("手势文件装载成功");
- }
- else
- {
- showMessage("手势文件装载失败");
- }
- }
-
- public static GestureLibrary getStore()
- {
- return sStore;
- }
-
-
- @Override
- public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture)
- {
-
- creatDialog(gesture);
- }
-
- private void creatDialog(final Gesture gesture)
- {
- View dialogView = getLayoutInflater().inflate(R.layout.show_gesture, null);
-
- ImageView imageView = (ImageView) dialogView.findViewById(R.id.show);
-
- EditText editText = (EditText)dialogView.findViewById(R.id.name);
- final String name = editText.getText().toString();
-
- final Bitmap bitmap = gesture.toBitmap(128, 128, 10, gestureColor(R.color.showColor));
- imageView.setImageBitmap(bitmap);
-
- Builder dialogBuider = new AlertDialog.Builder(DrawGestureTest.this);
- dialogBuider.setView(dialogView);
-
- dialogBuider.setPositiveButton(
- "保存", new OnClickListener()
- {
-
- @Override
- public void onClick(DialogInterface dialog, int which)
- {
-
- sStore.addGesture(name, gesture);
-
- sStore.save();
- }
- });
-
- dialogBuider.setNegativeButton("取消", new OnClickListener()
- {
-
- @Override
- public void onClick(DialogInterface dialog, int which)
- {
-
-
- }
- });
-
- dialogBuider.show();
- }
-
- private int gestureColor(int resId)
- {
- return getResources().getColor(resId);
- }
-
- private void showMessage(String s)
- {
- Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
- }
-
- @Override
- protected void onDestroy()
- {
-
- super.onDestroy();
-
- mDrawGestureView.removeOnGesturePerformedListener(this);
- }
-
- }
main.xml的代码如下:
- xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns: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>
对话框对应的布局文件show_gesture.xml代码如下:
- xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:orientation="vertical" >
-
- <LinearLayout
- android:orientation="horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content">
-
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="8dip"
- android:text="@string/set_gesture_name"/>
- <EditText
- android:id="@+id/name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
-
- LinearLayout>
-
- <ImageView
- android:id="@+id/show"
- android:layout_gravity="center"
- android:layout_width="128dp"
- android:layout_height="128dp"
- android:layout_marginTop="10dp"/>
- LinearLayout>
通过上面Demo代码的实现,可以知道手势库创建保存手势的文件有以下四种方式:
1. GestureLibraries.fromFile(String path): GestureLibraries静态方法,参数path为文件的指定存放路径。返回的是FileGestureLibrary类型的对象;
2. GestureLibraries.fromPrivateFile(Context context, String name):GestureLibraries静态方法,参数path为文件的指定存放路径;返回的是FileGestureLibrary类型的对象;
3. GestureLibraries.fromFile(File path):GestureLibraries的静态方法,参数path为File对象,返回的是FileGestureLibrary类型的对象;
4. GestureLibraries.romRawResource(Contextcontext, int resourceId):GestureLibraries的静态方法, 参数resourceId为文件所在的资源id,返回的是ResourceGestureLibrary类型的对象;
二. 手势保存和加载源码浅析
在分析源码之前,我们先来看看有关涉及到手势保存和加载源码类之间的关系,如下图:
通过上图可以知道:
1. GestureLibrary为抽象类,ResourceGestureLibrary和FileGestureLibrary均继承它;
2. ResourceGestureLibrary和FileGestureLibrary又作为GestureLibraries的内部类;
3. GestureLibrary类中的save和load方法为抽象方法,它们的具体实现在子类ResourceGestureLibrary和FileGestureLibrary中;
通过上文Demo的介绍,我们知道,要想保持用户绘制的手势,前提是需要通过创建相应的手势库来实现;如下步骤:sStore = GestureLibraries.fromFile(mStoreFile)-->sStore.addGesture(name, gesture)-->sStore.save()
接下来根据上面的保存手势步骤来分析源码中的实现:
Step1: GestureLibraries.fromFile(mStoreFile):
- public final class GestureLibraries {
- ...
- public static GestureLibrary fromFile(File path) {
- return new FileGestureLibrary(path);
- }
- ...
- }
该方法返回的是FileGestureLibrary对象,FileGestureLibrary为GestureLibraries内部类;
FileGestureLibrary类的代码如下:
- public final class GestureLibraries {
- ...
- private static class FileGestureLibrary extends GestureLibrary {
- private final File mPath;
-
- public FileGestureLibrary(File path) {
- mPath = path;
- }
-
-
- @Override
- public boolean isReadOnly() {
- return !mPath.canWrite();
- }
-
- public boolean save() {
- if (!mStore.hasChanged()) return true;
-
- final File file = mPath;
-
- final File parentFile = file.getParentFile();
- if (!parentFile.exists()) {
- if (!parentFile.mkdirs()) {
- return false;
- }
- }
-
- boolean result = false;
- try {
-
- file.createNewFile();
-
- mStore.save(new FileOutputStream(file), true);
- result = true;
- } catch (FileNotFoundException e) {
- Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
- } catch (IOException e) {
- Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
- }
-
- return result;
- }
-
- public boolean load() {
- boolean result = false;
- final File file = mPath;
- if (file.exists() && file.canRead()) {
- try {
-
- mStore.load(new FileInputStream(file), true);
- result = true;
- } catch (FileNotFoundException e) {
- Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e);
- } catch (IOException e) {
- Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e);
- }
- }
-
- return result;
- }
- }
- ...
- }
FileGestureLibrary类中的代码实现简介:
1). isReadOnly():该方法实现判断所创建的保存手势文件是否可读;
2). save():实现保存手势的重要方法,在该方法中,实例化所创建文件的输出流,然后根据输出流调用GestureStore的save(OutputStream stream, Boolean closeStream)方法,然后将GestureStore得到的有关手势的信息通过输出流写入文件;
3). Load():该方法实现加载当前已保存手势的文件,当我们需要取出已保存的手势和当前手势进行相似度匹配时,就需要通过手势库加载之前保存的手势文件;
Step2: FileGestureLibrary类没有addGesture方法,所以sStore.addGesture(name, gesture)方法的实现应该在它的父类GestureLibrary中,代码如下:
- public abstract class GestureLibrary {
- ...
- protected final GestureStore mStore;
- ...
-
- public void addGesture(String entryName, Gesture gesture) {
- mStore.addGesture(entryName, gesture);
- }
- ...
- }
Step3: 接着调用到GestureStore中的addGesture方法,如下:
- public class GestureStore {
- ...
- private final HashMap> mNamedGestures =
- new HashMap>();
-
- private Learner mClassifier;
- ...
-
-
-
-
-
-
-
-
- public void addGesture(String entryName, Gesture gesture) {
- if (entryName == null || entryName.length() == 0) {
- return;
- }
- ArrayList gestures = mNamedGestures.get(entryName);
- if (gestures == null) {
- gestures = new ArrayList();
- mNamedGestures.put(entryName, gestures);
- }
- gestures.add(gesture);
-
- mClassifier.addInstance(
- Instance.createInstance(mSequenceType, mOrientationStyle, gesture, entryName));
- mChanged = true;
- }
- ...
- }
GestureStore的addGesture方法中代码实现如下:
1). 实现将用户绘制的手势存放到mNamedGestures(HashMap类型)中;
2). 通过用户绘制的gesture得到的Instance类型的对象(Instance.createInstance);
3). 将Instance类型的对象存放到mClassifier对象(Learner类型)的成员mInstances集合中;
Step4: 执行完sStore.addGesture(name, gesture)添加手势后,我们接着执行sStore.save()保存所添加的手势相关的信息。sStore.save()方法的实现在FileGestureLibrary中,代码如下:
- public final class GestureLibraries {
- ...
- private static class FileGestureLibrary extends GestureLibrary {
- private final File mPath;
- ...
- public boolean save() {
- if (!mStore.hasChanged()) return true;
-
- final File file = mPath;
-
- final File parentFile = file.getParentFile();
- if (!parentFile.exists()) {
- if (!parentFile.mkdirs()) {
- return false;
- }
- }
-
- boolean result = false;
- try {
-
- file.createNewFile();
-
- mStore.save(new FileOutputStream(file), true);
- result = true;
- } catch (FileNotFoundException e) {
- Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
- } catch (IOException e) {
- Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
- }
-
- return result;
- }
- ...
- }
- ...
- }
FileGestureLibrary的save方法中的代码实现:
1). 通过传进来的File对象创建其对应的输出流(new FileOutputStream(file))
2). 通过创建的输出流执行调用GestureStore的save方法(mStore.save(new FileOutputStream(file), true))
Step5: GestureStore的save方法代码实现如下:
- public class GestureStore {
- ...
- private static final short FILE_FORMAT_VERSION = 1;
- private final HashMap> mNamedGestures =
- new HashMap>();
- ...
- public void save(OutputStream stream, boolean closeStream) throws IOException {
- DataOutputStream out = null;
-
- try {
- long start;
- if (PROFILE_LOADING_SAVING) {
- start = SystemClock.elapsedRealtime();
- }
-
- final HashMap> maps = mNamedGestures;
-
- out = new DataOutputStream((stream instanceof BufferedOutputStream) ? stream :
- new BufferedOutputStream(stream, GestureConstants.IO_BUFFER_SIZE));
-
-
- out.writeShort(FILE_FORMAT_VERSION);
-
-
- out.writeInt(maps.size());
-
-
- for (Map.Entry> entry : maps.entrySet()) {
- final String key = entry.getKey();
- final ArrayList examples = entry.getValue();
- final int count = examples.size();
-
-
- out.writeUTF(key);
-
- out.writeInt(count);
-
-
- for (int i = 0; i < count; i++) {
- examples.get(i).serialize(out);
- }
- }
-
- out.flush();
-
- if (PROFILE_LOADING_SAVING) {
- long end = SystemClock.elapsedRealtime();
- Log.d(LOG_TAG, "Saving gestures library = " + (end - start) + " ms");
- }
-
- mChanged = false;
- } finally {
- if (closeStream) GestureUtils.closeStream(out);
- }
- }
- ...
- }
GestureStore的save方法中代码实现如下:
1). 将执行Step3中得到的mNamedGestures赋值给maps;
2). 通过传进来的输出流创建对应的DataOutputStream类型对象out;
3). 将FILE_FORMAT_VERSION和maps.size()写入out中;
4). 遍历maps,将遍历出的每个ArrayList在maps中的key值和自身存放Gesture的个数count值,分别写入out中;
5). 遍历ArrayList中的Gesture,然后将out作为实参调用执行Gesture的serialize方法;
Step6:继续跟踪到 Gesture的serialize方法,代码如下:
- public class Gesture implements Parcelable {
- ...
- private long mGestureID;
- private final ArrayList mStrokes = new ArrayList();
- ...
- public Gesture() {
- mGestureID = GESTURE_ID_BASE + sGestureCount.incrementAndGet();
- }
- ...
- void serialize(DataOutputStream out) throws IOException {
- final ArrayList strokes = mStrokes;
- final int count = strokes.size();
-
-
- out.writeLong(mGestureID);
-
- out.writeInt(count);
-
-
-
-
- for (int i = 0; i < count; i++) {
- strokes.get(i).serialize(out);
- }
- }
- ...
- }
Gesture的serialize方法中代码实现如下:
1). 将Gesture对应的mStrokes赋值给strokes;
2). 将Gesture的mGestureID和GestureStroke在strokes中的个数count分别写入DataOutputStream类型的对象out;
3). 遍历strokes中的GestureStroke,然后将out作为实参调用执行GestureStroke的serialize方法;
Step7: 继续跟踪到 GestureStroke的serialize方法,代码如下:
- public class GestureStroke {
- ...
- public final float[] points;
- private final long[] timestamps;
- ...
- void serialize(DataOutputStream out) throws IOException {
-
- final float[] pts = points;
- final long[] times = timestamps;
- final int count = points.length;
-
-
- out.writeInt(count / 2);
-
- for (int i = 0; i < count; i += 2) {
-
- out.writeFloat(pts[i]);
-
- out.writeFloat(pts[i + 1]);
-
- out.writeLong(times[i / 2]);
- }
- }
- ...
- }
GestureStroke的serialize方法中代码实现如下:
1). 将GestureStroke中对应的点数组points和时间戳数组timestamps分别赋值给数组pts和times
2). 将GestureStroke中组成手势的点数count / 2写入DataOutputStream类型的对象out;(pts数组中每两个元素保存一个点对应的x,y值,所以,总点数为数组所有元素个数count除以2)
3). 遍历数组pts,将每个点对应的x,y轴坐标值和时间戳分别写入out;
关于手势保存源码的浅析就到此结束了,至于手势的加载sStore.load(),其实和手势的保存就是一个逆过程(一个是写入信息,一个读取加载信息)。如果熟悉了手势的保存机制,那么手势的加载机制就不言而喻了!
Android手势源码浅析----手势识别
前言:手势识别在Android手势中是最重要的部分,基本上算是手势的精髓;手势识别的算法有点类似人脸识别;手势识别的利用很普遍,涉及到用户安全操作的领域也比较多;比如可以通过手势识别来实现手机的解锁,安全启动用户设置的用户模式应用等;
一. 基于第三方开发的手势识别
一般情况下,如果需要将用户当前绘制手势和已保存的手势进行匹配。那么,在用户绘制完当前手势时就可以进行匹配处理。这个过程可以在手势监听器GesturePerformedListener的onGesturePerformed方法中处理。
如下示例代码片段:
- ...
- @Override
- public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture)
- {
-
- final GestureLibrary store = MainActivity.getStore();
- store.setOrientationStyle(4);
-
- ArrayList predictions = store.recognize(gesture);
-
- for(Prediction pred : predictions)
- {
-
- if (pred.score > 2.0)
- {
-
- Log.d("RecogniseGesture", "name-->" + pred.name);
- startApp(pred.name);
- return;
- }
- else
- {
-
- new AlertDialog.Builder(RecogniseGesture.this).setMessage("不存在该手势").
- setPositiveButton("确定", null).show();
- }
- }
-
- }
- ...
通过上面的代码片段可知,调用GestureLibrary的recognize对当前用户绘制的手势进行匹配操作,recognize的参数gesture对应的是当前用户绘制的手势。recognize方法返回类型为ArrayList集合,该集合中的元素Prediction类的源代码如下:
- package android.gesture;
-
- public class Prediction {
- public final String name;
-
- public double score;
-
- Prediction(String label, double predictionScore) {
- name = label;
- score = predictionScore;
- }
-
- @Override
- public String toString() {
- return name;
- }
- }
Prediction的属性name为被匹配的手势名字,score为手势的匹配分数(匹配分数越高,说明对手势的相似度匹配要求就越高)。所以,通过调用GestureLibrary的recognize方法返回的Prediction,就可以知道当前手势和已保存的手势匹配的相似度。
二. 手势匹配源码实现
在分析手势匹配源码实现之前,先总体来看看有关涉及到手势匹配相关的源码类之间的关系,如下图:
上图中的相关类简介:
GestureLibrary:手势库类,对外提供recognize函数,是实现手势匹配的关键入口。
GestureStore:GestureLibrary的recognize函数真正的源码内部实现类。
Instance:封装手势通过时间采样或空间采样后的离散点。该类的vector属性描述的就是当前被采样后的手势对应的多个离散点(这些点是有方向的,所以又可以称为向量)。
Learner:抽象类,内部提供了对Instance进行添加、获取、移除操作的方法。同时提供了抽象方法classify。
InstanceLearner:继承Learner,覆盖实现Learner中的抽象方法classify。
GestureUtils:手势源码中的工具类。手势匹配源码实现,基本上最终都会通过调用该工具类中提供的方法来实现。
GestureUtils类中相关的重点方法实现介绍:
1). spatialSampling:对连续点进行空间采样(gesture由多个连续点组成)
2). temporalSampling: 对连续点进行时间采样(gesture由多个连续点组成)
3). computeCentroid:计算一组点的质心
4). computeCoVariance: 计算一组点的方差-协方差矩阵
5). computeTotalLength: 计算一组点的总长度
6). computeStraightness: 计算一组点的直线度
7). squaredEuclideanDistance:计算两个向量之间的平方欧式距离
8). cosineDistance: 计算两个向量之间的余弦值,返回的是0到π之间的值
9). minimumCosineDistance: 计算两个向量之前最小的余弦距离,参数vector1为之前保存的向量,vector2为当前输入的向量。
10). computeOrientedBoundingBox:计算一个点集的最小边界框
11). rotate: 旋转一组点
12). translate: 移动一组点
13). scale: 缩放一组点
在前篇文章《手势的保存和加载》中,可以知道,用户绘制的手势是通过调用GestureLibrary的addGesture添加到手势库中,而该函数最终是通过调用GestureStore对象的addGesture来实现的。
回顾一下GestureStore类中的addGesture方法实现代码,如下:
- public class GestureStore {
- ...
- private final HashMap> mNamedGestures =
- new HashMap>();
-
- private Learner mClassifier;
-
- private boolean mChanged = false;
-
- public GestureStore() {
- mClassifier = new InstanceLearner();
- }
-
- public void addGesture(String entryName, Gesture gesture) {
- if (entryName == null || entryName.length() == 0) {
- return;
- }
- ArrayList gestures = mNamedGestures.get(entryName);
- if (gestures == null) {
- gestures = new ArrayList();
- mNamedGestures.put(entryName, gestures);
- }
- gestures.add(gesture);
-
- mClassifier.addInstance(
- Instance.createInstance(mSequenceType, mOrientationStyle, gesture, entryName));
- mChanged = true;
- }
- ...
- }
在addGesture方法中:
Step1. 根据保存的手势及手势名字通过调用Instance的静态方法createInstance,创建对应的Instance对象(参数),然后将创建的Instance对象通过调用Learner的addInstance方法保存到Learner的成员变量mInstance集合中(ArrayList类型)。
Step2. 因此,Learner中的mInstance集合将保存着各手势对应的Instance对象。这样的话,在进行手势匹配时,就可以通过执行Learner的getInstances方法取出保存手势对应的Instance对象,然后将保存的Instance对象和当前绘制的手势创建的Instance对象进行匹配;
对当前手势进行匹配是通过调用GestureLibrary的recognize方法实现的,该法返回描述匹配相似度的ArrayList集合。接下来对该方法的源码实现进行分析;
--->GestureLibrary的recognize方法实现代码如下:
- public abstract class GestureLibrary {
- protected final GestureStore mStore;
-
- protected GestureLibrary() {
- mStore = new GestureStore();
- }
- ...
- public ArrayList recognize(Gesture gesture) {
- return mStore.recognize(gesture);
- }
- ...
- }
通过上面代码可知,GestureLibrary的recognize方法通过调用GestureStore对象的recognize方法来实现的。
--->GestureStore的recognize方法实现代码如下:
- public class GestureStore {
- ...
- public static final int SEQUENCE_SENSITIVE = 2;
- ...
- public static final int ORIENTATION_SENSITIVE = 2;
- ...
- private int mSequenceType = SEQUENCE_SENSITIVE;
- private int mOrientationStyle = ORIENTATION_SENSITIVE;
- ...
- private Learner mClassifier;
- ...
- public GestureStore() {
- mClassifier = new InstanceLearner();
- }
- ...
- public ArrayList recognize(Gesture gesture) {
-
- Instance instance = Instance.createInstance(mSequenceType,
- mOrientationStyle, gesture, null);
-
- return mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector);
- }
- ...
- }
在GestureStore的recognize方法中:
Step1.根据得到的mSequenceType值(默认值为SEQUENCE_SENSITIVE)、mOrientationStyle值(默认值为ORIENTATION_SENSITIVE)、gesture对象(当前手势),通过调用Instance的静态方法createInstance创建相应的Instance对象instance。
Step2. 根据得到的mSequenceType值值、mOrientationStyle值、instance对象的属性vector,通过调用InstanceLearner对象的classify方法,方法返回的是ArrayList类型。
--->Instance的静态方法createInstance代码实现如下:
- class Instance {
- ...
-
-
-
-
-
-
-
- static Instance createInstance(int sequenceType, int orientationType, Gesture gesture, String label) {
- float[] pts;
- Instance instance;
- if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {
-
- pts = temporalSampler(orientationType, gesture);
- instance = new Instance(gesture.getID(), pts, label);
-
- instance.normalize();
- } else {
-
- pts = spatialSampler(gesture);
- instance = new Instance(gesture.getID(), pts, label);
- }
- return instance;
- }
- ...
- }
在Instance的静态方法createInstance中:
Step1.根据得到的gesture中的第一个GestureStroke对象(一个Gesture对象由单个或多个GestureStroke组成)、SEQUENCE_SAMPLE_SIZE值(默认值为16),通过调用GestureUtils的静态方法temporalSampling将GestureStroke对象中封装的多个连续点进行时间采样成多个离散点。然后将得到的多个离散点赋值给浮点型数组pts。
Step2.对通过时间采样得到的多个离散点进行一些处理操作(计算质心、移动、旋转等),然后将这些通过处理的离散点返回给Instance的createInstance静态方法中的局部变量pts。最终,通过执行temporalSampler(orientationType, gesture)后,局部变量pts存放的是,当前用户绘制的手势中的第一个GestureStroke(单笔画识别)对象经过时间采样生成的多个离散点。
Step3.执行完temporalSampler(orientationType, gesture)得到pts后,接着,根据得到的当前手势的ID、pts、label(为空)创建Instance对象instance。在Instance的构造函数中,会将ID、pts、label分别赋值给Instance的属性id、vector、label。所以instance. vector即为当前用户绘制的手势经过时间采样后的离散点。
Step4.在执行创建Instance对象后,通过调用Instance对象的normalize对离散点进行正常化。
到此,Instance的temporalSampler方法就执行完了,接着会返回创建得到的Instance对象instance。
--->回到GestureStore的recognize方法中:
在执行完Instance的静态方法createInstance返回得到的instance对象后,接着继续执行mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector),instance.vector就是已经经过时间采样或空间采样后得到的当前手势对应的多个离散点。mClassifier为InstanceLearner对象,InstanceLearner的classify方法代码实现如下:
- class InstanceLearner extends Learner {
- ...
-
- @Override
- ArrayList classify(int sequenceType, int orientationType, float[] vector) {
- ArrayList predictions = new ArrayList();
- ArrayList instances = getInstances();
- int count = instances.size();
- TreeMap label2score = new TreeMap();
- for (int i = 0; i < count; i++) {
-
- Instance sample = instances.get(i);
- if (sample.vector.length != vector.length) {
- continue;
- }
- double distance;
- if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {
-
- distance = GestureUtils.minimumCosineDistance(sample.vector, vector, orientationType);
- } else {
-
-
-
- distance = GestureUtils.squaredEuclideanDistance(sample.vector, vector);
- }
- double weight;
- if (distance == 0) {
- weight = Double.MAX_VALUE;
- } else {
- weight = 1 / distance;
- }
- Double score = label2score.get(sample.label);
- if (score == null || weight > score) {
- label2score.put(sample.label, weight);
- }
- }
-
-
- for (String name : label2score.keySet()) {
- double score = label2score.get(name);
-
- predictions.add(new Prediction(name, score));
- }
-
-
-
-
-
-
- Collections.sort(predictions, sComparator);
-
- return predictions;
- }
- ...
- }
InstanceLearner的classify中做了如下处理:
Step1.创建ArrayList集合对象predictions。获取保存在手势库中的所有Instance对象instances(ArrayList集合)。创建TreeMap类型映射表label2score,以保存手势的名字为键值,对应保存当前手势与保存手势之间的匹配分数score。
Step2. 遍历手势库中已保存的instances集合中的每个Instance对象,将遍历出的每个Instance对象中封装的vector和当前Instance对象的vector(classify方法传进来的实参instance.vector)进行处理。
Step3. 在遍历instances的处理中,当classify方法传进来的sequenceType为GestureStore.SEQUENCE_SENSITIVE(默认值)时,则根据遍历出的每个Instance对象中封装的vector和当前Instance对象的vector,调用GestureUtils的minimumCosineDistance方法,计算这两个vector的最小的余弦值。minimumCosineDistance返回distance(double类型)。
Step4. 在Step3中,当sequenceType不为GestureStore.SEQUENCE_SENSITIVE时,则调用GestureUtils的squaredEuclideanDistance方法,计算这两个vector平方欧氏距离。squaredEuclideanDistance返回distance(double类型)。
Step4. 通过Step3或Step4得到distance转化为手势匹配分数score(权重weight = 1/distance即为score), 然后将其以对应被遍历的Instance对象的名字(即已保存的某一手势对应的名字)为键值,保存到集合label2score中。
Step5. 遍历完instances,得到label2score后,接着将label2score保存的信息进行遍历,将根据遍历得到的每个key值name和value值score,创建对应的Prediction对象,然后将创建得到的Prediction对象添加到集合predictions中。
Step6. 对predictions集合中的内容进行排序,然后返回predictions。
所以,调用GestureLibrary的recognize方法进行手势匹配操作,最终返回的是ArrayList集合predictions。遍历predictions中的Prediction对象,通过Prediction对象的score值就可以知道当前手势和已保存手势之间的匹配相似度(即手势匹配分数)。
转自CSDN博客 stevenhu_223博主,感谢博主的源码分析!