ScaleGestureDetector.java revision ae542ff055301a4c3c8a18e8da1739df3a771958
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; 20 21/** 22 * Detects transformation gestures involving more than one pointer ("multitouch") 23 * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} 24 * callback will notify users when a particular gesture event has occurred. 25 * This class should only be used with {@link MotionEvent}s reported via touch. 26 * 27 * To use this class: 28 * <ul> 29 * <li>Create an instance of the {@code ScaleGestureDetector} for your 30 * {@link View} 31 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 32 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 33 * callback will be executed when the events occur. 34 * </ul> 35 * @hide Pending API approval 36 */ 37public class ScaleGestureDetector { 38 /** 39 * The listener for receiving notifications when gestures occur. 40 * If you want to listen for all the different gestures then implement 41 * this interface. If you only want to listen for a subset it might 42 * be easier to extend {@link SimpleOnScaleGestureListener}. 43 * 44 * An application will receive events in the following order: 45 * <ul> 46 * <li>One {@link OnScaleGestureListener#onScaleBegin()} 47 * <li>Zero or more {@link OnScaleGestureListener#onScale()} 48 * <li>One {@link OnScaleGestureListener#onTransformEnd()} 49 * </ul> 50 */ 51 public interface OnScaleGestureListener { 52 /** 53 * Responds to scaling events for a gesture in progress. 54 * Reported by pointer motion. 55 * 56 * @param detector The detector reporting the event - use this to 57 * retrieve extended info about event state. 58 * @return Whether or not the detector should consider this event 59 * as handled. If an event was not handled, the detector 60 * will continue to accumulate movement until an event is 61 * handled. This can be useful if an application, for example, 62 * only wants to update scaling factors if the change is 63 * greater than 0.01. 64 */ 65 public boolean onScale(ScaleGestureDetector detector); 66 67 /** 68 * Responds to the beginning of a scaling gesture. Reported by 69 * new pointers going down. 70 * 71 * @param detector The detector reporting the event - use this to 72 * retrieve extended info about event state. 73 * @return Whether or not the detector should continue recognizing 74 * this gesture. For example, if a gesture is beginning 75 * with a focal point outside of a region where it makes 76 * sense, onScaleBegin() may return false to ignore the 77 * rest of the gesture. 78 */ 79 public boolean onScaleBegin(ScaleGestureDetector detector); 80 81 /** 82 * Responds to the end of a scale gesture. Reported by existing 83 * pointers going up. If the end of a gesture would result in a fling, 84 * {@link onTransformFling()} is called instead. 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)} and 101 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} return 102 * {@code true}. 103 */ 104 public class SimpleOnScaleGestureListener implements OnScaleGestureListener { 105 106 public boolean onScale(ScaleGestureDetector detector) { 107 return true; 108 } 109 110 public boolean onScaleBegin(ScaleGestureDetector detector) { 111 return true; 112 } 113 114 public void onScaleEnd(ScaleGestureDetector detector) { 115 // Intentionally empty 116 } 117 } 118 119 private static final float PRESSURE_THRESHOLD = 0.67f; 120 121 private Context mContext; 122 private OnScaleGestureListener mListener; 123 private boolean mGestureInProgress; 124 125 private MotionEvent mPrevEvent; 126 private MotionEvent mCurrEvent; 127 128 private float mFocusX; 129 private float mFocusY; 130 private float mPrevFingerDiffX; 131 private float mPrevFingerDiffY; 132 private float mCurrFingerDiffX; 133 private float mCurrFingerDiffY; 134 private float mCurrLen; 135 private float mPrevLen; 136 private float mScaleFactor; 137 private float mCurrPressure; 138 private float mPrevPressure; 139 private long mTimeDelta; 140 141 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 142 mContext = context; 143 mListener = listener; 144 } 145 146 public boolean onTouchEvent(MotionEvent event) { 147 final int action = event.getAction(); 148 boolean handled = true; 149 150 if (!mGestureInProgress) { 151 if ((action == MotionEvent.ACTION_POINTER_1_DOWN || 152 action == MotionEvent.ACTION_POINTER_2_DOWN) && 153 event.getPointerCount() >= 2) { 154 // We have a new multi-finger gesture 155 156 // Be paranoid in case we missed an event 157 reset(); 158 159 mPrevEvent = MotionEvent.obtain(event); 160 mTimeDelta = 0; 161 162 setContext(event); 163 mGestureInProgress = mListener.onScaleBegin(this); 164 } 165 } else { 166 // Transform gesture in progress - attempt to handle it 167 switch (action) { 168 case MotionEvent.ACTION_POINTER_1_UP: 169 case MotionEvent.ACTION_POINTER_2_UP: 170 // Gesture ended 171 setContext(event); 172 173 // Set focus point to the remaining finger 174 int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK) 175 >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0; 176 mFocusX = event.getX(id); 177 mFocusY = event.getY(id); 178 179 mListener.onScaleEnd(this); 180 mGestureInProgress = false; 181 182 reset(); 183 break; 184 185 case MotionEvent.ACTION_CANCEL: 186 mListener.onScaleEnd(this); 187 mGestureInProgress = false; 188 189 reset(); 190 break; 191 192 case MotionEvent.ACTION_MOVE: 193 setContext(event); 194 195 // Only accept the event if our relative pressure is within 196 // a certain limit - this can help filter shaky data as a 197 // finger is lifted. 198 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 199 final boolean updatePrevious = mListener.onScale(this); 200 201 if (updatePrevious) { 202 mPrevEvent.recycle(); 203 mPrevEvent = MotionEvent.obtain(event); 204 } 205 } 206 break; 207 } 208 } 209 return handled; 210 } 211 212 private void setContext(MotionEvent curr) { 213 if (mCurrEvent != null) { 214 mCurrEvent.recycle(); 215 } 216 mCurrEvent = MotionEvent.obtain(curr); 217 218 mCurrLen = -1; 219 mPrevLen = -1; 220 mScaleFactor = -1; 221 222 final MotionEvent prev = mPrevEvent; 223 224 final float px0 = prev.getX(0); 225 final float py0 = prev.getY(0); 226 final float px1 = prev.getX(1); 227 final float py1 = prev.getY(1); 228 final float cx0 = curr.getX(0); 229 final float cy0 = curr.getY(0); 230 final float cx1 = curr.getX(1); 231 final float cy1 = curr.getY(1); 232 233 final float pvx = px1 - px0; 234 final float pvy = py1 - py0; 235 final float cvx = cx1 - cx0; 236 final float cvy = cy1 - cy0; 237 mPrevFingerDiffX = pvx; 238 mPrevFingerDiffY = pvy; 239 mCurrFingerDiffX = cvx; 240 mCurrFingerDiffY = cvy; 241 242 mFocusX = cx0 + cvx * 0.5f; 243 mFocusY = cy0 + cvy * 0.5f; 244 mTimeDelta = curr.getEventTime() - prev.getEventTime(); 245 mCurrPressure = curr.getPressure(0) + curr.getPressure(1); 246 mPrevPressure = prev.getPressure(0) + prev.getPressure(1); 247 } 248 249 private void reset() { 250 if (mPrevEvent != null) { 251 mPrevEvent.recycle(); 252 mPrevEvent = null; 253 } 254 if (mCurrEvent != null) { 255 mCurrEvent.recycle(); 256 mCurrEvent = null; 257 } 258 } 259 260 /** 261 * Returns {@code true} if a two-finger scale gesture is in progress. 262 * @return {@code true} if a scale gesture is in progress, {@code false} otherwise. 263 */ 264 public boolean isInProgress() { 265 return mGestureInProgress; 266 } 267 268 /** 269 * Get the X coordinate of the current gesture's focal point. 270 * If a gesture is in progress, the focal point is directly between 271 * the two pointers forming the gesture. 272 * If a gesture is ending, the focal point is the location of the 273 * remaining pointer on the screen. 274 * If {@link isInProgress()} would return false, the result of this 275 * function is undefined. 276 * 277 * @return X coordinate of the focal point in pixels. 278 */ 279 public float getFocusX() { 280 return mFocusX; 281 } 282 283 /** 284 * Get the Y coordinate of the current gesture's focal point. 285 * If a gesture is in progress, the focal point is directly between 286 * the two pointers forming the gesture. 287 * If a gesture is ending, the focal point is the location of the 288 * remaining pointer on the screen. 289 * If {@link isInProgress()} would return false, the result of this 290 * function is undefined. 291 * 292 * @return Y coordinate of the focal point in pixels. 293 */ 294 public float getFocusY() { 295 return mFocusY; 296 } 297 298 /** 299 * Return the current distance between the two pointers forming the 300 * gesture in progress. 301 * 302 * @return Distance between pointers in pixels. 303 */ 304 public float getCurrentSpan() { 305 if (mCurrLen == -1) { 306 final float cvx = mCurrFingerDiffX; 307 final float cvy = mCurrFingerDiffY; 308 mCurrLen = (float)Math.sqrt(cvx*cvx + cvy*cvy); 309 } 310 return mCurrLen; 311 } 312 313 /** 314 * Return the previous distance between the two pointers forming the 315 * gesture in progress. 316 * 317 * @return Previous distance between pointers in pixels. 318 */ 319 public float getPreviousSpan() { 320 if (mPrevLen == -1) { 321 final float pvx = mPrevFingerDiffX; 322 final float pvy = mPrevFingerDiffY; 323 mPrevLen = (float)Math.sqrt(pvx*pvx + pvy*pvy); 324 } 325 return mPrevLen; 326 } 327 328 /** 329 * Return the scaling factor from the previous scale event to the current 330 * event. This value is defined as 331 * ({@link getCurrentSpan()} / {@link getPreviousSpan()}). 332 * 333 * @return The current scaling factor. 334 */ 335 public float getScaleFactor() { 336 if (mScaleFactor == -1) { 337 mScaleFactor = getCurrentSpan() / getPreviousSpan(); 338 } 339 return mScaleFactor; 340 } 341 342 /** 343 * Return the time difference in milliseconds between the previous 344 * accepted scaling event and the current scaling event. 345 * 346 * @return Time difference since the last scaling event in milliseconds. 347 */ 348 public long getTimeDelta() { 349 return mTimeDelta; 350 } 351 352 /** 353 * Return the event time of the current event being processed. 354 * 355 * @return Current event time in milliseconds. 356 */ 357 public long getEventTime() { 358 return mCurrEvent.getEventTime(); 359 } 360} 361