Commit 3cedc9c7 authored by Lijin Shen's avatar Lijin Shen Committed by Commit Bot

Introduce a general swipe listener for android widgets

Add a new general swipe listener, which is derived from
SwipeRecognizer.

Bug: 1123947, 1142475
Change-Id: I1e1518300bd4dbf36b889970cb89981b0ff3a15c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2493364
Commit-Queue: Lijin Shen <lazzzis@google.com>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarMatthew Jones <mdjones@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821448}
parent fcbea1a7
...@@ -52,6 +52,8 @@ android_library("java") { ...@@ -52,6 +52,8 @@ android_library("java") {
"java/src/org/chromium/components/browser_ui/widget/displaystyle/ViewResizer.java", "java/src/org/chromium/components/browser_ui/widget/displaystyle/ViewResizer.java",
"java/src/org/chromium/components/browser_ui/widget/dragreorder/DragReorderableListAdapter.java", "java/src/org/chromium/components/browser_ui/widget/dragreorder/DragReorderableListAdapter.java",
"java/src/org/chromium/components/browser_ui/widget/dragreorder/DragStateDelegate.java", "java/src/org/chromium/components/browser_ui/widget/dragreorder/DragStateDelegate.java",
"java/src/org/chromium/components/browser_ui/widget/gesture/EmptySwipeHandler.java",
"java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListener.java",
"java/src/org/chromium/components/browser_ui/widget/highlight/PulseDrawable.java", "java/src/org/chromium/components/browser_ui/widget/highlight/PulseDrawable.java",
"java/src/org/chromium/components/browser_ui/widget/highlight/PulseInterpolator.java", "java/src/org/chromium/components/browser_ui/widget/highlight/PulseInterpolator.java",
"java/src/org/chromium/components/browser_ui/widget/highlight/ViewHighlighter.java", "java/src/org/chromium/components/browser_ui/widget/highlight/ViewHighlighter.java",
...@@ -308,6 +310,7 @@ java_library("junit") { ...@@ -308,6 +310,7 @@ java_library("junit") {
"java/src/org/chromium/components/browser_ui/widget/CompositeTouchDelegateUnitTest.java", "java/src/org/chromium/components/browser_ui/widget/CompositeTouchDelegateUnitTest.java",
"java/src/org/chromium/components/browser_ui/widget/InsetObserverViewTest.java", "java/src/org/chromium/components/browser_ui/widget/InsetObserverViewTest.java",
"java/src/org/chromium/components/browser_ui/widget/LoadingViewTest.java", "java/src/org/chromium/components/browser_ui/widget/LoadingViewTest.java",
"java/src/org/chromium/components/browser_ui/widget/gesture/SwipeGestureListenerTest.java",
"java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectionDelegateTest.java", "java/src/org/chromium/components/browser_ui/widget/selectable_list/SelectionDelegateTest.java",
] ]
deps = [ deps = [
......
...@@ -92,6 +92,10 @@ ...@@ -92,6 +92,10 @@
<!-- Default list dimensions --> <!-- Default list dimensions -->
<dimen name="default_list_row_padding">16dp</dimen> <dimen name="default_list_row_padding">16dp</dimen>
<!-- Swipe gesture listener dimensions -->
<dimen name="swipe_vertical_drag_threshold">5dp</dimen>
<dimen name="swipe_horizontal_drag_threshold">10dp</dimen>
<!-- Query tile dimensions --> <!-- Query tile dimensions -->
<dimen name="tile_ideal_width">80dp</dimen> <dimen name="tile_ideal_width">80dp</dimen>
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget.gesture;
import android.view.MotionEvent;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
/**
* An empty implementation of {@link SwipeHandler}.
*/
public class EmptySwipeHandler implements SwipeHandler {
@Override
public void onSwipeStarted(int direction, MotionEvent ev) {}
@Override
public void onSwipeUpdated(MotionEvent start, MotionEvent current) {}
@Override
public void onSwipeFinished(MotionEvent end) {}
@Override
public void onFling(int direction, MotionEvent start, MotionEvent end) {}
@Override
public boolean isSwipeEnabled(int direction) {
return true;
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget.gesture;
import android.content.Context;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.components.browser_ui.widget.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Recognizes directional swipe gestures using supplied {@link MotionEvent}s.
* The {@link SwipeHandler} callbacks will notify users when a particular gesture
* has occurred, if the handler supports the particular direction of the swipe.
*
* To use this class:
* <ul>
* <li>Create an instance of the {@link SwipeGestureListener} for your View
* <li>In the View#onTouchEvent(MotionEvent) method ensure you call
* {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
* will be executed when the gestures occur.
* <li>Before trying to recognize the gesture, the class will call
* {@link #shouldRecognizeSwipe(MotionEvent, MotionEvent)}, which allows
* ignoring swipe recognition based on the MotionEvents.
* <li>Once a swipe gesture is detected, the class will check if the the direction
* is supported by calling {@link SwipeHandler#isSwipeEnabled}.
* </ul>
* Internally, this class uses a {@link GestureDetector} to recognize swipe gestures.
* For convenience, this class also extends {@link SimpleOnGestureListener} which
* is passed to the {@link GestureDetector}. This means that this class can also be
* used to detect simple gestures defined in {@link GestureDetector}.
*/
public class SwipeGestureListener extends SimpleOnGestureListener {
@IntDef({ScrollDirection.LEFT, ScrollDirection.RIGHT, ScrollDirection.UP, ScrollDirection.DOWN})
@Retention(RetentionPolicy.SOURCE)
public @interface ScrollDirection {
int UNKNOWN = 0;
int LEFT = 1;
int RIGHT = 2;
int UP = 3;
int DOWN = 4;
}
public interface SwipeHandler {
/**
* @param direction The {@link ScrollDirection} representing the swipe direction.
* @param ev The first down motion event triggering the swipe.
*/
void onSwipeStarted(@ScrollDirection int direction, MotionEvent ev);
/**
* @param start The first down motion event triggering the swipe.
* @param current The move motion event triggering the current swipe.
*/
void onSwipeUpdated(MotionEvent start, MotionEvent current);
/**
* @param end The last motion event canceling the swipe.
*/
void onSwipeFinished(MotionEvent end);
/**
* @param direction The {@link ScrollDirection} representing the swipe direction.
* @param start The first down motion event triggering the swipe.
* @param end The last motion event canceling the swipe.
*/
void onFling(@ScrollDirection int direction, MotionEvent start, MotionEvent end);
/**
* @param direction The direction of the on-going swipe.
* @return False if this direction should be ignored.
*/
boolean isSwipeEnabled(@ScrollDirection int direction);
}
/**
* The internal {@link GestureDetector} used to recognize swipe gestures.
*/
private final GestureDetector mGestureDetector;
@ScrollDirection
private int mDirection = ScrollDirection.UNKNOWN;
private final SwipeHandler mHandler;
/**
* The threshold for a vertical swipe gesture, in px.
*/
private final int mSwipeVerticalDragThreshold;
/**
* The threshold for a horizontal swipe gesture, in px.
*/
private final int mSwipeHorizontalDragThreshold;
/**
* @param context The {@link Context}.
* @param handler The {@link SwipeHandler} to handle the swipe events.
*/
public SwipeGestureListener(Context context, SwipeHandler handler) {
mGestureDetector = new GestureDetector(context, this, ThreadUtils.getUiThreadHandler());
mSwipeVerticalDragThreshold = context.getResources().getDimensionPixelOffset(
R.dimen.swipe_vertical_drag_threshold);
mSwipeHorizontalDragThreshold = context.getResources().getDimensionPixelOffset(
R.dimen.swipe_horizontal_drag_threshold);
mHandler = handler;
}
@VisibleForTesting
SwipeGestureListener(
Context context, SwipeHandler handler, int verticalThreshold, int horizontalThreshold) {
mGestureDetector = new GestureDetector(context, this, ThreadUtils.getUiThreadHandler());
mSwipeVerticalDragThreshold = verticalThreshold;
mSwipeHorizontalDragThreshold = horizontalThreshold;
mHandler = handler;
}
/**
* Analyzes the given motion event by feeding it to a {@link GestureDetector}. Depending on the
* results from the onScroll() and onFling() methods, it triggers the appropriate callbacks
* on the {@link SwipeHandler} supplied.
*
* @param event The {@link MotionEvent}.
* @return Whether the event has been consumed.
*/
public boolean onTouchEvent(MotionEvent event) {
boolean consumed = mGestureDetector.onTouchEvent(event);
if (mHandler != null) {
final int action = event.getAction();
if ((action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)
&& mDirection != ScrollDirection.UNKNOWN) {
mHandler.onSwipeFinished(event);
mDirection = ScrollDirection.UNKNOWN;
consumed = true;
}
}
return consumed;
}
/**
* Checks whether the swipe gestures should be recognized. If this method returns false,
* then the whole swipe recognition process will be ignored. By default this method returns
* true. If a more complex logic is needed, this method should be overridden.
*
* @param e1 The first {@link MotionEvent}.
* @param e2 The second {@link MotionEvent}.
* @return Whether the swipe gestures should be recognized
*/
public boolean shouldRecognizeSwipe(MotionEvent e1, MotionEvent e2) {
return true;
}
// ============================================================================================
// Swipe Recognition Helpers
// ============================================================================================
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mHandler == null || e1 == null || e2 == null) return false;
if (mDirection == ScrollDirection.UNKNOWN && shouldRecognizeSwipe(e1, e2)) {
float tx = (e2.getRawX() - e1.getRawX());
float ty = (e2.getRawY() - e1.getRawY());
@ScrollDirection
int direction = ScrollDirection.UNKNOWN;
if (Math.abs(tx) < mSwipeHorizontalDragThreshold
&& Math.abs(ty) < mSwipeVerticalDragThreshold) {
return false;
}
if (Math.abs(tx) > Math.abs(ty)) {
direction = tx > 0.f ? ScrollDirection.RIGHT : ScrollDirection.LEFT;
} else {
direction = ty > 0.f ? ScrollDirection.DOWN : ScrollDirection.UP;
}
if (direction != ScrollDirection.UNKNOWN && mHandler.isSwipeEnabled(direction)) {
mDirection = direction;
mHandler.onSwipeStarted(direction, e1);
}
}
if (mDirection != ScrollDirection.UNKNOWN) {
mHandler.onSwipeUpdated(e1, e2);
return true;
}
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (mHandler == null) return false;
if (mDirection != ScrollDirection.UNKNOWN) {
mHandler.onFling(mDirection, e1, e2);
return true;
}
return false;
}
}
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.browser_ui.widget.gesture;
import static org.mockito.ArgumentMatchers.anyInt;
import android.view.MotionEvent;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection;
import java.util.ArrayList;
import java.util.List;
/**
* The Unittest of {@link SwipeGestureListener}.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SwipeGestureListenerTest {
private SwipeGestureListener mListener;
@Mock
private EmptySwipeHandler mHandler;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mListener = new SwipeGestureListener(null, mHandler, 1, 1);
}
@Test
@SmallTest
public void testOnSwipeLeftDirection() {
testSwipeByGivenDirection(
ScrollDirection.LEFT, buildEventStream(100.0f, 100.0f, -5.0f, -3.0f, 10));
}
@Test
@SmallTest
public void testOnSwipeRightDirection() {
testSwipeByGivenDirection(
ScrollDirection.RIGHT, buildEventStream(100.0f, 100.0f, 5.0f, -3.0f, 10));
}
@Test
@SmallTest
public void testOnSwipeUpDirection() {
testSwipeByGivenDirection(
ScrollDirection.UP, buildEventStream(100.0f, 100.0f, 3.0f, -5.0f, 10));
}
@Test
@SmallTest
public void testOnSwipeDownDirection() {
testSwipeByGivenDirection(
ScrollDirection.DOWN, buildEventStream(100.0f, 100.0f, 2.0f, 5.0f, 10));
}
private void testSwipeByGivenDirection(int expectedDirection, List<MotionEvent> eventStream) {
Mockito.when(mHandler.isSwipeEnabled(anyInt())).thenReturn(true);
for (MotionEvent event : eventStream) {
mListener.onTouchEvent(event);
}
ArgumentCaptor<MotionEvent> argumentCaptor = ArgumentCaptor.forClass(MotionEvent.class);
Mockito.verify(mHandler).onSwipeStarted(
Mockito.eq(expectedDirection), argumentCaptor.capture());
Assert.assertEquals(argumentCaptor.getValue().getRawX(), eventStream.get(0).getRawX(), 0.1);
Mockito.verify(mHandler).onSwipeFinished(argumentCaptor.capture());
Assert.assertEquals(argumentCaptor.getValue().getRawX(),
eventStream.get(eventStream.size() - 1).getRawX(), 0.1);
}
private List<MotionEvent> buildEventStream(
float startX, float startY, float offsetX, float offSetY, int count) {
List<MotionEvent> list = new ArrayList<>();
list.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, startX, startY, 0));
for (int i = 1; i < count - 1; i++) {
list.add(MotionEvent.obtain(
0, 0, MotionEvent.ACTION_MOVE, startX + i * offsetX, startY + i * offSetY, 0));
}
list.add(MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, startX + (count - 1) * offsetX,
startY + (count - 1) * offSetY, 0));
return list;
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment