1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.view; 18 19import android.content.Context; 20import android.util.DisplayMetrics; 21import android.util.FloatMath; 22 23/** 24 * Detects transformation gestures involving more than one pointer ("multitouch") 25 * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} 26 * callback will notify users when a particular gesture event has occurred. 27 * This class should only be used with {@link MotionEvent}s reported via touch. 28 * 29 * To use this class: 30 * <ul> 31 * <li>Create an instance of the {@code ScaleGestureDetector} for your 32 * {@link View} 33 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 34 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 35 * callback will be executed when the events occur. 36 * </ul> 37 */ 38public class ScaleGestureDetector { 39 /** 40 * The listener for receiving notifications when gestures occur. 41 * If you want to listen for all the different gestures then implement 42 * this interface. If you only want to listen for a subset it might 43 * be easier to extend {@link SimpleOnScaleGestureListener}. 44 * 45 * An application will receive events in the following order: 46 * <ul> 47 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 48 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 49 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 50 * </ul> 51 */ 52 public interface OnScaleGestureListener { 53 /** 54 * Responds to scaling events for a gesture in progress. 55 * Reported by pointer motion. 56 * 57 * @param detector The detector reporting the event - use this to 58 * retrieve extended info about event state. 59 * @return Whether or not the detector should consider this event 60 * as handled. If an event was not handled, the detector 61 * will continue to accumulate movement until an event is 62 * handled. This can be useful if an application, for example, 63 * only wants to update scaling factors if the change is 64 * greater than 0.01. 65 */ 66 public boolean onScale(ScaleGestureDetector detector); 67 68 /** 69 * Responds to the beginning of a scaling gesture. Reported by 70 * new pointers going down. 71 * 72 * @param detector The detector reporting the event - use this to 73 * retrieve extended info about event state. 74 * @return Whether or not the detector should continue recognizing 75 * this gesture. For example, if a gesture is beginning 76 * with a focal point outside of a region where it makes 77 * sense, onScaleBegin() may return false to ignore the 78 * rest of the gesture. 79 */ 80 public boolean onScaleBegin(ScaleGestureDetector detector); 81 82 /** 83 * Responds to the end of a scale gesture. Reported by existing 84 * pointers going up. 85 * 86 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 87 * and {@link ScaleGestureDetector#getFocusY()} will return the location 88 * of the pointer remaining on the screen. 89 * 90 * @param detector The detector reporting the event - use this to 91 * retrieve extended info about event state. 92 */ 93 public void onScaleEnd(ScaleGestureDetector detector); 94 } 95 96 /** 97 * A convenience class to extend when you only want to listen for a subset 98 * of scaling-related events. This implements all methods in 99 * {@link OnScaleGestureListener} but does nothing. 100 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns 101 * {@code false} so that a subclass can retrieve the accumulated scale 102 * factor in an overridden onScaleEnd. 103 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns 104 * {@code true}. 105 */ 106 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { 107 108 public boolean onScale(ScaleGestureDetector detector) { 109 return false; 110 } 111 112 public boolean onScaleBegin(ScaleGestureDetector detector) { 113 return true; 114 } 115 116 public void onScaleEnd(ScaleGestureDetector detector) { 117 // Intentionally empty 118 } 119 } 120 121 /** 122 * This value is the threshold ratio between our previous combined pressure 123 * and the current combined pressure. We will only fire an onScale event if 124 * the computed ratio between the current and previous event pressures is 125 * greater than this value. When pressure decreases rapidly between events 126 * the position values can often be imprecise, as it usually indicates 127 * that the user is in the process of lifting a pointer off of the device. 128 * Its value was tuned experimentally. 129 */ 130 private static final float PRESSURE_THRESHOLD = 0.67f; 131 132 private final Context mContext; 133 private final OnScaleGestureListener mListener; 134 private boolean mGestureInProgress; 135 136 private MotionEvent mPrevEvent; 137 private MotionEvent mCurrEvent; 138 139 private float mFocusX; 140 private float mFocusY; 141 private float mPrevFingerDiffX; 142 private float mPrevFingerDiffY; 143 private float mCurrFingerDiffX; 144 private float mCurrFingerDiffY; 145 private float mCurrLen; 146 private float mPrevLen; 147 private float mScaleFactor; 148 private float mCurrPressure; 149 private float mPrevPressure; 150 private long mTimeDelta; 151 152 private final float mEdgeSlop; 153 private float mRightSlopEdge; 154 private float mBottomSlopEdge; 155 private boolean mSloppyGesture; 156 157 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 158 ViewConfiguration config = ViewConfiguration.get(context); 159 mContext = context; 160 mListener = listener; 161 mEdgeSlop = config.getScaledEdgeSlop(); 162 } 163 164 public boolean onTouchEvent(MotionEvent event) { 165 final int action = event.getAction(); 166 boolean handled = true; 167 168 if (!mGestureInProgress) { 169 switch (action & MotionEvent.ACTION_MASK) { 170 case MotionEvent.ACTION_POINTER_DOWN: { 171 // We have a new multi-finger gesture 172 173 // as orientation can change, query the metrics in touch down 174 DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); 175 mRightSlopEdge = metrics.widthPixels - mEdgeSlop; 176 mBottomSlopEdge = metrics.heightPixels - mEdgeSlop; 177 178 // Be paranoid in case we missed an event 179 reset(); 180 181 mPrevEvent = MotionEvent.obtain(event); 182 mTimeDelta = 0; 183 184 setContext(event); 185 186 // Check if we have a sloppy gesture. If so, delay 187 // the beginning of the gesture until we're sure that's 188 // what the user wanted. Sloppy gestures can happen if the 189 // edge of the user's hand is touching the screen, for example. 190 final float edgeSlop = mEdgeSlop; 191 final float rightSlop = mRightSlopEdge; 192 final float bottomSlop = mBottomSlopEdge; 193 final float x0 = event.getRawX(); 194 final float y0 = event.getRawY(); 195 final float x1 = getRawX(event, 1); 196 final float y1 = getRawY(event, 1); 197 198 boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop 199 || x0 > rightSlop || y0 > bottomSlop; 200 boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop 201 || x1 > rightSlop || y1 > bottomSlop; 202 203 if (p0sloppy && p1sloppy) { 204 mFocusX = -1; 205 mFocusY = -1; 206 mSloppyGesture = true; 207 } else if (p0sloppy) { 208 mFocusX = event.getX(1); 209 mFocusY = event.getY(1); 210 mSloppyGesture = true; 211 } else if (p1sloppy) { 212 mFocusX = event.getX(0); 213 mFocusY = event.getY(0); 214 mSloppyGesture = true; 215 } else { 216 mGestureInProgress = mListener.onScaleBegin(this); 217 } 218 } 219 break; 220 221 case MotionEvent.ACTION_MOVE: 222 if (mSloppyGesture) { 223 // Initiate sloppy gestures if we've moved outside of the slop area. 224 final float edgeSlop = mEdgeSlop; 225 final float rightSlop = mRightSlopEdge; 226 final float bottomSlop = mBottomSlopEdge; 227 final float x0 = event.getRawX(); 228 final float y0 = event.getRawY(); 229 final float x1 = getRawX(event, 1); 230 final float y1 = getRawY(event, 1); 231 232 boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop 233 || x0 > rightSlop || y0 > bottomSlop; 234 boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop 235 || x1 > rightSlop || y1 > bottomSlop; 236 237 if(p0sloppy && p1sloppy) { 238 mFocusX = -1; 239 mFocusY = -1; 240 } else if (p0sloppy) { 241 mFocusX = event.getX(1); 242 mFocusY = event.getY(1); 243 } else if (p1sloppy) { 244 mFocusX = event.getX(0); 245 mFocusY = event.getY(0); 246 } else { 247 mSloppyGesture = false; 248 mGestureInProgress = mListener.onScaleBegin(this); 249 } 250 } 251 break; 252 253 case MotionEvent.ACTION_POINTER_UP: 254 if (mSloppyGesture) { 255 // Set focus point to the remaining finger 256 int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) 257 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0; 258 mFocusX = event.getX(id); 259 mFocusY = event.getY(id); 260 } 261 break; 262 } 263 } else { 264 // Transform gesture in progress - attempt to handle it 265 switch (action & MotionEvent.ACTION_MASK) { 266 case MotionEvent.ACTION_POINTER_UP: 267 // Gesture ended 268 setContext(event); 269 270 // Set focus point to the remaining finger 271 int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) 272 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0; 273 mFocusX = event.getX(id); 274 mFocusY = event.getY(id); 275 276 if (!mSloppyGesture) { 277 mListener.onScaleEnd(this); 278 } 279 280 reset(); 281 break; 282 283 case MotionEvent.ACTION_CANCEL: 284 if (!mSloppyGesture) { 285 mListener.onScaleEnd(this); 286 } 287 288 reset(); 289 break; 290 291 case MotionEvent.ACTION_MOVE: 292 setContext(event); 293 294 // Only accept the event if our relative pressure is within 295 // a certain limit - this can help filter shaky data as a 296 // finger is lifted. 297 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 298 final boolean updatePrevious = mListener.onScale(this); 299 300 if (updatePrevious) { 301 mPrevEvent.recycle(); 302 mPrevEvent = MotionEvent.obtain(event); 303 } 304 } 305 break; 306 } 307 } 308 return handled; 309 } 310 311 /** 312 * MotionEvent has no getRawX(int) method; simulate it pending future API approval. 313 */ 314 private static float getRawX(MotionEvent event, int pointerIndex) { 315 float offset = event.getRawX() - event.getX(); 316 return event.getX(pointerIndex) + offset; 317 } 318 319 /** 320 * MotionEvent has no getRawY(int) method; simulate it pending future API approval. 321 */ 322 private static float getRawY(MotionEvent event, int pointerIndex) { 323 float offset = event.getRawY() - event.getY(); 324 return event.getY(pointerIndex) + offset; 325 } 326 327 private void setContext(MotionEvent curr) { 328 if (mCurrEvent != null) { 329 mCurrEvent.recycle(); 330 } 331 mCurrEvent = MotionEvent.obtain(curr); 332 333 mCurrLen = -1; 334 mPrevLen = -1; 335 mScaleFactor = -1; 336 337 final MotionEvent prev = mPrevEvent; 338 339 final float px0 = prev.getX(0); 340 final float py0 = prev.getY(0); 341 final float px1 = prev.getX(1); 342 final float py1 = prev.getY(1); 343 final float cx0 = curr.getX(0); 344 final float cy0 = curr.getY(0); 345 final float cx1 = curr.getX(1); 346 final float cy1 = curr.getY(1); 347 348 final float pvx = px1 - px0; 349 final float pvy = py1 - py0; 350 final float cvx = cx1 - cx0; 351 final float cvy = cy1 - cy0; 352 mPrevFingerDiffX = pvx; 353 mPrevFingerDiffY = pvy; 354 mCurrFingerDiffX = cvx; 355 mCurrFingerDiffY = cvy; 356 357 mFocusX = cx0 + cvx * 0.5f; 358 mFocusY = cy0 + cvy * 0.5f; 359 mTimeDelta = curr.getEventTime() - prev.getEventTime(); 360 mCurrPressure = curr.getPressure(0) + curr.getPressure(1); 361 mPrevPressure = prev.getPressure(0) + prev.getPressure(1); 362 } 363 364 private void reset() { 365 if (mPrevEvent != null) { 366 mPrevEvent.recycle(); 367 mPrevEvent = null; 368 } 369 if (mCurrEvent != null) { 370 mCurrEvent.recycle(); 371 mCurrEvent = null; 372 } 373 mSloppyGesture = false; 374 mGestureInProgress = false; 375 } 376 377 /** 378 * Returns {@code true} if a two-finger scale gesture is in progress. 379 * @return {@code true} if a scale gesture is in progress, {@code false} otherwise. 380 */ 381 public boolean isInProgress() { 382 return mGestureInProgress; 383 } 384 385 /** 386 * Get the X coordinate of the current gesture's focal point. 387 * If a gesture is in progress, the focal point is directly between 388 * the two pointers forming the gesture. 389 * If a gesture is ending, the focal point is the location of the 390 * remaining pointer on the screen. 391 * If {@link #isInProgress()} would return false, the result of this 392 * function is undefined. 393 * 394 * @return X coordinate of the focal point in pixels. 395 */ 396 public float getFocusX() { 397 return mFocusX; 398 } 399 400 /** 401 * Get the Y coordinate of the current gesture's focal point. 402 * If a gesture is in progress, the focal point is directly between 403 * the two pointers forming the gesture. 404 * If a gesture is ending, the focal point is the location of the 405 * remaining pointer on the screen. 406 * If {@link #isInProgress()} would return false, the result of this 407 * function is undefined. 408 * 409 * @return Y coordinate of the focal point in pixels. 410 */ 411 public float getFocusY() { 412 return mFocusY; 413 } 414 415 /** 416 * Return the current distance between the two pointers forming the 417 * gesture in progress. 418 * 419 * @return Distance between pointers in pixels. 420 */ 421 public float getCurrentSpan() { 422 if (mCurrLen == -1) { 423 final float cvx = mCurrFingerDiffX; 424 final float cvy = mCurrFingerDiffY; 425 mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy); 426 } 427 return mCurrLen; 428 } 429 430 /** 431 * Return the previous distance between the two pointers forming the 432 * gesture in progress. 433 * 434 * @return Previous distance between pointers in pixels. 435 */ 436 public float getPreviousSpan() { 437 if (mPrevLen == -1) { 438 final float pvx = mPrevFingerDiffX; 439 final float pvy = mPrevFingerDiffY; 440 mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy); 441 } 442 return mPrevLen; 443 } 444 445 /** 446 * Return the scaling factor from the previous scale event to the current 447 * event. This value is defined as 448 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 449 * 450 * @return The current scaling factor. 451 */ 452 public float getScaleFactor() { 453 if (mScaleFactor == -1) { 454 mScaleFactor = getCurrentSpan() / getPreviousSpan(); 455 } 456 return mScaleFactor; 457 } 458 459 /** 460 * Return the time difference in milliseconds between the previous 461 * accepted scaling event and the current scaling event. 462 * 463 * @return Time difference since the last scaling event in milliseconds. 464 */ 465 public long getTimeDelta() { 466 return mTimeDelta; 467 } 468 469 /** 470 * Return the event time of the current event being processed. 471 * 472 * @return Current event time in milliseconds. 473 */ 474 public long getEventTime() { 475 return mCurrEvent.getEventTime(); 476 } 477} 478