Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guard against move events coming from different view trees #71

Merged
merged 2 commits into from
Jun 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ dependencies {
androidTestImplementation dependenciesList.mockitoAndroid
androidTestImplementation dependenciesList.testRunner
androidTestImplementation dependenciesList.testEspressoCore
androidTestImplementation dependenciesList.testEspressoIntents

implementation project(":library")
}

apply from: "${rootDir}/gradle/checkstyle.gradle"
apply from: "${rootDir}/gradle/checkstyle.gradle"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.mapbox.android.gestures;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import com.mapbox.android.gestures.testapp.OverlaidScrollActivity;
import com.mapbox.android.gestures.testapp.R;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static com.mapbox.android.gestures.UiTestUtils.pinchIn;

@RunWith(AndroidJUnit4.class)
public class MultipleViewTreesInputTest {

@Rule
public ActivityTestRule<OverlaidScrollActivity> activityTestRule =
new ActivityTestRule<>(OverlaidScrollActivity.class);

@Test
public void testPinch() {
onView(withId(R.id.testView)).perform(pinchIn());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package com.mapbox.android.gestures;

import android.graphics.Point;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.test.espresso.InjectEventSecurityException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.matcher.ViewMatchers;
import android.view.MotionEvent;
import android.view.View;

import org.hamcrest.Matcher;

public class UiTestUtils {

public static ViewAction pinchOut() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return ViewMatchers.isEnabled();
}

@Override
public String getDescription() {
return "Pinch out";
}

@Override
public void perform(UiController uiController, View view) {
Point middlePosition = getCenterPoint(view);

final int startDelta = 0;
final int endDelta = 500;

Point startPoint1 = new Point(middlePosition.x - startDelta, middlePosition.y);
Point startPoint2 = new Point(middlePosition.x + startDelta, middlePosition.y);
Point endPoint1 = new Point(middlePosition.x - endDelta, middlePosition.y);
Point endPoint2 = new Point(middlePosition.x + endDelta, middlePosition.y);

performPinch(uiController, startPoint1, startPoint2, endPoint1, endPoint2);
}
};
}

public static ViewAction pinchIn() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return ViewMatchers.isEnabled();
}

@Override
public String getDescription() {
return "Pinch in";
}

@Override
public void perform(UiController uiController, View view) {
Point middlePosition = getCenterPoint(view);

final int startDelta = 500;
final int endDelta = 0;

Point startPoint1 = new Point(middlePosition.x - startDelta, middlePosition.y);
Point startPoint2 = new Point(middlePosition.x + startDelta, middlePosition.y);
Point endPoint1 = new Point(middlePosition.x - endDelta, middlePosition.y);
Point endPoint2 = new Point(middlePosition.x + endDelta, middlePosition.y);

performPinch(uiController, startPoint1, startPoint2, endPoint1, endPoint2);
}
};
}

@NonNull
private static Point getCenterPoint(View view) {
int[] locationOnScreen = new int[2];
view.getLocationOnScreen(locationOnScreen);
float viewHeight = view.getHeight() * view.getScaleY();
float viewWidth = view.getWidth() * view.getScaleX();
return new Point(
(int) (locationOnScreen[0] + viewWidth / 2),
(int) (locationOnScreen[1] + viewHeight / 2));
}

// https://stackoverflow.com/a/46443628/9126211
private static void performPinch(UiController uiController, Point startPoint1, Point startPoint2, Point endPoint1,
Point endPoint2) {
final int duration = 500;
final long eventMinInterval = 10;
final long startTime = SystemClock.uptimeMillis();
long eventTime = startTime;
MotionEvent event;
float eventX1;
float eventY1;
float eventX2;
float eventY2;

eventX1 = startPoint1.x;
eventY1 = startPoint1.y;
eventX2 = startPoint2.x;
eventY2 = startPoint2.y;

// Specify the property for the two touch points
MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[2];
MotionEvent.PointerProperties pp1 = new MotionEvent.PointerProperties();
pp1.id = 0;
pp1.toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerProperties pp2 = new MotionEvent.PointerProperties();
pp2.id = 1;
pp2.toolType = MotionEvent.TOOL_TYPE_FINGER;

properties[0] = pp1;
properties[1] = pp2;

// Specify the coordinations of the two touch points
// NOTE: you MUST set the pressure and size value, or it doesn't work
MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2];
MotionEvent.PointerCoords pc1 = new MotionEvent.PointerCoords();
pc1.x = eventX1;
pc1.y = eventY1;
pc1.pressure = 1;
pc1.size = 1;
MotionEvent.PointerCoords pc2 = new MotionEvent.PointerCoords();
pc2.x = eventX2;
pc2.y = eventY2;
pc2.pressure = 1;
pc2.size = 1;
pointerCoords[0] = pc1;
pointerCoords[1] = pc2;

/*
* Events sequence of zoom gesture:
*
* 1. Send ACTION_DOWN event of one start point
* 2. Send ACTION_POINTER_DOWN of two start points
* 3. Send ACTION_MOVE of two middle points
* 4. Repeat step 3 with updated middle points (x,y), until reach the end points
* 5. Send ACTION_POINTER_UP of two end points
* 6. Send ACTION_UP of one end point
*/

try {
// Step 1
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_DOWN, 1, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
injectMotionEventToUiController(uiController, event);

// Step 2
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_POINTER_DOWN + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2,
properties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
injectMotionEventToUiController(uiController, event);

// Step 3, 4
long moveEventNumber = duration / eventMinInterval;

float stepX1;
float stepY1;
float stepX2;
float stepY2;

stepX1 = (endPoint1.x - startPoint1.x) / moveEventNumber;
stepY1 = (endPoint1.y - startPoint1.y) / moveEventNumber;
stepX2 = (endPoint2.x - startPoint2.x) / moveEventNumber;
stepY2 = (endPoint2.y - startPoint2.y) / moveEventNumber;

for (int i = 0; i < moveEventNumber; i++) {
// Update the move events
eventTime += eventMinInterval;
eventX1 += stepX1;
eventY1 += stepY1;
eventX2 += stepX2;
eventY2 += stepY2;

pc1.x = eventX1;
pc1.y = eventY1;
pc2.x = eventX2;
pc2.y = eventY2;

pointerCoords[0] = pc1;
pointerCoords[1] = pc2;

event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_MOVE, 2, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
injectMotionEventToUiController(uiController, event);
}

// Step 5
pc1.x = endPoint1.x;
pc1.y = endPoint1.y;
pc2.x = endPoint2.x;
pc2.y = endPoint2.y;
pointerCoords[0] = pc1;
pointerCoords[1] = pc2;

eventTime += eventMinInterval;
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_POINTER_UP + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
injectMotionEventToUiController(uiController, event);

// Step 6
eventTime += eventMinInterval;
event = MotionEvent.obtain(startTime, eventTime,
MotionEvent.ACTION_UP, 1, properties,
pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
injectMotionEventToUiController(uiController, event);
} catch (InjectEventSecurityException ex) {
throw new RuntimeException("Could not perform pinch", ex);
}
}

/**
* Safely call uiController.injectMotionEvent(event): Detect any error and "convert" it to an
* IllegalStateException
*/
private static void injectMotionEventToUiController(UiController uiController, MotionEvent event)
throws InjectEventSecurityException {
boolean injectEventSucceeded = uiController.injectMotionEvent(event);
if (!injectEventSucceeded) {
throw new IllegalStateException("Error performing event " + event);
}
}
}
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".OverlaidScrollActivity" />
<activity
android:name=".MainActivity"
android:screenOrientation="portrait">
Expand All @@ -19,7 +20,7 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".MapboxActivity"/>
<activity android:name=".MapboxActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.Style;

import timber.log.Timber;

Expand All @@ -26,7 +27,6 @@ public class MapboxActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mapbox);

String mapboxAccessToken = Utils.getMapboxAccessToken(getApplicationContext());
if (TextUtils.isEmpty(mapboxAccessToken) || mapboxAccessToken.equals(DEFAULT_MAPBOX_ACCESS_TOKEN)) {
Expand All @@ -35,8 +35,11 @@ protected void onCreate(Bundle savedInstanceState) {

Mapbox.getInstance(getApplicationContext(), mapboxAccessToken);

setContentView(R.layout.activity_mapbox);

mapView = (MapView) findViewById(R.id.map_view);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(mapboxMap -> mapboxMap.setStyle(Style.MAPBOX_STREETS));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.mapbox.android.gestures.testapp;

import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;
import android.view.View;

public class OverlaidScrollActivity extends AppCompatActivity {

private TestView testView;

@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_overlaid_scroll);

testView = findViewById(R.id.testView);
findViewById(R.id.scroll).setOnTouchListener((v, event) -> {
if (isTouchInView(findViewById(R.id.spacer), event)) {
testView.onTouchEvent(event);
return true;
}
return false;
});
}

public static boolean isTouchInView(View view, MotionEvent event) {
if (view == null || event == null) {
return false;
}
Rect hitBox = new Rect();
view.getGlobalVisibleRect(hitBox);
return hitBox.contains((int) event.getRawX(), (int) event.getRawY());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mapbox.android.gestures.testapp;

import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.mapbox.android.gestures.AndroidGesturesManager;

public class TestView extends View {

private AndroidGesturesManager androidGesturesManager;

public TestView(Context context) {
this(context, null);
}

public TestView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

private void init(Context context) {
androidGesturesManager = new AndroidGesturesManager(context);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
return androidGesturesManager.onTouchEvent(event) || super.onTouchEvent(event);
}
}
Loading