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'
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
android {
compileSdkVersion safeExtGet("compileSdkVersion", 28)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
}
// Include "lib/" as sources, unfortunetely react-native link can't handle
// setting up alternative gradle modules. We still have "lib" defined as a
// standalone gradle module just to be used in AndroidNativeExample
sourceSets {
main.java.srcDirs += 'lib/src/main/java'
}
}
dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+'
}

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);
}

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.swmansion.gesturehandler.react">
</manifest>

View File

@ -0,0 +1,21 @@
package com.facebook.react.views.modal;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.ViewParent;
/**
* For handling gestures inside RNGH we need to have access to some methods of
* `ReactModalHostView.DialogRootViewGroup`. This class is not available outside
* package so this file exports important features.
*/
public class RNGHModalUtils {
public static void dialogRootViewGroupOnChildStartedNativeGesture(ViewGroup modal, MotionEvent androidEvent) {
((ReactModalHostView.DialogRootViewGroup) modal).onChildStartedNativeGesture(androidEvent);
}
public static boolean isDialogRootViewGroup(ViewParent modal) {
return modal instanceof ReactModalHostView.DialogRootViewGroup;
}
}

View File

@ -0,0 +1,261 @@
package com.swmansion.gesturehandler.react;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.PaintDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.ViewGroup;
import com.facebook.react.bridge.SoftAssertions;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewProps;
import com.facebook.react.uimanager.annotations.ReactProp;
public class RNGestureHandlerButtonViewManager extends
ViewGroupManager<RNGestureHandlerButtonViewManager.ButtonViewGroup> {
static class ButtonViewGroup extends ViewGroup {
static TypedValue sResolveOutValue = new TypedValue();
static ButtonViewGroup sResponder;
int mBackgroundColor = Color.TRANSPARENT;
// Using object because of handling null representing no value set.
Integer mRippleColor;
Integer mRippleRadius;
boolean mUseForeground = false;
boolean mUseBorderless = false;
float mBorderRadius = 0;
boolean mNeedBackgroundUpdate = false;
public static final String SELECTABLE_ITEM_BACKGROUND = "selectableItemBackground";
public static final String SELECTABLE_ITEM_BACKGROUND_BORDERLESS = "selectableItemBackgroundBorderless";
public ButtonViewGroup(Context context) {
super(context);
setClickable(true);
setFocusable(true);
mNeedBackgroundUpdate = true;
}
@Override
public void setBackgroundColor(int color) {
mBackgroundColor = color;
mNeedBackgroundUpdate = true;
}
public void setRippleColor(Integer color) {
mRippleColor = color;
mNeedBackgroundUpdate = true;
}
public void setRippleRadius(Integer radius) {
mRippleRadius = radius;
mNeedBackgroundUpdate = true;
}
public void setBorderRadius(float borderRadius) {
mBorderRadius = borderRadius * getResources().getDisplayMetrics().density;
mNeedBackgroundUpdate = true;
}
private Drawable applyRippleEffectWhenNeeded(Drawable selectable) {
if (mRippleColor != null
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& selectable instanceof RippleDrawable) {
int[][] states = new int[][]{ new int[]{ android.R.attr.state_enabled } };
int[] colors = new int[]{ mRippleColor };
ColorStateList colorStateList = new ColorStateList(states, colors);
((RippleDrawable) selectable).setColor(colorStateList);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& mRippleRadius != null
&& selectable instanceof RippleDrawable) {
RippleDrawable rippleDrawable = (RippleDrawable) selectable;
rippleDrawable.setRadius((int) PixelUtil.toPixelFromDIP(mRippleRadius));
}
return selectable;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (super.onInterceptTouchEvent(ev)) {
return true;
}
// We call `onTouchEvent` to and wait until button changes state to `pressed`, if it's pressed
// we return true so that the gesture handler can activate
onTouchEvent(ev);
return isPressed();
}
private void updateBackground() {
if (!mNeedBackgroundUpdate) {
return;
}
mNeedBackgroundUpdate = false;
if (mBackgroundColor == Color.TRANSPARENT) {
// reset background
setBackground(null);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// reset foreground
setForeground(null);
}
if (mUseForeground && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setForeground(applyRippleEffectWhenNeeded(createSelectableDrawable()));
if (mBackgroundColor != Color.TRANSPARENT) {
setBackgroundColor(mBackgroundColor);
}
} else if (mBackgroundColor == Color.TRANSPARENT && mRippleColor == null) {
setBackground(createSelectableDrawable());
} else {
PaintDrawable colorDrawable = new PaintDrawable(mBackgroundColor);
Drawable selectable = createSelectableDrawable();
if (mBorderRadius != 0) {
// Radius-connected lines below ought to be considered
// as a temporary solution. It do not allow to set
// different radius on each corner. However, I suppose it's fairly
// fine for button-related use cases.
// Therefore it might be used as long as:
// 1. ReactViewManager is not a generic class with a possibility to handle another ViewGroup
// 2. There's no way to force native behavior of ReactViewGroup's superclass's onTouchEvent
colorDrawable.setCornerRadius(mBorderRadius);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& selectable instanceof RippleDrawable) {
PaintDrawable mask = new PaintDrawable(Color.WHITE);
mask.setCornerRadius(mBorderRadius);
((RippleDrawable) selectable).setDrawableByLayerId(android.R.id.mask, mask);
}
}
applyRippleEffectWhenNeeded(selectable);
LayerDrawable layerDrawable = new LayerDrawable(
new Drawable[] { colorDrawable, selectable});
setBackground(layerDrawable);
}
}
public void setUseDrawableOnForeground(boolean useForeground) {
mUseForeground = useForeground;
mNeedBackgroundUpdate = true;
}
public void setUseBorderlessDrawable(boolean useBorderless) {
mUseBorderless = useBorderless;
}
private Drawable createSelectableDrawable() {
final int version = Build.VERSION.SDK_INT;
String identifier = mUseBorderless && version >= 21 ? SELECTABLE_ITEM_BACKGROUND_BORDERLESS
: SELECTABLE_ITEM_BACKGROUND;
int attrID = getAttrId(getContext(), identifier);
getContext().getTheme().resolveAttribute(attrID, sResolveOutValue, true);
if (version >= 21) {
return getResources().getDrawable(sResolveOutValue.resourceId, getContext().getTheme());
} else {
return getResources().getDrawable(sResolveOutValue.resourceId);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static int getAttrId(Context context, String attr) {
SoftAssertions.assertNotNull(attr);
if (SELECTABLE_ITEM_BACKGROUND.equals(attr)) {
return android.R.attr.selectableItemBackground;
} else if (SELECTABLE_ITEM_BACKGROUND_BORDERLESS.equals(attr)) {
return android.R.attr.selectableItemBackgroundBorderless;
} else {
return context.getResources().getIdentifier(attr, "attr", "android");
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// No-op
}
@Override
public void drawableHotspotChanged(float x, float y) {
if (sResponder == null || sResponder == this) {
super.drawableHotspotChanged(x, y);
}
}
@Override
public void setPressed(boolean pressed) {
if (pressed && sResponder == null) {
// first button to be pressed grabs button responder
sResponder = this;
}
if (!pressed || sResponder == this) {
// we set pressed state only for current responder
super.setPressed(pressed);
}
if (!pressed && sResponder == this) {
// if the responder is no longer pressed we release button responder
sResponder = null;
}
}
@Override
public void dispatchDrawableHotspotChanged(float x, float y) {
// by default viewgroup would pass hotspot change events
}
}
@Override
public String getName() {
return "RNGestureHandlerButton";
}
@Override
public ButtonViewGroup createViewInstance(ThemedReactContext context) {
return new ButtonViewGroup(context);
}
@TargetApi(Build.VERSION_CODES.M)
@ReactProp(name = "foreground")
public void setForeground(ButtonViewGroup view, boolean useDrawableOnForeground) {
view.setUseDrawableOnForeground(useDrawableOnForeground);
}
@ReactProp(name = "borderless")
public void setBorderless(ButtonViewGroup view, boolean useBorderlessDrawable) {
view.setUseBorderlessDrawable(useBorderlessDrawable);
}
@ReactProp(name = "enabled")
public void setEnabled(ButtonViewGroup view, boolean enabled) {
view.setEnabled(enabled);
}
@ReactProp(name = ViewProps.BORDER_RADIUS)
public void setBorderRadius(ButtonViewGroup view, float borderRadius) {
view.setBorderRadius(borderRadius);
}
@ReactProp(name = "rippleColor")
public void setRippleColor(ButtonViewGroup view, Integer rippleColor) {
view.setRippleColor(rippleColor);
}
@ReactProp(name = "rippleRadius")
public void setRippleRadius(ButtonViewGroup view, Integer rippleRadius) {
view.setRippleRadius(rippleRadius);
}
@Override
protected void onAfterUpdateTransaction(ButtonViewGroup view) {
view.updateBackground();
}
}

View File

@ -0,0 +1,72 @@
package com.swmansion.gesturehandler.react;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import androidx.annotation.Nullable;
public class RNGestureHandlerEnabledRootView extends ReactRootView {
private @Nullable ReactInstanceManager mReactInstanceManager;
private @Nullable RNGestureHandlerRootHelper mGestureRootHelper;
public RNGestureHandlerEnabledRootView(Context context) {
super(context);
}
public RNGestureHandlerEnabledRootView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (mGestureRootHelper != null) {
mGestureRootHelper.requestDisallowInterceptTouchEvent(disallowIntercept);
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mGestureRootHelper != null && mGestureRootHelper.dispatchTouchEvent(ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
/**
* This method is used to enable root view to start processing touch events through the gesture
* handler library logic. Unless this method is called (which happens as a result of instantiating
* new gesture handler from JS) the root view component will just proxy all touch related methods
* to its superclass. Thus in the "disabled" state all touch related events will fallback to
* default RN behavior.
*/
public void initialize() {
if (mGestureRootHelper != null) {
throw new IllegalStateException("GestureHandler already initialized for root view " + this);
}
mGestureRootHelper = new RNGestureHandlerRootHelper(
mReactInstanceManager.getCurrentReactContext(), this);
}
public void tearDown() {
if (mGestureRootHelper != null) {
mGestureRootHelper.tearDown();
mGestureRootHelper = null;
}
}
@Override
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties) {
super.startReactApplication(reactInstanceManager, moduleName, initialProperties);
mReactInstanceManager = reactInstanceManager;
}
}

View File

@ -0,0 +1,77 @@
package com.swmansion.gesturehandler.react;
import androidx.core.util.Pools;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.swmansion.gesturehandler.GestureHandler;
import androidx.annotation.Nullable;
public class RNGestureHandlerEvent extends Event<RNGestureHandlerEvent> {
public static final String EVENT_NAME = "onGestureHandlerEvent";
private static final int TOUCH_EVENTS_POOL_SIZE = 7; // magic
private static final Pools.SynchronizedPool<RNGestureHandlerEvent> EVENTS_POOL =
new Pools.SynchronizedPool<>(TOUCH_EVENTS_POOL_SIZE);
public static RNGestureHandlerEvent obtain(
GestureHandler handler,
@Nullable RNGestureHandlerEventDataExtractor dataExtractor) {
RNGestureHandlerEvent event = EVENTS_POOL.acquire();
if (event == null) {
event = new RNGestureHandlerEvent();
}
event.init(handler, dataExtractor);
return event;
}
private WritableMap mExtraData;
private short mCoalescingKey;
private RNGestureHandlerEvent() {
}
private void init(
GestureHandler handler,
@Nullable RNGestureHandlerEventDataExtractor dataExtractor) {
super.init(handler.getView().getId());
mExtraData = Arguments.createMap();
if (dataExtractor != null) {
dataExtractor.extractEventData(handler, mExtraData);
}
mExtraData.putInt("handlerTag", handler.getTag());
mExtraData.putInt("state", handler.getState());
mCoalescingKey = handler.getEventCoalescingKey();
}
@Override
public void onDispose() {
mExtraData = null;
EVENTS_POOL.release(this);
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public boolean canCoalesce() {
return true;
}
@Override
public short getCoalescingKey() {
return mCoalescingKey;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, mExtraData);
}
}

View File

@ -0,0 +1,8 @@
package com.swmansion.gesturehandler.react;
import com.facebook.react.bridge.WritableMap;
import com.swmansion.gesturehandler.GestureHandler;
public interface RNGestureHandlerEventDataExtractor<T extends GestureHandler> {
void extractEventData(T handler, WritableMap eventData);
}

View File

@ -0,0 +1,86 @@
package com.swmansion.gesturehandler.react;
import android.util.SparseArray;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.swmansion.gesturehandler.GestureHandler;
import com.swmansion.gesturehandler.GestureHandlerInteractionController;
public class RNGestureHandlerInteractionManager implements GestureHandlerInteractionController {
private static final String KEY_WAIT_FOR = "waitFor";
private static final String KEY_SIMULTANEOUS_HANDLERS = "simultaneousHandlers";
private SparseArray<int[]> mWaitForRelations = new SparseArray<>();
private SparseArray<int[]> mSimultaneousRelations = new SparseArray<>();
public void dropRelationsForHandlerWithTag(int handlerTag) {
mWaitForRelations.remove(handlerTag);
mSimultaneousRelations.remove(handlerTag);
}
private int[] convertHandlerTagsArray(ReadableMap config, String key) {
ReadableArray array = config.getArray(key);
int[] result = new int[array.size()];
for (int i = 0; i < result.length; i++) {
result[i] = array.getInt(i);
}
return result;
}
public void configureInteractions(GestureHandler handler, ReadableMap config) {
handler.setInteractionController(this);
if (config.hasKey(KEY_WAIT_FOR)) {
int[] tags = convertHandlerTagsArray(config, KEY_WAIT_FOR);
mWaitForRelations.put(handler.getTag(), tags);
}
if (config.hasKey(KEY_SIMULTANEOUS_HANDLERS)) {
int[] tags = convertHandlerTagsArray(config, KEY_SIMULTANEOUS_HANDLERS);
mSimultaneousRelations.put(handler.getTag(), tags);
}
}
@Override
public boolean shouldWaitForHandlerFailure(GestureHandler handler, GestureHandler otherHandler) {
int[] waitForTags = mWaitForRelations.get(handler.getTag());
if (waitForTags != null) {
for (int i = 0; i < waitForTags.length; i++) {
if (waitForTags[i] == otherHandler.getTag()) {
return true;
}
}
}
return false;
}
@Override
public boolean shouldRequireHandlerToWaitForFailure(GestureHandler handler,
GestureHandler otherHandler) {
return false;
}
@Override
public boolean shouldHandlerBeCancelledBy(GestureHandler handler, GestureHandler otherHandler) {
return false;
}
@Override
public boolean shouldRecognizeSimultaneously(GestureHandler handler,
GestureHandler otherHandler) {
int[] simultHandlerTags = mSimultaneousRelations.get(handler.getTag());
if (simultHandlerTags != null) {
for (int i = 0; i < simultHandlerTags.length; i++) {
if (simultHandlerTags[i] == otherHandler.getTag()) {
return true;
}
}
}
return false;
}
public void reset() {
mWaitForRelations.clear();
mSimultaneousRelations.clear();
}
}

View File

@ -0,0 +1,731 @@
package com.swmansion.gesturehandler.react;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.swmansion.gesturehandler.FlingGestureHandler;
import com.swmansion.gesturehandler.GestureHandler;
import com.swmansion.gesturehandler.LongPressGestureHandler;
import com.swmansion.gesturehandler.NativeViewGestureHandler;
import com.swmansion.gesturehandler.OnTouchEventListener;
import com.swmansion.gesturehandler.PanGestureHandler;
import com.swmansion.gesturehandler.PinchGestureHandler;
import com.swmansion.gesturehandler.RotationGestureHandler;
import com.swmansion.gesturehandler.TapGestureHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import androidx.annotation.Nullable;
import static com.swmansion.gesturehandler.GestureHandler.HIT_SLOP_NONE;
@ReactModule(name=RNGestureHandlerModule.MODULE_NAME)
public class RNGestureHandlerModule extends ReactContextBaseJavaModule {
public static final String MODULE_NAME = "RNGestureHandlerModule";
private static final String KEY_SHOULD_CANCEL_WHEN_OUTSIDE = "shouldCancelWhenOutside";
private static final String KEY_ENABLED = "enabled";
private static final String KEY_HIT_SLOP = "hitSlop";
private static final String KEY_HIT_SLOP_LEFT = "left";
private static final String KEY_HIT_SLOP_TOP = "top";
private static final String KEY_HIT_SLOP_RIGHT = "right";
private static final String KEY_HIT_SLOP_BOTTOM = "bottom";
private static final String KEY_HIT_SLOP_VERTICAL = "vertical";
private static final String KEY_HIT_SLOP_HORIZONTAL = "horizontal";
private static final String KEY_HIT_SLOP_WIDTH = "width";
private static final String KEY_HIT_SLOP_HEIGHT = "height";
private static final String KEY_NATIVE_VIEW_SHOULD_ACTIVATE_ON_START = "shouldActivateOnStart";
private static final String KEY_NATIVE_VIEW_DISALLOW_INTERRUPTION = "disallowInterruption";
private static final String KEY_TAP_NUMBER_OF_TAPS = "numberOfTaps";
private static final String KEY_TAP_MAX_DURATION_MS = "maxDurationMs";
private static final String KEY_TAP_MAX_DELAY_MS = "maxDelayMs";
private static final String KEY_TAP_MAX_DELTA_X = "maxDeltaX";
private static final String KEY_TAP_MAX_DELTA_Y = "maxDeltaY";
private static final String KEY_TAP_MAX_DIST = "maxDist";
private static final String KEY_TAP_MIN_POINTERS = "minPointers";
private static final String KEY_LONG_PRESS_MIN_DURATION_MS = "minDurationMs";
private static final String KEY_LONG_PRESS_MAX_DIST = "maxDist";
private static final String KEY_PAN_ACTIVE_OFFSET_X_START = "activeOffsetXStart";
private static final String KEY_PAN_ACTIVE_OFFSET_X_END = "activeOffsetXEnd";
private static final String KEY_PAN_FAIL_OFFSET_RANGE_X_START = "failOffsetXStart";
private static final String KEY_PAN_FAIL_OFFSET_RANGE_X_END = "failOffsetXEnd";
private static final String KEY_PAN_ACTIVE_OFFSET_Y_START = "activeOffsetYStart";
private static final String KEY_PAN_ACTIVE_OFFSET_Y_END = "activeOffsetYEnd";
private static final String KEY_PAN_FAIL_OFFSET_RANGE_Y_START = "failOffsetYStart";
private static final String KEY_PAN_FAIL_OFFSET_RANGE_Y_END = "failOffsetYEnd";
private static final String KEY_PAN_MIN_DIST = "minDist";
private static final String KEY_PAN_MIN_VELOCITY = "minVelocity";
private static final String KEY_PAN_MIN_VELOCITY_X = "minVelocityX";
private static final String KEY_PAN_MIN_VELOCITY_Y = "minVelocityY";
private static final String KEY_PAN_MIN_POINTERS = "minPointers";
private static final String KEY_PAN_MAX_POINTERS = "maxPointers";
private static final String KEY_PAN_AVG_TOUCHES = "avgTouches";
private static final String KEY_NUMBER_OF_POINTERS = "numberOfPointers";
private static final String KEY_DIRECTION= "direction";
private abstract static class HandlerFactory<T extends GestureHandler>
implements RNGestureHandlerEventDataExtractor<T> {
public abstract Class<T> getType();
public abstract String getName();
public abstract T create(Context context);
public void configure(T handler, ReadableMap config) {
if (config.hasKey(KEY_SHOULD_CANCEL_WHEN_OUTSIDE)) {
handler.setShouldCancelWhenOutside(config.getBoolean(KEY_SHOULD_CANCEL_WHEN_OUTSIDE));
}
if (config.hasKey(KEY_ENABLED)) {
handler.setEnabled(config.getBoolean(KEY_ENABLED));
}
if (config.hasKey(KEY_HIT_SLOP)) {
handleHitSlopProperty(handler, config);
}
}
@Override
public void extractEventData(T handler, WritableMap eventData) {
eventData.putDouble("numberOfPointers", handler.getNumberOfPointers());
}
}
private static class NativeViewGestureHandlerFactory extends
HandlerFactory<NativeViewGestureHandler> {
@Override
public Class<NativeViewGestureHandler> getType() {
return NativeViewGestureHandler.class;
}
@Override
public String getName() {
return "NativeViewGestureHandler";
}
@Override
public NativeViewGestureHandler create(Context context) {
return new NativeViewGestureHandler();
}
@Override
public void configure(NativeViewGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
if (config.hasKey(KEY_NATIVE_VIEW_SHOULD_ACTIVATE_ON_START)) {
handler.setShouldActivateOnStart(
config.getBoolean(KEY_NATIVE_VIEW_SHOULD_ACTIVATE_ON_START));
}
if (config.hasKey(KEY_NATIVE_VIEW_DISALLOW_INTERRUPTION)) {
handler.setDisallowInterruption(config.getBoolean(KEY_NATIVE_VIEW_DISALLOW_INTERRUPTION));
}
}
@Override
public void extractEventData(NativeViewGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putBoolean("pointerInside", handler.isWithinBounds());
}
}
private static class TapGestureHandlerFactory extends HandlerFactory<TapGestureHandler> {
@Override
public Class<TapGestureHandler> getType() {
return TapGestureHandler.class;
}
@Override
public String getName() {
return "TapGestureHandler";
}
@Override
public TapGestureHandler create(Context context) {
return new TapGestureHandler();
}
@Override
public void configure(TapGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
if (config.hasKey(KEY_TAP_NUMBER_OF_TAPS)) {
handler.setNumberOfTaps(config.getInt(KEY_TAP_NUMBER_OF_TAPS));
}
if (config.hasKey(KEY_TAP_MAX_DURATION_MS)) {
handler.setMaxDurationMs(config.getInt(KEY_TAP_MAX_DURATION_MS));
}
if (config.hasKey(KEY_TAP_MAX_DELAY_MS)) {
handler.setMaxDelayMs(config.getInt(KEY_TAP_MAX_DELAY_MS));
}
if (config.hasKey(KEY_TAP_MAX_DELTA_X)) {
handler.setMaxDx(PixelUtil.toPixelFromDIP(config.getDouble(KEY_TAP_MAX_DELTA_X)));
}
if (config.hasKey(KEY_TAP_MAX_DELTA_Y)) {
handler.setMaxDy(PixelUtil.toPixelFromDIP(config.getDouble(KEY_TAP_MAX_DELTA_Y)));
}
if (config.hasKey(KEY_TAP_MAX_DIST)) {
handler.setMaxDist(PixelUtil.toPixelFromDIP(config.getDouble(KEY_TAP_MAX_DIST)));
}
if (config.hasKey(KEY_TAP_MIN_POINTERS)) {
handler.setMinNumberOfPointers(config.getInt(KEY_TAP_MIN_POINTERS));
}
}
@Override
public void extractEventData(TapGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("x", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionX()));
eventData.putDouble("y", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionY()));
eventData.putDouble("absoluteX", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionX()));
eventData.putDouble("absoluteY", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionY()));
}
}
private static class LongPressGestureHandlerFactory extends
HandlerFactory<LongPressGestureHandler> {
@Override
public Class<LongPressGestureHandler> getType() {
return LongPressGestureHandler.class;
}
@Override
public String getName() {
return "LongPressGestureHandler";
}
@Override
public LongPressGestureHandler create(Context context) {
return new LongPressGestureHandler(context);
}
@Override
public void configure(LongPressGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
if (config.hasKey(KEY_LONG_PRESS_MIN_DURATION_MS)) {
handler.setMinDurationMs(config.getInt(KEY_LONG_PRESS_MIN_DURATION_MS));
}
if (config.hasKey(KEY_LONG_PRESS_MAX_DIST)) {
handler.setMaxDist(PixelUtil.toPixelFromDIP(config.getDouble(KEY_LONG_PRESS_MAX_DIST)));
}
}
@Override
public void extractEventData(LongPressGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("x", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionX()));
eventData.putDouble("y", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionY()));
eventData.putDouble("absoluteX", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionX()));
eventData.putDouble("absoluteY", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionY()));
}
}
private static class PanGestureHandlerFactory extends HandlerFactory<PanGestureHandler> {
@Override
public Class<PanGestureHandler> getType() {
return PanGestureHandler.class;
}
@Override
public String getName() {
return "PanGestureHandler";
}
@Override
public PanGestureHandler create(Context context) {
return new PanGestureHandler(context);
}
@Override
public void configure(PanGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
boolean hasCustomActivationCriteria = false;
if(config.hasKey(KEY_PAN_ACTIVE_OFFSET_X_START)) {
handler.setActiveOffsetXStart(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_ACTIVE_OFFSET_X_START)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_ACTIVE_OFFSET_X_END)) {
handler.setActiveOffsetXEnd(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_ACTIVE_OFFSET_X_END)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_FAIL_OFFSET_RANGE_X_START)) {
handler.setFailOffsetXStart(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_FAIL_OFFSET_RANGE_X_START)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_FAIL_OFFSET_RANGE_X_END)) {
handler.setFailOffsetXEnd(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_FAIL_OFFSET_RANGE_X_END)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_ACTIVE_OFFSET_Y_START)) {
handler.setActiveOffsetYStart(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_ACTIVE_OFFSET_Y_START)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_ACTIVE_OFFSET_Y_END)) {
handler.setActiveOffsetYEnd(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_ACTIVE_OFFSET_Y_END)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_FAIL_OFFSET_RANGE_Y_START)) {
handler.setFailOffsetYStart(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_FAIL_OFFSET_RANGE_Y_START)));
hasCustomActivationCriteria = true;
}
if(config.hasKey(KEY_PAN_FAIL_OFFSET_RANGE_Y_END)) {
handler.setFailOffsetYEnd(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_FAIL_OFFSET_RANGE_Y_END)));
hasCustomActivationCriteria = true;
}
if (config.hasKey(KEY_PAN_MIN_VELOCITY)) {
// This value is actually in DPs/ms, but we can use the same function as for converting
// from DPs to pixels as the unit we're converting is in the numerator
handler.setMinVelocity(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_MIN_VELOCITY)));
hasCustomActivationCriteria = true;
}
if (config.hasKey(KEY_PAN_MIN_VELOCITY_X)) {
handler.setMinVelocityX(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_MIN_VELOCITY_X)));
hasCustomActivationCriteria = true;
}
if (config.hasKey(KEY_PAN_MIN_VELOCITY_Y)) {
handler.setMinVelocityY(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_MIN_VELOCITY_Y)));
hasCustomActivationCriteria = true;
}
// PanGestureHandler sets minDist by default, if there are custom criteria specified we want
// to reset that setting and use provided criteria instead.
if (config.hasKey(KEY_PAN_MIN_DIST)) {
handler.setMinDist(PixelUtil.toPixelFromDIP(config.getDouble(KEY_PAN_MIN_DIST)));
} else if (hasCustomActivationCriteria) {
handler.setMinDist(Float.MAX_VALUE);
}
if (config.hasKey(KEY_PAN_MIN_POINTERS)) {
handler.setMinPointers(config.getInt(KEY_PAN_MIN_POINTERS));
}
if (config.hasKey(KEY_PAN_MAX_POINTERS)) {
handler.setMaxPointers(config.getInt(KEY_PAN_MAX_POINTERS));
}
if (config.hasKey(KEY_PAN_AVG_TOUCHES)) {
handler.setAverageTouches(config.getBoolean(KEY_PAN_AVG_TOUCHES));
}
}
@Override
public void extractEventData(PanGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("x", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionX()));
eventData.putDouble("y", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionY()));
eventData.putDouble("absoluteX", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionX()));
eventData.putDouble("absoluteY", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionY()));
eventData.putDouble("translationX", PixelUtil.toDIPFromPixel(handler.getTranslationX()));
eventData.putDouble("translationY", PixelUtil.toDIPFromPixel(handler.getTranslationY()));
eventData.putDouble("velocityX", PixelUtil.toDIPFromPixel(handler.getVelocityX()));
eventData.putDouble("velocityY", PixelUtil.toDIPFromPixel(handler.getVelocityY()));
}
}
private static class PinchGestureHandlerFactory extends HandlerFactory<PinchGestureHandler> {
@Override
public Class<PinchGestureHandler> getType() {
return PinchGestureHandler.class;
}
@Override
public String getName() {
return "PinchGestureHandler";
}
@Override
public PinchGestureHandler create(Context context) {
return new PinchGestureHandler();
}
@Override
public void extractEventData(PinchGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("scale", handler.getScale());
eventData.putDouble("focalX", PixelUtil.toDIPFromPixel(handler.getFocalPointX()));
eventData.putDouble("focalY", PixelUtil.toDIPFromPixel(handler.getFocalPointY()));
eventData.putDouble("velocity", handler.getVelocity());
}
}
private static class FlingGestureHandlerFactory extends HandlerFactory<FlingGestureHandler> {
@Override
public Class<FlingGestureHandler> getType() {
return FlingGestureHandler.class;
}
@Override
public String getName() {
return "FlingGestureHandler";
}
@Override
public FlingGestureHandler create(Context context) {
return new FlingGestureHandler();
}
@Override
public void configure(FlingGestureHandler handler, ReadableMap config) {
super.configure(handler, config);
if (config.hasKey(KEY_NUMBER_OF_POINTERS)) {
handler.setNumberOfPointersRequired(config.getInt(KEY_NUMBER_OF_POINTERS));
}
if (config.hasKey(KEY_DIRECTION)) {
handler.setDirection(config.getInt(KEY_DIRECTION));
}
}
@Override
public void extractEventData(FlingGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("x", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionX()));
eventData.putDouble("y", PixelUtil.toDIPFromPixel(handler.getLastRelativePositionY()));
eventData.putDouble("absoluteX", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionX()));
eventData.putDouble("absoluteY", PixelUtil.toDIPFromPixel(handler.getLastAbsolutePositionY()));
}
}
private static class RotationGestureHandlerFactory extends HandlerFactory<RotationGestureHandler> {
@Override
public Class<RotationGestureHandler> getType() {
return RotationGestureHandler.class;
}
@Override
public String getName() {
return "RotationGestureHandler";
}
@Override
public RotationGestureHandler create(Context context) {
return new RotationGestureHandler();
}
@Override
public void extractEventData(RotationGestureHandler handler, WritableMap eventData) {
super.extractEventData(handler, eventData);
eventData.putDouble("rotation", handler.getRotation());
eventData.putDouble("anchorX", PixelUtil.toDIPFromPixel(handler.getAnchorX()));
eventData.putDouble("anchorY", PixelUtil.toDIPFromPixel(handler.getAnchorY()));
eventData.putDouble("velocity", handler.getVelocity());
}
}
private OnTouchEventListener mEventListener = new OnTouchEventListener() {
@Override
public void onTouchEvent(GestureHandler handler, MotionEvent event) {
RNGestureHandlerModule.this.onTouchEvent(handler, event);
}
@Override
public void onStateChange(GestureHandler handler, int newState, int oldState) {
RNGestureHandlerModule.this.onStateChange(handler, newState, oldState);
}
};
private HandlerFactory[] mHandlerFactories = new HandlerFactory[] {
new NativeViewGestureHandlerFactory(),
new TapGestureHandlerFactory(),
new LongPressGestureHandlerFactory(),
new PanGestureHandlerFactory(),
new PinchGestureHandlerFactory(),
new RotationGestureHandlerFactory(),
new FlingGestureHandlerFactory()
};
private final RNGestureHandlerRegistry mRegistry = new RNGestureHandlerRegistry();
private RNGestureHandlerInteractionManager mInteractionManager =
new RNGestureHandlerInteractionManager();
private List<RNGestureHandlerRootHelper> mRoots = new ArrayList<>();
private List<Integer> mEnqueuedRootViewInit = new ArrayList<>();
public RNGestureHandlerModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return MODULE_NAME;
}
@ReactMethod
public void createGestureHandler(
String handlerName,
int handlerTag,
ReadableMap config) {
for (int i = 0; i < mHandlerFactories.length; i++) {
HandlerFactory handlerFactory = mHandlerFactories[i];
if (handlerFactory.getName().equals(handlerName)) {
GestureHandler handler = handlerFactory.create(getReactApplicationContext());
handler.setTag(handlerTag);
handler.setOnTouchEventListener(mEventListener);
mRegistry.registerHandler(handler);
mInteractionManager.configureInteractions(handler, config);
handlerFactory.configure(handler, config);
return;
}
}
throw new JSApplicationIllegalArgumentException("Invalid handler name " + handlerName);
}
@ReactMethod
public void attachGestureHandler(int handlerTag, int viewTag) {
tryInitializeHandlerForReactRootView(viewTag);
if (!mRegistry.attachHandlerToView(handlerTag, viewTag)) {
throw new JSApplicationIllegalArgumentException(
"Handler with tag " + handlerTag + " does not exists");
}
}
@ReactMethod
public void updateGestureHandler(
int handlerTag,
ReadableMap config) {
GestureHandler handler = mRegistry.getHandler(handlerTag);
if (handler != null) {
HandlerFactory factory = findFactoryForHandler(handler);
if (factory != null) {
mInteractionManager.dropRelationsForHandlerWithTag(handlerTag);
mInteractionManager.configureInteractions(handler, config);
factory.configure(handler, config);
}
}
}
@ReactMethod
public void dropGestureHandler(int handlerTag) {
mInteractionManager.dropRelationsForHandlerWithTag(handlerTag);
mRegistry.dropHandler(handlerTag);
}
@ReactMethod
public void handleSetJSResponder(int viewTag, boolean blockNativeResponder) {
if (mRegistry != null) {
RNGestureHandlerRootHelper rootView = findRootHelperForViewAncestor(viewTag);
if (rootView != null) {
rootView.handleSetJSResponder(viewTag, blockNativeResponder);
}
}
}
@ReactMethod
public void handleClearJSResponder() {
}
@Override
public @Nullable Map getConstants() {
return MapBuilder.of("State", MapBuilder.of(
"UNDETERMINED", GestureHandler.STATE_UNDETERMINED,
"BEGAN", GestureHandler.STATE_BEGAN,
"ACTIVE", GestureHandler.STATE_ACTIVE,
"CANCELLED", GestureHandler.STATE_CANCELLED,
"FAILED", GestureHandler.STATE_FAILED,
"END", GestureHandler.STATE_END
), "Direction", MapBuilder.of(
"RIGHT", GestureHandler.DIRECTION_RIGHT,
"LEFT", GestureHandler.DIRECTION_LEFT,
"UP", GestureHandler.DIRECTION_UP,
"DOWN", GestureHandler.DIRECTION_DOWN
));
}
public RNGestureHandlerRegistry getRegistry() {
return mRegistry;
}
@Override
public void onCatalystInstanceDestroy() {
mRegistry.dropAllHandlers();
mInteractionManager.reset();
synchronized (mRoots) {
while (!mRoots.isEmpty()) {
int sizeBefore = mRoots.size();
RNGestureHandlerRootHelper root = mRoots.get(0);
ViewGroup reactRootView = root.getRootView();
if (reactRootView instanceof RNGestureHandlerEnabledRootView) {
((RNGestureHandlerEnabledRootView) reactRootView).tearDown();
} else {
root.tearDown();
}
if (mRoots.size() >= sizeBefore) {
throw new IllegalStateException("Expected root helper to get unregistered while tearing down");
}
}
}
super.onCatalystInstanceDestroy();
}
private void tryInitializeHandlerForReactRootView(int ancestorViewTag) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
final int rootViewTag = uiManager.resolveRootTagFromReactTag(ancestorViewTag);
if (rootViewTag < 1) {
throw new JSApplicationIllegalArgumentException("Could find root view for a given ancestor with tag "
+ ancestorViewTag);
}
synchronized (mRoots) {
for (int i = 0; i < mRoots.size(); i++) {
RNGestureHandlerRootHelper root = mRoots.get(i);
ViewGroup rootView = root.getRootView();
if (rootView instanceof ReactRootView && ((ReactRootView) rootView).getRootViewTag() == rootViewTag) {
// we have found root helper registered for a given react root, we don't need to
// initialize a new one then
return;
}
}
}
synchronized (mEnqueuedRootViewInit) {
if (mEnqueuedRootViewInit.contains(rootViewTag)) {
// root view initialization already enqueued -> we skip
return;
}
mEnqueuedRootViewInit.add(rootViewTag);
}
// root helper for a given root tag has not been found, we may wat to check if the root view is
// an instance of RNGestureHandlerEnabledRootView and then initialize gesture handler with it
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
View view = nativeViewHierarchyManager.resolveView(rootViewTag);
if (view instanceof RNGestureHandlerEnabledRootView) {
((RNGestureHandlerEnabledRootView) view).initialize();
} else {
// Seems like the root view is something else than RNGestureHandlerEnabledRootView, this
// is fine though as long as gestureHandlerRootHOC is used in JS
// FIXME: check and warn about gestureHandlerRootHOC
}
synchronized (mEnqueuedRootViewInit) {
mEnqueuedRootViewInit.remove(new Integer(rootViewTag));
}
}
});
}
public void registerRootHelper(RNGestureHandlerRootHelper root) {
synchronized (mRoots) {
if (mRoots.contains(root)) {
throw new IllegalStateException("Root helper" + root + " already registered");
}
mRoots.add(root);
}
}
public void unregisterRootHelper(RNGestureHandlerRootHelper root) {
synchronized (mRoots) {
mRoots.remove(root);
}
}
private @Nullable RNGestureHandlerRootHelper findRootHelperForViewAncestor(int viewTag) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
int rootViewTag = uiManager.resolveRootTagFromReactTag(viewTag);
if (rootViewTag < 1) {
return null;
}
synchronized (mRoots) {
for (int i = 0; i < mRoots.size(); i++) {
RNGestureHandlerRootHelper root = mRoots.get(i);
ViewGroup rootView = root.getRootView();
if (rootView instanceof ReactRootView && ((ReactRootView) rootView).getRootViewTag() == rootViewTag) {
return root;
}
}
}
return null;
}
private @Nullable HandlerFactory findFactoryForHandler(GestureHandler handler) {
for (int i = 0; i < mHandlerFactories.length; i++) {
HandlerFactory factory = mHandlerFactories[i];
if (factory.getType().equals(handler.getClass())) {
return factory;
}
}
return null;
}
private void onTouchEvent(GestureHandler handler, MotionEvent motionEvent) {
if (handler.getTag() < 0) {
// root containers use negative tags, we don't need to dispatch events for them to the JS
return;
}
if (handler.getState() == GestureHandler.STATE_ACTIVE) {
HandlerFactory handlerFactory = findFactoryForHandler(handler);
EventDispatcher eventDispatcher = getReactApplicationContext()
.getNativeModule(UIManagerModule.class)
.getEventDispatcher();
RNGestureHandlerEvent event = RNGestureHandlerEvent.obtain(handler, handlerFactory);
eventDispatcher.dispatchEvent(event);
}
}
private void onStateChange(GestureHandler handler, int newState, int oldState) {
if (handler.getTag() < 0) {
// root containers use negative tags, we don't need to dispatch events for them to the JS
return;
}
HandlerFactory handlerFactory = findFactoryForHandler(handler);
EventDispatcher eventDispatcher = getReactApplicationContext()
.getNativeModule(UIManagerModule.class)
.getEventDispatcher();
RNGestureHandlerStateChangeEvent event = RNGestureHandlerStateChangeEvent.obtain(
handler,
newState,
oldState,
handlerFactory);
eventDispatcher.dispatchEvent(event);
}
private static void handleHitSlopProperty(GestureHandler handler, ReadableMap config) {
if (config.getType(KEY_HIT_SLOP) == ReadableType.Number) {
float hitSlop = PixelUtil.toPixelFromDIP(config.getDouble(KEY_HIT_SLOP));
handler.setHitSlop(hitSlop, hitSlop, hitSlop, hitSlop, HIT_SLOP_NONE, HIT_SLOP_NONE);
} else {
ReadableMap hitSlop = config.getMap(KEY_HIT_SLOP);
float left = HIT_SLOP_NONE, top = HIT_SLOP_NONE, right = HIT_SLOP_NONE, bottom = HIT_SLOP_NONE;
float width = HIT_SLOP_NONE, height = HIT_SLOP_NONE;
if (hitSlop.hasKey(KEY_HIT_SLOP_HORIZONTAL)) {
float horizontalPad = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_HORIZONTAL));
left = right = horizontalPad;
}
if (hitSlop.hasKey(KEY_HIT_SLOP_VERTICAL)) {
float verticalPad = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_VERTICAL));
top = bottom = verticalPad;
}
if (hitSlop.hasKey(KEY_HIT_SLOP_LEFT)) {
left = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_LEFT));
}
if (hitSlop.hasKey(KEY_HIT_SLOP_TOP)) {
top = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_TOP));
}
if (hitSlop.hasKey(KEY_HIT_SLOP_RIGHT)) {
right = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_RIGHT));
}
if (hitSlop.hasKey(KEY_HIT_SLOP_BOTTOM)) {
bottom = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_BOTTOM));
}
if (hitSlop.hasKey(KEY_HIT_SLOP_WIDTH)) {
width = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_WIDTH));
}
if (hitSlop.hasKey(KEY_HIT_SLOP_HEIGHT)) {
height = PixelUtil.toPixelFromDIP(hitSlop.getDouble(KEY_HIT_SLOP_HEIGHT));
}
handler.setHitSlop(left, top, right, bottom, width, height);
}
}
}

View File

@ -0,0 +1,31 @@
package com.swmansion.gesturehandler.react;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.views.view.ReactViewManager;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import androidx.annotation.Nullable;
public class RNGestureHandlerPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new RNGestureHandlerModule(reactContext));
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new RNGestureHandlerRootViewManager(),
new RNGestureHandlerButtonViewManager());
}
}

View File

@ -0,0 +1,101 @@
package com.swmansion.gesturehandler.react;
import android.util.SparseArray;
import android.view.View;
import com.facebook.react.bridge.UiThreadUtil;
import com.swmansion.gesturehandler.GestureHandler;
import com.swmansion.gesturehandler.GestureHandlerRegistry;
import java.util.ArrayList;
import androidx.annotation.Nullable;
public class RNGestureHandlerRegistry implements GestureHandlerRegistry {
private final SparseArray<GestureHandler> mHandlers = new SparseArray<>();
private final SparseArray<Integer> mAttachedTo = new SparseArray<>();
private final SparseArray<ArrayList<GestureHandler>> mHandlersForView = new SparseArray<>();
public synchronized void registerHandler(GestureHandler handler) {
mHandlers.put(handler.getTag(), handler);
}
public synchronized @Nullable GestureHandler getHandler(int handlerTag) {
return mHandlers.get(handlerTag);
}
public synchronized boolean attachHandlerToView(int handlerTag, int viewTag) {
GestureHandler handler = mHandlers.get(handlerTag);
if (handler != null) {
detachHandler(handler);
registerHandlerForViewWithTag(viewTag, handler);
return true;
} else {
return false;
}
}
private synchronized void registerHandlerForViewWithTag(int viewTag, GestureHandler handler) {
if (mAttachedTo.get(handler.getTag()) != null) {
throw new IllegalStateException("Handler " + handler + " already attached");
}
mAttachedTo.put(handler.getTag(), viewTag);
ArrayList<GestureHandler> listToAdd = mHandlersForView.get(viewTag);
if (listToAdd == null) {
listToAdd = new ArrayList<>(1);
listToAdd.add(handler);
mHandlersForView.put(viewTag, listToAdd);
} else {
listToAdd.add(handler);
}
}
private synchronized void detachHandler(final GestureHandler handler) {
Integer attachedToView = mAttachedTo.get(handler.getTag());
if (attachedToView != null) {
mAttachedTo.remove(handler.getTag());
ArrayList<GestureHandler> attachedHandlers = mHandlersForView.get(attachedToView);
if (attachedHandlers != null) {
attachedHandlers.remove(handler);
if (attachedHandlers.size() == 0) {
mHandlersForView.remove(attachedToView);
}
}
}
if (handler.getView() != null) {
// Handler is in "prepared" state which means it is registered in the orchestrator and can
// receive touch events. This means that before we remove it from the registry we need to
// "cancel" it so that orchestrator does no longer keep a reference to it.
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
handler.cancel();
}
});
}
}
public synchronized void dropHandler(int handlerTag) {
GestureHandler handler = mHandlers.get(handlerTag);
if (handler != null) {
detachHandler(handler);
mHandlers.remove(handlerTag);
}
}
public synchronized void dropAllHandlers() {
mHandlers.clear();
mAttachedTo.clear();
mHandlersForView.clear();
}
public synchronized ArrayList<GestureHandler> getHandlersForViewWithTag(int viewTag) {
return mHandlersForView.get(viewTag);
}
@Override
public synchronized ArrayList<GestureHandler> getHandlersForView(View view) {
return getHandlersForViewWithTag(view.getId());
}
}

View File

@ -0,0 +1,151 @@
package com.swmansion.gesturehandler.react;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.views.modal.RNGHModalUtils;
import com.swmansion.gesturehandler.GestureHandler;
import com.swmansion.gesturehandler.GestureHandlerOrchestrator;
public class RNGestureHandlerRootHelper {
private static final float MIN_ALPHA_FOR_TOUCH = 0.1f;
private final ReactContext mContext;
private final GestureHandlerOrchestrator mOrchestrator;
private final GestureHandler mJSGestureHandler;
private final ViewGroup mRootView;
private boolean mShouldIntercept = false;
private boolean mPassingTouch = false;
private static ViewGroup findRootViewTag(ViewGroup viewGroup) {
UiThreadUtil.assertOnUiThread();
ViewParent parent = viewGroup;
while (parent != null && !(parent instanceof ReactRootView || RNGHModalUtils.isDialogRootViewGroup(parent))) {
parent = parent.getParent();
}
if (parent == null) {
throw new IllegalStateException("View " + viewGroup + " has not been mounted under" +
" ReactRootView");
}
return (ViewGroup) parent;
}
public RNGestureHandlerRootHelper(ReactContext context, ViewGroup wrappedView) {
UiThreadUtil.assertOnUiThread();
int wrappedViewTag = wrappedView.getId();
if (wrappedViewTag < 1) {
throw new IllegalStateException("Expect view tag to be set for " + wrappedView);
}
RNGestureHandlerModule module = context.getNativeModule(RNGestureHandlerModule.class);
RNGestureHandlerRegistry registry = module.getRegistry();
mRootView = findRootViewTag(wrappedView);
Log.i(
ReactConstants.TAG,
"[GESTURE HANDLER] Initialize gesture handler for root view " + mRootView);
mContext = context;
mOrchestrator = new GestureHandlerOrchestrator(
wrappedView, registry, new RNViewConfigurationHelper());
mOrchestrator.setMinimumAlphaForTraversal(MIN_ALPHA_FOR_TOUCH);
mJSGestureHandler = new RootViewGestureHandler();
mJSGestureHandler.setTag(-wrappedViewTag);
registry.registerHandler(mJSGestureHandler);
registry.attachHandlerToView(mJSGestureHandler.getTag(), wrappedViewTag);
module.registerRootHelper(this);
}
public void tearDown() {
Log.i(
ReactConstants.TAG,
"[GESTURE HANDLER] Tearing down gesture handler registered for root view " + mRootView);
RNGestureHandlerModule module = mContext.getNativeModule(RNGestureHandlerModule.class);
module.getRegistry().dropHandler(mJSGestureHandler.getTag());
module.unregisterRootHelper(this);
}
public ViewGroup getRootView() {
return mRootView;
}
private class RootViewGestureHandler extends GestureHandler {
@Override
protected void onHandle(MotionEvent event) {
int currentState = getState();
if (currentState == STATE_UNDETERMINED) {
begin();
mShouldIntercept = false;
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
end();
}
}
@Override
protected void onCancel() {
mShouldIntercept = true;
long time = SystemClock.uptimeMillis();
MotionEvent event = MotionEvent.obtain(time, time, MotionEvent.ACTION_CANCEL, 0, 0, 0);
event.setAction(MotionEvent.ACTION_CANCEL);
if (mRootView instanceof ReactRootView) {
((ReactRootView) mRootView).onChildStartedNativeGesture(event);
} else {
RNGHModalUtils.dialogRootViewGroupOnChildStartedNativeGesture(mRootView, event);
}
}
}
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// If this method gets called it means that some native view is attempting to grab lock for
// touch event delivery. In that case we cancel all gesture recognizers
if (mOrchestrator != null && !mPassingTouch) {
// if we are in the process of delivering touch events via GH orchestrator, we don't want to
// treat it as a native gesture capturing the lock
tryCancelAllHandlers();
}
}
public boolean dispatchTouchEvent(MotionEvent ev) {
// We mark `mPassingTouch` before we get into `mOrchestrator.onTouchEvent` so that we can tell
// if `requestDisallow` has been called as a result of a normal gesture handling process or
// as a result of one of the gesture handlers activating
mPassingTouch = true;
mOrchestrator.onTouchEvent(ev);
mPassingTouch = false;
return mShouldIntercept;
}
private void tryCancelAllHandlers() {
// In order to cancel handlers we activate handler that is hooked to the root view
if (mJSGestureHandler != null && mJSGestureHandler.getState() == GestureHandler.STATE_BEGAN) {
// Try activate main JS handler
mJSGestureHandler.activate();
mJSGestureHandler.end();
}
}
/*package*/ void handleSetJSResponder(final int viewTag, final boolean blockNativeResponder) {
if (blockNativeResponder) {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
tryCancelAllHandlers();
}
});
}
}
}

View File

@ -0,0 +1,7 @@
package com.swmansion.gesturehandler.react;
import androidx.annotation.Nullable;
public interface RNGestureHandlerRootInterface {
@Nullable RNGestureHandlerRootHelper getRootHelper();
}

View File

@ -0,0 +1,76 @@
package com.swmansion.gesturehandler.react;
import android.util.Log;
import android.content.Context;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.ViewParent;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.views.view.ReactViewGroup;
import androidx.annotation.Nullable;
public class RNGestureHandlerRootView extends ReactViewGroup {
private static boolean hasGestureHandlerEnabledRootView(ViewGroup viewGroup) {
UiThreadUtil.assertOnUiThread();
ViewParent parent = viewGroup.getParent();
while (parent != null) {
if (parent instanceof RNGestureHandlerEnabledRootView || parent instanceof RNGestureHandlerRootView) {
return true;
}
parent = parent.getParent();
}
return false;
}
private boolean mEnabled;
private @Nullable RNGestureHandlerRootHelper mRootHelper;
public RNGestureHandlerRootView(Context context) {
super(context);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mEnabled = !hasGestureHandlerEnabledRootView(this);
if (!mEnabled) {
Log.i(
ReactConstants.TAG,
"[GESTURE HANDLER] Gesture handler is already enabled for a parent view");
}
if (mEnabled && mRootHelper == null) {
mRootHelper = new RNGestureHandlerRootHelper((ReactContext) getContext(), this);
}
}
public void tearDown() {
if (mRootHelper != null) {
mRootHelper.tearDown();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mEnabled && Assertions.assertNotNull(mRootHelper).dispatchTouchEvent(ev)) {
return true;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (mEnabled) {
Assertions.assertNotNull(mRootHelper).requestDisallowInterceptTouchEvent(disallowIntercept);
}
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

View File

@ -0,0 +1,49 @@
package com.swmansion.gesturehandler.react;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import java.util.Map;
import androidx.annotation.Nullable;
/**
* React native's view manager used for creating instances of {@link }RNGestureHandlerRootView}. It
* is being used by projects using react-native-navigation where for each screen new root view need
* to be provided.
*/
@ReactModule(name = RNGestureHandlerRootViewManager.REACT_CLASS)
public class RNGestureHandlerRootViewManager extends ViewGroupManager<RNGestureHandlerRootView> {
public static final String REACT_CLASS = "GestureHandlerRootView";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected RNGestureHandlerRootView createViewInstance(ThemedReactContext reactContext) {
return new RNGestureHandlerRootView(reactContext);
}
@Override
public void onDropViewInstance(RNGestureHandlerRootView view) {
view.tearDown();
}
/**
* The following event configuration is necessary even if you are not using
* GestureHandlerRootView component directly.
*/
@Override
public @Nullable Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
RNGestureHandlerEvent.EVENT_NAME,
MapBuilder.of("registrationName", RNGestureHandlerEvent.EVENT_NAME),
RNGestureHandlerStateChangeEvent.EVENT_NAME,
MapBuilder.of("registrationName", RNGestureHandlerStateChangeEvent.EVENT_NAME));
}
}

View File

@ -0,0 +1,82 @@
package com.swmansion.gesturehandler.react;
import androidx.core.util.Pools;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.swmansion.gesturehandler.GestureHandler;
import androidx.annotation.Nullable;
public class RNGestureHandlerStateChangeEvent extends Event<RNGestureHandlerStateChangeEvent>{
public static final String EVENT_NAME = "onGestureHandlerStateChange";
private static final int TOUCH_EVENTS_POOL_SIZE = 7; // magic
private static final Pools.SynchronizedPool<RNGestureHandlerStateChangeEvent> EVENTS_POOL =
new Pools.SynchronizedPool<>(TOUCH_EVENTS_POOL_SIZE);
public static RNGestureHandlerStateChangeEvent obtain(
GestureHandler handler,
int newState,
int oldState,
@Nullable RNGestureHandlerEventDataExtractor dataExtractor) {
RNGestureHandlerStateChangeEvent event = EVENTS_POOL.acquire();
if (event == null) {
event = new RNGestureHandlerStateChangeEvent();
}
event.init(handler, newState, oldState, dataExtractor);
return event;
}
private WritableMap mExtraData;
private RNGestureHandlerStateChangeEvent() {
}
private void init(
GestureHandler handler,
int newState,
int oldState,
@Nullable RNGestureHandlerEventDataExtractor dataExtractor) {
super.init(handler.getView().getId());
mExtraData = Arguments.createMap();
if (dataExtractor != null) {
dataExtractor.extractEventData(handler, mExtraData);
}
mExtraData.putInt("handlerTag", handler.getTag());
mExtraData.putInt("state", newState);
mExtraData.putInt("oldState", oldState);
}
@Override
public void onDispose() {
mExtraData = null;
EVENTS_POOL.release(this);
}
@Override
public String getEventName() {
return EVENT_NAME;
}
@Override
public boolean canCoalesce() {
// TODO: coalescing
return false;
}
@Override
public short getCoalescingKey() {
// TODO: coalescing
return 0;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, mExtraData);
}
}

View File

@ -0,0 +1,61 @@
package com.swmansion.gesturehandler.react;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.react.uimanager.PointerEvents;
import com.facebook.react.uimanager.ReactPointerEventsView;
import com.facebook.react.views.view.ReactViewGroup;
import com.swmansion.gesturehandler.PointerEventsConfig;
import com.swmansion.gesturehandler.ViewConfigurationHelper;
public class RNViewConfigurationHelper implements ViewConfigurationHelper {
@Override
public PointerEventsConfig getPointerEventsConfigForView(View view) {
PointerEvents pointerEvents;
pointerEvents = view instanceof ReactPointerEventsView ?
((ReactPointerEventsView) view).getPointerEvents() :
PointerEvents.AUTO;
// Views that are disabled should never be the target of pointer events. However, their children
// can be because some views (SwipeRefreshLayout) use enabled but still have children that can
// be valid targets.
if (!view.isEnabled()) {
if (pointerEvents == PointerEvents.AUTO) {
return PointerEventsConfig.BOX_NONE;
} else if (pointerEvents == PointerEvents.BOX_ONLY) {
return PointerEventsConfig.NONE;
}
}
switch (pointerEvents) {
case BOX_ONLY: return PointerEventsConfig.BOX_ONLY;
case BOX_NONE: return PointerEventsConfig.BOX_NONE;
case NONE: return PointerEventsConfig.NONE;
}
return PointerEventsConfig.AUTO;
}
@Override
public View getChildInDrawingOrderAtIndex(ViewGroup parent, int index) {
if (parent instanceof ReactViewGroup) {
return parent.getChildAt(((ReactViewGroup) parent).getZIndexMappedChildIndex(index));
}
return parent.getChildAt(index);
}
@Override
public boolean isViewClippingChildren(ViewGroup view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !view.getClipChildren()) {
if (view instanceof ReactViewGroup) {
String overflow = ((ReactViewGroup) view).getOverflow();
return "hidden".equals(overflow);
}
return false;
}
return true;
}
}