151 | * This reports the total translation over time since the {@link #BEGAN beginning} of the 152 | * gesture. This is not a delta value from the last {@link #CHANGED update}. 153 | */ 154 | public float getTranslationX() { 155 | return currentCentroidX - initialCentroidX; 156 | } 157 | 158 | /** 159 | * Returns the translationY of the drag gesture. 160 | *
161 | * This reports the total translation over time since the {@link #BEGAN beginning} of the 162 | * gesture. This is not a delta value from the last {@link #CHANGED update}. 163 | */ 164 | public float getTranslationY() { 165 | return currentCentroidY - initialCentroidY; 166 | } 167 | 168 | /** 169 | * Returns the positional velocityX of the drag gesture. 170 | *
171 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 172 | * 173 | * @return The velocity in pixels per second. 174 | */ 175 | public float getVelocityX() { 176 | return centroidXVelocityTracker != null ? centroidXVelocityTracker.getCurrentVelocity() : 0f; 177 | } 178 | 179 | /** 180 | * Returns the positional velocityY of the drag gesture. 181 | *
182 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 183 | * 184 | * @return The velocity in pixels per second. 185 | */ 186 | public float getVelocityY() { 187 | return centroidYVelocityTracker != null ? centroidYVelocityTracker.getCurrentVelocity() : 0f; 188 | } 189 | 190 | @Override 191 | public float getUntransformedCentroidX() { 192 | return currentCentroidX; 193 | } 194 | 195 | @Override 196 | public float getUntransformedCentroidY() { 197 | return currentCentroidY; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/GestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.graphics.Matrix; 19 | import android.graphics.PointF; 20 | import android.support.annotation.IntDef; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.view.MotionEventCompat; 23 | import android.view.MotionEvent; 24 | import android.view.View; 25 | import android.view.View.OnTouchListener; 26 | 27 | import java.lang.annotation.Retention; 28 | import java.lang.annotation.RetentionPolicy; 29 | import java.util.List; 30 | import java.util.concurrent.CopyOnWriteArrayList; 31 | 32 | /** 33 | * A gesture recognizer generates continuous or discrete events from a stream of device input 34 | * events. When attached to an element, any interactions with that element will be interpreted by 35 | * the gesture recognizer and turned into gesture events. The output is often a linear 36 | * transformation of translation, rotation, and/or scale. 37 | *
38 | * To use an instance of this class, forward all touch events from the element's parent to {@link 39 | * #onTouch(View, MotionEvent)}. 40 | */ 41 | public abstract class GestureRecognizer implements OnTouchListener { 42 | 43 | /** 44 | * A listener that receives {@link GestureRecognizer} events. 45 | */ 46 | public interface GestureStateChangeListener { 47 | 48 | /** 49 | * Notifies every time on {@link GestureRecognizerState state} change. 50 | *
51 | * Implementations should query the provided gesture recognizer for its current state and
52 | * properties.
53 | *
54 | * @param gestureRecognizer the gesture recognizer where the event originated from.
55 | */
56 | void onStateChanged(GestureRecognizer gestureRecognizer);
57 | }
58 |
59 | /**
60 | * The gesture recognizer has not yet recognized its gesture, but may be evaluating touch
61 | * events. This is the default state.
62 | */
63 | public static final int POSSIBLE = 0;
64 | /**
65 | * The gesture recognizer has received touch objects recognized as a continuous gesture.
66 | */
67 | public static final int BEGAN = 1;
68 | /**
69 | * The gesture recognizer has received touches recognized as a change to a continuous gesture.
70 | */
71 | public static final int CHANGED = 2;
72 | /**
73 | * The gesture recognizer has received touches recognized as the end of a continuous gesture. At
74 | * the next cycle of the run loop, the gesture recognizer resets its state to {@link
75 | * #POSSIBLE}.
76 | */
77 | public static final int RECOGNIZED = 3;
78 | /**
79 | * The gesture recognizer has received touches resulting in the cancellation of a continuous
80 | * gesture. At the next cycle of the run loop, the gesture recognizer resets its state to {@link
81 | * #POSSIBLE}.
82 | */
83 | public static final int CANCELLED = 4;
84 |
85 | /**
86 | * The state of the gesture recognizer.
87 | */
88 | @IntDef({POSSIBLE, BEGAN, CHANGED, RECOGNIZED, CANCELLED})
89 | @Retention(RetentionPolicy.SOURCE)
90 | public @interface GestureRecognizerState {
91 |
92 | }
93 |
94 | protected static final int UNSET_SLOP = -1;
95 |
96 | /* Temporary variables. */
97 | private final Matrix matrix = new Matrix();
98 | private final float[] array = new float[2];
99 | private final PointF pointF = new PointF();
100 |
101 | /**
102 | * Inverse transformation matrix that is updated on a untransformed point calculation. Use this
103 | * to convert untransformed points back to the element's local coordinate system.
104 | */
105 | private final Matrix inverse = new Matrix();
106 |
107 | private final List
291 | * An untransformed coordinate represents the location of a pointer that is not transformed by
292 | * the element's transformation matrix. {@code calculateUntransformedPoint(event, 0).x} is not
293 | * necessarily equal to {@code event.getRawX()}.
294 | *
295 | * @return A point representing the untransformed x and y. The caller should read the values
296 | * immediately as the object may be reused in other calculations.
297 | */
298 | protected PointF calculateUntransformedPoint(MotionEvent event, int pointerIndex) {
299 | array[0] = event.getX(pointerIndex);
300 | array[1] = event.getY(pointerIndex);
301 |
302 | getTransformationMatrix(element, matrix, inverse);
303 | matrix.mapPoints(array);
304 | pointF.set(array[0], array[1]);
305 |
306 | return pointF;
307 | }
308 |
309 | /**
310 | * Calculates the transformation matrices that can convert from local to untransformed
311 | * coordinate spaces.
312 | *
313 | * @param matrix This output matrix can convert from local to untransformed coordinate space.
314 | * @param inverse This output matrix can convert from untransformed to local coordinate space.
315 | */
316 | public static void getTransformationMatrix(View element, Matrix matrix, Matrix inverse) {
317 | matrix.reset();
318 | matrix.postScale(
319 | element.getScaleX(), element.getScaleY(), element.getPivotX(), element.getPivotY());
320 | matrix.postRotate(element.getRotation(), element.getPivotX(), element.getPivotY());
321 | matrix.postTranslate(element.getTranslationX(), element.getTranslationY());
322 |
323 | // Save the inverse matrix.
324 | matrix.invert(inverse);
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/library/src/main/java/com/google/android/material/motion/gestures/RotateGestureRecognizer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.graphics.PointF;
19 | import android.support.annotation.Nullable;
20 | import android.support.annotation.VisibleForTesting;
21 | import android.support.v4.view.MotionEventCompat;
22 | import android.view.MotionEvent;
23 | import android.view.View;
24 |
25 | import static com.google.android.material.motion.gestures.ValueVelocityTracker.ADDITIVE;
26 |
27 | /**
28 | * A gesture recognizer that generates scale events.
29 | */
30 | public class RotateGestureRecognizer extends GestureRecognizer {
31 |
32 | /**
33 | * Touch slop for rotate. Amount of radians that the angle needs to change.
34 | */
35 | public float rotateSlop = UNSET_SLOP;
36 |
37 | private float currentCentroidX;
38 | private float currentCentroidY;
39 |
40 | private float initialAngle;
41 | private float currentAngle;
42 |
43 | @Nullable
44 | private ValueVelocityTracker angleVelocityTracker;
45 |
46 | @Override
47 | public void setElement(@Nullable View element) {
48 | super.setElement(element);
49 |
50 | if (element == null) {
51 | return;
52 | }
53 |
54 | if (rotateSlop == UNSET_SLOP) {
55 | rotateSlop = (float) (Math.PI / 180);
56 | }
57 | if (angleVelocityTracker == null) {
58 | angleVelocityTracker = new ValueVelocityTracker(element.getContext(), ADDITIVE);
59 | }
60 | }
61 |
62 | @Override
63 | protected boolean onTouch(MotionEvent event) {
64 | PointF centroid = calculateUntransformedCentroid(event, 2);
65 | float centroidX = centroid.x;
66 | float centroidY = centroid.y;
67 | float angle = calculateAngle(event);
68 |
69 | int action = MotionEventCompat.getActionMasked(event);
70 | int pointerCount = event.getPointerCount();
71 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2) {
72 | currentCentroidX = centroidX;
73 | currentCentroidY = centroidY;
74 |
75 | initialAngle = angle;
76 | currentAngle = angle;
77 |
78 | angleVelocityTracker.onGestureStart(event, angle);
79 |
80 | if (rotateSlop == 0) {
81 | setState(BEGAN);
82 | }
83 | }
84 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount > 2
85 | || action == MotionEvent.ACTION_POINTER_UP && pointerCount > 2) {
86 | float adjustX = centroidX - currentCentroidX;
87 | float adjustY = centroidY - currentCentroidY;
88 |
89 | currentCentroidX += adjustX;
90 | currentCentroidY += adjustY;
91 |
92 | float adjustAngle = angle - currentAngle;
93 |
94 | initialAngle += adjustAngle;
95 | currentAngle += adjustAngle;
96 |
97 | angleVelocityTracker.onGestureAdjust(-adjustAngle);
98 | }
99 | if (action == MotionEvent.ACTION_MOVE && pointerCount >= 2) {
100 | currentCentroidX = centroidX;
101 | currentCentroidY = centroidY;
102 |
103 | if (!isInProgress()) {
104 | float deltaAngle = angle - initialAngle;
105 | if (Math.abs(deltaAngle) > rotateSlop) {
106 | float adjustAngle = Math.signum(deltaAngle) * rotateSlop;
107 |
108 | initialAngle += adjustAngle;
109 | currentAngle += adjustAngle;
110 |
111 | setState(BEGAN);
112 | }
113 | }
114 |
115 | if (isInProgress()) {
116 | currentAngle = angle;
117 |
118 | setState(CHANGED);
119 | }
120 |
121 | angleVelocityTracker.onGestureMove(event, angle);
122 | }
123 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2
124 | || action == MotionEvent.ACTION_CANCEL && pointerCount >= 2) {
125 | currentCentroidX = centroidX;
126 | currentCentroidY = centroidY;
127 |
128 | initialAngle = 0;
129 | currentAngle = 0;
130 |
131 | angleVelocityTracker.onGestureEnd(event, angle);
132 |
133 | if (isInProgress()) {
134 | if (action == MotionEvent.ACTION_POINTER_UP) {
135 | setState(RECOGNIZED);
136 | } else {
137 | setState(CANCELLED);
138 | }
139 | }
140 | }
141 |
142 | return true;
143 | }
144 |
145 | /**
146 | * Returns the rotation of the rotate gesture in radians.
147 | *
148 | * This reports the total rotation over time since the {@link #BEGAN beginning} of the gesture.
149 | * This is not a delta value from the last {@link #CHANGED update}.
150 | */
151 | public float getRotation() {
152 | return currentAngle - initialAngle;
153 | }
154 |
155 | /**
156 | * Returns the angular velocity of the angle gesture.
157 | *
158 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}.
159 | *
160 | * @return The velocity in radians per second.
161 | */
162 | public float getVelocity() {
163 | return angleVelocityTracker != null ? angleVelocityTracker.getCurrentVelocity() : 0f;
164 | }
165 |
166 | @Override
167 | public float getUntransformedCentroidX() {
168 | return currentCentroidX;
169 | }
170 |
171 | @Override
172 | public float getUntransformedCentroidY() {
173 | return currentCentroidY;
174 | }
175 |
176 | /**
177 | * Calculates the angle between the first two pointers in the given motion event.
178 | *
179 | * Angle is calculated from finger 0 to finger 1.
180 | */
181 | private float calculateAngle(MotionEvent event) {
182 | int action = MotionEventCompat.getActionMasked(event);
183 | int pointerIndex = MotionEventCompat.getActionIndex(event);
184 | int pointerCount = event.getPointerCount();
185 | if (pointerCount < 2) {
186 | return 0;
187 | }
188 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2) {
189 | return 0;
190 | }
191 |
192 | int i0 = 0;
193 | int i1 = 1;
194 | if (action == MotionEvent.ACTION_POINTER_UP) {
195 | if (pointerIndex == 0) {
196 | i0++;
197 | i1++;
198 | } else if (pointerIndex == 1) {
199 | i1++;
200 | }
201 | }
202 |
203 | PointF point = calculateUntransformedPoint(event, i0);
204 | float x0 = point.x;
205 | float y0 = point.y;
206 |
207 | point = calculateUntransformedPoint(event, i1);
208 | float x1 = point.x;
209 | float y1 = point.y;
210 |
211 | return angle(x0, y0, x1, y1);
212 | }
213 |
214 | @VisibleForTesting
215 | static float angle(float x0, float y0, float x1, float y1) {
216 | return (float) Math.atan2(y1 - y0, x1 - x0);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/library/src/main/java/com/google/android/material/motion/gestures/ScaleGestureRecognizer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.content.Context;
19 | import android.graphics.PointF;
20 | import android.support.annotation.Nullable;
21 | import android.support.annotation.VisibleForTesting;
22 | import android.support.v4.view.MotionEventCompat;
23 | import android.view.MotionEvent;
24 | import android.view.View;
25 | import android.view.ViewConfiguration;
26 |
27 | import static com.google.android.material.motion.gestures.ValueVelocityTracker.MULTIPLICATIVE;
28 |
29 | /**
30 | * A gesture recognizer that generates scale events.
31 | */
32 | public class ScaleGestureRecognizer extends GestureRecognizer {
33 |
34 | /**
35 | * Touch slop for scale. Amount of pixels that the span needs to change.
36 | */
37 | public int scaleSlop = UNSET_SLOP;
38 |
39 | private float currentCentroidX;
40 | private float currentCentroidY;
41 |
42 | private float initialSpan;
43 | private float currentSpan;
44 |
45 | @Nullable
46 | private ValueVelocityTracker spanVelocityTracker;
47 |
48 | @Override
49 | public void setElement(@Nullable View element) {
50 | super.setElement(element);
51 |
52 | if (element == null) {
53 | return;
54 | }
55 |
56 | if (scaleSlop == UNSET_SLOP) {
57 | Context context = element.getContext();
58 | scaleSlop = ViewConfiguration.get(context).getScaledTouchSlop();
59 | }
60 | if (spanVelocityTracker == null) {
61 | spanVelocityTracker = new ValueVelocityTracker(element.getContext(), MULTIPLICATIVE);
62 | }
63 | }
64 |
65 | @Override
66 | protected boolean onTouch(MotionEvent event) {
67 | PointF centroid = calculateUntransformedCentroid(event);
68 | float centroidX = centroid.x;
69 | float centroidY = centroid.y;
70 | float span = calculateAverageSpan(event, centroidX, centroidY);
71 |
72 | int action = MotionEventCompat.getActionMasked(event);
73 | int pointerCount = event.getPointerCount();
74 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2) {
75 | currentCentroidX = centroidX;
76 | currentCentroidY = centroidY;
77 |
78 | initialSpan = span;
79 | currentSpan = span;
80 |
81 | spanVelocityTracker.onGestureStart(event, span);
82 |
83 | if (scaleSlop == 0) {
84 | setState(BEGAN);
85 | }
86 | }
87 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount > 2
88 | || action == MotionEvent.ACTION_POINTER_UP && pointerCount > 2) {
89 | float adjustX = centroidX - currentCentroidX;
90 | float adjustY = centroidY - currentCentroidY;
91 |
92 | currentCentroidX += adjustX;
93 | currentCentroidY += adjustY;
94 |
95 | float adjustSpan = span / currentSpan;
96 |
97 | initialSpan *= adjustSpan;
98 | currentSpan *= adjustSpan;
99 |
100 | spanVelocityTracker.onGestureAdjust(1 / adjustSpan);
101 | }
102 | if (action == MotionEvent.ACTION_MOVE && pointerCount >= 2) {
103 | currentCentroidX = centroidX;
104 | currentCentroidY = centroidY;
105 |
106 | if (!isInProgress()) {
107 | float deltaSpan = span - initialSpan;
108 | if (Math.abs(deltaSpan) > scaleSlop) {
109 | float adjustSpan = 1 + Math.signum(deltaSpan) * (scaleSlop / initialSpan);
110 |
111 | initialSpan *= adjustSpan;
112 | currentSpan *= adjustSpan;
113 |
114 | setState(BEGAN);
115 | }
116 | }
117 |
118 | if (isInProgress()) {
119 | currentSpan = span;
120 |
121 | setState(CHANGED);
122 | }
123 |
124 | spanVelocityTracker.onGestureMove(event, span);
125 | }
126 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2
127 | || action == MotionEvent.ACTION_CANCEL && pointerCount >= 2) {
128 | currentCentroidX = centroidX;
129 | currentCentroidY = centroidY;
130 |
131 | initialSpan = 0;
132 | currentSpan = 0;
133 |
134 | spanVelocityTracker.onGestureEnd(event, span);
135 |
136 | if (isInProgress()) {
137 | if (action == MotionEvent.ACTION_POINTER_UP) {
138 | setState(RECOGNIZED);
139 | } else {
140 | setState(CANCELLED);
141 | }
142 | }
143 | }
144 |
145 | return true;
146 | }
147 |
148 | /**
149 | * Returns the scale of the pinch gesture.
150 | *
151 | * This reports the total scale over time since the {@link #BEGAN beginning} of the gesture.
152 | * This is not a delta value from the last {@link #CHANGED update}.
153 | */
154 | public float getScale() {
155 | return initialSpan > 0 ? currentSpan / initialSpan : 1;
156 | }
157 |
158 | /**
159 | * Returns the scalar velocity of the scale gesture.
160 | *
161 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}.
162 | *
163 | * @return The velocity in pixels per second.
164 | */
165 | public float getVelocity() {
166 | return spanVelocityTracker != null ? spanVelocityTracker.getCurrentVelocity() : 0f;
167 | }
168 |
169 | @Override
170 | public float getUntransformedCentroidX() {
171 | return currentCentroidX;
172 | }
173 |
174 | @Override
175 | public float getUntransformedCentroidY() {
176 | return currentCentroidY;
177 | }
178 |
179 | /**
180 | * Calculates the average span of all the active pointers in the given motion event.
181 | *
182 | * The average span is twice the average distance of all pointers to the given centroid.
183 | */
184 | private float calculateAverageSpan(MotionEvent event, float centroidX, float centroidY) {
185 | int action = MotionEventCompat.getActionMasked(event);
186 | int index = MotionEventCompat.getActionIndex(event);
187 |
188 | float sum = 0;
189 | int num = 0;
190 | for (int i = 0, count = event.getPointerCount(); i < count; i++) {
191 | if (action == MotionEvent.ACTION_POINTER_UP && index == i) {
192 | continue;
193 | }
194 |
195 | sum += calculateDistance(event, i, centroidX, centroidY);
196 | num++;
197 | }
198 |
199 | float averageDistance = sum / num;
200 | return averageDistance * 2;
201 | }
202 |
203 | /**
204 | * Calculates the distance between the pointer given by the pointer index and the given
205 | * centroid.
206 | */
207 | private float calculateDistance(
208 | MotionEvent event, int pointerIndex, float centroidX, float centroidY) {
209 | PointF untransformedPoint = calculateUntransformedPoint(event, pointerIndex);
210 |
211 | return dist(centroidX, centroidY, untransformedPoint.x, untransformedPoint.y);
212 | }
213 |
214 | @VisibleForTesting
215 | static float dist(float x0, float y0, float x1, float y1) {
216 | float dx = x1 - x0;
217 | float dy = y1 - y0;
218 | return (float) Math.sqrt(dx * dx + dy * dy);
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/library/src/main/java/com/google/android/material/motion/gestures/ValueVelocityTracker.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.content.Context;
19 | import android.support.annotation.IntDef;
20 | import android.support.annotation.Nullable;
21 | import android.support.v4.view.MotionEventCompat;
22 | import android.view.MotionEvent;
23 | import android.view.VelocityTracker;
24 | import android.view.ViewConfiguration;
25 |
26 | import java.lang.annotation.Retention;
27 | import java.lang.annotation.RetentionPolicy;
28 |
29 | /**
30 | * A velocity tracker for any arbitrary value. Uses a {@link VelocityTracker} under the hood which
31 | * is fed specially crafted {@link MotionEvent}s.
32 | */
33 | class ValueVelocityTracker {
34 |
35 | /**
36 | * A type of value that is accumulated as a additive sum.
37 | */
38 | public static final int ADDITIVE = 0;
39 |
40 | /**
41 | * A type of value that is accumulated as a multiplicative product.
42 | */
43 | public static final int MULTIPLICATIVE = 1;
44 |
45 | /**
46 | * A type that describes how a value is accumulated.
47 | */
48 | @IntDef({ADDITIVE, MULTIPLICATIVE})
49 | @Retention(RetentionPolicy.SOURCE)
50 | public @interface AccumulationType {
51 |
52 | }
53 |
54 | private static final int PIXELS_PER_SECOND = 1000;
55 | private static final float DONT_CARE = 0f;
56 |
57 | private final float maximumFlingVelocity;
58 | @AccumulationType
59 | private final int type;
60 |
61 | @Nullable
62 | private VelocityTracker velocityTracker;
63 | private float adjust;
64 | private float currentVelocity;
65 |
66 | public ValueVelocityTracker(Context context, @AccumulationType int type) {
67 | this.maximumFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
68 | this.type = type;
69 | }
70 |
71 | /**
72 | * Returns the velocity calculated in the most recent {@link #onGestureEnd(MotionEvent,
73 | * float)}.
74 | */
75 | public float getCurrentVelocity() {
76 | return currentVelocity;
77 | }
78 |
79 | /**
80 | * Processes the start of a gesture.
81 | *
82 | * Must be balanced with a call to {@link #onGestureEnd(MotionEvent, float)} to end the
83 | * gesture.
84 | */
85 | public void onGestureStart(MotionEvent event, float value) {
86 | velocityTracker = VelocityTracker.obtain();
87 | if (type == ADDITIVE) {
88 | adjust = 0f;
89 | } else {
90 | adjust = 1f;
91 | }
92 | currentVelocity = 0f;
93 |
94 | addValueMovement(event, value);
95 | }
96 |
97 | /**
98 | * Processes the adjustment of a gesture. Call this if you do not want the value to jump
99 | * discontinuously on additional fingers entering and exiting the gesture.
100 | *
101 | * May be called multiple times during a gesture.
102 | */
103 | public void onGestureAdjust(float adjust) {
104 | this.adjust = adjust;
105 | }
106 |
107 | /**
108 | * Processes the movement of a gesture.
109 | *
110 | * May be called multiple times during a gesture.
111 | */
112 | public void onGestureMove(MotionEvent event, float value) {
113 | addValueMovement(event, value);
114 | }
115 |
116 | /**
117 | * Processes the end of a gesture.
118 | *
119 | * Must be balanced with a previous call to {@link #onGestureStart(MotionEvent, float)}.
120 | */
121 | public void onGestureEnd(MotionEvent event, float value) {
122 | if (velocityTracker == null) {
123 | return;
124 | }
125 |
126 | addValueMovement(event, value);
127 |
128 | velocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, maximumFlingVelocity);
129 | currentVelocity = velocityTracker.getXVelocity();
130 |
131 | velocityTracker.recycle();
132 | velocityTracker = null;
133 | }
134 |
135 | private void addValueMovement(MotionEvent event, float value) {
136 | if (velocityTracker == null) {
137 | return;
138 | }
139 |
140 | int valueMovementAction;
141 |
142 | int action = MotionEventCompat.getActionMasked(event);
143 | switch (action) {
144 | case MotionEvent.ACTION_DOWN:
145 | case MotionEvent.ACTION_MOVE:
146 | case MotionEvent.ACTION_UP:
147 | case MotionEvent.ACTION_CANCEL:
148 | valueMovementAction = action;
149 | break;
150 | case MotionEvent.ACTION_POINTER_DOWN:
151 | valueMovementAction = MotionEvent.ACTION_DOWN;
152 | break;
153 | case MotionEvent.ACTION_POINTER_UP:
154 | valueMovementAction = MotionEvent.ACTION_UP;
155 | break;
156 | default:
157 | throw new IllegalArgumentException("Unexpected action for event: " + event);
158 | }
159 | velocityTracker.addMovement(
160 | MotionEvent.obtain(
161 | event.getDownTime(),
162 | event.getEventTime(),
163 | valueMovementAction,
164 | apply(value, adjust),
165 | DONT_CARE,
166 | event.getMetaState()));
167 | }
168 |
169 | private float apply(float value, float adjust) {
170 | if (type == ADDITIVE) {
171 | return value + adjust;
172 | } else {
173 | return value * adjust;
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/library/src/main/java/com/google/android/material/motion/gestures/testing/SimulatedGestureRecognizer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures.testing;
17 |
18 | import android.graphics.PointF;
19 | import android.view.MotionEvent;
20 | import android.view.View;
21 |
22 | import com.google.android.material.motion.gestures.GestureRecognizer;
23 |
24 | /**
25 | * A no-op gesture recognizer for testing that exposes {@link #setState(int)}.
26 | */
27 | public class SimulatedGestureRecognizer extends GestureRecognizer {
28 |
29 | private float untransformedCentroidX;
30 | private float untransformedCentroidY;
31 |
32 | public SimulatedGestureRecognizer(View element) {
33 | setElement(element);
34 | }
35 |
36 | @Override
37 | public void setState(@GestureRecognizerState int state) {
38 | super.setState(state);
39 | }
40 |
41 | public void setCentroid(float x, float y) {
42 | PointF centroid =
43 | calculateUntransformedCentroid(MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0));
44 | untransformedCentroidX = centroid.x;
45 | untransformedCentroidY = centroid.y;
46 |
47 | setState(CHANGED);
48 | }
49 |
50 | @Override
51 | protected boolean onTouch(MotionEvent event) {
52 | return false;
53 | }
54 |
55 | @Override
56 | public float getUntransformedCentroidX() {
57 | return untransformedCentroidX;
58 | }
59 |
60 | @Override
61 | public float getUntransformedCentroidY() {
62 | return untransformedCentroidY;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/library/src/test/java/com/google/android/material/motion/gestures/DragGestureRecognizerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.app.Activity;
19 | import android.content.Context;
20 | import android.view.MotionEvent;
21 | import android.view.View;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.robolectric.Robolectric;
27 | import org.robolectric.RobolectricTestRunner;
28 | import org.robolectric.annotation.Config;
29 |
30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN;
31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED;
32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED;
33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE;
34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED;
35 | import static com.google.common.truth.Truth.assertThat;
36 | import static org.mockito.Mockito.mock;
37 | import static org.mockito.Mockito.when;
38 |
39 | @RunWith(RobolectricTestRunner.class)
40 | @Config(constants = BuildConfig.class, sdk = 21)
41 | public class DragGestureRecognizerTests {
42 |
43 | private static final float E = 0.0001f;
44 |
45 | private View element;
46 | private DragGestureRecognizer dragGestureRecognizer;
47 |
48 | private long eventDownTime;
49 | private long eventTime;
50 |
51 | @Before
52 | public void setUp() {
53 | Context context = Robolectric.setupActivity(Activity.class);
54 | element = new View(context);
55 | dragGestureRecognizer = new DragGestureRecognizer();
56 | dragGestureRecognizer.dragSlop = 0;
57 |
58 | eventDownTime = 0;
59 | eventTime = -16;
60 | }
61 |
62 | @Test
63 | public void defaultState() {
64 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
65 | assertThat(dragGestureRecognizer.getElement()).isEqualTo(null);
66 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f);
67 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f);
68 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(0).of(0f);
69 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(0).of(0f);
70 | assertThat(dragGestureRecognizer.getVelocityX()).isWithin(0).of(0f);
71 | assertThat(dragGestureRecognizer.getVelocityY()).isWithin(0).of(0f);
72 | }
73 |
74 | @Test
75 | public void smallMovementIsNotRecognized() {
76 | dragGestureRecognizer.dragSlop = 24;
77 |
78 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
79 | dragGestureRecognizer.addStateChangeListener(listener);
80 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
81 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
82 |
83 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
84 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
86 |
87 | // Move 1 pixel. Should not change the state.
88 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 1, 0));
89 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
91 | }
92 |
93 | @Test
94 | public void largeHorizontalMovementIsRecognized() {
95 | dragGestureRecognizer.dragSlop = 24;
96 |
97 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
98 | dragGestureRecognizer.addStateChangeListener(listener);
99 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
100 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
101 |
102 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
103 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
104 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
105 |
106 | // Move 100 pixel right. Should change the state.
107 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0));
108 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED);
109 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
110 |
111 | // Move 1 pixel. Should still change the state.
112 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 101, 0));
113 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED);
114 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
115 | }
116 |
117 | @Test
118 | public void largeVerticalMovementIsRecognized() {
119 | dragGestureRecognizer.dragSlop = 24;
120 |
121 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
122 | dragGestureRecognizer.addStateChangeListener(listener);
123 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
124 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
125 |
126 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
127 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
128 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
129 |
130 | // Move 100 pixel right. Should change the state.
131 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 0, 100));
132 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED);
133 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
134 |
135 | // Move 1 pixel. Should still change the state.
136 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 0, 101));
137 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED);
138 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
139 | }
140 |
141 | @Test
142 | public void completedGestureIsRecognized() {
143 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
144 | dragGestureRecognizer.addStateChangeListener(listener);
145 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
146 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0));
147 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 100, 0));
148 |
149 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
150 | assertThat(listener.states.toArray())
151 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
152 | }
153 |
154 | @Test
155 | public void cancelledGestureIsNotRecognized() {
156 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
157 | dragGestureRecognizer.addStateChangeListener(listener);
158 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
159 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0));
160 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0));
161 |
162 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
163 | assertThat(listener.states.toArray())
164 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE});
165 | }
166 |
167 | @Test
168 | public void noMovementIsNotRecognized() {
169 | dragGestureRecognizer.dragSlop = 24;
170 |
171 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
172 | dragGestureRecognizer.addStateChangeListener(listener);
173 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
174 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
175 |
176 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
177 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
178 | }
179 |
180 | @Test
181 | public void irrelevantMotionIsIgnored() {
182 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
183 | dragGestureRecognizer.addStateChangeListener(listener);
184 |
185 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0));
186 |
187 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE);
188 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
189 | }
190 |
191 | @Test
192 | public void multitouchHasCorrectCentroidAndTranslation() {
193 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
194 | dragGestureRecognizer.addStateChangeListener(listener);
195 |
196 | // First finger down. Centroid is at finger location and translation is 0.
197 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
198 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
199 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
200 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0);
201 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0);
202 |
203 | // Second finger down. Centroid is in between fingers and translation is 0.
204 | dragGestureRecognizer.onTouch(element,
205 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
206 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50);
207 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50);
208 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0);
209 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0);
210 |
211 | // Second finger moves [dx, dy]. Centroid and translation moves [dx/2, dy/2].
212 | float dx = 505;
213 | float dy = 507;
214 | dragGestureRecognizer.onTouch(element,
215 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy));
216 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2);
217 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2);
218 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(dx / 2);
219 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(dy / 2);
220 |
221 | // Second finger up. Centroid is at first finger location and translation stays the same.
222 | dragGestureRecognizer.onTouch(element,
223 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy));
224 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
225 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
226 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(dx / 2);
227 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(dy / 2);
228 |
229 | // Finger up. Centroid is at first finger location and translation is reset.
230 | dragGestureRecognizer.onTouch(element,
231 | createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
232 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
233 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
234 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0);
235 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0);
236 |
237 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
238 | }
239 |
240 | @Test(expected = NullPointerException.class)
241 | public void crashesForNullElement() {
242 | dragGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
243 | }
244 |
245 | @Test
246 | public void allowsSettingElementAgain() {
247 | dragGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
248 | dragGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
249 | }
250 |
251 | private MotionEvent createMotionEvent(int action, float x, float y) {
252 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0);
253 | }
254 |
255 | private MotionEvent createMultiTouchMotionEvent(
256 | int action, int index, float x0, float y0, float x1, float y1) {
257 | MotionEvent event = mock(MotionEvent.class);
258 |
259 | when(event.getDownTime()).thenReturn(eventDownTime);
260 | when(event.getEventTime()).thenReturn(eventTime += 16);
261 |
262 | when(event.getPointerCount()).thenReturn(2);
263 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
264 | when(event.getActionMasked()).thenReturn(action);
265 | when(event.getActionIndex()).thenReturn(index);
266 |
267 | when(event.getRawX()).thenReturn(x0);
268 | when(event.getRawY()).thenReturn(y0);
269 |
270 | when(event.getX(0)).thenReturn(x0);
271 | when(event.getY(0)).thenReturn(y0);
272 |
273 | when(event.getX(1)).thenReturn(x1);
274 | when(event.getY(1)).thenReturn(y1);
275 |
276 | return event;
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/library/src/test/java/com/google/android/material/motion/gestures/GestureRecognizerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.app.Activity;
19 | import android.view.View;
20 |
21 | import com.google.android.material.motion.gestures.testing.SimulatedGestureRecognizer;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.robolectric.Robolectric;
27 | import org.robolectric.RobolectricTestRunner;
28 | import org.robolectric.annotation.Config;
29 |
30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN;
31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED;
32 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE;
33 | import static com.google.common.truth.Truth.assertThat;
34 |
35 | @RunWith(RobolectricTestRunner.class)
36 | @Config(constants = BuildConfig.class, sdk = 21)
37 | public class GestureRecognizerTests {
38 |
39 | private SimulatedGestureRecognizer gestureRecognizer;
40 |
41 | @Before
42 | public void setUp() {
43 | View element = new View(Robolectric.setupActivity(Activity.class));
44 | gestureRecognizer = new SimulatedGestureRecognizer(element);
45 | }
46 |
47 | @Test
48 | public void removedListenerDoesNotGetEvents() {
49 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
50 | gestureRecognizer.addStateChangeListener(listener);
51 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
52 |
53 | gestureRecognizer.setState(BEGAN);
54 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN});
55 |
56 | gestureRecognizer.removeStateChangeListener(listener);
57 | gestureRecognizer.setState(CHANGED);
58 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN});
59 |
60 | }
61 |
62 | @Test
63 | public void addingSameListenerTwiceDoesNotSendTwoEvents() {
64 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
65 |
66 | gestureRecognizer.addStateChangeListener(listener);
67 | gestureRecognizer.addStateChangeListener(listener);
68 |
69 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
70 |
71 | gestureRecognizer.setState(BEGAN);
72 |
73 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN});
74 | }
75 |
76 | @Test
77 | public void canSetNullElement() {
78 | gestureRecognizer.setElement(null);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/library/src/test/java/com/google/android/material/motion/gestures/RotateGestureRecognizerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.app.Activity;
19 | import android.content.Context;
20 | import android.view.MotionEvent;
21 | import android.view.View;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.robolectric.Robolectric;
27 | import org.robolectric.RobolectricTestRunner;
28 | import org.robolectric.annotation.Config;
29 |
30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN;
31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED;
32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED;
33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE;
34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED;
35 | import static com.google.android.material.motion.gestures.RotateGestureRecognizer.angle;
36 | import static com.google.common.truth.Truth.assertThat;
37 | import static org.mockito.Mockito.mock;
38 | import static org.mockito.Mockito.when;
39 |
40 | @RunWith(RobolectricTestRunner.class)
41 | @Config(constants = BuildConfig.class, sdk = 21)
42 | public class RotateGestureRecognizerTests {
43 |
44 | private static final float E = 0.0001f;
45 |
46 | private View element;
47 | private RotateGestureRecognizer rotateGestureRecognizer;
48 |
49 | private long eventDownTime;
50 | private long eventTime;
51 |
52 | @Before
53 | public void setUp() {
54 | Context context = Robolectric.setupActivity(Activity.class);
55 | element = new View(context);
56 | rotateGestureRecognizer = new RotateGestureRecognizer();
57 | rotateGestureRecognizer.rotateSlop = 0;
58 |
59 | eventDownTime = 0;
60 | eventTime = -16;
61 | }
62 |
63 | @Test
64 | public void defaultState() {
65 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
66 | assertThat(rotateGestureRecognizer.getElement()).isEqualTo(null);
67 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f);
68 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f);
69 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(0).of(0f);
70 | assertThat(rotateGestureRecognizer.getVelocity()).isWithin(0).of(0f);
71 | }
72 |
73 | @Test
74 | public void smallMovementIsNotRecognized() {
75 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees.
76 |
77 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
78 | rotateGestureRecognizer.addStateChangeListener(listener);
79 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
80 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
81 |
82 | // First finger down.
83 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
84 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
86 |
87 | // Second finger down.
88 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
89 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
91 |
92 | // Move second finger up less than 45 degrees. Should not change the state.
93 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 99));
94 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
95 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
96 | }
97 |
98 | @Test
99 | public void largeCounterClockwiseMovementIsRecognized() {
100 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees.
101 |
102 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
103 | rotateGestureRecognizer.addStateChangeListener(listener);
104 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
105 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
106 |
107 | // First finger down.
108 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
109 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
110 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
111 |
112 | // Second finger down.
113 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
114 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
115 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
116 |
117 | // Move second finger up more than 45 degrees. Should change the state.
118 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 101));
119 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED);
120 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
121 |
122 | // Move second finger 1 pixel. Should still change the state.
123 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 102));
124 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED);
125 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
126 | }
127 |
128 | @Test
129 | public void largeClockwiseMovementIsRecognized() {
130 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees.
131 |
132 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
133 | rotateGestureRecognizer.addStateChangeListener(listener);
134 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
135 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
136 |
137 | // First finger down.
138 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
139 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
140 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
141 |
142 | // Second finger down.
143 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
144 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
145 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
146 |
147 | // Move second finger down more than 45 degrees. Should change the state.
148 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, -101));
149 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED);
150 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
151 |
152 | // Move second finger 1 pixel. Should still change the state.
153 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, -102));
154 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED);
155 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
156 | }
157 |
158 | @Test
159 | public void completedGestureIsRecognized() {
160 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
161 | rotateGestureRecognizer.addStateChangeListener(listener);
162 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
163 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
164 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 100, 200, 200));
165 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100, 200, 200));
166 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 2, 0, 0, 200, 100, 200, 200));
167 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 200, 100));
168 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
169 |
170 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
171 | assertThat(listener.states.toArray())
172 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
173 | }
174 |
175 | @Test
176 | public void cancelledOneFingerGestureIsNotRecognized() {
177 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
178 | rotateGestureRecognizer.addStateChangeListener(listener);
179 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
180 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0));
181 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0));
182 |
183 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
184 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
185 | }
186 |
187 | @Test
188 | public void cancelledTwoFingerGestureIsNotRecognized() {
189 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
190 | rotateGestureRecognizer.addStateChangeListener(listener);
191 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
192 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
193 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100));
194 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_CANCEL, 0, 0, 0, 200, 100));
195 |
196 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
197 | assertThat(listener.states.toArray())
198 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE});
199 | }
200 |
201 | @Test
202 | public void noMovementIsNotRecognized() {
203 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees.
204 |
205 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
206 | rotateGestureRecognizer.addStateChangeListener(listener);
207 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
208 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
209 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100));
210 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
211 |
212 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
213 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
214 | }
215 |
216 | @Test
217 | public void irrelevantMotionIsIgnored() {
218 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
219 | rotateGestureRecognizer.addStateChangeListener(listener);
220 |
221 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0));
222 |
223 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE);
224 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
225 | }
226 |
227 | @Test
228 | public void oneFingerDoesNotAffectRotate() {
229 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
230 | rotateGestureRecognizer.addStateChangeListener(listener);
231 |
232 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
233 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0f);
234 |
235 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 100));
236 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0f);
237 | }
238 |
239 | @Test
240 | public void multitouchHasCorrectCentroidAndRotation() {
241 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
242 | rotateGestureRecognizer.addStateChangeListener(listener);
243 |
244 | // First finger down. Centroid is at finger location and rotation is 0.
245 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
246 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
247 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
248 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
249 |
250 | // Second finger down. Centroid is in between fingers and rotation is 1.
251 | rotateGestureRecognizer.onTouch(element,
252 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
253 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50);
254 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50);
255 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
256 |
257 | // Second finger moves [dx, dy]. Centroid moves [dx/2, dy/2], rotation is calculated correctly.
258 | float dx = 5;
259 | float dy = 507;
260 | rotateGestureRecognizer.onTouch(element,
261 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy));
262 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2);
263 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2);
264 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(
265 | angle(0, 0, 100 + dx, 100 + dy) - angle(0, 0, 100, 100));
266 |
267 | // Second finger up. State is now reset.
268 | rotateGestureRecognizer.onTouch(element,
269 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy));
270 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
271 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
272 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
273 |
274 | assertThat(listener.states.toArray()).isEqualTo(
275 | new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
276 | }
277 |
278 | @Test
279 | public void thirdFingerDoesNotAffectRotation() {
280 | // First finger.
281 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
282 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
283 |
284 | // Second finger on horizontal line.
285 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
286 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
287 |
288 | // Third finger also on horizontal line.
289 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0));
290 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
291 |
292 | // Move third finger.
293 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 2, 0, 0, 100, 0, 200, 200));
294 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
295 | }
296 |
297 | @Test
298 | public void rotationIsStableOnFirstFingerUp() {
299 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
300 | rotateGestureRecognizer.addStateChangeListener(listener);
301 |
302 | // First finger down.
303 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
304 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
305 |
306 | // Second finger down on horizontal line.
307 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
308 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
309 |
310 | // Third finger also down on horizontal line.
311 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0));
312 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
313 |
314 | // Move second finger 45 degrees.
315 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 100, 200, 0));
316 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4));
317 |
318 | // First finger up. Rotation stays the same.
319 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, 0, 100, 100, 200, 0));
320 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4));
321 | }
322 |
323 | @Test
324 | public void rotationIsStableOnSecondFingerUp() {
325 | // First finger down.
326 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
327 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
328 |
329 | // Second finger down on horizontal line.
330 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0));
331 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
332 |
333 | // Third finger also down on horizontal line.
334 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0));
335 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0);
336 |
337 | // Move second finger 45 degrees.
338 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 100, 200, 0));
339 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4));
340 |
341 | // Second finger up. Rotation stays the same.
342 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100, 200, 0));
343 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4));
344 | }
345 |
346 | @Test
347 | public void nonZeroVelocity() {
348 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
349 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 10, 0));
350 |
351 | float move = 0;
352 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10, 0 + (move += 10)));
353 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10, 0 + (move += 10)));
354 |
355 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 10 + move, 0));
356 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
357 |
358 | assertThat(rotateGestureRecognizer.getVelocity()).isGreaterThan(0f);
359 | }
360 |
361 | @Test(expected = NullPointerException.class)
362 | public void crashesForNullElement() {
363 | rotateGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
364 | }
365 |
366 | @Test
367 | public void allowsSettingElementAgain() {
368 | rotateGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
369 | rotateGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
370 | }
371 |
372 | private MotionEvent createMotionEvent(int action, float x, float y) {
373 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0);
374 | }
375 |
376 | private MotionEvent createMultiTouchMotionEvent(
377 | int action, int index, float x0, float y0, float x1, float y1) {
378 | MotionEvent event = mock(MotionEvent.class);
379 |
380 | when(event.getDownTime()).thenReturn(eventDownTime);
381 | when(event.getEventTime()).thenReturn(eventTime += 16);
382 |
383 | when(event.getPointerCount()).thenReturn(2);
384 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
385 | when(event.getActionMasked()).thenReturn(action);
386 | when(event.getActionIndex()).thenReturn(index);
387 |
388 | when(event.getRawX()).thenReturn(x0);
389 | when(event.getRawY()).thenReturn(y0);
390 |
391 | when(event.getX(0)).thenReturn(x0);
392 | when(event.getY(0)).thenReturn(y0);
393 |
394 | when(event.getX(1)).thenReturn(x1);
395 | when(event.getY(1)).thenReturn(y1);
396 |
397 | return event;
398 | }
399 |
400 | private MotionEvent createMultiTouchMotionEvent(
401 | int action, int index, float x0, float y0, float x1, float y1, float x2, float y2) {
402 | MotionEvent event = mock(MotionEvent.class);
403 |
404 | when(event.getDownTime()).thenReturn(eventDownTime);
405 | when(event.getEventTime()).thenReturn(eventTime += 16);
406 |
407 | when(event.getPointerCount()).thenReturn(3);
408 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
409 | when(event.getActionMasked()).thenReturn(action);
410 | when(event.getActionIndex()).thenReturn(index);
411 |
412 | when(event.getRawX()).thenReturn(x0);
413 | when(event.getRawY()).thenReturn(y0);
414 |
415 | when(event.getX(0)).thenReturn(x0);
416 | when(event.getY(0)).thenReturn(y0);
417 |
418 | when(event.getX(1)).thenReturn(x1);
419 | when(event.getY(1)).thenReturn(y1);
420 |
421 | when(event.getX(2)).thenReturn(x2);
422 | when(event.getY(2)).thenReturn(y2);
423 |
424 | return event;
425 | }
426 | }
427 |
--------------------------------------------------------------------------------
/library/src/test/java/com/google/android/material/motion/gestures/ScaleGestureRecognizerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import android.app.Activity;
19 | import android.content.Context;
20 | import android.view.MotionEvent;
21 | import android.view.View;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.robolectric.Robolectric;
27 | import org.robolectric.RobolectricTestRunner;
28 | import org.robolectric.annotation.Config;
29 |
30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN;
31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED;
32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED;
33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE;
34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED;
35 | import static com.google.android.material.motion.gestures.ScaleGestureRecognizer.dist;
36 | import static com.google.common.truth.Truth.assertThat;
37 | import static org.mockito.Mockito.mock;
38 | import static org.mockito.Mockito.when;
39 |
40 | @RunWith(RobolectricTestRunner.class)
41 | @Config(constants = BuildConfig.class, sdk = 21)
42 | public class ScaleGestureRecognizerTests {
43 |
44 | private static final float E = 0.0001f;
45 |
46 | private View element;
47 | private ScaleGestureRecognizer scaleGestureRecognizer;
48 |
49 | private long eventDownTime;
50 | private long eventTime;
51 |
52 | @Before
53 | public void setUp() {
54 | Context context = Robolectric.setupActivity(Activity.class);
55 | element = new View(context);
56 | scaleGestureRecognizer = new ScaleGestureRecognizer();
57 | scaleGestureRecognizer.scaleSlop = 0;
58 |
59 | eventDownTime = 0;
60 | eventTime = -16;
61 | }
62 |
63 | @Test
64 | public void defaultState() {
65 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
66 | assertThat(scaleGestureRecognizer.getElement()).isEqualTo(null);
67 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f);
68 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f);
69 | assertThat(scaleGestureRecognizer.getScale()).isWithin(0).of(1f);
70 | assertThat(scaleGestureRecognizer.getVelocity()).isWithin(0).of(0f);
71 | }
72 |
73 | @Test
74 | public void smallMovementIsNotRecognized() {
75 | scaleGestureRecognizer.scaleSlop = 24;
76 |
77 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
78 | scaleGestureRecognizer.addStateChangeListener(listener);
79 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
80 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
81 |
82 | // First finger down.
83 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
84 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
86 |
87 | // Second finger down.
88 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
89 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
91 |
92 | // Move second finger 1 pixel. Should not change the state.
93 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 101, 100));
94 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
95 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
96 | }
97 |
98 | @Test
99 | public void largeHorizontalMovementIsRecognized() {
100 | scaleGestureRecognizer.scaleSlop = 24;
101 |
102 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
103 | scaleGestureRecognizer.addStateChangeListener(listener);
104 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
105 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
106 |
107 | // First finger down.
108 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
109 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
110 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
111 |
112 | // Second finger down.
113 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
114 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
115 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
116 |
117 | // Move second finger 100 pixel right. Should change the state.
118 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100));
119 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED);
120 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
121 |
122 | // Move second finger 1 pixel. Should still change the state.
123 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 201, 100));
124 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED);
125 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
126 | }
127 |
128 | @Test
129 | public void largeVerticalMovementIsRecognized() {
130 | scaleGestureRecognizer.scaleSlop = 24;
131 |
132 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
133 | scaleGestureRecognizer.addStateChangeListener(listener);
134 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
135 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
136 |
137 | // First finger down.
138 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
139 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
140 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
141 |
142 | // Second finger down.
143 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
144 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
145 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
146 |
147 | // Move second finger 100 pixel down. Should change the state.
148 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 200));
149 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED);
150 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED});
151 |
152 | // Move second finger 1 pixel. Should still change the state.
153 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 201));
154 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED);
155 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED});
156 | }
157 |
158 | @Test
159 | public void completedGestureIsRecognized() {
160 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
161 | scaleGestureRecognizer.addStateChangeListener(listener);
162 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
163 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
164 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 100, 200, 200));
165 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100, 200, 200));
166 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 2, 0, 0, 200, 100, 200, 200));
167 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 200, 100));
168 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
169 |
170 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
171 | assertThat(listener.states.toArray())
172 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
173 | }
174 |
175 | @Test
176 | public void cancelledOneFingerGestureIsNotRecognized() {
177 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
178 | scaleGestureRecognizer.addStateChangeListener(listener);
179 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
180 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0));
181 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0));
182 |
183 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
184 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
185 | }
186 |
187 | @Test
188 | public void cancelledTwoFingerGestureIsNotRecognized() {
189 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
190 | scaleGestureRecognizer.addStateChangeListener(listener);
191 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
192 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
193 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100));
194 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_CANCEL, 0, 0, 0, 200, 100));
195 |
196 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
197 | assertThat(listener.states.toArray())
198 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE});
199 | }
200 |
201 | @Test
202 | public void noMovementIsNotRecognized() {
203 | scaleGestureRecognizer.scaleSlop = 24;
204 |
205 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
206 | scaleGestureRecognizer.addStateChangeListener(listener);
207 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
208 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
209 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100));
210 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
211 |
212 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
213 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
214 | }
215 |
216 | @Test
217 | public void irrelevantMotionIsIgnored() {
218 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
219 | scaleGestureRecognizer.addStateChangeListener(listener);
220 |
221 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0));
222 |
223 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE);
224 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE});
225 | }
226 |
227 | @Test
228 | public void oneFingerDoesNotAffectScale() {
229 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
230 | scaleGestureRecognizer.addStateChangeListener(listener);
231 |
232 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
233 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1f);
234 |
235 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 100));
236 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1f);
237 | }
238 |
239 | @Test
240 | public void multitouchHasCorrectCentroidAndScale() {
241 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener();
242 | scaleGestureRecognizer.addStateChangeListener(listener);
243 |
244 | // First finger down. Centroid is at finger location and scale is 1.
245 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
246 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
247 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
248 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1);
249 |
250 | // Second finger down. Centroid is in between fingers and scale is 1.
251 | scaleGestureRecognizer.onTouch(element,
252 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100));
253 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50);
254 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50);
255 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1);
256 |
257 | // Second finger moves [dx, dy]. Centroid moves [dx/2, dy/2], scale is calculated correctly.
258 | float dx = 505;
259 | float dy = 507;
260 | scaleGestureRecognizer.onTouch(element,
261 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy));
262 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2);
263 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2);
264 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(
265 | dist(0, 0, 100 + dx, 100 + dy) / dist(0, 0, 100, 100));
266 |
267 | // Second finger up. State is now reset.
268 | scaleGestureRecognizer.onTouch(element,
269 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy));
270 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0);
271 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0);
272 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1);
273 |
274 | assertThat(listener.states.toArray()).isEqualTo(
275 | new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE});
276 | }
277 |
278 | @Test
279 | public void nonZeroVelocity() {
280 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
281 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 10, 0));
282 |
283 | float move = 0;
284 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10 + (move += 10), 0));
285 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10 + (move += 10), 0));
286 |
287 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 10 + move, 0));
288 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0));
289 |
290 | assertThat(scaleGestureRecognizer.getVelocity()).isGreaterThan(0f);
291 | }
292 |
293 | @Test(expected = NullPointerException.class)
294 | public void crashesForNullElement() {
295 | scaleGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
296 | }
297 |
298 | @Test
299 | public void allowsSettingElementAgain() {
300 | scaleGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
301 | scaleGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0));
302 | }
303 |
304 | private MotionEvent createMotionEvent(int action, float x, float y) {
305 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0);
306 | }
307 |
308 | private MotionEvent createMultiTouchMotionEvent(
309 | int action, int index, float x0, float y0, float x1, float y1) {
310 | MotionEvent event = mock(MotionEvent.class);
311 |
312 | when(event.getDownTime()).thenReturn(eventDownTime);
313 | when(event.getEventTime()).thenReturn(eventTime += 16);
314 |
315 | when(event.getPointerCount()).thenReturn(2);
316 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
317 | when(event.getActionMasked()).thenReturn(action);
318 | when(event.getActionIndex()).thenReturn(index);
319 |
320 | when(event.getRawX()).thenReturn(x0);
321 | when(event.getRawY()).thenReturn(y0);
322 |
323 | when(event.getX(0)).thenReturn(x0);
324 | when(event.getY(0)).thenReturn(y0);
325 |
326 | when(event.getX(1)).thenReturn(x1);
327 | when(event.getY(1)).thenReturn(y1);
328 |
329 | return event;
330 | }
331 |
332 | private MotionEvent createMultiTouchMotionEvent(
333 | int action, int index, float x0, float y0, float x1, float y1, float x2, float y2) {
334 | MotionEvent event = mock(MotionEvent.class);
335 |
336 | when(event.getDownTime()).thenReturn(eventDownTime);
337 | when(event.getEventTime()).thenReturn(eventTime += 16);
338 |
339 | when(event.getPointerCount()).thenReturn(3);
340 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
341 | when(event.getActionMasked()).thenReturn(action);
342 | when(event.getActionIndex()).thenReturn(index);
343 |
344 | when(event.getRawX()).thenReturn(x0);
345 | when(event.getRawY()).thenReturn(y0);
346 |
347 | when(event.getX(0)).thenReturn(x0);
348 | when(event.getY(0)).thenReturn(y0);
349 |
350 | when(event.getX(1)).thenReturn(x1);
351 | when(event.getY(1)).thenReturn(y1);
352 |
353 | when(event.getX(2)).thenReturn(x2);
354 | when(event.getY(2)).thenReturn(y2);
355 |
356 | return event;
357 | }
358 | }
359 |
--------------------------------------------------------------------------------
/library/src/test/java/com/google/android/material/motion/gestures/TrackingGestureStateChangeListener.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.google.android.material.motion.gestures;
17 |
18 | import com.google.android.material.motion.gestures.GestureRecognizer.GestureStateChangeListener;
19 | import com.google.common.collect.Lists;
20 |
21 | import java.util.List;
22 |
23 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE;
24 |
25 | /**
26 | * A GestureStateChangeListener that tracks the state changes. Useful for tests.
27 | */
28 | public class TrackingGestureStateChangeListener implements GestureStateChangeListener {
29 | List