/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import android.content.Context; import android.content.res.Resources; import android.os.SystemClock; import android.util.FloatMath; /** * 根据接收的{@link MotionEvent},侦测由多个触点(“多点触控”)引发的变形手势. * {@link OnScaleGestureListener}的回调函数会在特定手势事件发生时通知用户. * 该类仅能和触控事件引发的{@link MotionEvent}配合使用. * * 使用该类需要: * <ul> * <li>为你的{@link View 视图}创建{@code ScaleGestureDetector}. * <li>保证在{@link View#onTouchEvent(MotionEvent)}方法中调用了该类的 * {@link #onTouchEvent(MotionEvent)}方法. * 事件发生时执行你定义的回调函数. * </ul> */ public class ScaleGestureDetector { private static final String TAG = "ScaleGestureDetector"; /** * 手势发生时接收通知的监听器. * If you want to listen for all the different gestures then implement * this interface. If you only want to listen for a subset it might * be easier to extend {@link SimpleOnScaleGestureListener}. * * An application will receive events in the following order: * <ul> * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} * </ul> */ public interface OnScaleGestureListener { /** * Responds to scaling events for a gesture in progress. * Reported by pointer motion. * * @param detector The detector reporting the event - use this to * retrieve extended info about event state. * @return Whether or not the detector should consider this event * as handled. If an event was not handled, the detector * will continue to accumulate movement until an event is * handled. This can be useful if an application, for example, * only wants to update scaling factors if the change is * greater than 0.01. */ public boolean onScale(ScaleGestureDetector detector); /** * Responds to the beginning of a scaling gesture. Reported by * new pointers going down. * * @param detector The detector reporting the event - use this to * retrieve extended info about event state. * @return Whether or not the detector should continue recognizing * this gesture. For example, if a gesture is beginning * with a focal point outside of a region where it makes * sense, onScaleBegin() may return false to ignore the * rest of the gesture. */ public boolean onScaleBegin(ScaleGestureDetector detector); /** * Responds to the end of a scale gesture. Reported by existing * pointers going up. * * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} * and {@link ScaleGestureDetector#getFocusY()} will return focal point * of the pointers remaining on the screen. * * @param detector The detector reporting the event - use this to * retrieve extended info about event state. */ public void onScaleEnd(ScaleGestureDetector detector); } /** * 便于只实现一部分缩放相关手势时继承的类. This implements all methods in * {@link OnScaleGestureListener} but does nothing. * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns * {@code false} so that a subclass can retrieve the accumulated scale * factor in an overridden onScaleEnd. * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns * {@code true}. */ public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { public boolean onScale(ScaleGestureDetector detector) { return false; } public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } public void onScaleEnd(ScaleGestureDetector detector) { // Intentionally empty } } private final Context mContext; private final OnScaleGestureListener mListener; private float mFocusX; private float mFocusY; private float mCurrSpan; private float mPrevSpan; private float mInitialSpan; private float mCurrSpanX; private float mCurrSpanY; private float mPrevSpanX; private float mPrevSpanY; private long mCurrTime; private long mPrevTime; private boolean mInProgress; private int mSpanSlop; private int mMinSpan; // Bounds for recently seen values private float mTouchUpper; private float mTouchLower; private float mTouchHistoryLastAccepted; private int mTouchHistoryDirection; private long mTouchHistoryLastAcceptedTime; private int mTouchMinMajor; private static final long TOUCH_STABILIZE_TIME = 128; // ms private static final int TOUCH_MIN_MAJOR = 48; // dp /** * Consistency verifier for debugging purposes. */ private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = InputEventConsistencyVerifier.isInstrumentationEnabled() ? new InputEventConsistencyVerifier(this, 0) : null; public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { mContext = context; mListener = listener; mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; final Resources res = context.getResources(); mTouchMinMajor = res.getDimensionPixelSize( com.android.internal.R.dimen.config_minScalingTouchMajor); mMinSpan = res.getDimensionPixelSize( com.android.internal.R.dimen.config_minScalingSpan); } /** * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on * some hardware/driver combos. Smooth it out to get kinder, gentler behavior. * @param ev MotionEvent to add to the ongoing history */ private void addTouchHistory(MotionEvent ev) { final long currentTime = SystemClock.uptimeMillis(); final int count = ev.getPointerCount(); boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME; float total = 0; int sampleCount = 0; for (int i = 0; i < count; i++) { final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted); final int historySize = ev.getHistorySize(); final int pointerSampleCount = historySize + 1; for (int h = 0; h < pointerSampleCount; h++) { float major; if (h < historySize) { major = ev.getHistoricalTouchMajor(i, h); } else { major = ev.getTouchMajor(i); } if (major < mTouchMinMajor) major = mTouchMinMajor; total += major; if (Float.isNaN(mTouchUpper) || major > mTouchUpper) { mTouchUpper = major; } if (Float.isNaN(mTouchLower) || major < mTouchLower) { mTouchLower = major; } if (hasLastAccepted) { final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted); if (directionSig != mTouchHistoryDirection || (directionSig == 0 && mTouchHistoryDirection == 0)) { mTouchHistoryDirection = directionSig; final long time = h < historySize ? ev.getHistoricalEventTime(h) : ev.getEventTime(); mTouchHistoryLastAcceptedTime = time; accept = false; } } } sampleCount += pointerSampleCount; } final float avg = total / sampleCount; if (accept) { float newAccepted = (mTouchUpper + mTouchLower + avg) / 3; mTouchUpper = (mTouchUpper + newAccepted) / 2; mTouchLower = (mTouchLower + newAccepted) / 2; mTouchHistoryLastAccepted = newAccepted; mTouchHistoryDirection = 0; mTouchHistoryLastAcceptedTime = ev.getEventTime(); } } /** * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP. * @see #addTouchHistory(MotionEvent) */ private void clearTouchHistory() { mTouchUpper = Float.NaN; mTouchLower = Float.NaN; mTouchHistoryLastAccepted = Float.NaN; mTouchHistoryDirection = 0; mTouchHistoryLastAcceptedTime = 0; } /** * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} * when appropriate. * * <p>Applications should pass a complete and consistent event stream to this method. * A complete and consistent event stream involves all MotionEvents from the initial * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> * * @param event The event to process * @return true if the event was processed and the detector wants to receive the * rest of the MotionEvents in this event stream. */ public boolean onTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } mCurrTime = event.getEventTime(); final int action = event.getActionMasked(); final boolean streamComplete = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL; if (action == MotionEvent.ACTION_DOWN || streamComplete) { // Reset any scale in progress with the listener. // If it's an ACTION_DOWN we're beginning a new event stream. // This means the app probably didn't give us all the events. Shame on it. if (mInProgress) { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = 0; } if (streamComplete) { clearTouchHistory(); return true; } } final boolean configChanged = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN; final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; final int skipIndex = pointerUp ? event.getActionIndex() : -1; // Determine focal point float sumX = 0, sumY = 0; final int count = event.getPointerCount(); for (int i = 0; i < count; i++) { if (skipIndex == i) continue; sumX += event.getX(i); sumY += event.getY(i); } final int div = pointerUp ? count - 1 : count; final float focusX = sumX / div; final float focusY = sumY / div; addTouchHistory(event); // Determine average deviation from focal point float devSumX = 0, devSumY = 0; for (int i = 0; i < count; i++) { if (skipIndex == i) continue; // Convert the resulting diameter into a radius. final float touchSize = mTouchHistoryLastAccepted / 2; devSumX += Math.abs(event.getX(i) - focusX) + touchSize; devSumY += Math.abs(event.getY(i) - focusY) + touchSize; } final float devX = devSumX / div; final float devY = devSumY / div; // Span is the average distance between touch points through the focal point; // i.e. the diameter of the circle with a radius of the average deviation from // the focal point. final float spanX = devX * 2; final float spanY = devY * 2; final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY); // Dispatch begin/end events as needed. // If the configuration changes, notify the app to reset its current state by beginning // a fresh scale event stream. final boolean wasInProgress = mInProgress; mFocusX = focusX; mFocusY = focusY; if (mInProgress && (span < mMinSpan || configChanged)) { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = span; } if (configChanged) { mPrevSpanX = mCurrSpanX = spanX; mPrevSpanY = mCurrSpanY = spanY; mInitialSpan = mPrevSpan = mCurrSpan = span; } if (!mInProgress && span >= mMinSpan && (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) { mPrevSpanX = mCurrSpanX = spanX; mPrevSpanY = mCurrSpanY = spanY; mPrevSpan = mCurrSpan = span; mPrevTime = mCurrTime; mInProgress = mListener.onScaleBegin(this); } // Handle motion; focal point and span/scale factor are changing. if (action == MotionEvent.ACTION_MOVE) { mCurrSpanX = spanX; mCurrSpanY = spanY; mCurrSpan = span; boolean updatePrev = true; if (mInProgress) { updatePrev = mListener.onScale(this); } if (updatePrev) { mPrevSpanX = mCurrSpanX; mPrevSpanY = mCurrSpanY; mPrevSpan = mCurrSpan; mPrevTime = mCurrTime; } } return true; } /** * 如果多点触控手势进行中返回{@code true 真}. * @return 如果多点触控手势进行中返回{@code true 真};否则返回{@code false 假}. */ public boolean isInProgress() { return mInProgress; } /** * 返回当前手势焦点的X坐标. 如果手势正在进行中,焦点位于组成手势的两个触点之间。 * 如果手势处于停顿状态,焦点为仍留在屏幕上的触点的位置。若{@link #isInProgress()} * 返回假,该方法的返回值未定义。 * * @return 焦点的X坐标值,以像素为单位. */ public float getFocusX() { return mFocusX; } /** * 返回当前手势焦点的Y坐标. 如果手势正在进行中,焦点位于组成手势的两个触点之间。 * 如果手势处于停顿状态,焦点为仍留在屏幕上的触点的位置。若{@link #isInProgress()} * 返回假,该方法的返回值未定义。 * * @return 焦点的Y坐标值,以像素为单位. */ public float getFocusY() { return mFocusY; } /** * Return the average distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 以像素为单位的触点距离. */ public float getCurrentSpan() { return mCurrSpan; } /** * Return the average X distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 两点之间距离的像素数. */ public float getCurrentSpanX() { return mCurrSpanX; } /** * Return the average Y distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 两点之间距离的像素数. */ public float getCurrentSpanY() { return mCurrSpanY; } /** * Return the previous average distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 以像素为单位的前一次测定的触点距离. */ public float getPreviousSpan() { return mPrevSpan; } /** * Return the previous average X distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 上一次两点之间距离的像素数. */ public float getPreviousSpanX() { return mPrevSpanX; } /** * Return the previous average Y distance between each of the pointers forming the * gesture in progress through the focal point. * * @return 上一次两点之间距离的像素数. */ public float getPreviousSpanY() { return mPrevSpanY; } /** * 返回前一个事件到当前事件的伸缩比率.该值为 * {@link #getCurrentSpan()} / {@link #getPreviousSpan()}. * * @return 当前伸缩率. */ public float getScaleFactor() { return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } /** * 返回前一次接收到的伸缩事件距当前伸缩事件的时间差,以毫秒为单位. * * @return 从前一次伸缩事件起始的时间差,以毫秒为单位. */ public long getTimeDelta() { return mCurrTime - mPrevTime; } /** * 返回当前事件执行时的时间戳. * * @return 以毫秒为单位的时间戳. */ public long getEventTime() { return mCurrTime; } }