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 com.cooliris.media; 18 19 import android.content.Context; 20 import android.util.Log; 21 import android.view.MotionEvent; 22 23 /** 24 * Detects transformation gestures involving more than one pointer 25 * ("multitouch") using the supplied {@link MotionEvent}s. The 26 * {@link OnScaleGestureListener} callback will notify users when a particular 27 * gesture event has occurred. This class should only be used with 28 * {@link MotionEvent}s reported via touch. 29 * 30 * To use this class: 31 * <ul> 32 * <li>Create an instance of the {@code ScaleGestureDetector} for your 33 * {@link View} 34 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 35 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will 36 * be executed when the events occur. 37 * </ul> 38 * 39 * @hide Pending API approval 40 */ 41 public class ScaleGestureDetector { 42 /** 43 * The listener for receiving notifications when gestures occur. If you want 44 * to listen for all the different gestures then implement this interface. 45 * If you only want to listen for a subset it might be easier to extend 46 * {@link SimpleOnScaleGestureListener}. 47 * 48 * An application will receive events in the following order: 49 * <ul> 50 * <li>One {@link OnScaleGestureListener#onScaleBegin()} 51 * <li>Zero or more {@link OnScaleGestureListener#onScale()} 52 * <li>One {@link OnScaleGestureListener#onTransformEnd()} 53 * </ul> 54 */ 55 public interface OnScaleGestureListener { 56 /** 57 * Responds to scaling events for a gesture in progress. Reported by 58 * pointer motion. 59 * 60 * @param detector 61 * The detector reporting the event - use this to retrieve 62 * extended info about event state. 63 * @return Whether or not the detector should consider this event as 64 * handled. If an event was not handled, the detector will 65 * continue to accumulate movement until an event is handled. 66 * This can be useful if an application, for example, only wants 67 * to update scaling factors if the change is greater than 0.01. 68 */ 69 public boolean onScale(ScaleGestureDetector detector); 70 71 /** 72 * Responds to the beginning of a scaling gesture. Reported by new 73 * pointers going down. 74 * 75 * @param detector 76 * The detector reporting the event - use this to retrieve 77 * extended info about event state. 78 * @return Whether or not the detector should continue recognizing this 79 * gesture. For example, if a gesture is beginning with a focal 80 * point outside of a region where it makes sense, 81 * onScaleBegin() may return false to ignore the rest of the 82 * gesture. 83 */ 84 public boolean onScaleBegin(ScaleGestureDetector detector); 85 86 /** 87 * Responds to the end of a scale gesture. Reported by existing pointers 88 * going up. If the end of a gesture would result in a fling, {@link 89 * onTransformFling()} is called instead. 90 * 91 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} and 92 * {@link ScaleGestureDetector#getFocusY()} will return the location of 93 * the pointer remaining on the screen. 94 * 95 * @param detector 96 * The detector reporting the event - use this to retrieve 97 * extended info about event state. 98 */ 99 public void onScaleEnd(ScaleGestureDetector detector, boolean cancel); 100 } 101 102 /** 103 * A convenience class to extend when you only want to listen for a subset 104 * of scaling-related events. This implements all methods in 105 * {@link OnScaleGestureListener} but does nothing. 106 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} and 107 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} return 108 * {@code true}. 109 */ 110 public class SimpleOnScaleGestureListener implements OnScaleGestureListener { 111 112 public boolean onScale(ScaleGestureDetector detector) { 113 return true; 114 } 115 116 public boolean onScaleBegin(ScaleGestureDetector detector) { 117 return true; 118 } 119 120 public void onScaleEnd(ScaleGestureDetector detector, boolean cancel) { 121 // Intentionally empty 122 } 123 } 124 125 private static final float PRESSURE_THRESHOLD = 0.67f; 126 127 private Context mContext; 128 private OnScaleGestureListener mListener; 129 private boolean mGestureInProgress; 130 131 private MotionEvent mPrevEvent; 132 private MotionEvent mCurrEvent; 133 134 private float mFocusX; 135 private float mFocusY; 136 private float mPrevFingerDiffX; 137 private float mPrevFingerDiffY; 138 private float mCurrFingerDiffX; 139 private float mCurrFingerDiffY; 140 private float mCurrLen; 141 private float mPrevLen; 142 private float mScaleFactor; 143 private float mCurrPressure; 144 private float mPrevPressure; 145 private long mTimeDelta; 146 147 // Tracking individual fingers. 148 private float mTopFingerBeginX; 149 private float mTopFingerBeginY; 150 private float mBottomFingerBeginX; 151 private float mBottomFingerBeginY; 152 153 private float mTopFingerCurrX; 154 private float mTopFingerCurrY; 155 private float mBottomFingerCurrX; 156 private float mBottomFingerCurrY; 157 158 private boolean mTopFingerIsPointer1; 159 private boolean mPointerOneUp; 160 private boolean mPointerTwoUp; 161 162 private static final String TAG = "ScaleGestureDetector"; 163 164 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 165 mContext = context; 166 mListener = listener; 167 } 168 169 public boolean onTouchEvent(MotionEvent event) { 170 final int action = event.getAction(); 171 boolean handled = true; 172 173 if (!mGestureInProgress) { 174 // Track individual fingers. 175 if (action == MotionEvent.ACTION_POINTER_1_DOWN) { 176 177 } 178 if (action == MotionEvent.ACTION_POINTER_2_DOWN) { 179 180 } 181 if ((action == MotionEvent.ACTION_POINTER_1_DOWN || action == MotionEvent.ACTION_POINTER_2_DOWN) 182 && event.getPointerCount() >= 2) { 183 // We have a new multi-finger gesture 184 mBottomFingerBeginX = event.getX(0); 185 mBottomFingerBeginY = event.getY(0); 186 mTopFingerBeginX = event.getX(1); 187 mTopFingerBeginY = event.getY(1); 188 189 mTopFingerCurrX = mTopFingerBeginX; 190 mTopFingerCurrY = mTopFingerBeginY; 191 mBottomFingerCurrX = mBottomFingerBeginX; 192 mBottomFingerCurrY = mBottomFingerBeginY; 193 mPointerOneUp = false; 194 mPointerTwoUp = false; 195 196 // Be paranoid in case we missed an event 197 reset(); 198 199 // We decide which finger should be designated as the top finger 200 if (mTopFingerBeginY > mBottomFingerBeginY) { 201 mTopFingerIsPointer1 = false; 202 } else { 203 mTopFingerIsPointer1 = true; 204 } 205 206 mPrevEvent = MotionEvent.obtain(event); 207 mTimeDelta = 0; 208 209 setContext(event); 210 mGestureInProgress = mListener.onScaleBegin(this); 211 } 212 } else { 213 // Transform gesture in progress - attempt to handle it 214 switch (action) { 215 case MotionEvent.ACTION_UP: 216 mPointerOneUp = true; 217 mPointerTwoUp = true; 218 case MotionEvent.ACTION_POINTER_1_UP: 219 if (mPointerOneUp) { 220 mPointerTwoUp = true; 221 } 222 mPointerOneUp = true; 223 case MotionEvent.ACTION_POINTER_2_UP: 224 if (action == MotionEvent.ACTION_POINTER_2_UP) { 225 if (mPointerTwoUp == true) { 226 mPointerOneUp = true; 227 } 228 mPointerTwoUp = true; 229 } 230 // Gesture ended 231 if (mPointerOneUp || mPointerTwoUp) { 232 setContext(event); 233 234 // Set focus point to the remaining finger 235 int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0; 236 mFocusX = event.getX(id); 237 mFocusY = event.getY(id); 238 239 mListener.onScaleEnd(this, false); 240 mGestureInProgress = false; 241 242 reset(); 243 } 244 break; 245 246 case MotionEvent.ACTION_CANCEL: 247 mListener.onScaleEnd(this, true); 248 mGestureInProgress = false; 249 250 reset(); 251 break; 252 253 case MotionEvent.ACTION_MOVE: 254 setContext(event); 255 256 // Only accept the event if our relative pressure is within 257 // a certain limit - this can help filter shaky data as a 258 // finger is lifted. 259 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 260 final boolean updatePrevious = mListener.onScale(this); 261 262 if (updatePrevious) { 263 mPrevEvent.recycle(); 264 mPrevEvent = MotionEvent.obtain(event); 265 } 266 } 267 break; 268 } 269 } 270 return handled; 271 } 272 273 private void setContext(MotionEvent curr) { 274 if (mCurrEvent != null) { 275 mCurrEvent.recycle(); 276 } 277 mCurrEvent = MotionEvent.obtain(curr); 278 279 mCurrLen = -1; 280 mPrevLen = -1; 281 mScaleFactor = -1; 282 283 final MotionEvent prev = mPrevEvent; 284 285 final float px0 = prev.getX(0); 286 final float py0 = prev.getY(0); 287 final float px1 = prev.getX(1); 288 final float py1 = prev.getY(1); 289 final float cx0 = curr.getX(0); 290 final float cy0 = curr.getY(0); 291 final float cx1 = curr.getX(1); 292 final float cy1 = curr.getY(1); 293 294 final float pvx = px1 - px0; 295 final float pvy = py1 - py0; 296 final float cvx = cx1 - cx0; 297 final float cvy = cy1 - cy0; 298 mPrevFingerDiffX = pvx; 299 mPrevFingerDiffY = pvy; 300 mCurrFingerDiffX = cvx; 301 mCurrFingerDiffY = cvy; 302 303 mFocusX = cx0 + cvx * 0.5f; 304 mFocusY = cy0 + cvy * 0.5f; 305 mTimeDelta = curr.getEventTime() - prev.getEventTime(); 306 mCurrPressure = curr.getPressure(0) + curr.getPressure(1); 307 mPrevPressure = prev.getPressure(0) + prev.getPressure(1); 308 309 // Update the correct finger. 310 mBottomFingerCurrX = cx0; 311 mBottomFingerCurrY = cy0; 312 mTopFingerCurrX = cx1; 313 mTopFingerCurrY = cy1; 314 } 315 316 private void reset() { 317 if (mPrevEvent != null) { 318 mPrevEvent.recycle(); 319 mPrevEvent = null; 320 } 321 if (mCurrEvent != null) { 322 mCurrEvent.recycle(); 323 mCurrEvent = null; 324 } 325 } 326 327 /** 328 * Returns {@code true} if a two-finger scale gesture is in progress. 329 * 330 * @return {@code true} if a scale gesture is in progress, {@code false} 331 * otherwise. 332 */ 333 public boolean isInProgress() { 334 return mGestureInProgress; 335 } 336 337 /** 338 * Get the X coordinate of the current gesture's focal point. If a gesture 339 * is in progress, the focal point is directly between the two pointers 340 * forming the gesture. If a gesture is ending, the focal point is the 341 * location of the remaining pointer on the screen. If {@link 342 * isInProgress()} would return false, the result of this function is 343 * undefined. 344 * 345 * @return X coordinate of the focal point in pixels. 346 */ 347 public float getFocusX() { 348 return mFocusX; 349 } 350 351 /** 352 * Get the Y coordinate of the current gesture's focal point. If a gesture 353 * is in progress, the focal point is directly between the two pointers 354 * forming the gesture. If a gesture is ending, the focal point is the 355 * location of the remaining pointer on the screen. If {@link 356 * isInProgress()} would return false, the result of this function is 357 * undefined. 358 * 359 * @return Y coordinate of the focal point in pixels. 360 */ 361 public float getFocusY() { 362 return mFocusY; 363 } 364 365 /** 366 * Return the current distance between the two pointers forming the gesture 367 * in progress. 368 * 369 * @return Distance between pointers in pixels. 370 */ 371 public float getCurrentSpan() { 372 if (mCurrLen == -1) { 373 final float cvx = mCurrFingerDiffX; 374 final float cvy = mCurrFingerDiffY; 375 mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy); 376 } 377 return mCurrLen; 378 } 379 380 /** 381 * Return the previous distance between the two pointers forming the gesture 382 * in progress. 383 * 384 * @return Previous distance between pointers in pixels. 385 */ 386 public float getPreviousSpan() { 387 if (mPrevLen == -1) { 388 final float pvx = mPrevFingerDiffX; 389 final float pvy = mPrevFingerDiffY; 390 mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy); 391 } 392 return mPrevLen; 393 } 394 395 /** 396 * Return the scaling factor from the previous scale event to the current 397 * event. This value is defined as ({@link getCurrentSpan()} / {@link 398 * getPreviousSpan()}). 399 * 400 * @return The current scaling factor. 401 */ 402 public float getScaleFactor() { 403 if (mScaleFactor == -1) { 404 mScaleFactor = getCurrentSpan() / getPreviousSpan(); 405 } 406 return mScaleFactor; 407 } 408 409 /** 410 * Return the time difference in milliseconds between the previous accepted 411 * scaling event and the current scaling event. 412 * 413 * @return Time difference since the last scaling event in milliseconds. 414 */ 415 public long getTimeDelta() { 416 return mTimeDelta; 417 } 418 419 /** 420 * Return the event time of the current event being processed. 421 * 422 * @return Current event time in milliseconds. 423 */ 424 public long getEventTime() { 425 return mCurrEvent.getEventTime(); 426 } 427 428 public float getTopFingerX() { 429 return (mTopFingerIsPointer1) ? mTopFingerCurrX : mBottomFingerCurrX; 430 } 431 432 public float getTopFingerY() { 433 return (mTopFingerIsPointer1) ? mTopFingerCurrY : mBottomFingerCurrY; 434 } 435 436 public float getTopFingerDeltaX() { 437 return (mTopFingerIsPointer1) ? mTopFingerCurrX - mTopFingerBeginX : mBottomFingerCurrX - mBottomFingerBeginX; 438 } 439 440 public float getTopFingerDeltaY() { 441 return (mTopFingerIsPointer1) ? mTopFingerCurrY - mTopFingerBeginY : mBottomFingerCurrY - mBottomFingerBeginY; 442 } 443 444 public float getBottomFingerX() { 445 return (!mTopFingerIsPointer1) ? mTopFingerCurrX : mBottomFingerCurrX; 446 } 447 448 public float getBottomFingerY() { 449 return (!mTopFingerIsPointer1) ? mTopFingerCurrY : mBottomFingerCurrY; 450 } 451 452 public float getBottomFingerDeltaX() { 453 return (!mTopFingerIsPointer1) ? mTopFingerCurrX - mTopFingerBeginX : mBottomFingerCurrX - mBottomFingerBeginX; 454 } 455 456 public float getBottomFingerDeltaY() { 457 return (!mTopFingerIsPointer1) ? mTopFingerCurrY - mTopFingerBeginY : mBottomFingerCurrY - mBottomFingerBeginY; 458 } 459 } 460