diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 4845aae..7171ef5 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -8,6 +8,7 @@ ext { version = [ supportLibVersion: '25.4.0', + timber : '4.6.1', junit : '4.12', mockito : '2.13.0', robolectric : '3.7', @@ -28,6 +29,9 @@ ext { supportV4 : "com.android.support:support-v4:${version.supportLibVersion}", supportDesign : "com.android.support:design:${version.supportLibVersion}", + // timber + timber : "com.jakewharton.timber:timber:${version.timber}", + // instrumentation test testRunner : "com.android.support.test:runner:${version.testRunnerVersion}", testRules : "com.android.support.test:rules:${version.testRunnerVersion}", diff --git a/library/build.gradle b/library/build.gradle index bc238e6..87c2bcc 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -33,6 +33,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation dependenciesList.timber implementation dependenciesList.supportAppcompatV7 testImplementation dependenciesList.junit diff --git a/library/src/main/java/com/mapbox/android/gestures/MultiFingerGesture.java b/library/src/main/java/com/mapbox/android/gestures/MultiFingerGesture.java index c4f5700..84ba625 100644 --- a/library/src/main/java/com/mapbox/android/gestures/MultiFingerGesture.java +++ b/library/src/main/java/com/mapbox/android/gestures/MultiFingerGesture.java @@ -13,6 +13,8 @@ import java.util.List; import java.util.NoSuchElementException; +import timber.log.Timber; + /** * Base class for all multi finger gesture detectors. * @@ -46,6 +48,14 @@ public abstract class MultiFingerGesture extends BaseGesture { final HashMap pointersDistanceMap = new HashMap<>(); private PointF focalPoint = new PointF(); + private static final int BITS_PER_ALLOWED_ACTION = 4; + private static final int ALLOWED_ACTION_MASK = ((1 << BITS_PER_ALLOWED_ACTION) - 1); + /** + * Variable that holds all possible at this point MotionEvents based on the previous one. + * Each one of them is written on {@link #BITS_PER_ALLOWED_ACTION} successive bits. + */ + private long allowedActions = MotionEvent.ACTION_DOWN; + public MultiFingerGesture(Context context, AndroidGesturesManager gesturesManager) { super(context, gesturesManager); @@ -56,35 +66,98 @@ public MultiFingerGesture(Context context, AndroidGesturesManager gesturesManage @Override protected boolean analyzeEvent(MotionEvent motionEvent) { int action = motionEvent.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - pointerIdList.add(motionEvent.getPointerId(motionEvent.getActionIndex())); - break; - - case MotionEvent.ACTION_POINTER_UP: - case MotionEvent.ACTION_UP: - pointerIdList.remove(Integer.valueOf(motionEvent.getPointerId(motionEvent.getActionIndex()))); - break; - - case MotionEvent.ACTION_MOVE: + + boolean isMissingActions = isMissingAction(action); + if (isMissingActions) { + // stopping ProgressiveGestures and clearing pointers + if (this instanceof ProgressiveGesture && ((ProgressiveGesture) this).isInProgress()) { + ((ProgressiveGesture) this).gestureStopped(); + } + pointerIdList.clear(); + pointersDistanceMap.clear(); + + allowedActions = MotionEvent.ACTION_DOWN; + } + + if (!isMissingActions || action == MotionEvent.ACTION_DOWN) { + // if we are not missing any actions or the invalid one happens + // to be ACTION_DOWN (therefore, we can start over immediately), then update pointers + updatePointerList(motionEvent); + updateAllowedActions(); + } + + if (isMissingActions) { + Timber.w("Some MotionEvents were not passed to the library."); + return false; + } else { + if (action == MotionEvent.ACTION_MOVE) { if (pointerIdList.size() >= getRequiredPointersCount() && checkPressure()) { calculateDistances(); if (!isSloppyGesture()) { focalPoint = Utils.determineFocalPoint(motionEvent); return analyzeMovement(); } - return false; } - break; - - default: - break; + } } return false; } + private void updatePointerList(MotionEvent motionEvent) { + int action = motionEvent.getActionMasked(); + + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + pointerIdList.add(motionEvent.getPointerId(motionEvent.getActionIndex())); + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + pointerIdList.remove(Integer.valueOf(motionEvent.getPointerId(motionEvent.getActionIndex()))); + } + } + + private boolean isMissingAction(int action) { + if (action == allowedActions) { + // this will only happen for action == allowedActions == ACTION_DOWN + return false; + } + + while (allowedActions != 0) { + // get one of actions, the one on the first BITS_PER_ALLOWED_ACTION bits + long testCase = allowedActions & ALLOWED_ACTION_MASK; + if (action == testCase) { + // we got a match, all good + return false; + } + + // remove the one we just checked and iterate + allowedActions = allowedActions >> BITS_PER_ALLOWED_ACTION; + } + + // no available matching actions, we are missing some! + return true; + } + + private void updateAllowedActions() { + allowedActions = 0; + + if (pointerIdList.size() == 0) { + // only ACTION_DOWN available when no other pointers registered + allowedActions = MotionEvent.ACTION_DOWN; + } else if (pointerIdList.size() >= 1) { + // add available actions accordingly, shifting by BITS_PER_ALLOWED_ACTION with each addition + allowedActions += MotionEvent.ACTION_POINTER_DOWN; + allowedActions = allowedActions << BITS_PER_ALLOWED_ACTION; + allowedActions += MotionEvent.ACTION_MOVE; + + if (pointerIdList.size() == 1) { + allowedActions = allowedActions << BITS_PER_ALLOWED_ACTION; + allowedActions += MotionEvent.ACTION_UP; + } else if (pointerIdList.size() > 1) { + allowedActions = allowedActions << BITS_PER_ALLOWED_ACTION; + allowedActions += MotionEvent.ACTION_POINTER_UP; + } + } + } + boolean checkPressure() { float currentPressure = getCurrentEvent().getPressure(); float previousPressure = getPreviousEvent().getPressure(); diff --git a/library/src/main/java/com/mapbox/android/gestures/ProgressiveGesture.java b/library/src/main/java/com/mapbox/android/gestures/ProgressiveGesture.java index d9887b2..3891b10 100644 --- a/library/src/main/java/com/mapbox/android/gestures/ProgressiveGesture.java +++ b/library/src/main/java/com/mapbox/android/gestures/ProgressiveGesture.java @@ -45,37 +45,23 @@ protected boolean analyzeEvent(MotionEvent motionEvent) { boolean movementHandled = super.analyzeEvent(motionEvent); - if (!movementHandled) { - int action = motionEvent.getActionMasked(); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - - if (velocityTracker != null) { - velocityTracker.clear(); - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - if (pointerIdList.size() < getRequiredPointersCount() && isInProgress) { - gestureStopped(); - return true; - } - break; - - case MotionEvent.ACTION_CANCEL: - if (velocityTracker != null) { - velocityTracker.clear(); - } - if (isInProgress) { - gestureStopped(); - return true; - } - break; - - default: - break; + int action = motionEvent.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + if (velocityTracker != null) { + velocityTracker.clear(); + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + if (pointerIdList.size() < getRequiredPointersCount() && isInProgress) { + gestureStopped(); + return true; + } + } else if (action == MotionEvent.ACTION_CANCEL) { + if (velocityTracker != null) { + velocityTracker.clear(); + } + if (isInProgress) { + gestureStopped(); + return true; } } diff --git a/library/src/test/java/com/mapbox/android/gestures/PointersManagementTest.java b/library/src/test/java/com/mapbox/android/gestures/PointersManagementTest.java new file mode 100644 index 0000000..11d38af --- /dev/null +++ b/library/src/test/java/com/mapbox/android/gestures/PointersManagementTest.java @@ -0,0 +1,120 @@ +package com.mapbox.android.gestures; + +import android.view.MotionEvent; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static com.mapbox.android.gestures.TestUtils.getMotionEvent; +import static org.mockito.Mockito.spy; + +@RunWith(RobolectricTestRunner.class) +public class PointersManagementTest extends + AbstractGestureDetectorTest { + + @Override + StandardScaleGestureDetector getDetectorObject() { + return spy(androidGesturesManager.getStandardScaleGestureDetector()); + } + + private void checkResult(int expected) { + int pointersCount = androidGesturesManager.getStandardScaleGestureDetector().getPointersCount(); + Assert.assertTrue( + String.format("Expected %d pointers, was %d.", expected, pointersCount), + pointersCount == expected + ); + } + + @Test + public void missingDownTest() { + MotionEvent pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + checkResult(0); + } + + @Test + public void missingUpTest() { + MotionEvent downEvent = getMotionEvent(MotionEvent.ACTION_DOWN, 0, 0); + androidGesturesManager.onTouchEvent(downEvent); + + downEvent = getMotionEvent(MotionEvent.ACTION_DOWN, 0, 0, downEvent); + androidGesturesManager.onTouchEvent(downEvent); + + checkResult(1); + } + + @Test + public void missingPointerDownTest() { + MotionEvent downEvent = getMotionEvent(MotionEvent.ACTION_DOWN, 0, 0); + androidGesturesManager.onTouchEvent(downEvent); + + MotionEvent pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, downEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + MotionEvent pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + checkResult(0); //expecting 0, because we are waiting for ACTION_DOWN to synchronise again + } + + @Test + public void missingPointerUpTest() { + MotionEvent downEvent = getMotionEvent(MotionEvent.ACTION_DOWN, 0, 0); + androidGesturesManager.onTouchEvent(downEvent); + + MotionEvent pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, downEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + MotionEvent pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + MotionEvent upEvent = getMotionEvent(MotionEvent.ACTION_UP, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(upEvent); + + checkResult(0); + } + + @Test + public void addingRemovingPointersTest() { + MotionEvent downEvent = getMotionEvent(MotionEvent.ACTION_DOWN, 0, 0); + androidGesturesManager.onTouchEvent(downEvent); + + MotionEvent pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, downEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + pointerDownEvent = getMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerDownEvent); + + checkResult(4); + + MotionEvent pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerDownEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + pointerUpEvent = getMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(pointerUpEvent); + + MotionEvent upEvent = getMotionEvent(MotionEvent.ACTION_UP, 0, 0, pointerUpEvent); + androidGesturesManager.onTouchEvent(upEvent); + + checkResult(0); + } +}