This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,28 @@
apply plugin: 'com.android.library'
repositories {
maven { url 'https://repo1.maven.org/maven2' }
}
android {
compileSdkVersion 23
buildToolsVersion '25.0.0'
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
}

View File

@ -0,0 +1,23 @@
package com.swmansion.gesturehandler;
public abstract class BaseGestureHandlerInteractionController
implements GestureHandlerInteractionController {
@Override
public boolean shouldWaitForHandlerFailure(GestureHandler handler,
GestureHandler otherHandler) {
return false;
}
@Override
public boolean shouldRequireHandlerToWaitForFailure(GestureHandler handler,
GestureHandler otherHandler) {
return false;
}
@Override
public boolean shouldRecognizeSimultaneously(GestureHandler handler,
GestureHandler otherHandler) {
return false;
}
}

View File

@ -0,0 +1,110 @@
package com.swmansion.gesturehandler;
import android.os.Handler;
import android.view.MotionEvent;
public class FlingGestureHandler extends GestureHandler<FlingGestureHandler> {
private static final long DEFAULT_MAX_DURATION_MS = 800;
private static final long DEFAULT_MIN_ACCEPTABLE_DELTA = 160;
private static final int DEFAULT_DIRECTION = DIRECTION_RIGHT;
private static final int DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1;
private long mMaxDurationMs = DEFAULT_MAX_DURATION_MS;
private long mMinAcceptableDelta = DEFAULT_MIN_ACCEPTABLE_DELTA;
private int mDirection = DEFAULT_DIRECTION;
private int mNumberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
private float mStartX, mStartY;
private Handler mHandler;
private int mMaxNumberOfPointersSimultaneously;
private final Runnable mFailDelayed = new Runnable() {
@Override
public void run() {
fail();
}
};
public void setNumberOfPointersRequired(int numberOfPointersRequired) {
mNumberOfPointersRequired = numberOfPointersRequired;
}
public void setDirection(int direction) {
mDirection = direction;
}
private void startFling(MotionEvent event) {
mStartX = event.getRawX();
mStartY = event.getRawY();
begin();
mMaxNumberOfPointersSimultaneously = 1;
if (mHandler == null) {
mHandler = new Handler();
} else {
mHandler.removeCallbacksAndMessages(null);
}
mHandler.postDelayed(mFailDelayed, mMaxDurationMs);
}
private boolean tryEndFling(MotionEvent event) {
if (mMaxNumberOfPointersSimultaneously == mNumberOfPointersRequired &&
(((mDirection & DIRECTION_RIGHT) != 0 &&
event.getRawX() - mStartX > mMinAcceptableDelta) ||
((mDirection & DIRECTION_LEFT) !=0 &&
mStartX - event.getRawX() > mMinAcceptableDelta) ||
((mDirection & DIRECTION_UP) !=0 &&
mStartY - event.getRawY() > mMinAcceptableDelta) ||
((mDirection & DIRECTION_DOWN) !=0 &&
event.getRawY() - mStartY > mMinAcceptableDelta))) {
mHandler.removeCallbacksAndMessages(null);
activate();
end();
return true;
} else {
return false;
}
}
private void endFling(MotionEvent event) {
if (!tryEndFling(event)) {
fail();
}
}
@Override
protected void onHandle(MotionEvent event) {
int state = getState();
if (state == STATE_UNDETERMINED) {
startFling(event);
}
if (state == STATE_BEGAN) {
tryEndFling(event);
if (event.getPointerCount() > mMaxNumberOfPointersSimultaneously) {
mMaxNumberOfPointersSimultaneously = event.getPointerCount();
}
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_UP) {
endFling(event);
}
}
}
@Override
protected void onCancel() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
@Override
protected void onReset() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
}

View File

@ -0,0 +1,531 @@
package com.swmansion.gesturehandler;
import android.view.MotionEvent;
import android.view.View;
import com.facebook.react.bridge.UiThreadUtil;
import java.util.Arrays;
public class GestureHandler<T extends GestureHandler> {
public static final int STATE_UNDETERMINED = 0;
public static final int STATE_FAILED = 1;
public static final int STATE_BEGAN = 2;
public static final int STATE_CANCELLED = 3;
public static final int STATE_ACTIVE = 4;
public static final int STATE_END = 5;
public static final float HIT_SLOP_NONE = Float.NaN;
private static final int HIT_SLOP_LEFT_IDX = 0;
private static final int HIT_SLOP_TOP_IDX = 1;
private static final int HIT_SLOP_RIGHT_IDX = 2;
private static final int HIT_SLOP_BOTTOM_IDX = 3;
private static final int HIT_SLOP_WIDTH_IDX = 4;
private static final int HIT_SLOP_HEIGHT_IDX = 5;
public static final int DIRECTION_RIGHT = 1;
public static final int DIRECTION_LEFT = 2;
public static final int DIRECTION_UP = 4;
public static final int DIRECTION_DOWN = 8;
private static int MAX_POINTERS_COUNT = 12;
private static MotionEvent.PointerProperties[] sPointerProps;
private static MotionEvent.PointerCoords[] sPointerCoords;
private static void initPointerProps(int size) {
if (sPointerProps == null) {
sPointerProps = new MotionEvent.PointerProperties[MAX_POINTERS_COUNT];
sPointerCoords = new MotionEvent.PointerCoords[MAX_POINTERS_COUNT];
}
for (; size > 0 && sPointerProps[size - 1] == null; size--) {
sPointerProps[size - 1] = new MotionEvent.PointerProperties();
sPointerCoords[size - 1] = new MotionEvent.PointerCoords();
}
}
private final int[] mTrackedPointerIDs = new int[MAX_POINTERS_COUNT];
private int mTrackedPointersCount = 0;
private int mTag;
private View mView;
private int mState = STATE_UNDETERMINED;
private float mX, mY;
private boolean mWithinBounds;
private boolean mEnabled = true;
private float mHitSlop[];
private static short sNextEventCoalescingKey = 0;
private short mEventCoalescingKey;
private float mLastX, mLastY;
private float mLastEventOffsetX, mLastEventOffsetY;
private boolean mShouldCancelWhenOutside;
private int mNumberOfPointers = 0;
private GestureHandlerOrchestrator mOrchestrator;
private OnTouchEventListener<T> mListener;
private GestureHandlerInteractionController mInteractionController;
/*package*/ int mActivationIndex; // set and accessed only by the orchestrator
/*package*/ boolean mIsActive; // set and accessed only by the orchestrator
/*package*/ boolean mIsAwaiting; // set and accessed only by the orchestrator
private static boolean hitSlopSet(float value) {
return !Float.isNaN(value);
}
/*package*/ void dispatchStateChange(int newState, int prevState) {
if (mListener != null) {
mListener.onStateChange((T) this, newState, prevState);
}
}
/*package*/ void dispatchTouchEvent(MotionEvent event) {
if (mListener != null) {
mListener.onTouchEvent((T) this, event);
}
}
public boolean hasCommonPointers(GestureHandler other) {
for (int i = 0; i < mTrackedPointerIDs.length; i++) {
if (mTrackedPointerIDs[i] != -1 && other.mTrackedPointerIDs[i] != -1) {
return true;
}
}
return false;
}
public T setShouldCancelWhenOutside(boolean shouldCancelWhenOutside) {
mShouldCancelWhenOutside = shouldCancelWhenOutside;
return (T) this;
}
public T setEnabled(boolean enabled) {
if (mView != null) {
// If view is set then handler is in "active" state. In that case we want to "cancel" handler
// when it changes enabled state so that it gets cleared from the orchestrator
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
cancel();
}
});
}
mEnabled = enabled;
return (T) this;
}
public boolean isEnabled() {
return mEnabled;
}
public T setHitSlop(float leftPad, float topPad, float rightPad, float bottomPad, float width, float height) {
if (mHitSlop == null) {
mHitSlop = new float[6];
}
mHitSlop[HIT_SLOP_LEFT_IDX] = leftPad;
mHitSlop[HIT_SLOP_TOP_IDX] = topPad;
mHitSlop[HIT_SLOP_RIGHT_IDX] = rightPad;
mHitSlop[HIT_SLOP_BOTTOM_IDX] = bottomPad;
mHitSlop[HIT_SLOP_WIDTH_IDX] = width;
mHitSlop[HIT_SLOP_HEIGHT_IDX] = height;
if (hitSlopSet(width) && hitSlopSet(leftPad) && hitSlopSet(rightPad)) {
throw new IllegalArgumentException("Cannot have all of left, right and width defined");
}
if (hitSlopSet(width) && !hitSlopSet(leftPad) && !hitSlopSet(rightPad)) {
throw new IllegalArgumentException("When width is set one of left or right pads need to be defined");
}
if (hitSlopSet(height) && hitSlopSet(bottomPad) && hitSlopSet(topPad)) {
throw new IllegalArgumentException("Cannot have all of top, bottom and height defined");
}
if (hitSlopSet(height) && !hitSlopSet(bottomPad) && !hitSlopSet(topPad)) {
throw new IllegalArgumentException("When height is set one of top or bottom pads need to be defined");
}
return (T) this;
}
public T setHitSlop(float padding) {
return setHitSlop(padding, padding, padding, padding, HIT_SLOP_NONE, HIT_SLOP_NONE);
}
public T setInteractionController(GestureHandlerInteractionController controller) {
mInteractionController = controller;
return (T) this;
}
public void setTag(int tag) {
mTag = tag;
}
public int getTag() {
return mTag;
}
public View getView() {
return mView;
}
public float getX() {
return mX;
}
public float getY() {
return mY;
}
public int getNumberOfPointers() {
return mNumberOfPointers;
}
public boolean isWithinBounds() {
return mWithinBounds;
}
public short getEventCoalescingKey() {
return mEventCoalescingKey;
}
public final void prepare(View view, GestureHandlerOrchestrator orchestrator) {
if (mView != null || mOrchestrator != null) {
throw new IllegalStateException("Already prepared or hasn't been reset");
}
Arrays.fill(mTrackedPointerIDs, -1);
mTrackedPointersCount = 0;
mState = STATE_UNDETERMINED;
mView = view;
mOrchestrator = orchestrator;
}
private int findNextLocalPointerId() {
int localPointerId = 0;
for (; localPointerId < mTrackedPointersCount; localPointerId++) {
int i = 0;
for (; i < mTrackedPointerIDs.length; i++) {
if (mTrackedPointerIDs[i] == localPointerId) {
break;
}
}
if (i == mTrackedPointerIDs.length) {
return localPointerId;
}
}
return localPointerId;
}
public void startTrackingPointer(int pointerId) {
if (mTrackedPointerIDs[pointerId] == -1) {
mTrackedPointerIDs[pointerId] = findNextLocalPointerId();
mTrackedPointersCount++;
}
}
public void stopTrackingPointer(int pointerId) {
if (mTrackedPointerIDs[pointerId] != -1) {
mTrackedPointerIDs[pointerId] = -1;
mTrackedPointersCount--;
}
}
private boolean needAdapt(MotionEvent event) {
if (event.getPointerCount() != mTrackedPointersCount) {
return true;
}
for (int i = 0; i < mTrackedPointerIDs.length; i++) {
if (mTrackedPointerIDs[i] != -1 && mTrackedPointerIDs[i] != i) {
return true;
}
}
return false;
}
private MotionEvent adaptEvent(MotionEvent event) {
if (!needAdapt(event)) {
return event;
}
int action = event.getActionMasked();
int actionIndex = -1;
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
actionIndex = event.getActionIndex();
int actionPointer = event.getPointerId(actionIndex);
if (mTrackedPointerIDs[actionPointer] != -1) {
action = mTrackedPointersCount == 1 ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_POINTER_DOWN;
} else {
action = MotionEvent.ACTION_MOVE;
}
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
actionIndex = event.getActionIndex();
int actionPointer = event.getPointerId(actionIndex);
if (mTrackedPointerIDs[actionPointer] != -1) {
action = mTrackedPointersCount == 1 ? MotionEvent.ACTION_UP : MotionEvent.ACTION_POINTER_UP;
} else {
action = MotionEvent.ACTION_MOVE;
}
}
initPointerProps(mTrackedPointersCount);
int count = 0;
float oldX = event.getX();
float oldY = event.getY();
event.setLocation(event.getRawX(), event.getRawY());
for (int index = 0, size = event.getPointerCount(); index < size; index++) {
int origPointerId = event.getPointerId(index);
if (mTrackedPointerIDs[origPointerId] != -1) {
event.getPointerProperties(index, sPointerProps[count]);
sPointerProps[count].id = mTrackedPointerIDs[origPointerId];
event.getPointerCoords(index, sPointerCoords[count]);
if (index == actionIndex) {
action = action | (count << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
}
count++;
}
}
MotionEvent result = MotionEvent.obtain(
event.getDownTime(),
event.getEventTime(),
action,
count,
sPointerProps, /* props are copied and hence it is safe to use static array here */
sPointerCoords, /* same applies to coords */
event.getMetaState(),
event.getButtonState(),
event.getXPrecision(),
event.getYPrecision(),
event.getDeviceId(),
event.getEdgeFlags(),
event.getSource(),
event.getFlags());
event.setLocation(oldX, oldY);
result.setLocation(oldX, oldY);
return result;
}
public final void handle(MotionEvent origEvent) {
if (!mEnabled || mState == STATE_CANCELLED || mState == STATE_FAILED
|| mState == STATE_END || mTrackedPointersCount < 1) {
return;
}
MotionEvent event = adaptEvent(origEvent);
mX = event.getX();
mY = event.getY();
mNumberOfPointers = event.getPointerCount();
mWithinBounds = isWithinBounds(mView, mX, mY);
if (mShouldCancelWhenOutside && !mWithinBounds) {
if (mState == STATE_ACTIVE) {
cancel();
} else if (mState == STATE_BEGAN) {
fail();
}
return;
}
mLastX = GestureUtils.getLastPointerX(event, true);
mLastY = GestureUtils.getLastPointerY(event, true);
mLastEventOffsetX = event.getRawX() - event.getX();
mLastEventOffsetY = event.getRawY() - event.getY();
onHandle(event);
if (event != origEvent) {
event.recycle();
}
}
private void moveToState(int newState) {
UiThreadUtil.assertOnUiThread();
if (mState == newState) {
return;
}
int oldState = mState;
mState = newState;
if (mState == STATE_ACTIVE) {
// Generate a unique coalescing-key each time the gesture-handler becomes active. All events will have
// the same coalescing-key allowing EventDispatcher to coalesce RNGestureHandlerEvents when events are
// generated faster than they can be treated by JS thread
mEventCoalescingKey = sNextEventCoalescingKey++;
}
mOrchestrator.onHandlerStateChange(this, newState, oldState);
onStateChange(newState, oldState);
}
public boolean wantEvents() {
return mEnabled && mState != STATE_FAILED && mState != STATE_CANCELLED
&& mState != STATE_END && mTrackedPointersCount > 0;
}
public int getState() {
return mState;
}
public boolean shouldRequireToWaitForFailure(GestureHandler handler) {
if (handler != this && mInteractionController != null) {
return mInteractionController.shouldRequireHandlerToWaitForFailure(this, handler);
}
return false;
}
public boolean shouldWaitForHandlerFailure(GestureHandler handler) {
if (handler != this && mInteractionController != null) {
return mInteractionController.shouldWaitForHandlerFailure(this, handler);
}
return false;
}
public boolean shouldRecognizeSimultaneously(GestureHandler handler) {
if (handler == this) {
return true;
}
if (mInteractionController != null) {
return mInteractionController.shouldRecognizeSimultaneously(this, handler);
}
return false;
}
public boolean shouldBeCancelledBy(GestureHandler handler) {
if (handler == this) {
return false;
}
if (mInteractionController != null) {
return mInteractionController.shouldHandlerBeCancelledBy(this, handler);
}
return false;
}
public boolean isWithinBounds(View view, float posX, float posY) {
float left = 0;
float top = 0;
float right = view.getWidth();
float bottom = view.getHeight();
if (mHitSlop != null) {
float padLeft = mHitSlop[HIT_SLOP_LEFT_IDX];
float padTop = mHitSlop[HIT_SLOP_TOP_IDX];
float padRight = mHitSlop[HIT_SLOP_RIGHT_IDX];
float padBottom = mHitSlop[HIT_SLOP_BOTTOM_IDX];
if (hitSlopSet(padLeft)) {
left -= padLeft;
}
if (hitSlopSet(padTop)) {
top -= padBottom;
}
if (hitSlopSet(padRight)) {
right += padRight;
}
if (hitSlopSet(padBottom)) {
bottom += padBottom;
}
float width = mHitSlop[HIT_SLOP_WIDTH_IDX];
float height= mHitSlop[HIT_SLOP_HEIGHT_IDX];
if (hitSlopSet(width)) {
if (!hitSlopSet(padLeft)) {
left = right - width;
} else if (!hitSlopSet(padRight)) {
right = left + width;
}
}
if (hitSlopSet(height)) {
if (!hitSlopSet(top)) {
top = bottom - height;
} else if (!hitSlopSet(bottom)) {
bottom = top + height;
}
}
}
return posX >= left && posX <= right && posY >= top && posY <= bottom;
}
public final void cancel() {
if (mState == STATE_ACTIVE || mState == STATE_UNDETERMINED || mState == STATE_BEGAN) {
onCancel();
moveToState(STATE_CANCELLED);
}
}
public final void fail() {
if (mState == STATE_ACTIVE || mState == STATE_UNDETERMINED || mState == STATE_BEGAN) {
moveToState(STATE_FAILED);
}
}
public final void activate() {
if (mState == STATE_UNDETERMINED || mState == STATE_BEGAN) {
moveToState(STATE_ACTIVE);
}
}
public final void begin() {
if (mState == STATE_UNDETERMINED) {
moveToState(STATE_BEGAN);
}
}
public final void end() {
if (mState == STATE_BEGAN || mState == STATE_ACTIVE) {
moveToState(STATE_END);
}
}
protected void onHandle(MotionEvent event) {
moveToState(STATE_FAILED);
}
protected void onStateChange(int newState, int previousState) {
}
protected void onReset() {
}
protected void onCancel() {
}
public final void reset() {
mView = null;
mOrchestrator = null;
Arrays.fill(mTrackedPointerIDs, -1);
mTrackedPointersCount = 0;
onReset();
}
public static String stateToString(int state) {
switch (state) {
case STATE_UNDETERMINED: return "UNDETERMINED";
case STATE_ACTIVE: return "ACTIVE";
case STATE_FAILED: return "FAILED";
case STATE_BEGAN: return "BEGIN";
case STATE_CANCELLED: return "CANCELLED";
case STATE_END: return "END";
}
return null;
}
public GestureHandler setOnTouchEventListener(OnTouchEventListener<T> listener) {
mListener = listener;
return this;
}
@Override
public String toString() {
String viewString = mView == null ? null : mView.getClass().getSimpleName();
return this.getClass().getSimpleName() + "@[" + mTag + "]:" + viewString;
}
public float getLastAbsolutePositionX() {
return mLastX;
}
public float getLastAbsolutePositionY() {
return mLastY;
}
public float getLastRelativePositionX() {
return mLastX - mLastEventOffsetX;
}
public float getLastRelativePositionY() {
return mLastY - mLastEventOffsetY;
}
}

View File

@ -0,0 +1,8 @@
package com.swmansion.gesturehandler;
public interface GestureHandlerInteractionController {
boolean shouldWaitForHandlerFailure(GestureHandler handler, GestureHandler otherHandler);
boolean shouldRequireHandlerToWaitForFailure(GestureHandler handler, GestureHandler otherHandler);
boolean shouldRecognizeSimultaneously(GestureHandler handler, GestureHandler otherHandler);
boolean shouldHandlerBeCancelledBy(GestureHandler handler, GestureHandler otherHandler);
}

View File

@ -0,0 +1,543 @@
package com.swmansion.gesturehandler;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import androidx.annotation.Nullable;
public class GestureHandlerOrchestrator {
// The limit doesn't necessarily need to exists, it was just simpler to implement it that way
// it is also more allocation-wise efficient to have a fixed limit
private static final int SIMULTANEOUS_GESTURE_HANDLER_LIMIT = 20;
// Be default fully transparent views can receive touch
private static final float DEFAULT_MIN_ALPHA_FOR_TRAVERSAL = 0f;
private static final PointF sTempPoint = new PointF();
private static final float[] sMatrixTransformCoords = new float[2];
private static final Matrix sInverseMatrix = new Matrix();
private static final float[] sTempCoords = new float[2];
private static final Comparator<GestureHandler> sHandlersComparator =
new Comparator<GestureHandler>() {
@Override
public int compare(GestureHandler a, GestureHandler b) {
if (a.mIsActive && b.mIsActive || a.mIsAwaiting && b.mIsAwaiting) {
// both A and B are either active or awaiting activation, in which case we prefer one that
// has activated (or turned into "awaiting" state) earlier
return Integer.signum(b.mActivationIndex - a.mActivationIndex);
} else if (a.mIsActive) {
return -1; // only A is active
} else if (b.mIsActive) {
return 1; // only B is active
} else if (a.mIsAwaiting) {
return -1; // only A is awaiting, B is inactive
} else if (b.mIsAwaiting) {
return 1; // only B is awaiting, A is inactive
}
return 0; // both A and B are inactive, stable order matters
}
};
private final ViewGroup mWrapperView;
private final GestureHandlerRegistry mHandlerRegistry;
private final ViewConfigurationHelper mViewConfigHelper;
private final GestureHandler[] mGestureHandlers
= new GestureHandler[SIMULTANEOUS_GESTURE_HANDLER_LIMIT];
private final GestureHandler[] mAwaitingHandlers
= new GestureHandler[SIMULTANEOUS_GESTURE_HANDLER_LIMIT];
private final GestureHandler[] mPreparedHandlers
= new GestureHandler[SIMULTANEOUS_GESTURE_HANDLER_LIMIT];
private final GestureHandler[] mHandlersToCancel
= new GestureHandler[SIMULTANEOUS_GESTURE_HANDLER_LIMIT];
private int mGestureHandlersCount = 0;
private int mAwaitingHandlersCount = 0;
private boolean mIsHandlingTouch = false;
private int mHandlingChangeSemaphore = 0;
private boolean mFinishedHandlersCleanupScheduled = false;
private int mActivationIndex = 0;
private float mMinAlphaForTraversal = DEFAULT_MIN_ALPHA_FOR_TRAVERSAL;
public GestureHandlerOrchestrator(
ViewGroup wrapperView,
GestureHandlerRegistry registry,
ViewConfigurationHelper viewConfigurationHelper) {
mWrapperView = wrapperView;
mHandlerRegistry = registry;
mViewConfigHelper = viewConfigurationHelper;
}
/**
* Minimum alpha (value from 0 to 1) that should be set to a view so that it can be treated as a
* gesture target. E.g. if set to 0.1 then views that less than 10% opaque will be ignored when
* traversing view hierarchy and looking for gesture handlers.
*/
public void setMinimumAlphaForTraversal(float alpha) {
mMinAlphaForTraversal = alpha;
}
/**
* Should be called from the view wrapper
*/
public boolean onTouchEvent(MotionEvent event) {
mIsHandlingTouch = true;
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
extractGestureHandlers(event);
} else if (action == MotionEvent.ACTION_CANCEL) {
cancelAll();
}
deliverEventToGestureHandlers(event);
mIsHandlingTouch = false;
if (mFinishedHandlersCleanupScheduled && mHandlingChangeSemaphore == 0) {
cleanupFinishedHandlers();
}
return true;
}
private void scheduleFinishedHandlersCleanup() {
if (mIsHandlingTouch || mHandlingChangeSemaphore != 0) {
mFinishedHandlersCleanupScheduled = true;
} else {
cleanupFinishedHandlers();
}
}
private void cleanupFinishedHandlers() {
boolean shouldCleanEmptyCells = false;
for (int i = mGestureHandlersCount - 1; i >= 0; i--) {
GestureHandler handler = mGestureHandlers[i];
if (isFinished(handler.getState()) && !handler.mIsAwaiting) {
mGestureHandlers[i] = null;
shouldCleanEmptyCells = true;
handler.reset();
handler.mIsActive = false;
handler.mIsAwaiting = false;
handler.mActivationIndex = Integer.MAX_VALUE;
}
}
if (shouldCleanEmptyCells) {
int out = 0;
for (int i = 0; i < mGestureHandlersCount; i++) {
if (mGestureHandlers[i] != null) {
mGestureHandlers[out++] = mGestureHandlers[i];
}
}
mGestureHandlersCount = out;
}
mFinishedHandlersCleanupScheduled = false;
}
private boolean hasOtherHandlerToWaitFor(GestureHandler handler) {
for (int i = 0; i < mGestureHandlersCount; i++) {
GestureHandler otherHandler = mGestureHandlers[i];
if (!isFinished(otherHandler.getState())
&& shouldHandlerWaitForOther(handler, otherHandler)) {
return true;
}
}
return false;
}
private void tryActivate(GestureHandler handler) {
// see if there is anyone else who we need to wait for
if (hasOtherHandlerToWaitFor(handler)) {
addAwaitingHandler(handler);
} else {
// we can activate handler right away
makeActive(handler);
handler.mIsAwaiting = false;
}
}
private void cleanupAwaitingHandlers() {
int out = 0;
for (int i = 0; i < mAwaitingHandlersCount; i++) {
if (mAwaitingHandlers[i].mIsAwaiting) {
mAwaitingHandlers[out++] = mAwaitingHandlers[i];
}
}
mAwaitingHandlersCount = out;
}
/*package*/ void onHandlerStateChange(GestureHandler handler, int newState, int prevState) {
mHandlingChangeSemaphore += 1;
if (isFinished(newState)) {
// if there were handlers awaiting completion of this handler, we can trigger active state
for (int i = 0; i < mAwaitingHandlersCount; i++) {
GestureHandler otherHandler = mAwaitingHandlers[i];
if (shouldHandlerWaitForOther(otherHandler, handler)) {
if (newState == GestureHandler.STATE_END) {
// gesture has ended, we need to kill the awaiting handler
otherHandler.cancel();
otherHandler.mIsAwaiting = false;
} else {
// gesture has failed recognition, we may try activating
tryActivate(otherHandler);
}
}
}
cleanupAwaitingHandlers();
}
if (newState == GestureHandler.STATE_ACTIVE) {
tryActivate(handler);
} else if (prevState == GestureHandler.STATE_ACTIVE || prevState == GestureHandler.STATE_END) {
if (handler.mIsActive) {
handler.dispatchStateChange(newState, prevState);
}
} else {
handler.dispatchStateChange(newState, prevState);
}
mHandlingChangeSemaphore -= 1;
scheduleFinishedHandlersCleanup();
}
private void makeActive(GestureHandler handler) {
int currentState = handler.getState();
handler.mIsAwaiting = false;
handler.mIsActive = true;
handler.mActivationIndex = mActivationIndex++;
int toCancelCount = 0;
// Cancel all handlers that are required to be cancel upon current handler's activation
for (int i = 0; i < mGestureHandlersCount; i++) {
GestureHandler otherHandler = mGestureHandlers[i];
if (shouldHandlerBeCancelledBy(otherHandler, handler)) {
mHandlersToCancel[toCancelCount++] = otherHandler;
}
}
for (int i = toCancelCount - 1; i >= 0; i--) {
mHandlersToCancel[i].cancel();
}
// Clear all awaiting handlers waiting for the current handler to fail
for (int i = mAwaitingHandlersCount - 1; i >= 0; i--) {
GestureHandler otherHandler = mAwaitingHandlers[i];
if (shouldHandlerBeCancelledBy(otherHandler, handler)) {
otherHandler.cancel();
otherHandler.mIsAwaiting = false;
}
}
cleanupAwaitingHandlers();
// Dispatch state change event if handler is no longer in the active state we should also
// trigger END state change and UNDETERMINED state change if necessary
handler.dispatchStateChange(GestureHandler.STATE_ACTIVE, GestureHandler.STATE_BEGAN);
if (currentState != GestureHandler.STATE_ACTIVE) {
handler.dispatchStateChange(GestureHandler.STATE_END, GestureHandler.STATE_ACTIVE);
if (currentState != GestureHandler.STATE_END) {
handler.dispatchStateChange(GestureHandler.STATE_UNDETERMINED, GestureHandler.STATE_END);
}
}
}
public void deliverEventToGestureHandlers(MotionEvent event) {
// Copy handlers to "prepared handlers" array, because the list of active handlers can change
// as a result of state updates
int handlersCount = mGestureHandlersCount;
System.arraycopy(mGestureHandlers, 0, mPreparedHandlers, 0, handlersCount);
// We want to deliver events to active handlers first in order of their activation (handlers
// that activated first will first get event delivered). Otherwise we deliver events in the
// order in which handlers has been added ("most direct" children goes first). Therefore we rely
// on Arrays.sort providing a stable sort (as children are registered in order in which they
// should be tested)
Arrays.sort(mPreparedHandlers, 0, handlersCount, sHandlersComparator);
for (int i = 0; i < handlersCount; i++) {
deliverEventToGestureHandler(mPreparedHandlers[i], event);
}
}
private void cancelAll() {
for (int i = mAwaitingHandlersCount - 1; i >= 0; i--) {
mAwaitingHandlers[i].cancel();
}
// Copy handlers to "prepared handlers" array, because the list of active handlers can change
// as a result of state updates
int handlersCount = mGestureHandlersCount;
for (int i = 0; i < handlersCount; i++) {
mPreparedHandlers[i] = mGestureHandlers[i];
}
for (int i = handlersCount - 1; i >= 0; i--) {
mPreparedHandlers[i].cancel();
}
}
private void deliverEventToGestureHandler(GestureHandler handler, MotionEvent event) {
if (!isViewAttachedUnderWrapper(handler.getView())) {
handler.cancel();
return;
}
if (!handler.wantEvents()) {
return;
}
int action = event.getActionMasked();
if (handler.mIsAwaiting && action == MotionEvent.ACTION_MOVE) {
return;
}
float[] coords = sTempCoords;
extractCoordsForView(handler.getView(), event, coords);
float oldX = event.getX();
float oldY = event.getY();
// TODO: we may conside scaling events if necessary using MotionEvent.transform
// for now the events are only offset to the top left corner of the view but if
// view or any ot the parents is scaled the other pointers position will not reflect
// their actual place in the view. On the other hand not scaling seems like a better
// approach when we want to use pointer coordinates to calculate velocity or distance
// for pinch so I don't know yet if we should transform or not...
event.setLocation(coords[0], coords[1]);
handler.handle(event);
if (handler.mIsActive) {
handler.dispatchTouchEvent(event);
}
event.setLocation(oldX, oldY);
// if event was of type UP or POINTER_UP we request handler to stop tracking now that
// the event has been dispatched
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
int pointerId = event.getPointerId(event.getActionIndex());
handler.stopTrackingPointer(pointerId);
}
}
/**
* isViewAttachedUnderWrapper checks whether all of parents for view related to handler
* view are attached. Since there might be an issue rarely observed when view
* has been detached and handler's state hasn't been change to canceled, failed or
* ended yet. Probably it's a result of some race condition and stopping delivering
* for this handler and changing its state to failed of end appear to be good enough solution.
*/
private boolean isViewAttachedUnderWrapper(@Nullable View view) {
if (view == null) {
return false;
}
if (view == mWrapperView) {
return true;
}
@Nullable ViewParent parent = view.getParent();
while (parent != null && parent != mWrapperView) {
parent = parent.getParent();
}
return parent == mWrapperView;
}
private void extractCoordsForView(View view, MotionEvent event, float[] outputCoords) {
if (view == mWrapperView) {
outputCoords[0] = event.getX();
outputCoords[1] = event.getY();
return;
}
if (view == null || !(view.getParent() instanceof ViewGroup)) {
throw new IllegalArgumentException("Parent is null? View is no longer in the tree");
}
ViewGroup parent = (ViewGroup) view.getParent();
extractCoordsForView(parent, event, outputCoords);
PointF childPoint = sTempPoint;
transformTouchPointToViewCoords(outputCoords[0], outputCoords[1], parent, view, childPoint);
outputCoords[0] = childPoint.x;
outputCoords[1] = childPoint.y;
}
private void addAwaitingHandler(GestureHandler handler) {
for (int i = 0; i < mAwaitingHandlersCount; i++) {
if (mAwaitingHandlers[i] == handler) {
return;
}
}
if (mAwaitingHandlersCount >= mAwaitingHandlers.length) {
throw new IllegalStateException("Too many recognizers");
}
mAwaitingHandlers[mAwaitingHandlersCount++] = handler;
handler.mIsAwaiting = true;
handler.mActivationIndex = mActivationIndex++;
}
private void recordHandlerIfNotPresent(GestureHandler handler, View view) {
for (int i = 0; i < mGestureHandlersCount; i++) {
if (mGestureHandlers[i] == handler) {
return;
}
}
if (mGestureHandlersCount >= mGestureHandlers.length) {
throw new IllegalStateException("Too many recognizers");
}
mGestureHandlers[mGestureHandlersCount++] = handler;
handler.mIsActive = false;
handler.mIsAwaiting = false;
handler.mActivationIndex = Integer.MAX_VALUE;
handler.prepare(view, this);
}
private boolean recordViewHandlersForPointer(View view, float[] coords, int pointerId) {
ArrayList<GestureHandler> handlers = mHandlerRegistry.getHandlersForView(view);
boolean found = false;
if (handlers != null) {
for (int i = 0, size = handlers.size(); i < size; i++) {
GestureHandler handler = handlers.get(i);
if (handler.isEnabled() && handler.isWithinBounds(view, coords[0], coords[1])) {
recordHandlerIfNotPresent(handler, view);
handler.startTrackingPointer(pointerId);
found = true;
}
}
}
return found;
}
private void extractGestureHandlers(MotionEvent event) {
int actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
sTempCoords[0] = event.getX(actionIndex);
sTempCoords[1] = event.getY(actionIndex);
traverseWithPointerEvents(mWrapperView, sTempCoords, pointerId);
extractGestureHandlers(mWrapperView, sTempCoords, pointerId);
}
private boolean extractGestureHandlers(ViewGroup viewGroup, float[] coords, int pointerId) {
int childrenCount = viewGroup.getChildCount();
for (int i = childrenCount - 1; i >= 0; i--) {
View child = mViewConfigHelper.getChildInDrawingOrderAtIndex(viewGroup, i);
if (canReceiveEvents(child)) {
PointF childPoint = sTempPoint;
transformTouchPointToViewCoords(coords[0], coords[1], viewGroup, child, childPoint);
float restoreX = coords[0];
float restoreY = coords[1];
coords[0] = childPoint.x;
coords[1] = childPoint.y;
boolean found = false;
if (!isClipping(child) || isTransformedTouchPointInView(coords[0], coords[1], child)) {
// we only consider the view if touch is inside the view bounds or if the view's children
// can render outside of the view bounds (overflow visible)
found = traverseWithPointerEvents(child, coords, pointerId);
}
coords[0] = restoreX;
coords[1] = restoreY;
if (found) {
return true;
}
}
}
return false;
}
private static boolean shouldHandlerlessViewBecomeTouchTarget(View view, float coords[]) {
// The following code is to match the iOS behavior where transparent parts of the views can
// pass touch events through them allowing sibling nodes to handle them.
// TODO: this is not an ideal solution as we only consider ViewGroups that has no background set
// TODO: ideally we should determine the pixel color under the given coordinates and return
// false if the color is transparent
boolean isLeafOrTransparent = !(view instanceof ViewGroup) || view.getBackground() != null;
return isLeafOrTransparent && isTransformedTouchPointInView(coords[0], coords[1], view);
}
private boolean traverseWithPointerEvents(View view, float coords[], int pointerId) {
PointerEventsConfig pointerEvents = mViewConfigHelper.getPointerEventsConfigForView(view);
if (pointerEvents == PointerEventsConfig.NONE) {
// This view and its children can't be the target
return false;
} else if (pointerEvents == PointerEventsConfig.BOX_ONLY) {
// This view is the target, its children don't matter
return recordViewHandlersForPointer(view, coords, pointerId)
|| shouldHandlerlessViewBecomeTouchTarget(view, coords);
} else if (pointerEvents == PointerEventsConfig.BOX_NONE) {
// This view can't be the target, but its children might
if (view instanceof ViewGroup) {
return extractGestureHandlers((ViewGroup) view, coords, pointerId);
}
return false;
} else if (pointerEvents == PointerEventsConfig.AUTO) {
// Either this view or one of its children is the target
boolean found = false;
if (view instanceof ViewGroup) {
found = extractGestureHandlers((ViewGroup) view, coords, pointerId);
}
return recordViewHandlersForPointer(view, coords, pointerId)
|| found || shouldHandlerlessViewBecomeTouchTarget(view, coords);
} else {
throw new IllegalArgumentException(
"Unknown pointer event type: " + pointerEvents.toString());
}
}
private boolean canReceiveEvents(View view) {
return view.getVisibility() == View.VISIBLE && view.getAlpha() >= mMinAlphaForTraversal;
}
private static void transformTouchPointToViewCoords(
float x,
float y,
ViewGroup parent,
View child,
PointF outLocalPoint) {
float localX = x + parent.getScrollX() - child.getLeft();
float localY = y + parent.getScrollY() - child.getTop();
Matrix matrix = child.getMatrix();
if (!matrix.isIdentity()) {
float[] localXY = sMatrixTransformCoords;
localXY[0] = localX;
localXY[1] = localY;
Matrix inverseMatrix = sInverseMatrix;
matrix.invert(inverseMatrix);
inverseMatrix.mapPoints(localXY);
localX = localXY[0];
localY = localXY[1];
}
outLocalPoint.set(localX, localY);
}
private boolean isClipping(View view) {
// if view is not a view group it is clipping, otherwise we check for `getClipChildren` flag to
// be turned on and also confirm with the ViewConfigHelper implementation
return !(view instanceof ViewGroup) || mViewConfigHelper.isViewClippingChildren((ViewGroup) view);
}
private static boolean isTransformedTouchPointInView(float x, float y, View child) {
return x >= 0 && x <= child.getWidth() && y >= 0 && y < child.getHeight();
}
private static boolean shouldHandlerWaitForOther(GestureHandler handler, GestureHandler other) {
return handler != other && (handler.shouldWaitForHandlerFailure(other)
|| other.shouldRequireToWaitForFailure(handler));
}
private static boolean canRunSimultaneously(GestureHandler a, GestureHandler b) {
return a == b || a.shouldRecognizeSimultaneously(b) || b.shouldRecognizeSimultaneously(a);
}
private static boolean shouldHandlerBeCancelledBy(GestureHandler handler, GestureHandler other) {
if (!handler.hasCommonPointers(other)) {
// if two handlers share no common pointer one can never trigger cancel for the other
return false;
}
if (canRunSimultaneously(handler, other)) {
// if handlers are allowed to run simultaneously, when first activates second can still remain
// in began state
return false;
}
if (handler != other &&
(handler.mIsAwaiting || handler.getState() == GestureHandler.STATE_ACTIVE)) {
// in every other case as long as the handler is about to be activated or already in active
// state, we delegate the decision to the implementation of GestureHandler#shouldBeCancelledBy
return handler.shouldBeCancelledBy(other);
}
return true;
}
private static boolean isFinished(int state) {
return state == GestureHandler.STATE_CANCELLED || state == GestureHandler.STATE_FAILED
|| state == GestureHandler.STATE_END;
}
}

View File

@ -0,0 +1,10 @@
package com.swmansion.gesturehandler;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
public interface GestureHandlerRegistry {
ArrayList<GestureHandler> getHandlersForView(View view);
}

View File

@ -0,0 +1,29 @@
package com.swmansion.gesturehandler;
import android.view.View;
import java.util.ArrayList;
import java.util.WeakHashMap;
public class GestureHandlerRegistryImpl implements GestureHandlerRegistry {
private WeakHashMap<View, ArrayList<GestureHandler>> mHandlers = new WeakHashMap<>();
public <T extends GestureHandler> T registerHandlerForView(View view, T handler) {
ArrayList<GestureHandler> listToAdd = mHandlers.get(view);
if (listToAdd == null) {
listToAdd = new ArrayList<>(1);
listToAdd.add(handler);
mHandlers.put(view, listToAdd);
} else {
listToAdd.add(handler);
}
return handler;
}
@Override
public ArrayList<GestureHandler> getHandlersForView(View view) {
return mHandlers.get(view);
}
}

View File

@ -0,0 +1,53 @@
package com.swmansion.gesturehandler;
import android.view.MotionEvent;
public class GestureUtils {
public static float getLastPointerX(MotionEvent event, boolean averageTouches) {
float offset = event.getRawX() - event.getX();
int excludeIndex = event.getActionMasked() == MotionEvent.ACTION_POINTER_UP ?
event.getActionIndex() : -1;
if (averageTouches) {
float sum = 0f;
int count = 0;
for (int i = 0, size = event.getPointerCount(); i < size; i++) {
if (i != excludeIndex) {
sum += event.getX(i) + offset;
count++;
}
}
return sum / count;
} else {
int lastPointerIdx = event.getPointerCount() - 1;
if (lastPointerIdx == excludeIndex) {
lastPointerIdx--;
}
return event.getX(lastPointerIdx) + offset;
}
}
public static float getLastPointerY(MotionEvent event, boolean averageTouches) {
float offset = event.getRawY() - event.getY();
int excludeIndex = event.getActionMasked() == MotionEvent.ACTION_POINTER_UP ?
event.getActionIndex() : -1;
if (averageTouches) {
float sum = 0f;
int count = 0;
for (int i = 0, size = event.getPointerCount(); i < size; i++) {
if (i != excludeIndex) {
sum += event.getY(i) + offset;
count++;
}
}
return sum / count;
} else {
int lastPointerIdx = event.getPointerCount() - 1;
if (lastPointerIdx == excludeIndex) {
lastPointerIdx -= 1;
}
return event.getY(lastPointerIdx) + offset;
}
}
}

View File

@ -0,0 +1,77 @@
package com.swmansion.gesturehandler;
import android.content.Context;
import android.os.Handler;
import android.view.MotionEvent;
public class LongPressGestureHandler extends GestureHandler<LongPressGestureHandler> {
private static final long DEFAULT_MIN_DURATION_MS = 500; // 1 sec
private static float DEFAULT_MAX_DIST_DP = 10; // 20dp
private long mMinDurationMs = DEFAULT_MIN_DURATION_MS;
private float mMaxDistSq;
private float mStartX, mStartY;
private Handler mHandler;
public LongPressGestureHandler(Context context) {
setShouldCancelWhenOutside(true);
mMaxDistSq = DEFAULT_MAX_DIST_DP * context.getResources().getDisplayMetrics().density;
}
public void setMinDurationMs(long minDurationMs) {
mMinDurationMs = minDurationMs;
}
public LongPressGestureHandler setMaxDist(float maxDist) {
mMaxDistSq = maxDist * maxDist;
return this;
}
@Override
protected void onHandle(MotionEvent event) {
if (getState() == STATE_UNDETERMINED) {
begin();
mStartX = event.getRawX();
mStartY = event.getRawY();
mHandler = new Handler();
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
activate();
}
}, mMinDurationMs);
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
if (getState() == STATE_ACTIVE) {
end();
} else {
fail();
}
} else {
// calculate distance from start
float deltaX = event.getRawX() - mStartX;
float deltaY = event.getRawY() - mStartY;
float distSq = deltaX * deltaX + deltaY * deltaY;
if (distSq > mMaxDistSq) {
if (getState() == STATE_ACTIVE) {
cancel();
} else {
fail();
}
}
}
}
@Override
protected void onStateChange(int newState, int previousState) {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
mHandler = null;
}
}
}

View File

@ -0,0 +1,110 @@
package com.swmansion.gesturehandler;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
public class NativeViewGestureHandler extends GestureHandler<NativeViewGestureHandler> {
private boolean mShouldActivateOnStart;
private boolean mDisallowInterruption;
public NativeViewGestureHandler() {
setShouldCancelWhenOutside(true);
}
public NativeViewGestureHandler setShouldActivateOnStart(boolean shouldActivateOnStart) {
mShouldActivateOnStart = shouldActivateOnStart;
return this;
}
/**
* Set this to {@code true} when wrapping native components that are supposed to be an exclusive
* target for a touch stream. Like for example switch or slider component which when activated
* aren't supposed to be cancelled by scrollview or other container that may also handle touches.
*/
public NativeViewGestureHandler setDisallowInterruption(boolean disallowInterruption) {
mDisallowInterruption = disallowInterruption;
return this;
}
@Override
public boolean shouldRequireToWaitForFailure(GestureHandler handler) {
return super.shouldRequireToWaitForFailure(handler);
}
@Override
public boolean shouldRecognizeSimultaneously(GestureHandler handler) {
if (handler instanceof NativeViewGestureHandler) {
// Special case when the peer handler is also an instance of NativeViewGestureHandler:
// For the `disallowInterruption` to work correctly we need to check the property when
// accessed as a peer, because simultaneous recognizers can be set on either side of the
// connection.
NativeViewGestureHandler nativeWrapper = (NativeViewGestureHandler) handler;
if (nativeWrapper.getState() == STATE_ACTIVE && nativeWrapper.mDisallowInterruption) {
// other handler is active and it disallows interruption, we don't want to get into its way
return false;
}
}
boolean canBeInterrupted = !mDisallowInterruption;
int state = getState();
int otherState = handler.getState();
if (state == STATE_ACTIVE && otherState == STATE_ACTIVE && canBeInterrupted) {
// if both handlers are active and the current handler can be interruped it we return `false`
// as it means the other handler has turned active and returning `true` would prevent it from
// interrupting the current handler
return false;
}
// otherwise we can only return `true` if already in an active state
return state == STATE_ACTIVE && canBeInterrupted;
}
@Override
public boolean shouldBeCancelledBy(GestureHandler handler) {
return !mDisallowInterruption;
}
@Override
protected void onHandle(MotionEvent event) {
View view = getView();
int state = getState();
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
view.onTouchEvent(event);
if ((state == STATE_UNDETERMINED || state == STATE_BEGAN) && view.isPressed()) {
activate();
}
end();
} else if (state == STATE_UNDETERMINED || state == STATE_BEGAN) {
if (mShouldActivateOnStart) {
tryIntercept(view, event);
view.onTouchEvent(event);
activate();
} else if (tryIntercept(view, event)) {
view.onTouchEvent(event);
activate();
} else if (state != STATE_BEGAN) {
begin();
}
} else if (state == STATE_ACTIVE) {
view.onTouchEvent(event);
}
}
private static boolean tryIntercept(View view, MotionEvent event) {
if (view instanceof ViewGroup && ((ViewGroup) view).onInterceptTouchEvent(event)) {
return true;
}
return false;
}
@Override
protected void onCancel() {
long time = SystemClock.uptimeMillis();
MotionEvent event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0, 0, 0);
event.setAction(MotionEvent.ACTION_CANCEL);
getView().onTouchEvent(event);
}
}

View File

@ -0,0 +1,8 @@
package com.swmansion.gesturehandler;
import android.view.MotionEvent;
public interface OnTouchEventListener<T extends GestureHandler> {
void onTouchEvent(T handler, MotionEvent event);
void onStateChange(T handler, int newState, int oldState);
}

View File

@ -0,0 +1,312 @@
package com.swmansion.gesturehandler;
import android.content.Context;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;
public class PanGestureHandler extends GestureHandler<PanGestureHandler> {
private static float MIN_VALUE_IGNORE = Float.MAX_VALUE;
private static float MAX_VALUE_IGNORE = Float.MIN_VALUE;
private static int DEFAULT_MIN_POINTERS = 1;
private static int DEFAULT_MAX_POINTERS = 10;
private float mMinDistSq = MAX_VALUE_IGNORE;
private float mActiveOffsetXStart = MIN_VALUE_IGNORE;
private float mActiveOffsetXEnd = MAX_VALUE_IGNORE;
private float mFailOffsetXStart = MAX_VALUE_IGNORE;
private float mFailOffsetXEnd = MIN_VALUE_IGNORE;
private float mActiveOffsetYStart = MIN_VALUE_IGNORE;
private float mActiveOffsetYEnd = MAX_VALUE_IGNORE;
private float mFailOffsetYStart = MAX_VALUE_IGNORE;
private float mFailOffsetYEnd = MIN_VALUE_IGNORE;
private float mMinVelocityX = MIN_VALUE_IGNORE;
private float mMinVelocityY = MIN_VALUE_IGNORE;
private float mMinVelocitySq = MIN_VALUE_IGNORE;
private int mMinPointers = DEFAULT_MIN_POINTERS;
private int mMaxPointers = DEFAULT_MAX_POINTERS;
private float mStartX, mStartY;
private float mOffsetX, mOffsetY;
private float mLastX, mLastY;
private float mLastVelocityX, mLastVelocityY;
private VelocityTracker mVelocityTracker;
private boolean mAverageTouches;
/**
* On Android when there are multiple pointers on the screen pan gestures most often just consider
* the last placed pointer. The behaviour on iOS is quite different where the x and y component
* of the pan pointer is calculated as an average out of all the pointers placed on the screen.
*
* This behaviour can be customized on android by setting averageTouches property of the handler
* object. This could be useful in particular for the usecases when we attach other handlers that
* recognizes multi-finger gestures such as rotation. In that case when we only rely on the last
* placed finger it is easier for the gesture handler to trigger when we do a rotation gesture
* because each finger when treated separately will travel some distance, whereas the average
* position of all the fingers will remain still while doing a rotation gesture.
*/
public PanGestureHandler(Context context) {
ViewConfiguration vc = ViewConfiguration.get(context);
int touchSlop = vc.getScaledTouchSlop();
mMinDistSq = touchSlop * touchSlop;
}
public PanGestureHandler setActiveOffsetXStart(float activeOffsetXStart) {
mActiveOffsetXStart = activeOffsetXStart;
return this;
}
public PanGestureHandler setActiveOffsetXEnd(float activeOffsetXEnd) {
mActiveOffsetXEnd = activeOffsetXEnd;
return this;
}
public PanGestureHandler setFailOffsetXStart(float failOffsetXStart) {
mFailOffsetXStart = failOffsetXStart;
return this;
}
public PanGestureHandler setFailOffsetXEnd(float failOffsetXEnd) {
mFailOffsetXEnd = failOffsetXEnd;
return this;
}
public PanGestureHandler setActiveOffsetYStart(float activeOffsetYStart) {
mActiveOffsetYStart = activeOffsetYStart;
return this;
}
public PanGestureHandler setActiveOffsetYEnd(float activeOffsetYEnd) {
mActiveOffsetYEnd = activeOffsetYEnd;
return this;
}
public PanGestureHandler setFailOffsetYStart(float failOffsetYStart) {
mFailOffsetYStart = failOffsetYStart;
return this;
}
public PanGestureHandler setFailOffsetYEnd(float failOffsetYEnd) {
mFailOffsetYEnd = failOffsetYEnd;
return this;
}
public PanGestureHandler setMinDist(float minDist) {
mMinDistSq = minDist * minDist;
return this;
}
public PanGestureHandler setMinPointers(int minPointers) {
mMinPointers = minPointers;
return this;
}
public PanGestureHandler setMaxPointers(int maxPointers) {
mMaxPointers = maxPointers;
return this;
}
public PanGestureHandler setAverageTouches(boolean averageTouches) {
mAverageTouches = averageTouches;
return this;
}
/**
* @param minVelocity in pixels per second
*/
public PanGestureHandler setMinVelocity(float minVelocity) {
mMinVelocitySq = minVelocity * minVelocity;
return this;
}
public PanGestureHandler setMinVelocityX(float minVelocityX) {
mMinVelocityX = minVelocityX;
return this;
}
public PanGestureHandler setMinVelocityY(float minVelocityY) {
mMinVelocityY = minVelocityY;
return this;
}
private boolean shouldActivate() {
float dx = mLastX - mStartX + mOffsetX;
if (mActiveOffsetXStart != MIN_VALUE_IGNORE && dx < mActiveOffsetXStart) {
return true;
}
if (mActiveOffsetXEnd != MAX_VALUE_IGNORE && dx > mActiveOffsetXEnd) {
return true;
}
float dy = mLastY - mStartY + mOffsetY;
if (mActiveOffsetYStart != MIN_VALUE_IGNORE && dy < mActiveOffsetYStart) {
return true;
}
if (mActiveOffsetYEnd != MAX_VALUE_IGNORE && dy > mActiveOffsetYEnd) {
return true;
}
float distSq = dx * dx + dy * dy;
if (mMinDistSq != MIN_VALUE_IGNORE && distSq >= mMinDistSq) {
return true;
}
float vx = mLastVelocityX;
if (mMinVelocityX != MIN_VALUE_IGNORE &&
((mMinVelocityX < 0 && vx <= mMinVelocityX) || (mMinVelocityX >= 0 && vx >= mMinVelocityX))) {
return true;
}
float vy = mLastVelocityY;
if (mMinVelocityY != MIN_VALUE_IGNORE &&
((mMinVelocityY < 0 && vx <= mMinVelocityY) || (mMinVelocityY >= 0 && vx >= mMinVelocityY))) {
return true;
}
float velocitySq = vx * vx + vy * vy;
if (mMinVelocitySq != MIN_VALUE_IGNORE && velocitySq >= mMinVelocitySq) {
return true;
}
return false;
}
private boolean shouldFail() {
float dx = mLastX - mStartX + mOffsetX;
if (mFailOffsetXStart != MAX_VALUE_IGNORE && dx < mFailOffsetXStart) {
return true;
}
if (mFailOffsetXEnd != MIN_VALUE_IGNORE && dx > mFailOffsetXEnd) {
return true;
}
float dy = mLastY - mStartY + mOffsetY;
if (mFailOffsetYStart != MAX_VALUE_IGNORE && dy < mFailOffsetYStart) {
return true;
}
if (mFailOffsetYEnd != MIN_VALUE_IGNORE && dy > mFailOffsetYEnd) {
return true;
}
return false;
}
@Override
protected void onHandle(MotionEvent event) {
int state = getState();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) {
// update offset if new pointer gets added or removed
mOffsetX += mLastX - mStartX;
mOffsetY += mLastY - mStartY;
// reset starting point
mLastX = GestureUtils.getLastPointerX(event, mAverageTouches);
mLastY = GestureUtils.getLastPointerY(event, mAverageTouches);
mStartX = mLastX;
mStartY = mLastY;
} else {
mLastX = GestureUtils.getLastPointerX(event, mAverageTouches);
mLastY = GestureUtils.getLastPointerY(event, mAverageTouches);
}
if (state == STATE_UNDETERMINED && event.getPointerCount() >= mMinPointers) {
mStartX = mLastX;
mStartY = mLastY;
mOffsetX = 0;
mOffsetY = 0;
mVelocityTracker = VelocityTracker.obtain();
addVelocityMovement(mVelocityTracker, event);
begin();
} else if (mVelocityTracker != null) {
addVelocityMovement(mVelocityTracker, event);
mVelocityTracker.computeCurrentVelocity(1000);
mLastVelocityX = mVelocityTracker.getXVelocity();
mLastVelocityY = mVelocityTracker.getYVelocity();
}
if (action == MotionEvent.ACTION_UP) {
if (state == STATE_ACTIVE || state == STATE_BEGAN) {
end();
} else {
fail();
}
} else if (action == MotionEvent.ACTION_POINTER_DOWN && event.getPointerCount() > mMaxPointers) {
// When new finger is placed down (POINTER_DOWN) we check if MAX_POINTERS is not exceeded
if (state == STATE_ACTIVE) {
cancel();
} else {
fail();
}
} else if (action == MotionEvent.ACTION_POINTER_UP && state == STATE_ACTIVE
&& event.getPointerCount() < mMinPointers) {
// When finger is lifted up (POINTER_UP) and the number of pointers falls below MIN_POINTERS
// threshold, we only want to take an action when the handler has already activated. Otherwise
// we can still expect more fingers to be placed on screen and fulfill MIN_POINTERS criteria.
fail();
} else if (state == STATE_BEGAN) {
if (shouldFail()) {
fail();
} else if (shouldActivate()) {
// reset starting point
mStartX = mLastX;
mStartY = mLastY;
activate();
}
}
}
@Override
protected void onReset() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
public float getTranslationX() {
return mLastX - mStartX + mOffsetX;
}
public float getTranslationY() {
return mLastY - mStartY + mOffsetY;
}
public float getVelocityX() {
return mLastVelocityX;
}
public float getVelocityY() {
return mLastVelocityY;
}
/**
* This method adds movement to {@class VelocityTracker} first resetting offset of the event so
* that the velocity is calculated based on the absolute position of touch pointers. This is
* because if the underlying view moves along with the finger using relative x/y coords yields
* incorrect results.
*/
private static void addVelocityMovement(VelocityTracker tracker, MotionEvent event) {
float offsetX = event.getRawX() - event.getX();
float offsetY = event.getRawY() - event.getY();
event.offsetLocation(offsetX, offsetY);
tracker.addMovement(event);
event.offsetLocation(-offsetX, -offsetY);
}
}

View File

@ -0,0 +1,109 @@
package com.swmansion.gesturehandler;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewConfiguration;
public class PinchGestureHandler extends GestureHandler<PinchGestureHandler> {
private ScaleGestureDetector mScaleGestureDetector;
private double mLastScaleFactor;
private double mLastVelocity;
private float mStartingSpan;
private float mSpanSlop;
private ScaleGestureDetector.OnScaleGestureListener mGestureListener =
new ScaleGestureDetector.OnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
double prevScaleFactor = mLastScaleFactor;
mLastScaleFactor *= detector.getScaleFactor();
long delta = detector.getTimeDelta();
if (delta > 0) {
mLastVelocity = (mLastScaleFactor - prevScaleFactor) / delta;
}
if (Math.abs(mStartingSpan - detector.getCurrentSpan()) >= mSpanSlop
&& getState() == STATE_BEGAN) {
activate();
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mStartingSpan = detector.getCurrentSpan();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
// ScaleGestureDetector thinks that when fingers are 27mm away that's a sufficiently good
// reason to trigger this method giving us no other choice but to ignore it completely.
}
};
public PinchGestureHandler() {
setShouldCancelWhenOutside(false);
}
@Override
protected void onHandle(MotionEvent event) {
if (getState() == STATE_UNDETERMINED) {
Context context = getView().getContext();
mLastVelocity = 0f;
mLastScaleFactor = 1f;
mScaleGestureDetector = new ScaleGestureDetector(context, mGestureListener);
ViewConfiguration configuration = ViewConfiguration.get(context);
mSpanSlop = configuration.getScaledTouchSlop();
begin();
}
if (mScaleGestureDetector != null) {
mScaleGestureDetector.onTouchEvent(event);
}
int activePointers = event.getPointerCount();
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
activePointers -= 1;
}
if (getState() == STATE_ACTIVE && activePointers < 2) {
end();
} else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
fail();
}
}
@Override
protected void onReset() {
mScaleGestureDetector = null;
mLastVelocity = 0f;
mLastScaleFactor = 1f;
}
public double getScale() {
return mLastScaleFactor;
}
public double getVelocity() {
return mLastVelocity;
}
public float getFocalPointX() {
if (mScaleGestureDetector == null) {
return Float.NaN;
}
return mScaleGestureDetector.getFocusX();
}
public float getFocalPointY() {
if (mScaleGestureDetector == null) {
return Float.NaN;
}
return mScaleGestureDetector.getFocusY();
}
}

View File

@ -0,0 +1,25 @@
package com.swmansion.gesturehandler;
public enum PointerEventsConfig {
/**
* Neither the container nor its children receive events.
*/
NONE,
/**
* Container doesn't get events but all of its children do.
*/
BOX_NONE,
/**
* Container gets events but none of its children do.
*/
BOX_ONLY,
/**
* Container and all of its children receive touch events (like pointerEvents is unspecified).
*/
AUTO,
;
}

View File

@ -0,0 +1,169 @@
package com.swmansion.gesturehandler;
import android.view.MotionEvent;
public class RotationGestureDetector {
public interface OnRotationGestureListener {
boolean onRotation(RotationGestureDetector detector);
boolean onRotationBegin(RotationGestureDetector detector);
void onRotationEnd(RotationGestureDetector detector);
}
private long mCurrTime;
private long mPrevTime;
private double mPrevAngle;
private double mAngleDiff;
private float mAnchorX;
private float mAnchorY;
private boolean mInProgress;
private int mPointerIds[] = new int[2];
private OnRotationGestureListener mListener;
public RotationGestureDetector(OnRotationGestureListener listener) {
mListener = listener;
}
private void updateCurrent(MotionEvent event) {
mPrevTime = mCurrTime;
mCurrTime = event.getEventTime();
int firstPointerIndex = event.findPointerIndex(mPointerIds[0]);
int secondPointerIndex = event.findPointerIndex(mPointerIds[1]);
float firstPtX = event.getX(firstPointerIndex);
float firstPtY = event.getY(firstPointerIndex);
float secondPtX = event.getX(secondPointerIndex);
float secondPtY = event.getY(secondPointerIndex);
float vectorX = secondPtX - firstPtX;
float vectorY = secondPtY - firstPtY;
mAnchorX = (firstPtX + secondPtX) * 0.5f;
mAnchorY = (firstPtY + secondPtY) * 0.5f;
// Angle diff should be positive when rotating in clockwise direction
double angle = -Math.atan2(vectorY, vectorX);
if (Double.isNaN(mPrevAngle)) {
mAngleDiff = 0.;
} else {
mAngleDiff = mPrevAngle - angle;
}
mPrevAngle = angle;
if (mAngleDiff > Math.PI) {
mAngleDiff -= Math.PI;
} else if (mAngleDiff < -Math.PI) {
mAngleDiff += Math.PI;
}
if (mAngleDiff > Math.PI / 2.) {
mAngleDiff -= Math.PI;
} else if (mAngleDiff < -Math.PI / 2.) {
mAngleDiff += Math.PI;
}
}
private void finish() {
if (mInProgress) {
mInProgress = false;
if (mListener != null) {
mListener.onRotationEnd(this);
}
}
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInProgress = false;
mPointerIds[0] = event.getPointerId(event.getActionIndex());
mPointerIds[1] = MotionEvent.INVALID_POINTER_ID;
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (!mInProgress) {
mPointerIds[1] = event.getPointerId(event.getActionIndex());
mInProgress = true;
mPrevTime = event.getEventTime();
mPrevAngle = Double.NaN;
updateCurrent(event);
if (mListener != null) {
mListener.onRotationBegin(this);
}
}
break;
case MotionEvent.ACTION_MOVE:
if (mInProgress) {
updateCurrent(event);
if (mListener != null) {
mListener.onRotation(this);
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
if (mInProgress) {
int pointerId = event.getPointerId(event.getActionIndex());
if (pointerId == mPointerIds[0] || pointerId == mPointerIds[1]) {
// One of the key pointer has been lifted up, we have to end the gesture
finish();
}
}
break;
case MotionEvent.ACTION_UP:
finish();
break;
}
return true;
}
/**
* Returns rotation in radians since the previous rotation event.
*
* @return current rotation step in radians.
*/
public double getRotation() {
return mAngleDiff;
}
/**
* Return the time difference in milliseconds between the previous
* accepted rotation event and the current rotation event.
*
* @return Time difference since the last rotation event in milliseconds.
*/
public long getTimeDelta() {
return mCurrTime - mPrevTime;
}
/**
* Returns X coordinate of the rotation anchor point relative to the view that the provided motion
* event coordinates (usually relative to the view event was sent to).
*
* @return X coordinate of the rotation anchor point
*/
public float getAnchorX() {
return mAnchorX;
}
/**
* Returns Y coordinate of the rotation anchor point relative to the view that the provided motion
* event coordinates (usually relative to the view event was sent to).
*
* @return Y coordinate of the rotation anchor point
*/
public float getAnchorY() {
return mAnchorY;
}
}

View File

@ -0,0 +1,96 @@
package com.swmansion.gesturehandler;
import android.view.MotionEvent;
public class RotationGestureHandler extends GestureHandler<RotationGestureHandler> {
private static final double ROTATION_RECOGNITION_THRESHOLD = Math.PI / 36.; // 5 deg in radians
private RotationGestureDetector mRotationGestureDetector;
private double mLastRotation;
private double mLastVelocity;
private RotationGestureDetector.OnRotationGestureListener mGestureListener = new RotationGestureDetector.OnRotationGestureListener() {
@Override
public boolean onRotation(RotationGestureDetector detector) {
double prevRotation = mLastRotation;
mLastRotation += detector.getRotation();
long delta = detector.getTimeDelta();
if (delta > 0) {
mLastVelocity = (mLastRotation - prevRotation) / delta;
}
if (Math.abs(mLastRotation) >= ROTATION_RECOGNITION_THRESHOLD && getState() == STATE_BEGAN) {
activate();
}
return true;
}
@Override
public boolean onRotationBegin(RotationGestureDetector detector) {
return true;
}
@Override
public void onRotationEnd(RotationGestureDetector detector) {
end();
}
};
public RotationGestureHandler() {
setShouldCancelWhenOutside(false);
}
@Override
protected void onHandle(MotionEvent event) {
int state = getState();
if (state == STATE_UNDETERMINED) {
mLastVelocity = 0f;
mLastRotation = 0f;
mRotationGestureDetector = new RotationGestureDetector(mGestureListener);
begin();
}
if (mRotationGestureDetector != null) {
mRotationGestureDetector.onTouchEvent(event);
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
if (state == STATE_ACTIVE) {
end();
} else {
fail();
}
}
}
@Override
protected void onReset() {
mRotationGestureDetector = null;
mLastVelocity = 0f;
mLastRotation = 0f;
}
public double getRotation() {
return mLastRotation;
}
public double getVelocity() {
return mLastVelocity;
}
public float getAnchorX() {
if (mRotationGestureDetector == null) {
return Float.NaN;
}
return mRotationGestureDetector.getAnchorX();
}
public float getAnchorY() {
if (mRotationGestureDetector == null) {
return Float.NaN;
}
return mRotationGestureDetector.getAnchorY();
}
}

View File

@ -0,0 +1,172 @@
package com.swmansion.gesturehandler;
import android.os.Handler;
import android.view.MotionEvent;
public class TapGestureHandler extends GestureHandler<TapGestureHandler> {
private static float MAX_VALUE_IGNORE = Float.MIN_VALUE;
private static final long DEFAULT_MAX_DURATION_MS = 500;
private static final long DEFAULT_MAX_DELAY_MS = 500;
private static final int DEFAULT_NUMBER_OF_TAPS = 1;
private static final int DEFAULT_MIN_NUMBER_OF_POINTERS = 1;
private float mMaxDeltaX = MAX_VALUE_IGNORE;
private float mMaxDeltaY = MAX_VALUE_IGNORE;
private float mMaxDistSq = MAX_VALUE_IGNORE;
private long mMaxDurationMs = DEFAULT_MAX_DURATION_MS;
private long mMaxDelayMs = DEFAULT_MAX_DELAY_MS;
private int mNumberOfTaps = DEFAULT_NUMBER_OF_TAPS;
private int mMinNumberOfPointers = DEFAULT_MIN_NUMBER_OF_POINTERS;
private int mNumberOfPointers = 1;
private float mStartX, mStartY;
private float mOffsetX, mOffsetY;
private float mLastX, mLastY;
private Handler mHandler;
private int mTapsSoFar;
private final Runnable mFailDelayed = new Runnable() {
@Override
public void run() {
fail();
}
};
public TapGestureHandler setNumberOfTaps(int numberOfTaps) {
mNumberOfTaps = numberOfTaps;
return this;
}
public TapGestureHandler setMaxDelayMs(long maxDelayMs) {
mMaxDelayMs = maxDelayMs;
return this;
}
public TapGestureHandler setMaxDurationMs(long maxDurationMs) {
mMaxDurationMs = maxDurationMs;
return this;
}
public TapGestureHandler setMaxDx(float deltaX) {
mMaxDeltaX = deltaX;
return this;
}
public TapGestureHandler setMaxDy(float deltaY) {
mMaxDeltaY = deltaY;
return this;
}
public TapGestureHandler setMaxDist(float maxDist) {
mMaxDistSq = maxDist * maxDist;
return this;
}
public TapGestureHandler setMinNumberOfPointers(int minNumberOfPointers) {
mMinNumberOfPointers = minNumberOfPointers;
return this;
}
public TapGestureHandler() {
setShouldCancelWhenOutside(true);
}
private void startTap() {
if (mHandler == null) {
mHandler = new Handler();
} else {
mHandler.removeCallbacksAndMessages(null);
}
mHandler.postDelayed(mFailDelayed, mMaxDurationMs);
}
private void endTap() {
if (mHandler == null) {
mHandler = new Handler();
} else {
mHandler.removeCallbacksAndMessages(null);
}
if (++mTapsSoFar == mNumberOfTaps && mNumberOfPointers >= mMinNumberOfPointers) {
activate();
end();
} else {
mHandler.postDelayed(mFailDelayed, mMaxDelayMs);
}
}
private boolean shouldFail() {
float dx = mLastX - mStartX + mOffsetX;
if (mMaxDeltaX != MAX_VALUE_IGNORE && Math.abs(dx) > mMaxDeltaX) {
return true;
}
float dy = mLastY - mStartY + mOffsetY;
if (mMaxDeltaY != MAX_VALUE_IGNORE && Math.abs(dy) > mMaxDeltaY) {
return true;
}
float dist = dy * dy + dx * dx;
return mMaxDistSq != MAX_VALUE_IGNORE && dist > mMaxDistSq;
}
@Override
protected void onHandle(MotionEvent event) {
int state = getState();
int action = event.getActionMasked();
if (state == STATE_UNDETERMINED) {
mOffsetX = 0;
mOffsetY = 0;
mStartX = event.getRawX();
mStartY = event.getRawY();
}
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN) {
mOffsetX += mLastX - mStartX;
mOffsetY += mLastY - mStartY;
mLastX = GestureUtils.getLastPointerX(event, true);
mLastY = GestureUtils.getLastPointerY(event, true);
mStartX = mLastX;
mStartY = mLastY;
} else {
mLastX = GestureUtils.getLastPointerX(event, true);
mLastY = GestureUtils.getLastPointerY(event, true);
}
if (mNumberOfPointers < event.getPointerCount()) {
mNumberOfPointers = event.getPointerCount();
}
if (shouldFail()) {
fail();
} else if (state == STATE_UNDETERMINED) {
if (action == MotionEvent.ACTION_DOWN) {
begin();
}
startTap();
} else if (state == STATE_BEGAN) {
if (action == MotionEvent.ACTION_UP) {
endTap();
} else if (action == MotionEvent.ACTION_DOWN) {
startTap();
}
}
}
@Override
protected void onCancel() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
@Override
protected void onReset() {
mTapsSoFar = 0;
mNumberOfPointers = 0;
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
}

View File

@ -0,0 +1,10 @@
package com.swmansion.gesturehandler;
import android.view.View;
import android.view.ViewGroup;
public interface ViewConfigurationHelper {
PointerEventsConfig getPointerEventsConfigForView(View view);
View getChildInDrawingOrderAtIndex(ViewGroup parent, int index);
boolean isViewClippingChildren(ViewGroup view);
}