blob: 37e40001478147359d7c12d556dc3ed8bcfdd025 [file] [log] [blame]
Adam Powellae542ff2010-01-13 16:29:27 -08001/*
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;
Adam Powell33079582012-10-09 11:20:39 -070020import android.content.res.Resources;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -070021import android.os.Build;
22import android.os.Handler;
Adam Powella4ce6ae2012-09-24 20:13:23 -070023import android.os.SystemClock;
Adam Powella4ce6ae2012-09-24 20:13:23 -070024
Adam Powellae542ff2010-01-13 16:29:27 -080025/**
Adam Powell618cbea2012-08-27 17:44:59 -070026 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
27 * The {@link OnScaleGestureListener} callback will notify users when a particular
28 * gesture event has occurred.
29 *
Adam Powellae542ff2010-01-13 16:29:27 -080030 * This class should only be used with {@link MotionEvent}s reported via touch.
Erik47c41e82010-09-01 15:39:08 -070031 *
Adam Powellae542ff2010-01-13 16:29:27 -080032 * To use this class:
33 * <ul>
34 * <li>Create an instance of the {@code ScaleGestureDetector} for your
35 * {@link View}
36 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
37 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your
38 * callback will be executed when the events occur.
39 * </ul>
Adam Powellae542ff2010-01-13 16:29:27 -080040 */
41public class ScaleGestureDetector {
Adam Powell08180202011-03-07 16:48:29 -080042 private static final String TAG = "ScaleGestureDetector";
43
Adam Powellae542ff2010-01-13 16:29:27 -080044 /**
45 * The listener for receiving notifications when gestures occur.
46 * If you want to listen for all the different gestures then implement
47 * this interface. If you only want to listen for a subset it might
48 * be easier to extend {@link SimpleOnScaleGestureListener}.
Erik47c41e82010-09-01 15:39:08 -070049 *
Adam Powellae542ff2010-01-13 16:29:27 -080050 * An application will receive events in the following order:
51 * <ul>
Adam Powellab905c872010-02-03 11:01:58 -080052 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
53 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
54 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
Adam Powellae542ff2010-01-13 16:29:27 -080055 * </ul>
56 */
57 public interface OnScaleGestureListener {
58 /**
59 * Responds to scaling events for a gesture in progress.
60 * Reported by pointer motion.
Erik47c41e82010-09-01 15:39:08 -070061 *
Adam Powellae542ff2010-01-13 16:29:27 -080062 * @param detector The detector reporting the event - use this to
63 * retrieve extended info about event state.
64 * @return Whether or not the detector should consider this event
65 * as handled. If an event was not handled, the detector
66 * will continue to accumulate movement until an event is
67 * handled. This can be useful if an application, for example,
68 * only wants to update scaling factors if the change is
69 * greater than 0.01.
70 */
71 public boolean onScale(ScaleGestureDetector detector);
72
73 /**
74 * Responds to the beginning of a scaling gesture. Reported by
75 * new pointers going down.
Erik47c41e82010-09-01 15:39:08 -070076 *
Adam Powellae542ff2010-01-13 16:29:27 -080077 * @param detector The detector reporting the event - use this to
78 * retrieve extended info about event state.
79 * @return Whether or not the detector should continue recognizing
80 * this gesture. For example, if a gesture is beginning
81 * with a focal point outside of a region where it makes
82 * sense, onScaleBegin() may return false to ignore the
83 * rest of the gesture.
84 */
85 public boolean onScaleBegin(ScaleGestureDetector detector);
86
87 /**
88 * Responds to the end of a scale gesture. Reported by existing
Adam Powell216bccf2010-02-01 15:03:17 -080089 * pointers going up.
Erik47c41e82010-09-01 15:39:08 -070090 *
Adam Powellae542ff2010-01-13 16:29:27 -080091 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
Adam Powell47ec2fb2012-08-31 17:15:32 -070092 * and {@link ScaleGestureDetector#getFocusY()} will return focal point
93 * of the pointers remaining on the screen.
Erik47c41e82010-09-01 15:39:08 -070094 *
Adam Powellae542ff2010-01-13 16:29:27 -080095 * @param detector The detector reporting the event - use this to
96 * retrieve extended info about event state.
97 */
98 public void onScaleEnd(ScaleGestureDetector detector);
99 }
Erik47c41e82010-09-01 15:39:08 -0700100
Adam Powellae542ff2010-01-13 16:29:27 -0800101 /**
102 * A convenience class to extend when you only want to listen for a subset
103 * of scaling-related events. This implements all methods in
104 * {@link OnScaleGestureListener} but does nothing.
Adam Powell346c8fb2010-03-12 10:52:35 -0800105 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
106 * {@code false} so that a subclass can retrieve the accumulated scale
107 * factor in an overridden onScaleEnd.
108 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
Erik47c41e82010-09-01 15:39:08 -0700109 * {@code true}.
Adam Powellae542ff2010-01-13 16:29:27 -0800110 */
Adam Powell216bccf2010-02-01 15:03:17 -0800111 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
Adam Powellae542ff2010-01-13 16:29:27 -0800112
113 public boolean onScale(ScaleGestureDetector detector) {
Adam Powell346c8fb2010-03-12 10:52:35 -0800114 return false;
Adam Powellae542ff2010-01-13 16:29:27 -0800115 }
116
117 public boolean onScaleBegin(ScaleGestureDetector detector) {
118 return true;
119 }
120
121 public void onScaleEnd(ScaleGestureDetector detector) {
122 // Intentionally empty
123 }
124 }
125
Adam Powell346c8fb2010-03-12 10:52:35 -0800126 private final Context mContext;
127 private final OnScaleGestureListener mListener;
Adam Powellae542ff2010-01-13 16:29:27 -0800128
129 private float mFocusX;
130 private float mFocusY;
Erik47c41e82010-09-01 15:39:08 -0700131
Mindy Pereira9f1221f2013-09-16 11:36:12 -0700132 private boolean mQuickScaleEnabled;
Mady Mellor68955182015-04-16 12:50:53 -0700133 private boolean mStylusScaleEnabled;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700134
Adam Powell618cbea2012-08-27 17:44:59 -0700135 private float mCurrSpan;
136 private float mPrevSpan;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700137 private float mInitialSpan;
Adam Powell618cbea2012-08-27 17:44:59 -0700138 private float mCurrSpanX;
139 private float mCurrSpanY;
140 private float mPrevSpanX;
141 private float mPrevSpanY;
142 private long mCurrTime;
143 private long mPrevTime;
144 private boolean mInProgress;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700145 private int mSpanSlop;
Adam Powell828e56e2012-09-14 18:56:16 -0700146 private int mMinSpan;
Adam Powelle33cef82011-02-23 16:51:20 -0800147
Adam Powell5b5c4142012-10-02 16:25:30 -0700148 // Bounds for recently seen values
149 private float mTouchUpper;
150 private float mTouchLower;
151 private float mTouchHistoryLastAccepted;
152 private int mTouchHistoryDirection;
153 private long mTouchHistoryLastAcceptedTime;
154 private int mTouchMinMajor;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700155 private final Handler mHandler;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700156
Mady Mellor847d17f2015-03-30 09:42:12 -0700157 private float mAnchoredScaleStartX;
158 private float mAnchoredScaleStartY;
159 private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
160
Adam Powelld736d202012-09-25 19:30:44 -0700161 private static final long TOUCH_STABILIZE_TIME = 128; // ms
Mindy Pereira24870ce2013-09-09 15:56:23 -0700162 private static final float SCALE_FACTOR = .5f;
Mady Mellor847d17f2015-03-30 09:42:12 -0700163 private static final int ANCHORED_SCALE_MODE_NONE = 0;
164 private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
Mady Mellor68955182015-04-16 12:50:53 -0700165 private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700166
Adam Powella4ce6ae2012-09-24 20:13:23 -0700167
Jeff Brown21bc5c92011-02-28 18:27:14 -0800168 /**
169 * Consistency verifier for debugging purposes.
170 */
171 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
172 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
173 new InputEventConsistencyVerifier(this, 0) : null;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700174 private GestureDetector mGestureDetector;
Jeff Brown21bc5c92011-02-28 18:27:14 -0800175
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700176 private boolean mEventBeforeOrAboveStartingGestureEvent;
177
178 /**
179 * Creates a ScaleGestureDetector with the supplied listener.
180 * You may only use this constructor from a {@link android.os.Looper Looper} thread.
181 *
182 * @param context the application's context
183 * @param listener the listener invoked for all the callbacks, this must
184 * not be null.
185 *
186 * @throws NullPointerException if {@code listener} is null.
187 */
Adam Powellae542ff2010-01-13 16:29:27 -0800188 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700189 this(context, listener, null);
190 }
191
192 /**
193 * Creates a ScaleGestureDetector with the supplied listener.
194 * @see android.os.Handler#Handler()
195 *
196 * @param context the application's context
197 * @param listener the listener invoked for all the callbacks, this must
198 * not be null.
199 * @param handler the handler to use for running deferred listener events.
200 *
201 * @throws NullPointerException if {@code listener} is null.
202 */
203 public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
Mindy Pereira24870ce2013-09-09 15:56:23 -0700204 Handler handler) {
Adam Powellae542ff2010-01-13 16:29:27 -0800205 mContext = context;
206 mListener = listener;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700207 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
Adam Powell33079582012-10-09 11:20:39 -0700208
209 final Resources res = context.getResources();
210 mTouchMinMajor = res.getDimensionPixelSize(
211 com.android.internal.R.dimen.config_minScalingTouchMajor);
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700212 mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
213 mHandler = handler;
214 // Quick scale is enabled by default after JB_MR2
Mady Mellor7c36a682015-04-22 11:52:31 -0700215 final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
216 if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700217 setQuickScaleEnabled(true);
218 }
Mady Mellor7c36a682015-04-22 11:52:31 -0700219 // Stylus scale is enabled by default after LOLLIPOP_MR1
220 if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
221 setStylusScaleEnabled(true);
222 }
Adam Powellae542ff2010-01-13 16:29:27 -0800223 }
224
Adam Powell618cbea2012-08-27 17:44:59 -0700225 /**
Adam Powella4ce6ae2012-09-24 20:13:23 -0700226 * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
227 * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
228 * @param ev MotionEvent to add to the ongoing history
229 */
230 private void addTouchHistory(MotionEvent ev) {
231 final long currentTime = SystemClock.uptimeMillis();
232 final int count = ev.getPointerCount();
Adam Powell5b5c4142012-10-02 16:25:30 -0700233 boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
234 float total = 0;
235 int sampleCount = 0;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700236 for (int i = 0; i < count; i++) {
Adam Powell5b5c4142012-10-02 16:25:30 -0700237 final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
Adam Powella4ce6ae2012-09-24 20:13:23 -0700238 final int historySize = ev.getHistorySize();
Adam Powell5b5c4142012-10-02 16:25:30 -0700239 final int pointerSampleCount = historySize + 1;
240 for (int h = 0; h < pointerSampleCount; h++) {
241 float major;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700242 if (h < historySize) {
243 major = ev.getHistoricalTouchMajor(i, h);
Adam Powella4ce6ae2012-09-24 20:13:23 -0700244 } else {
245 major = ev.getTouchMajor(i);
Adam Powella4ce6ae2012-09-24 20:13:23 -0700246 }
Adam Powell5b5c4142012-10-02 16:25:30 -0700247 if (major < mTouchMinMajor) major = mTouchMinMajor;
248 total += major;
249
250 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
251 mTouchUpper = major;
252 }
253 if (Float.isNaN(mTouchLower) || major < mTouchLower) {
254 mTouchLower = major;
255 }
Adam Powella4ce6ae2012-09-24 20:13:23 -0700256
257 if (hasLastAccepted) {
Adam Powell5b5c4142012-10-02 16:25:30 -0700258 final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
259 if (directionSig != mTouchHistoryDirection ||
260 (directionSig == 0 && mTouchHistoryDirection == 0)) {
261 mTouchHistoryDirection = directionSig;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700262 final long time = h < historySize ? ev.getHistoricalEventTime(h)
263 : ev.getEventTime();
Adam Powell5b5c4142012-10-02 16:25:30 -0700264 mTouchHistoryLastAcceptedTime = time;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700265 accept = false;
266 }
267 }
268 }
Adam Powell5b5c4142012-10-02 16:25:30 -0700269 sampleCount += pointerSampleCount;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700270 }
Adam Powella4ce6ae2012-09-24 20:13:23 -0700271
Adam Powell5b5c4142012-10-02 16:25:30 -0700272 final float avg = total / sampleCount;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700273
Adam Powell5b5c4142012-10-02 16:25:30 -0700274 if (accept) {
275 float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
276 mTouchUpper = (mTouchUpper + newAccepted) / 2;
277 mTouchLower = (mTouchLower + newAccepted) / 2;
278 mTouchHistoryLastAccepted = newAccepted;
279 mTouchHistoryDirection = 0;
280 mTouchHistoryLastAcceptedTime = ev.getEventTime();
Adam Powellf3a2bf82012-09-28 12:05:10 -0700281 }
Adam Powella4ce6ae2012-09-24 20:13:23 -0700282 }
283
284 /**
285 * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
286 * @see #addTouchHistory(MotionEvent)
287 */
288 private void clearTouchHistory() {
Adam Powell5b5c4142012-10-02 16:25:30 -0700289 mTouchUpper = Float.NaN;
290 mTouchLower = Float.NaN;
291 mTouchHistoryLastAccepted = Float.NaN;
292 mTouchHistoryDirection = 0;
293 mTouchHistoryLastAcceptedTime = 0;
Adam Powella4ce6ae2012-09-24 20:13:23 -0700294 }
295
296 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700297 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
298 * when appropriate.
299 *
300 * <p>Applications should pass a complete and consistent event stream to this method.
301 * A complete and consistent event stream involves all MotionEvents from the initial
302 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
303 *
304 * @param event The event to process
305 * @return true if the event was processed and the detector wants to receive the
306 * rest of the MotionEvents in this event stream.
307 */
Adam Powellae542ff2010-01-13 16:29:27 -0800308 public boolean onTouchEvent(MotionEvent event) {
Jeff Brown21bc5c92011-02-28 18:27:14 -0800309 if (mInputEventConsistencyVerifier != null) {
310 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
311 }
312
Adam Powell7232b0a2012-11-28 18:29:22 -0800313 mCurrTime = event.getEventTime();
314
Adam Powelle33cef82011-02-23 16:51:20 -0800315 final int action = event.getActionMasked();
Adam Powellae542ff2010-01-13 16:29:27 -0800316
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700317 // Forward the event to check for double tap gesture
Mindy Pereira9f1221f2013-09-16 11:36:12 -0700318 if (mQuickScaleEnabled) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700319 mGestureDetector.onTouchEvent(event);
320 }
321
Mady Mellor847d17f2015-03-30 09:42:12 -0700322 final int count = event.getPointerCount();
Mady Mellor772fcb92015-05-21 17:46:58 -0700323 final boolean isStylusButtonDown =
324 (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
Mady Mellor847d17f2015-03-30 09:42:12 -0700325
326 final boolean anchoredScaleCancelled =
Mady Mellor68955182015-04-16 12:50:53 -0700327 mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
Adam Powell618cbea2012-08-27 17:44:59 -0700328 final boolean streamComplete = action == MotionEvent.ACTION_UP ||
Mady Mellor847d17f2015-03-30 09:42:12 -0700329 action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700330
Adam Powell618cbea2012-08-27 17:44:59 -0700331 if (action == MotionEvent.ACTION_DOWN || streamComplete) {
332 // Reset any scale in progress with the listener.
333 // If it's an ACTION_DOWN we're beginning a new event stream.
334 // This means the app probably didn't give us all the events. Shame on it.
335 if (mInProgress) {
Adam Powelld0197f32011-03-17 00:09:03 -0700336 mListener.onScaleEnd(this);
Adam Powell618cbea2012-08-27 17:44:59 -0700337 mInProgress = false;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700338 mInitialSpan = 0;
Mady Mellor847d17f2015-03-30 09:42:12 -0700339 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
340 } else if (inAnchoredScaleMode() && streamComplete) {
Mindy Pereira58233522013-11-14 17:01:25 -0800341 mInProgress = false;
342 mInitialSpan = 0;
Mady Mellor847d17f2015-03-30 09:42:12 -0700343 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
Adam Powelld0197f32011-03-17 00:09:03 -0700344 }
Adam Powell618cbea2012-08-27 17:44:59 -0700345
346 if (streamComplete) {
Adam Powella4ce6ae2012-09-24 20:13:23 -0700347 clearTouchHistory();
Adam Powell618cbea2012-08-27 17:44:59 -0700348 return true;
349 }
Adam Powelld0197f32011-03-17 00:09:03 -0700350 }
351
Mady Mellor68955182015-04-16 12:50:53 -0700352 if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
353 && !streamComplete && isStylusButtonDown) {
Mady Mellor847d17f2015-03-30 09:42:12 -0700354 // Start of a button scale gesture
355 mAnchoredScaleStartX = event.getX();
356 mAnchoredScaleStartY = event.getY();
Mady Mellor68955182015-04-16 12:50:53 -0700357 mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
Mady Mellor847d17f2015-03-30 09:42:12 -0700358 mInitialSpan = 0;
359 }
360
Adam Powellabde0422012-09-26 17:12:50 -0700361 final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
Adam Powell618cbea2012-08-27 17:44:59 -0700362 action == MotionEvent.ACTION_POINTER_UP ||
Mady Mellor847d17f2015-03-30 09:42:12 -0700363 action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700364
Adam Powell618cbea2012-08-27 17:44:59 -0700365 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
366 final int skipIndex = pointerUp ? event.getActionIndex() : -1;
Adam Powellae542ff2010-01-13 16:29:27 -0800367
Adam Powell618cbea2012-08-27 17:44:59 -0700368 // Determine focal point
369 float sumX = 0, sumY = 0;
Adam Powell618cbea2012-08-27 17:44:59 -0700370 final int div = pointerUp ? count - 1 : count;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700371 final float focusX;
372 final float focusY;
Mady Mellor847d17f2015-03-30 09:42:12 -0700373 if (inAnchoredScaleMode()) {
374 // In anchored scale mode, the focal pt is always where the double tap
375 // or button down gesture started
376 focusX = mAnchoredScaleStartX;
377 focusY = mAnchoredScaleStartY;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700378 if (event.getY() < focusY) {
379 mEventBeforeOrAboveStartingGestureEvent = true;
380 } else {
381 mEventBeforeOrAboveStartingGestureEvent = false;
382 }
383 } else {
384 for (int i = 0; i < count; i++) {
385 if (skipIndex == i) continue;
386 sumX += event.getX(i);
387 sumY += event.getY(i);
388 }
Adam Powell618cbea2012-08-27 17:44:59 -0700389
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700390 focusX = sumX / div;
391 focusY = sumY / div;
392 }
Adam Powell5b5c4142012-10-02 16:25:30 -0700393
394 addTouchHistory(event);
Adam Powella4ce6ae2012-09-24 20:13:23 -0700395
Adam Powell618cbea2012-08-27 17:44:59 -0700396 // Determine average deviation from focal point
397 float devSumX = 0, devSumY = 0;
398 for (int i = 0; i < count; i++) {
399 if (skipIndex == i) continue;
Adam Powell828e56e2012-09-14 18:56:16 -0700400
Adam Powell5b5c4142012-10-02 16:25:30 -0700401 // Convert the resulting diameter into a radius.
402 final float touchSize = mTouchHistoryLastAccepted / 2;
Adam Powell828e56e2012-09-14 18:56:16 -0700403 devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
404 devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
Adam Powellae542ff2010-01-13 16:29:27 -0800405 }
Adam Powell618cbea2012-08-27 17:44:59 -0700406 final float devX = devSumX / div;
407 final float devY = devSumY / div;
408
409 // Span is the average distance between touch points through the focal point;
410 // i.e. the diameter of the circle with a radius of the average deviation from
411 // the focal point.
412 final float spanX = devX * 2;
413 final float spanY = devY * 2;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700414 final float span;
Mady Mellor847d17f2015-03-30 09:42:12 -0700415 if (inAnchoredScaleMode()) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700416 span = spanY;
417 } else {
Neil Fuller33253a42014-10-01 11:55:10 +0100418 span = (float) Math.hypot(spanX, spanY);
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700419 }
Adam Powell618cbea2012-08-27 17:44:59 -0700420
421 // Dispatch begin/end events as needed.
422 // If the configuration changes, notify the app to reset its current state by beginning
423 // a fresh scale event stream.
Adam Powell47ec2fb2012-08-31 17:15:32 -0700424 final boolean wasInProgress = mInProgress;
425 mFocusX = focusX;
426 mFocusY = focusY;
Mady Mellor847d17f2015-03-30 09:42:12 -0700427 if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
Adam Powell618cbea2012-08-27 17:44:59 -0700428 mListener.onScaleEnd(this);
429 mInProgress = false;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700430 mInitialSpan = span;
Adam Powell618cbea2012-08-27 17:44:59 -0700431 }
432 if (configChanged) {
433 mPrevSpanX = mCurrSpanX = spanX;
434 mPrevSpanY = mCurrSpanY = spanY;
Adam Powell47ec2fb2012-08-31 17:15:32 -0700435 mInitialSpan = mPrevSpan = mCurrSpan = span;
Adam Powell618cbea2012-08-27 17:44:59 -0700436 }
Mindy Pereira24870ce2013-09-09 15:56:23 -0700437
Mady Mellor847d17f2015-03-30 09:42:12 -0700438 final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
Mindy Pereira24870ce2013-09-09 15:56:23 -0700439 if (!mInProgress && span >= minSpan &&
Adam Powell47ec2fb2012-08-31 17:15:32 -0700440 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
441 mPrevSpanX = mCurrSpanX = spanX;
442 mPrevSpanY = mCurrSpanY = spanY;
443 mPrevSpan = mCurrSpan = span;
Adam Powell7232b0a2012-11-28 18:29:22 -0800444 mPrevTime = mCurrTime;
Adam Powell618cbea2012-08-27 17:44:59 -0700445 mInProgress = mListener.onScaleBegin(this);
446 }
447
448 // Handle motion; focal point and span/scale factor are changing.
449 if (action == MotionEvent.ACTION_MOVE) {
450 mCurrSpanX = spanX;
451 mCurrSpanY = spanY;
452 mCurrSpan = span;
Adam Powell618cbea2012-08-27 17:44:59 -0700453
454 boolean updatePrev = true;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700455
Adam Powell618cbea2012-08-27 17:44:59 -0700456 if (mInProgress) {
457 updatePrev = mListener.onScale(this);
458 }
459
460 if (updatePrev) {
461 mPrevSpanX = mCurrSpanX;
462 mPrevSpanY = mCurrSpanY;
463 mPrevSpan = mCurrSpan;
Adam Powell7232b0a2012-11-28 18:29:22 -0800464 mPrevTime = mCurrTime;
Adam Powell618cbea2012-08-27 17:44:59 -0700465 }
466 }
467
468 return true;
Adam Powellae542ff2010-01-13 16:29:27 -0800469 }
470
Mady Mellor847d17f2015-03-30 09:42:12 -0700471 private boolean inAnchoredScaleMode() {
472 return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700473 }
474
475 /**
476 * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
477 * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
478 * if the app targets API 19 and newer.
479 * @param scales true to enable quick scaling, false to disable
480 */
481 public void setQuickScaleEnabled(boolean scales) {
Mindy Pereira9f1221f2013-09-16 11:36:12 -0700482 mQuickScaleEnabled = scales;
483 if (mQuickScaleEnabled && mGestureDetector == null) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700484 GestureDetector.SimpleOnGestureListener gestureListener =
485 new GestureDetector.SimpleOnGestureListener() {
486 @Override
487 public boolean onDoubleTap(MotionEvent e) {
488 // Double tap: start watching for a swipe
Mady Mellor847d17f2015-03-30 09:42:12 -0700489 mAnchoredScaleStartX = e.getX();
490 mAnchoredScaleStartY = e.getY();
491 mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700492 return true;
493 }
Mindy Pereira24870ce2013-09-09 15:56:23 -0700494 };
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700495 mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
496 }
497 }
498
Mindy Pereira9f1221f2013-09-16 11:36:12 -0700499 /**
500 * Return whether the quick scale gesture, in which the user performs a double tap followed by a
501 * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
502 */
503 public boolean isQuickScaleEnabled() {
504 return mQuickScaleEnabled;
505 }
506
Adam Powellae542ff2010-01-13 16:29:27 -0800507 /**
Mady Mellor68955182015-04-16 12:50:53 -0700508 * Sets whether the associates {@link OnScaleGestureListener} should receive
509 * onScale callbacks when the user uses a stylus and presses the button.
510 * Note that this is enabled by default if the app targets API 23 and newer.
Mady Mellor847d17f2015-03-30 09:42:12 -0700511 *
Mady Mellor68955182015-04-16 12:50:53 -0700512 * @param scales true to enable stylus scaling, false to disable.
Mady Mellor847d17f2015-03-30 09:42:12 -0700513 */
Mady Mellor68955182015-04-16 12:50:53 -0700514 public void setStylusScaleEnabled(boolean scales) {
515 mStylusScaleEnabled = scales;
Mady Mellor847d17f2015-03-30 09:42:12 -0700516 }
517
518 /**
Mady Mellord9ff4df2015-06-04 18:03:49 -0700519 * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
520 * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
Mady Mellor847d17f2015-03-30 09:42:12 -0700521 */
Mady Mellor68955182015-04-16 12:50:53 -0700522 public boolean isStylusScaleEnabled() {
523 return mStylusScaleEnabled;
Mady Mellor847d17f2015-03-30 09:42:12 -0700524 }
525
526 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700527 * Returns {@code true} if a scale gesture is in progress.
Adam Powellae542ff2010-01-13 16:29:27 -0800528 */
529 public boolean isInProgress() {
Adam Powell618cbea2012-08-27 17:44:59 -0700530 return mInProgress;
Adam Powellae542ff2010-01-13 16:29:27 -0800531 }
532
533 /**
534 * Get the X coordinate of the current gesture's focal point.
Adam Powell618cbea2012-08-27 17:44:59 -0700535 * If a gesture is in progress, the focal point is between
536 * each of the pointers forming the gesture.
537 *
Adam Powellab905c872010-02-03 11:01:58 -0800538 * If {@link #isInProgress()} would return false, the result of this
Adam Powellae542ff2010-01-13 16:29:27 -0800539 * function is undefined.
Erik47c41e82010-09-01 15:39:08 -0700540 *
Adam Powellae542ff2010-01-13 16:29:27 -0800541 * @return X coordinate of the focal point in pixels.
542 */
543 public float getFocusX() {
544 return mFocusX;
545 }
546
547 /**
548 * Get the Y coordinate of the current gesture's focal point.
Adam Powell618cbea2012-08-27 17:44:59 -0700549 * If a gesture is in progress, the focal point is between
550 * each of the pointers forming the gesture.
551 *
Adam Powellab905c872010-02-03 11:01:58 -0800552 * If {@link #isInProgress()} would return false, the result of this
Adam Powellae542ff2010-01-13 16:29:27 -0800553 * function is undefined.
Erik47c41e82010-09-01 15:39:08 -0700554 *
Adam Powellae542ff2010-01-13 16:29:27 -0800555 * @return Y coordinate of the focal point in pixels.
556 */
557 public float getFocusY() {
558 return mFocusY;
559 }
560
561 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700562 * Return the average distance between each of the pointers forming the
563 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700564 *
Adam Powellae542ff2010-01-13 16:29:27 -0800565 * @return Distance between pointers in pixels.
566 */
567 public float getCurrentSpan() {
Adam Powell618cbea2012-08-27 17:44:59 -0700568 return mCurrSpan;
Adam Powellae542ff2010-01-13 16:29:27 -0800569 }
570
571 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700572 * Return the average X distance between each of the pointers forming the
573 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700574 *
575 * @return Distance between pointers in pixels.
576 */
577 public float getCurrentSpanX() {
Adam Powell618cbea2012-08-27 17:44:59 -0700578 return mCurrSpanX;
Erik47c41e82010-09-01 15:39:08 -0700579 }
580
581 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700582 * Return the average Y distance between each of the pointers forming the
583 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700584 *
585 * @return Distance between pointers in pixels.
586 */
587 public float getCurrentSpanY() {
Adam Powell618cbea2012-08-27 17:44:59 -0700588 return mCurrSpanY;
Erik47c41e82010-09-01 15:39:08 -0700589 }
590
591 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700592 * Return the previous average distance between each of the pointers forming the
593 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700594 *
Adam Powellae542ff2010-01-13 16:29:27 -0800595 * @return Previous distance between pointers in pixels.
596 */
597 public float getPreviousSpan() {
Adam Powell618cbea2012-08-27 17:44:59 -0700598 return mPrevSpan;
Adam Powellae542ff2010-01-13 16:29:27 -0800599 }
600
601 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700602 * Return the previous average X distance between each of the pointers forming the
603 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700604 *
605 * @return Previous distance between pointers in pixels.
606 */
607 public float getPreviousSpanX() {
Adam Powell618cbea2012-08-27 17:44:59 -0700608 return mPrevSpanX;
Erik47c41e82010-09-01 15:39:08 -0700609 }
610
611 /**
Adam Powell618cbea2012-08-27 17:44:59 -0700612 * Return the previous average Y distance between each of the pointers forming the
613 * gesture in progress through the focal point.
Erik47c41e82010-09-01 15:39:08 -0700614 *
615 * @return Previous distance between pointers in pixels.
616 */
617 public float getPreviousSpanY() {
Adam Powell618cbea2012-08-27 17:44:59 -0700618 return mPrevSpanY;
Erik47c41e82010-09-01 15:39:08 -0700619 }
620
621 /**
Adam Powellae542ff2010-01-13 16:29:27 -0800622 * Return the scaling factor from the previous scale event to the current
623 * event. This value is defined as
Adam Powellab905c872010-02-03 11:01:58 -0800624 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
Erik47c41e82010-09-01 15:39:08 -0700625 *
Adam Powellae542ff2010-01-13 16:29:27 -0800626 * @return The current scaling factor.
627 */
628 public float getScaleFactor() {
Mady Mellor847d17f2015-03-30 09:42:12 -0700629 if (inAnchoredScaleMode()) {
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700630 // Drag is moving up; the further away from the gesture
631 // start, the smaller the span should be, the closer,
632 // the larger the span, and therefore the larger the scale
Mindy Pereira24870ce2013-09-09 15:56:23 -0700633 final boolean scaleUp =
634 (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
635 (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
636 final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
637 return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700638 }
Adam Powell618cbea2012-08-27 17:44:59 -0700639 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
Adam Powellae542ff2010-01-13 16:29:27 -0800640 }
Erik47c41e82010-09-01 15:39:08 -0700641
Adam Powellae542ff2010-01-13 16:29:27 -0800642 /**
643 * Return the time difference in milliseconds between the previous
644 * accepted scaling event and the current scaling event.
Erik47c41e82010-09-01 15:39:08 -0700645 *
Adam Powellae542ff2010-01-13 16:29:27 -0800646 * @return Time difference since the last scaling event in milliseconds.
647 */
648 public long getTimeDelta() {
Adam Powell618cbea2012-08-27 17:44:59 -0700649 return mCurrTime - mPrevTime;
Adam Powellae542ff2010-01-13 16:29:27 -0800650 }
Erik47c41e82010-09-01 15:39:08 -0700651
Adam Powellae542ff2010-01-13 16:29:27 -0800652 /**
653 * Return the event time of the current event being processed.
Erik47c41e82010-09-01 15:39:08 -0700654 *
Adam Powellae542ff2010-01-13 16:29:27 -0800655 * @return Current event time in milliseconds.
656 */
657 public long getEventTime() {
Adam Powell618cbea2012-08-27 17:44:59 -0700658 return mCurrTime;
Adam Powellae542ff2010-01-13 16:29:27 -0800659 }
Mindy Pereirae8ce8ba2013-08-21 15:59:36 -0700660}