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 17 package android.view; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.os.SystemClock; 22 import android.util.FloatMath; 23 24 /** 25 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s. 26 * The {@link OnScaleGestureListener} callback will notify users when a particular 27 * gesture event has occurred. 28 * 29 * This class should only be used with {@link MotionEvent}s reported via touch. 30 * 31 * To use this class: 32 * <ul> 33 * <li>Create an instance of the {@code ScaleGestureDetector} for your 34 * {@link View} 35 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 36 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 37 * callback will be executed when the events occur. 38 * </ul> 39 */ 40 public class ScaleGestureDetector { 41 private static final String TAG = "ScaleGestureDetector"; 42 43 /** 44 * The listener for receiving notifications when gestures occur. 45 * If you want to listen for all the different gestures then implement 46 * this interface. If you only want to listen for a subset it might 47 * be easier to extend {@link SimpleOnScaleGestureListener}. 48 * 49 * An application will receive events in the following order: 50 * <ul> 51 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 52 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 53 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 54 * </ul> 55 */ 56 public interface OnScaleGestureListener { 57 /** 58 * Responds to scaling events for a gesture in progress. 59 * Reported by pointer motion. 60 * 61 * @param detector The detector reporting the event - use this to 62 * retrieve extended info about event state. 63 * @return Whether or not the detector should consider this event 64 * as handled. If an event was not handled, the detector 65 * will continue to accumulate movement until an event is 66 * handled. This can be useful if an application, for example, 67 * only wants to update scaling factors if the change is 68 * greater than 0.01. 69 */ 70 public boolean onScale(ScaleGestureDetector detector); 71 72 /** 73 * Responds to the beginning of a scaling gesture. Reported by 74 * new pointers going down. 75 * 76 * @param detector The detector reporting the event - use this to 77 * retrieve extended info about event state. 78 * @return Whether or not the detector should continue recognizing 79 * this gesture. For example, if a gesture is beginning 80 * with a focal point outside of a region where it makes 81 * sense, onScaleBegin() may return false to ignore the 82 * rest of the gesture. 83 */ 84 public boolean onScaleBegin(ScaleGestureDetector detector); 85 86 /** 87 * Responds to the end of a scale gesture. Reported by existing 88 * pointers going up. 89 * 90 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 91 * and {@link ScaleGestureDetector#getFocusY()} will return focal point 92 * of the pointers remaining on the screen. 93 * 94 * @param detector The detector reporting the event - use this to 95 * retrieve extended info about event state. 96 */ 97 public void onScaleEnd(ScaleGestureDetector detector); 98 } 99 100 /** 101 * A convenience class to extend when you only want to listen for a subset 102 * of scaling-related events. This implements all methods in 103 * {@link OnScaleGestureListener} but does nothing. 104 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns 105 * {@code false} so that a subclass can retrieve the accumulated scale 106 * factor in an overridden onScaleEnd. 107 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns 108 * {@code true}. 109 */ 110 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { 111 112 public boolean onScale(ScaleGestureDetector detector) { 113 return false; 114 } 115 116 public boolean onScaleBegin(ScaleGestureDetector detector) { 117 return true; 118 } 119 120 public void onScaleEnd(ScaleGestureDetector detector) { 121 // Intentionally empty 122 } 123 } 124 125 private final Context mContext; 126 private final OnScaleGestureListener mListener; 127 128 private float mFocusX; 129 private float mFocusY; 130 131 private float mCurrSpan; 132 private float mPrevSpan; 133 private float mInitialSpan; 134 private float mCurrSpanX; 135 private float mCurrSpanY; 136 private float mPrevSpanX; 137 private float mPrevSpanY; 138 private long mCurrTime; 139 private long mPrevTime; 140 private boolean mInProgress; 141 private int mSpanSlop; 142 private int mMinSpan; 143 144 // Bounds for recently seen values 145 private float mTouchUpper; 146 private float mTouchLower; 147 private float mTouchHistoryLastAccepted; 148 private int mTouchHistoryDirection; 149 private long mTouchHistoryLastAcceptedTime; 150 private int mTouchMinMajor; 151 152 private static final long TOUCH_STABILIZE_TIME = 128; // ms 153 private static final int TOUCH_MIN_MAJOR = 48; // dp 154 155 /** 156 * Consistency verifier for debugging purposes. 157 */ 158 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = 159 InputEventConsistencyVerifier.isInstrumentationEnabled() ? 160 new InputEventConsistencyVerifier(this, 0) : null; 161 162 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 163 mContext = context; 164 mListener = listener; 165 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; 166 167 final Resources res = context.getResources(); 168 mTouchMinMajor = res.getDimensionPixelSize( 169 com.android.internal.R.dimen.config_minScalingTouchMajor); 170 mMinSpan = res.getDimensionPixelSize( 171 com.android.internal.R.dimen.config_minScalingSpan); 172 } 173 174 /** 175 * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on 176 * some hardware/driver combos. Smooth it out to get kinder, gentler behavior. 177 * @param ev MotionEvent to add to the ongoing history 178 */ 179 private void addTouchHistory(MotionEvent ev) { 180 final long currentTime = SystemClock.uptimeMillis(); 181 final int count = ev.getPointerCount(); 182 boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME; 183 float total = 0; 184 int sampleCount = 0; 185 for (int i = 0; i < count; i++) { 186 final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted); 187 final int historySize = ev.getHistorySize(); 188 final int pointerSampleCount = historySize + 1; 189 for (int h = 0; h < pointerSampleCount; h++) { 190 float major; 191 if (h < historySize) { 192 major = ev.getHistoricalTouchMajor(i, h); 193 } else { 194 major = ev.getTouchMajor(i); 195 } 196 if (major < mTouchMinMajor) major = mTouchMinMajor; 197 total += major; 198 199 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) { 200 mTouchUpper = major; 201 } 202 if (Float.isNaN(mTouchLower) || major < mTouchLower) { 203 mTouchLower = major; 204 } 205 206 if (hasLastAccepted) { 207 final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted); 208 if (directionSig != mTouchHistoryDirection || 209 (directionSig == 0 && mTouchHistoryDirection == 0)) { 210 mTouchHistoryDirection = directionSig; 211 final long time = h < historySize ? ev.getHistoricalEventTime(h) 212 : ev.getEventTime(); 213 mTouchHistoryLastAcceptedTime = time; 214 accept = false; 215 } 216 } 217 } 218 sampleCount += pointerSampleCount; 219 } 220 221 final float avg = total / sampleCount; 222 223 if (accept) { 224 float newAccepted = (mTouchUpper + mTouchLower + avg) / 3; 225 mTouchUpper = (mTouchUpper + newAccepted) / 2; 226 mTouchLower = (mTouchLower + newAccepted) / 2; 227 mTouchHistoryLastAccepted = newAccepted; 228 mTouchHistoryDirection = 0; 229 mTouchHistoryLastAcceptedTime = ev.getEventTime(); 230 } 231 } 232 233 /** 234 * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP. 235 * @see #addTouchHistory(MotionEvent) 236 */ 237 private void clearTouchHistory() { 238 mTouchUpper = Float.NaN; 239 mTouchLower = Float.NaN; 240 mTouchHistoryLastAccepted = Float.NaN; 241 mTouchHistoryDirection = 0; 242 mTouchHistoryLastAcceptedTime = 0; 243 } 244 245 /** 246 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} 247 * when appropriate. 248 * 249 * <p>Applications should pass a complete and consistent event stream to this method. 250 * A complete and consistent event stream involves all MotionEvents from the initial 251 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> 252 * 253 * @param event The event to process 254 * @return true if the event was processed and the detector wants to receive the 255 * rest of the MotionEvents in this event stream. 256 */ 257 public boolean onTouchEvent(MotionEvent event) { 258 if (mInputEventConsistencyVerifier != null) { 259 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 260 } 261 262 mCurrTime = event.getEventTime(); 263 264 final int action = event.getActionMasked(); 265 266 final boolean streamComplete = action == MotionEvent.ACTION_UP || 267 action == MotionEvent.ACTION_CANCEL; 268 if (action == MotionEvent.ACTION_DOWN || streamComplete) { 269 // Reset any scale in progress with the listener. 270 // If it's an ACTION_DOWN we're beginning a new event stream. 271 // This means the app probably didn't give us all the events. Shame on it. 272 if (mInProgress) { 273 mListener.onScaleEnd(this); 274 mInProgress = false; 275 mInitialSpan = 0; 276 } 277 278 if (streamComplete) { 279 clearTouchHistory(); 280 return true; 281 } 282 } 283 284 final boolean configChanged = action == MotionEvent.ACTION_DOWN || 285 action == MotionEvent.ACTION_POINTER_UP || 286 action == MotionEvent.ACTION_POINTER_DOWN; 287 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; 288 final int skipIndex = pointerUp ? event.getActionIndex() : -1; 289 290 // Determine focal point 291 float sumX = 0, sumY = 0; 292 final int count = event.getPointerCount(); 293 for (int i = 0; i < count; i++) { 294 if (skipIndex == i) continue; 295 sumX += event.getX(i); 296 sumY += event.getY(i); 297 } 298 final int div = pointerUp ? count - 1 : count; 299 final float focusX = sumX / div; 300 final float focusY = sumY / div; 301 302 303 addTouchHistory(event); 304 305 // Determine average deviation from focal point 306 float devSumX = 0, devSumY = 0; 307 for (int i = 0; i < count; i++) { 308 if (skipIndex == i) continue; 309 310 // Convert the resulting diameter into a radius. 311 final float touchSize = mTouchHistoryLastAccepted / 2; 312 devSumX += Math.abs(event.getX(i) - focusX) + touchSize; 313 devSumY += Math.abs(event.getY(i) - focusY) + touchSize; 314 } 315 final float devX = devSumX / div; 316 final float devY = devSumY / div; 317 318 // Span is the average distance between touch points through the focal point; 319 // i.e. the diameter of the circle with a radius of the average deviation from 320 // the focal point. 321 final float spanX = devX * 2; 322 final float spanY = devY * 2; 323 final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY); 324 325 // Dispatch begin/end events as needed. 326 // If the configuration changes, notify the app to reset its current state by beginning 327 // a fresh scale event stream. 328 final boolean wasInProgress = mInProgress; 329 mFocusX = focusX; 330 mFocusY = focusY; 331 if (mInProgress && (span < mMinSpan || configChanged)) { 332 mListener.onScaleEnd(this); 333 mInProgress = false; 334 mInitialSpan = span; 335 } 336 if (configChanged) { 337 mPrevSpanX = mCurrSpanX = spanX; 338 mPrevSpanY = mCurrSpanY = spanY; 339 mInitialSpan = mPrevSpan = mCurrSpan = span; 340 } 341 if (!mInProgress && span >= mMinSpan && 342 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) { 343 mPrevSpanX = mCurrSpanX = spanX; 344 mPrevSpanY = mCurrSpanY = spanY; 345 mPrevSpan = mCurrSpan = span; 346 mPrevTime = mCurrTime; 347 mInProgress = mListener.onScaleBegin(this); 348 } 349 350 // Handle motion; focal point and span/scale factor are changing. 351 if (action == MotionEvent.ACTION_MOVE) { 352 mCurrSpanX = spanX; 353 mCurrSpanY = spanY; 354 mCurrSpan = span; 355 356 boolean updatePrev = true; 357 if (mInProgress) { 358 updatePrev = mListener.onScale(this); 359 } 360 361 if (updatePrev) { 362 mPrevSpanX = mCurrSpanX; 363 mPrevSpanY = mCurrSpanY; 364 mPrevSpan = mCurrSpan; 365 mPrevTime = mCurrTime; 366 } 367 } 368 369 return true; 370 } 371 372 /** 373 * Returns {@code true} if a scale gesture is in progress. 374 */ 375 public boolean isInProgress() { 376 return mInProgress; 377 } 378 379 /** 380 * Get the X coordinate of the current gesture's focal point. 381 * If a gesture is in progress, the focal point is between 382 * each of the pointers forming the gesture. 383 * 384 * If {@link #isInProgress()} would return false, the result of this 385 * function is undefined. 386 * 387 * @return X coordinate of the focal point in pixels. 388 */ 389 public float getFocusX() { 390 return mFocusX; 391 } 392 393 /** 394 * Get the Y coordinate of the current gesture's focal point. 395 * If a gesture is in progress, the focal point is between 396 * each of the pointers forming the gesture. 397 * 398 * If {@link #isInProgress()} would return false, the result of this 399 * function is undefined. 400 * 401 * @return Y coordinate of the focal point in pixels. 402 */ 403 public float getFocusY() { 404 return mFocusY; 405 } 406 407 /** 408 * Return the average distance between each of the pointers forming the 409 * gesture in progress through the focal point. 410 * 411 * @return Distance between pointers in pixels. 412 */ 413 public float getCurrentSpan() { 414 return mCurrSpan; 415 } 416 417 /** 418 * Return the average X distance between each of the pointers forming the 419 * gesture in progress through the focal point. 420 * 421 * @return Distance between pointers in pixels. 422 */ 423 public float getCurrentSpanX() { 424 return mCurrSpanX; 425 } 426 427 /** 428 * Return the average Y distance between each of the pointers forming the 429 * gesture in progress through the focal point. 430 * 431 * @return Distance between pointers in pixels. 432 */ 433 public float getCurrentSpanY() { 434 return mCurrSpanY; 435 } 436 437 /** 438 * Return the previous average distance between each of the pointers forming the 439 * gesture in progress through the focal point. 440 * 441 * @return Previous distance between pointers in pixels. 442 */ 443 public float getPreviousSpan() { 444 return mPrevSpan; 445 } 446 447 /** 448 * Return the previous average X distance between each of the pointers forming the 449 * gesture in progress through the focal point. 450 * 451 * @return Previous distance between pointers in pixels. 452 */ 453 public float getPreviousSpanX() { 454 return mPrevSpanX; 455 } 456 457 /** 458 * Return the previous average Y distance between each of the pointers forming the 459 * gesture in progress through the focal point. 460 * 461 * @return Previous distance between pointers in pixels. 462 */ 463 public float getPreviousSpanY() { 464 return mPrevSpanY; 465 } 466 467 /** 468 * Return the scaling factor from the previous scale event to the current 469 * event. This value is defined as 470 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 471 * 472 * @return The current scaling factor. 473 */ 474 public float getScaleFactor() { 475 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; 476 } 477 478 /** 479 * Return the time difference in milliseconds between the previous 480 * accepted scaling event and the current scaling event. 481 * 482 * @return Time difference since the last scaling event in milliseconds. 483 */ 484 public long getTimeDelta() { 485 return mCurrTime - mPrevTime; 486 } 487 488 /** 489 * Return the event time of the current event being processed. 490 * 491 * @return Current event time in milliseconds. 492 */ 493 public long getEventTime() { 494 return mCurrTime; 495 } 496 } 497