前言: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;
- /** Called when the activity is first created. */
- @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);
- /*手势绘制完成后淡出屏幕的时间间隔,即绘制完手指离开屏幕后相隔多长时间手势从屏幕上消失;
- * 可以理解为手势绘制完成手指离开屏幕后到调用onGesturePerformed的时间间隔
- * 默认值为420毫秒,这里设置为2秒
- */
- mDrawGestureView.setFadeOffset(2000);
- //绑定监听器
- mDrawGestureView.addOnGesturePerformedListener(this);
- mDrawGestureView.addOnGesturingListener(this);
- }
- //手势绘制完成时调用
- @Override
- public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture)
- {
- // TODO Auto-generated method stub
- showMessage("手势绘制完成");
- }
- private int gestureColor(int resId)
- {
- return getResources().getColor(resId);
- }
- private void showMessage(String s)
- {
- Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
- }
- //结束正在绘制手势时调用(手势绘制完成时一般是先调用它在调用onGesturePerformed)
- @Override
- public void onGesturingEnded(GestureOverlayView overlay)
- {
- // TODO Auto-generated method stub
- showMessage("结束正在绘制手势");
- }
- //正在绘制手势时调用
- @Override
- public void onGesturingStarted(GestureOverlayView overlay)
- {
- // TODO Auto-generated method stub
- showMessage("正在绘制手势");
- }
- @Override
- protected void onDestroy()
- {
- // TODO Auto-generated method stub
- 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 there is fading out going on, stop it.
- //如果手势已正在淡出,则停止它
- 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);
- //mInvalidateExtraBorder值由设置手势画笔粗细值决定
- final int border = mInvalidateExtraBorder;
- mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
- mCurveEndX = x;
- mCurveEndY = y;
- // pass the event to handlers
- final ArrayList<OnGestureListener> 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);
- //手势在屏幕滑动的两点之间的距离大于GestureStroke.TOUCH_TOLERANCE的值,则显示手势的绘制
- if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
- areaToRefresh = mInvalidRect;
- // start with the curve end
- 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;
- //二次贝塞尔,实现平滑曲线;previousX, previousY为操作点,cX, cY为终点
- mPath.quadTo(previousX, previousY, cX, cY);
- // union with the control point of the new curve
- /*areaToRefresh矩形扩大了border(宽和高扩大了两倍border),
- * border值由设置手势画笔粗细值决定
- */
- areaToRefresh.union((int) previousX - border, (int) previousY - border,
- (int) previousX + border, (int) previousY + border);
- // union with the end point of the new curve
- 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()));
- //当调用addOnGesturePerformedListener添加手势完成调用的监听器时,mHandleGestureActions为true;
- 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<OnGesturingListener> listeners = mOnGesturingListeners;
- int count = listeners.size();
- for (int i = 0; i < count; i++) {
- listeners.get(i).onGesturingStarted(this);
- }
- }
- }
- }
- // pass the event to handlers
- final ArrayList<OnGestureListener> 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;
- // A gesture wasn't started or was cancelled
- if (mCurrentGesture != null) {
- // add the stroke to the current gesture
- /*将之前调用touchDonw和touchMove收集得到GesturePoint的组成的数组集合mStrokeBuffer,
- * 做为GestureStroke构造函数的实参创建GestureStroke对象,
- * 然后将GestureStroke对象通过调用addStroke方法添加到mCurrentGesture中
- */
- mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
- if (!cancel) {
- // pass the event to handlers
- final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
- int count = listeners.size();
- for (int i = 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;
- final ArrayList<OnGesturingListener> 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) { //调用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();
- } 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类,该类的实现代码如下:
- ...
- /*处理手势淡出;
- * 手势淡出的条件:
- * 1.前面一次画完手势,且画完的同时没有调用onGesturePerformed,
- * 则当用户再次画手势时,前面画出的保留在屏幕上的手势将淡出;
- * 2.当画完手势,且添加OnGesturePerformedListener监听器时,
- * 在完成手势,调用onGesturePerformed时,将手势轨迹画笔淡出
- */
- private class FadeOutRunnable implements Runnable {
- //调用addOnGesturePerformedListener时为true;
- boolean fireActionPerformed;
- //手势设置为多笔画绘制时为true;
- boolean resetMultipleStrokes;
- public void run() {
- if (mIsFadingOut) { //fireActionPerformed为true且mCurrentGesture不为空是成立
- final long now = AnimationUtils.currentAnimationTimeMillis();
- final long duration = 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;
- float interpolatedTime = 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);
- }
- } else if (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,代码如下:
- ...
- private void fireOnGesturePerformed() {
- final ArrayList<OnGesturePerformedListener> 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的浅析就结束了!
借鉴:http://blog.csdn.net/stevenhu_223/article/details/9394491
本文转自 一点点征服 博客园博客,原文链接:http://www.cnblogs.com/ldq2016/p/5290933.html,如需转载请自行联系原作者