/*
* 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;
}
}