mirror of https://github.com/NekoX-Dev/NekoX.git
450 lines
18 KiB
Java
450 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2008 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 org.telegram.ui.Components;
|
|
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.view.MotionEvent;
|
|
import android.view.VelocityTracker;
|
|
import android.view.ViewConfiguration;
|
|
|
|
public class GestureDetector2 {
|
|
|
|
public interface OnGestureListener {
|
|
boolean onDown(MotionEvent e);
|
|
void onUp(MotionEvent e);
|
|
void onShowPress(MotionEvent e);
|
|
boolean onSingleTapUp(MotionEvent e);
|
|
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
|
|
void onLongPress(MotionEvent e);
|
|
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
|
|
}
|
|
|
|
public interface OnDoubleTapListener {
|
|
boolean onSingleTapConfirmed(MotionEvent e);
|
|
boolean onDoubleTap(MotionEvent e);
|
|
boolean onDoubleTapEvent(MotionEvent e);
|
|
default boolean canDoubleTap(MotionEvent e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private int mTouchSlopSquare;
|
|
private int mDoubleTapTouchSlopSquare;
|
|
private int mDoubleTapSlopSquare;
|
|
|
|
private int mMinimumFlingVelocity;
|
|
private int mMaximumFlingVelocity;
|
|
|
|
private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
|
|
private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
|
|
public static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
|
|
private static final int DOUBLE_TAP_MIN_TIME = 40;
|
|
|
|
private static final int SHOW_PRESS = 1;
|
|
private static final int LONG_PRESS = 2;
|
|
private static final int TAP = 3;
|
|
|
|
private final Handler mHandler;
|
|
private final OnGestureListener mListener;
|
|
private OnDoubleTapListener mDoubleTapListener;
|
|
|
|
private boolean mStillDown;
|
|
private boolean mDeferConfirmSingleTap;
|
|
private boolean mInLongPress;
|
|
private boolean mInContextClick;
|
|
private boolean mAlwaysInTapRegion;
|
|
private boolean mAlwaysInBiggerTapRegion;
|
|
private boolean mIgnoreNextUpEvent;
|
|
|
|
private MotionEvent mCurrentDownEvent;
|
|
private MotionEvent mCurrentMotionEvent;
|
|
private MotionEvent mPreviousUpEvent;
|
|
|
|
private boolean mIsDoubleTapping;
|
|
|
|
private float mLastFocusX;
|
|
private float mLastFocusY;
|
|
private float mDownFocusX;
|
|
private float mDownFocusY;
|
|
|
|
private boolean mIsLongpressEnabled;
|
|
|
|
private VelocityTracker mVelocityTracker;
|
|
|
|
private class GestureHandler extends Handler {
|
|
GestureHandler() {
|
|
super();
|
|
}
|
|
|
|
GestureHandler(Handler handler) {
|
|
super(handler.getLooper());
|
|
}
|
|
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
switch (msg.what) {
|
|
case SHOW_PRESS:
|
|
mListener.onShowPress(mCurrentDownEvent);
|
|
break;
|
|
|
|
case LONG_PRESS:
|
|
dispatchLongPress();
|
|
break;
|
|
|
|
case TAP:
|
|
if (mDoubleTapListener != null) {
|
|
if (!mStillDown) {
|
|
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
|
|
} else {
|
|
mDeferConfirmSingleTap = true;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new RuntimeException("Unknown message " + msg); //never
|
|
}
|
|
}
|
|
}
|
|
|
|
@Deprecated
|
|
public GestureDetector2(OnGestureListener listener, Handler handler) {
|
|
this(null, listener, handler);
|
|
}
|
|
|
|
@Deprecated
|
|
public GestureDetector2(OnGestureListener listener) {
|
|
this(null, listener, null);
|
|
}
|
|
|
|
public GestureDetector2(Context context, OnGestureListener listener) {
|
|
this(context, listener, null);
|
|
}
|
|
|
|
public GestureDetector2(Context context, OnGestureListener listener, Handler handler) {
|
|
if (handler != null) {
|
|
mHandler = new GestureHandler(handler);
|
|
} else {
|
|
mHandler = new GestureHandler();
|
|
}
|
|
mListener = listener;
|
|
if (listener instanceof OnDoubleTapListener) {
|
|
setOnDoubleTapListener((OnDoubleTapListener) listener);
|
|
}
|
|
init(context);
|
|
}
|
|
|
|
public GestureDetector2(Context context, OnGestureListener listener, Handler handler, boolean unused) {
|
|
this(context, listener, handler);
|
|
}
|
|
|
|
private void init(Context context) {
|
|
if (mListener == null) {
|
|
throw new NullPointerException("OnGestureListener must not be null");
|
|
}
|
|
mIsLongpressEnabled = true;
|
|
|
|
int touchSlop, doubleTapSlop, doubleTapTouchSlop;
|
|
if (context == null) {
|
|
touchSlop = ViewConfiguration.getTouchSlop();
|
|
doubleTapTouchSlop = touchSlop;
|
|
doubleTapSlop = 100;
|
|
mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
|
|
mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
|
|
} else {
|
|
final ViewConfiguration configuration = ViewConfiguration.get(context);
|
|
touchSlop = configuration.getScaledTouchSlop();
|
|
doubleTapTouchSlop = configuration.getScaledTouchSlop();
|
|
doubleTapSlop = configuration.getScaledDoubleTapSlop();
|
|
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
|
|
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
|
|
}
|
|
mTouchSlopSquare = touchSlop * touchSlop;
|
|
mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
|
|
mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
|
|
}
|
|
|
|
public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
|
|
mDoubleTapListener = onDoubleTapListener;
|
|
}
|
|
|
|
public void setIsLongpressEnabled(boolean isLongpressEnabled) {
|
|
mIsLongpressEnabled = isLongpressEnabled;
|
|
}
|
|
|
|
public boolean isLongpressEnabled() {
|
|
return mIsLongpressEnabled;
|
|
}
|
|
|
|
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
final int action = ev.getAction();
|
|
|
|
if (mCurrentMotionEvent != null) {
|
|
mCurrentMotionEvent.recycle();
|
|
}
|
|
mCurrentMotionEvent = MotionEvent.obtain(ev);
|
|
|
|
if (mVelocityTracker == null) {
|
|
mVelocityTracker = VelocityTracker.obtain();
|
|
}
|
|
mVelocityTracker.addMovement(ev);
|
|
|
|
final boolean pointerUp = (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
|
|
final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
|
|
|
|
float sumX = 0, sumY = 0;
|
|
final int count = ev.getPointerCount();
|
|
for (int i = 0; i < count; i++) {
|
|
if (skipIndex == i) continue;
|
|
sumX += ev.getX(i);
|
|
sumY += ev.getY(i);
|
|
}
|
|
final int div = pointerUp ? count - 1 : count;
|
|
final float focusX = sumX / div;
|
|
final float focusY = sumY / div;
|
|
|
|
boolean handled = false;
|
|
|
|
switch (action & MotionEvent.ACTION_MASK) {
|
|
case MotionEvent.ACTION_POINTER_DOWN:
|
|
mDownFocusX = mLastFocusX = focusX;
|
|
mDownFocusY = mLastFocusY = focusY;
|
|
cancelTaps();
|
|
break;
|
|
|
|
case MotionEvent.ACTION_POINTER_UP:
|
|
mDownFocusX = mLastFocusX = focusX;
|
|
mDownFocusY = mLastFocusY = focusY;
|
|
|
|
mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
|
|
final int upIndex = ev.getActionIndex();
|
|
final int id1 = ev.getPointerId(upIndex);
|
|
final float x1 = mVelocityTracker.getXVelocity(id1);
|
|
final float y1 = mVelocityTracker.getYVelocity(id1);
|
|
for (int i = 0; i < count; i++) {
|
|
if (i == upIndex) continue;
|
|
|
|
final int id2 = ev.getPointerId(i);
|
|
final float x = x1 * mVelocityTracker.getXVelocity(id2);
|
|
final float y = y1 * mVelocityTracker.getYVelocity(id2);
|
|
|
|
final float dot = x + y;
|
|
if (dot < 0) {
|
|
mVelocityTracker.clear();
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_DOWN:
|
|
mDeferConfirmSingleTap = false;
|
|
if (mDoubleTapListener != null) {
|
|
if (mDoubleTapListener.canDoubleTap(ev)) {
|
|
boolean hadTapMessage = mHandler.hasMessages(TAP);
|
|
if (hadTapMessage) mHandler.removeMessages(TAP);
|
|
if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
|
|
mIsDoubleTapping = true;
|
|
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
|
|
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
|
|
} else {
|
|
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
|
|
}
|
|
} else {
|
|
mDeferConfirmSingleTap = true;
|
|
}
|
|
}
|
|
|
|
mDownFocusX = mLastFocusX = focusX;
|
|
mDownFocusY = mLastFocusY = focusY;
|
|
if (mCurrentDownEvent != null) {
|
|
mCurrentDownEvent.recycle();
|
|
}
|
|
mCurrentDownEvent = MotionEvent.obtain(ev);
|
|
mAlwaysInTapRegion = true;
|
|
mAlwaysInBiggerTapRegion = true;
|
|
mStillDown = true;
|
|
mInLongPress = false;
|
|
|
|
if (mIsLongpressEnabled) {
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
mHandler.sendMessageAtTime(mHandler.obtainMessage(LONG_PRESS, 0, 0), mCurrentDownEvent.getDownTime() + ViewConfiguration.getLongPressTimeout());
|
|
}
|
|
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
|
|
handled |= mListener.onDown(ev);
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
if (mInLongPress || mInContextClick) {
|
|
break;
|
|
}
|
|
|
|
final int motionClassification = Build.VERSION.SDK_INT >= 29 ? ev.getClassification() : 0;
|
|
final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
|
|
|
|
final float scrollX = mLastFocusX - focusX;
|
|
final float scrollY = mLastFocusY - focusY;
|
|
if (mIsDoubleTapping) {
|
|
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
|
|
} else if (mAlwaysInTapRegion) {
|
|
final int deltaX = (int) (focusX - mDownFocusX);
|
|
final int deltaY = (int) (focusY - mDownFocusY);
|
|
int distance = (deltaX * deltaX) + (deltaY * deltaY);
|
|
int slopSquare = mTouchSlopSquare;
|
|
|
|
final boolean ambiguousGesture = Build.VERSION.SDK_INT >= 29 && motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
|
|
final boolean shouldInhibitDefaultAction = hasPendingLongPress && ambiguousGesture;
|
|
if (shouldInhibitDefaultAction) {
|
|
final float multiplier = 2f;
|
|
if (distance > slopSquare) {
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
|
|
mHandler.sendMessageAtTime(mHandler.obtainMessage(LONG_PRESS, 0, 0), ev.getDownTime() + (long) (longPressTimeout * multiplier));
|
|
}
|
|
slopSquare *= multiplier * multiplier;
|
|
}
|
|
|
|
if (distance > slopSquare) {
|
|
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
|
|
mLastFocusX = focusX;
|
|
mLastFocusY = focusY;
|
|
mAlwaysInTapRegion = false;
|
|
mHandler.removeMessages(TAP);
|
|
mHandler.removeMessages(SHOW_PRESS);
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
}
|
|
int doubleTapSlopSquare = mDoubleTapTouchSlopSquare;
|
|
if (distance > doubleTapSlopSquare) {
|
|
mAlwaysInBiggerTapRegion = false;
|
|
}
|
|
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
|
|
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
|
|
mLastFocusX = focusX;
|
|
mLastFocusY = focusY;
|
|
}
|
|
if (Build.VERSION.SDK_INT >= 29) {
|
|
final boolean deepPress = motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
|
|
if (deepPress && hasPendingLongPress) {
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
mHandler.sendMessage(mHandler.obtainMessage(LONG_PRESS, 0, 0));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
mStillDown = false;
|
|
mListener.onUp(ev);
|
|
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
|
|
if (mIsDoubleTapping) {
|
|
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
|
|
} else if (mInLongPress) {
|
|
mHandler.removeMessages(TAP);
|
|
mInLongPress = false;
|
|
} else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
|
|
handled = mListener.onSingleTapUp(ev);
|
|
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
|
|
mDoubleTapListener.onSingleTapConfirmed(ev);
|
|
}
|
|
} else if (!mIgnoreNextUpEvent) {
|
|
final VelocityTracker velocityTracker = mVelocityTracker;
|
|
final int pointerId = ev.getPointerId(0);
|
|
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
|
|
final float velocityY = velocityTracker.getYVelocity(pointerId);
|
|
final float velocityX = velocityTracker.getXVelocity(pointerId);
|
|
|
|
if ((Math.abs(velocityY) > mMinimumFlingVelocity) || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
|
|
handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
|
|
}
|
|
}
|
|
if (mPreviousUpEvent != null) {
|
|
mPreviousUpEvent.recycle();
|
|
}
|
|
mPreviousUpEvent = currentUpEvent;
|
|
if (mVelocityTracker != null) {
|
|
mVelocityTracker.recycle();
|
|
mVelocityTracker = null;
|
|
}
|
|
mIsDoubleTapping = false;
|
|
mDeferConfirmSingleTap = false;
|
|
mIgnoreNextUpEvent = false;
|
|
mHandler.removeMessages(SHOW_PRESS);
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
cancel();
|
|
break;
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
private void cancel() {
|
|
mHandler.removeMessages(SHOW_PRESS);
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
mHandler.removeMessages(TAP);
|
|
mVelocityTracker.recycle();
|
|
mVelocityTracker = null;
|
|
mIsDoubleTapping = false;
|
|
mStillDown = false;
|
|
mAlwaysInTapRegion = false;
|
|
mAlwaysInBiggerTapRegion = false;
|
|
mDeferConfirmSingleTap = false;
|
|
mInLongPress = false;
|
|
mInContextClick = false;
|
|
mIgnoreNextUpEvent = false;
|
|
}
|
|
|
|
private void cancelTaps() {
|
|
mHandler.removeMessages(SHOW_PRESS);
|
|
mHandler.removeMessages(LONG_PRESS);
|
|
mHandler.removeMessages(TAP);
|
|
mIsDoubleTapping = false;
|
|
mAlwaysInTapRegion = false;
|
|
mAlwaysInBiggerTapRegion = false;
|
|
mDeferConfirmSingleTap = false;
|
|
mInLongPress = false;
|
|
mInContextClick = false;
|
|
mIgnoreNextUpEvent = false;
|
|
}
|
|
|
|
private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, MotionEvent secondDown) {
|
|
if (!mAlwaysInBiggerTapRegion) {
|
|
return false;
|
|
}
|
|
|
|
final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
|
|
if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
|
|
return false;
|
|
}
|
|
|
|
int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
|
|
int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
|
|
int slopSquare = mDoubleTapSlopSquare;
|
|
return (deltaX * deltaX + deltaY * deltaY < slopSquare);
|
|
}
|
|
|
|
private void dispatchLongPress() {
|
|
mHandler.removeMessages(TAP);
|
|
mDeferConfirmSingleTap = false;
|
|
mInLongPress = true;
|
|
mListener.onLongPress(mCurrentDownEvent);
|
|
}
|
|
}
|