| Index: chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/eventfilter/ContextualSearchEventFilter.java
|
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/eventfilter/ContextualSearchEventFilter.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/eventfilter/ContextualSearchEventFilter.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..f85861da782d3d42fee2f8813af5b14589f4e52d
|
| --- /dev/null
|
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/eventfilter/ContextualSearchEventFilter.java
|
| @@ -0,0 +1,524 @@
|
| +// Copyright 2015 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.chrome.browser.compositor.layouts.eventfilter;
|
| +
|
| +import android.content.Context;
|
| +import android.view.GestureDetector;
|
| +import android.view.MotionEvent;
|
| +import android.view.ViewConfiguration;
|
| +
|
| +import org.chromium.base.VisibleForTesting;
|
| +import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
|
| +import org.chromium.chrome.browser.contextualsearch.ContextualSearchManagementDelegate;
|
| +
|
| +import java.util.ArrayList;
|
| +
|
| +/**
|
| + * The {@link GestureEventFilter} used when Contextual Search Layout is being shown. It filters
|
| + * events that happen in the Search Content View area and propagates them to the appropriate
|
| + * Content View Core via {@link EventFilterHost}. Events that happen outside that area are
|
| + * propagated to the {@code ContextualSearchLayout} via {@code LayoutManagerPhone}.
|
| + */
|
| +public class ContextualSearchEventFilter extends GestureEventFilter {
|
| +
|
| + /**
|
| + * The targets that can handle MotionEvents.
|
| + */
|
| + private enum EventTarget {
|
| + UNDETERMINED,
|
| + SEARCH_PANEL,
|
| + SEARCH_CONTENT_VIEW
|
| + }
|
| +
|
| + /**
|
| + * The direction of the gesture.
|
| + */
|
| + private enum GestureOrientation {
|
| + UNDETERMINED,
|
| + HORIZONTAL,
|
| + VERTICAL
|
| + }
|
| +
|
| + /**
|
| + * The boost factor that can be applied to prioritize vertical movements over horizontal ones.
|
| + */
|
| + private static final float VERTICAL_DETERMINATION_BOOST = 1.25f;
|
| +
|
| + /**
|
| + * The shared state of the UI.
|
| + */
|
| + private ContextualSearchPanel mSearchPanel;
|
| +
|
| + /**
|
| + * The delegate to talk to ContextualSearchManager.
|
| + */
|
| + private ContextualSearchManagementDelegate mManagementDelegate;
|
| +
|
| + /**
|
| + * The {@link GestureDetector} used to distinguish tap and scroll gestures.
|
| + */
|
| + private final GestureDetector mGestureDetector;
|
| +
|
| + /**
|
| + * The target to propagate events to.
|
| + */
|
| + private EventTarget mEventTarget;
|
| +
|
| + /**
|
| + * Whether the code is in the middle of the process of determining the event target.
|
| + */
|
| + private boolean mIsDeterminingEventTarget;
|
| +
|
| + /**
|
| + * Whether the event target has been determined.
|
| + */
|
| + private boolean mHasDeterminedEventTarget;
|
| +
|
| + /**
|
| + * The previous target the events were propagated to.
|
| + */
|
| + private EventTarget mPreviousEventTarget;
|
| +
|
| + /**
|
| + * Whether the event target has changed since the last touch event.
|
| + */
|
| + private boolean mHasChangedEventTarget;
|
| +
|
| + /**
|
| + * Whether the event target might change. This will be true in cases we know the overscroll
|
| + * and/or underscroll might happen, which means we'll have to constantly monitor the event
|
| + * targets in order to determine the exact moment the target has changed.
|
| + */
|
| + private boolean mMayChangeEventTarget;
|
| +
|
| + /**
|
| + * Whether the gesture orientation has been determined.
|
| + */
|
| + private boolean mHasDeterminedGestureOrientation;
|
| +
|
| + /**
|
| + * The current gesture orientation.
|
| + */
|
| + private GestureOrientation mGestureOrientation;
|
| +
|
| + /**
|
| + * Whether the events are being recorded.
|
| + */
|
| + private boolean mIsRecordingEvents;
|
| +
|
| + /**
|
| + * Whether the ACTION_DOWN that initiated the MotionEvent's stream was synthetic.
|
| + */
|
| + private boolean mWasActionDownEventSynthetic;
|
| +
|
| + /**
|
| + * The X coordinate of the synthetic ACTION_DOWN MotionEvent.
|
| + */
|
| + private float mSyntheticActionDownX;
|
| +
|
| + /**
|
| + * The Y coordinate of the synthetic ACTION_DOWN MotionEvent.
|
| + */
|
| + private float mSyntheticActionDownY;
|
| +
|
| + /**
|
| + * The list of recorded events.
|
| + */
|
| + private final ArrayList<MotionEvent> mRecordedEvents = new ArrayList<MotionEvent>();
|
| +
|
| + /**
|
| + * The initial Y position of the current gesture.
|
| + */
|
| + private float mInitialEventY;
|
| +
|
| + /**
|
| + * The square of ViewConfiguration.getScaledTouchSlop() in pixels used to calculate whether
|
| + * the finger has moved beyond the established threshold.
|
| + */
|
| + private final float mTouchSlopSquarePx;
|
| +
|
| + /**
|
| + * Creates a {@link GestureEventFilter} with offset touch events.
|
| + */
|
| + public ContextualSearchEventFilter(Context context, EventFilterHost host,
|
| + GestureHandler handler, ContextualSearchPanel contextualSearchPanel) {
|
| + super(context, host, handler, false, false);
|
| +
|
| + mGestureDetector = new GestureDetector(context, new InternalGestureDetector());
|
| + mSearchPanel = contextualSearchPanel;
|
| +
|
| + // Store the square of the platform touch slop in pixels to use in the scroll detection.
|
| + // See {@link ContextualSearchEventFilter#isDistanceGreaterThanTouchSlop}.
|
| + float touchSlopPx = ViewConfiguration.get(context).getScaledTouchSlop();
|
| + mTouchSlopSquarePx = touchSlopPx * touchSlopPx;
|
| +
|
| + reset();
|
| + }
|
| +
|
| + /**
|
| + * Sets the {@code ContextualSearchManagementDelegate} associated with this Event Filter.
|
| + * @param delegate The {@code ContextualSearchManagementDelegate}.
|
| + */
|
| + public void setManagementDelegate(ContextualSearchManagementDelegate delegate) {
|
| + mManagementDelegate = delegate;
|
| + }
|
| +
|
| + /**
|
| + * Gets the Search Content View's vertical scroll position. If the Search Content View
|
| + * is not available it returns -1.
|
| + * @return The Search Content View scroll position.
|
| + */
|
| + @VisibleForTesting
|
| + protected float getSearchContentViewVerticalScroll() {
|
| + return mManagementDelegate.getSearchContentViewVerticalScroll();
|
| + }
|
| +
|
| + @Override
|
| + public boolean onTouchEventInternal(MotionEvent e) {
|
| + final int action = e.getActionMasked();
|
| +
|
| + if (action == MotionEvent.ACTION_POINTER_DOWN
|
| + && e.getPointerCount() == 2
|
| + && !mSearchPanel.isMaximized()) {
|
| + // We don't want the Search Content View's zoom level to change when the Search Panel
|
| + // is expanded (that is, not maximized) so we'll forward the events to Panel to
|
| + // prevent it from happening.
|
| + setEventTarget(EventTarget.SEARCH_PANEL);
|
| + } else if (!mIsDeterminingEventTarget && action == MotionEvent.ACTION_DOWN) {
|
| + mInitialEventY = e.getY();
|
| + if (mSearchPanel.isYCoordinateInsideSearchContentView(mInitialEventY * mPxToDp)) {
|
| + // If the DOWN event happened inside the Search Content View, we'll need
|
| + // to wait until the user has moved the finger beyond a certain threshold,
|
| + // so we can determine the gesture's orientation and consequently be able
|
| + // to tell if the Content View will accept the gesture.
|
| + mIsDeterminingEventTarget = true;
|
| + mMayChangeEventTarget = true;
|
| + } else {
|
| + // If the DOWN event happened outside the Search Content View, then we know
|
| + // that the Search Panel will start handling the event right away.
|
| + setEventTarget(EventTarget.SEARCH_PANEL);
|
| + mMayChangeEventTarget = false;
|
| + }
|
| + }
|
| +
|
| + // Send the event to the GestureDetector so we can distinguish between scroll and tap.
|
| + mGestureDetector.onTouchEvent(e);
|
| +
|
| + if (mHasDeterminedEventTarget) {
|
| + // If the event target has been determined, resume pending events, then propagate
|
| + // the current event to the appropriate target.
|
| + resumeAndPropagateEvent(e);
|
| + } else {
|
| + // If the event target has not been determined, we need to record a copy of the event
|
| + // until we are able to determine the event target.
|
| + MotionEvent event = MotionEvent.obtain(e);
|
| + mRecordedEvents.add(event);
|
| + mIsRecordingEvents = true;
|
| + }
|
| +
|
| + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
| + reset();
|
| + }
|
| +
|
| + return true;
|
| + }
|
| +
|
| + /**
|
| + * Resets the current and previous {@link EventTarget} as well the {@link GestureOrientation}
|
| + * to the UNDETERMINED state.
|
| + */
|
| + private void reset() {
|
| + mEventTarget = EventTarget.UNDETERMINED;
|
| + mIsDeterminingEventTarget = false;
|
| + mHasDeterminedEventTarget = false;
|
| +
|
| + mPreviousEventTarget = EventTarget.UNDETERMINED;
|
| + mHasChangedEventTarget = false;
|
| + mMayChangeEventTarget = false;
|
| +
|
| + mWasActionDownEventSynthetic = false;
|
| +
|
| + mGestureOrientation = GestureOrientation.UNDETERMINED;
|
| + mHasDeterminedGestureOrientation = false;
|
| + }
|
| +
|
| + /**
|
| + * Resumes pending events then propagates the given event to the current {@link EventTarget}.
|
| + *
|
| + * Resuming events might consist in simply propagating previously recorded events if the
|
| + * EventTarget was UNDETERMINED when the gesture started.
|
| + *
|
| + * For the case where the EventTarget has changed during the course of the gesture, we'll
|
| + * need to simulate a gesture end in the previous target (by simulating an ACTION_CANCEL
|
| + * event) and a gesture start in the new target (by simulating an ACTION_DOWN event).
|
| + *
|
| + * @param e The {@link MotionEvent} to be propagated after resuming the pending events.
|
| + */
|
| + private void resumeAndPropagateEvent(MotionEvent e) {
|
| + if (mIsRecordingEvents) {
|
| + resumeRecordedEvents();
|
| + }
|
| +
|
| + if (mHasChangedEventTarget) {
|
| + // TODO(pedrosimonetti): handle cases with multiple pointers.
|
| + float y = e.getY();
|
| + // If the event target has changed since the beginning of the gesture, then we need
|
| + // to send a ACTION_CANCEL to the previous event target to make sure it no longer
|
| + // expects events.
|
| + propagateAndRecycleEvent(createEvent(e, MotionEvent.ACTION_CANCEL, y),
|
| + mPreviousEventTarget);
|
| +
|
| + // Similarly we need to send an ACTION_DOWN to the new event target so subsequent
|
| + // events can be analyzed properly by the Gesture Detector.
|
| + MotionEvent syntheticActionDownEvent = createEvent(e, MotionEvent.ACTION_DOWN, y);
|
| +
|
| + // Store the synthetic ACTION_DOWN coordinates to prevent unwanted taps from
|
| + // happening. See {@link ContextualSearchEventFilter#propagateEventToSearchContentView}.
|
| + mWasActionDownEventSynthetic = true;
|
| + mSyntheticActionDownX = syntheticActionDownEvent.getX();
|
| + mSyntheticActionDownY = syntheticActionDownEvent.getY()
|
| + - mSearchPanel.getSearchContentViewOffsetY() / mPxToDp;
|
| +
|
| + propagateAndRecycleEvent(syntheticActionDownEvent, mEventTarget);
|
| +
|
| + mHasChangedEventTarget = false;
|
| + }
|
| +
|
| + propagateEvent(e, mEventTarget);
|
| + }
|
| +
|
| + /**
|
| + * Resumes recorded events by propagating all of them to the current {@link EventTarget}.
|
| + */
|
| + private void resumeRecordedEvents() {
|
| + for (int i = 0, size = mRecordedEvents.size(); i < size; i++) {
|
| + propagateAndRecycleEvent(mRecordedEvents.get(i), mEventTarget);
|
| + }
|
| +
|
| + mRecordedEvents.clear();
|
| + mIsRecordingEvents = false;
|
| + }
|
| +
|
| + /**
|
| + * Propagates the given {@link MotionEvent} to the given {@link EventTarget}, recycling it
|
| + * afterwards. This is intended for synthetic events only, those create by
|
| + * {@link MotionEvent#obtain} or the helper {@link ContextualSearchEventFilter#createEvent}.
|
| + * @param e The {@link MotionEvent} to be propagated.
|
| + * @param target The {@link EventTarget} to propagate events to.
|
| + */
|
| + private void propagateAndRecycleEvent(MotionEvent e, EventTarget target) {
|
| + propagateEvent(e, target);
|
| + e.recycle();
|
| + }
|
| +
|
| + /**
|
| + * Propagates the given {@link MotionEvent} to the given {@link EventTarget}.
|
| + * @param e The {@link MotionEvent} to be propagated.
|
| + * @param target The {@link EventTarget} to propagate events to.
|
| + */
|
| + private void propagateEvent(MotionEvent e, EventTarget target) {
|
| + if (target == EventTarget.SEARCH_PANEL) {
|
| + super.onTouchEventInternal(e);
|
| + } else if (target == EventTarget.SEARCH_CONTENT_VIEW) {
|
| + propagateEventToSearchContentView(e);
|
| + }
|
| + }
|
| +
|
| + /**
|
| + * Propagates the given {@link MotionEvent} to the Search Content View.
|
| + * @param e The {@link MotionEvent} to be propagated.
|
| + */
|
| + @VisibleForTesting
|
| + protected void propagateEventToSearchContentView(MotionEvent e) {
|
| + MotionEvent event = e;
|
| + boolean isSyntheticEvent = false;
|
| + if (mGestureOrientation == GestureOrientation.HORIZONTAL
|
| + && !mSearchPanel.isMaximized()) {
|
| + // Lock horizontal motion, ignoring all vertical changes, when the Panel is not
|
| + // maximized. This is to prevent the Search Result Page from scrolling when
|
| + // side swiping on the expanded Panel.
|
| + event = createEvent(e, e.getAction(), mInitialEventY);
|
| + isSyntheticEvent = true;
|
| + }
|
| +
|
| + int action = event.getActionMasked();
|
| + float searchContentViewOffsetYPx = mSearchPanel.getSearchContentViewOffsetY() / mPxToDp;
|
| +
|
| + // Adjust the offset to be relative to the Search Contents View.
|
| + event.offsetLocation(0.f, -searchContentViewOffsetYPx);
|
| +
|
| + boolean wasEventCanceled = false;
|
| + if (mWasActionDownEventSynthetic && action == MotionEvent.ACTION_UP) {
|
| + float deltaX = event.getX() - mSyntheticActionDownX;
|
| + float deltaY = event.getY() - mSyntheticActionDownY;
|
| + // NOTE(pedrosimonetti): If the ACTION_DOWN event was synthetic and the distance
|
| + // between it and the ACTION_UP event was short, then we should synthesize an
|
| + // ACTION_CANCEL event to prevent a Tap gesture from being triggered on the Search
|
| + // Content View. See crbug.com/408654
|
| + if (!isDistanceGreaterThanTouchSlop(deltaX, deltaY)) {
|
| + event.setAction(MotionEvent.ACTION_CANCEL);
|
| + mHost.propagateEvent(event);
|
| + wasEventCanceled = true;
|
| + }
|
| + } else if (action == MotionEvent.ACTION_DOWN) {
|
| + mSearchPanel.onTouchSearchContentViewAck();
|
| + }
|
| +
|
| + // Propagate the event to the appropriate view
|
| + if (!wasEventCanceled) mHost.propagateEvent(event);
|
| +
|
| + // Synthetic events should be recycled.
|
| + if (isSyntheticEvent) event.recycle();
|
| + }
|
| +
|
| + /**
|
| + * Creates a {@link MotionEvent} inheriting from a given |e| event.
|
| + * @param e The {@link MotionEvent} to inherit properties from.
|
| + * @param action The MotionEvent's Action to be used.
|
| + * @param y The y coordinate to be used.
|
| + * @return A new {@link MotionEvent}.
|
| + */
|
| + private MotionEvent createEvent(MotionEvent e, int action, float y) {
|
| + return MotionEvent.obtain(
|
| + e.getDownTime(),
|
| + e.getEventTime(),
|
| + action,
|
| + e.getX(),
|
| + y,
|
| + e.getMetaState());
|
| + }
|
| +
|
| + /**
|
| + * Handles the tap event, determining the event target.
|
| + * @param e The tap {@link MotionEvent}.
|
| + * @return Whether the event has been consumed.
|
| + */
|
| + private boolean handleSingleTapUp(MotionEvent e) {
|
| + setEventTarget(mSearchPanel.isYCoordinateInsideSearchContentView(e.getY() * mPxToDp)
|
| + ? EventTarget.SEARCH_CONTENT_VIEW : EventTarget.SEARCH_PANEL);
|
| +
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Handles the scroll event, determining the gesture orientation and event target,
|
| + * when appropriate.
|
| + * @param e1 The first down {@link MotionEvent} that started the scrolling.
|
| + * @param e2 The move {@link MotionEvent} that triggered the current scroll.
|
| + * @param distanceY The distance along the Y axis that has been scrolled since the last call
|
| + * to handleScroll.
|
| + * @return Whether the event has been consumed.
|
| + */
|
| + private boolean handleScroll(MotionEvent e1, MotionEvent e2, float distanceY) {
|
| + // Only determines the gesture orientation if it hasn't been determined yet,
|
| + // affectively "locking" the orientation once the gesture has started.
|
| + if (!mHasDeterminedGestureOrientation && isDistanceGreaterThanTouchSlop(e1, e2)) {
|
| + determineGestureOrientation(e1, e2);
|
| + }
|
| +
|
| + // Only determines the event target after determining the gesture orientation and
|
| + // if it hasn't been determined yet or if changing the event target during the
|
| + // middle of the gesture is supported. This will allow a smooth transition from
|
| + // swiping the Panel and scrolling the Search Content View.
|
| + if (mHasDeterminedGestureOrientation
|
| + && (!mHasDeterminedEventTarget || mMayChangeEventTarget)) {
|
| + determineEventTarget(distanceY);
|
| + }
|
| +
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Determines the gesture orientation.
|
| + * @param e1 The first down {@link MotionEvent} that started the scrolling.
|
| + * @param e2 The move {@link MotionEvent} that triggered the current scroll.
|
| + */
|
| + private void determineGestureOrientation(MotionEvent e1, MotionEvent e2) {
|
| + float deltaX = Math.abs(e2.getX() - e1.getX());
|
| + float deltaY = Math.abs(e2.getY() - e1.getY());
|
| + mGestureOrientation = deltaY * VERTICAL_DETERMINATION_BOOST > deltaX
|
| + ? GestureOrientation.VERTICAL : GestureOrientation.HORIZONTAL;
|
| + mHasDeterminedGestureOrientation = true;
|
| + }
|
| +
|
| + /**
|
| + * Determines the target to propagate events to. This will not only update the
|
| + * {@code mEventTarget} but also save the previous target and determine whether the
|
| + * target has changed.
|
| + * @param distanceY The distance along the Y axis that has been scrolled since the last call
|
| + * to handleScroll.
|
| + */
|
| + private void determineEventTarget(float distanceY) {
|
| + boolean isVertical = mGestureOrientation == GestureOrientation.VERTICAL;
|
| +
|
| + boolean shouldPropagateEventsToSearchPanel;
|
| + if (mSearchPanel.isMaximized()) {
|
| + // Allow overscroll in the Search Content View to move the Search Panel instead
|
| + // of scrolling the Search Result Page.
|
| + boolean isMovingDown = distanceY < 0;
|
| + shouldPropagateEventsToSearchPanel = isVertical
|
| + && isMovingDown
|
| + && getSearchContentViewVerticalScroll() == 0;
|
| + } else {
|
| + // Only allow horizontal movements to be propagated to the Search Content View
|
| + // when the Panel is expanded (that is, not maximized).
|
| + shouldPropagateEventsToSearchPanel = isVertical;
|
| + }
|
| +
|
| + mPreviousEventTarget = mEventTarget;
|
| + setEventTarget(shouldPropagateEventsToSearchPanel
|
| + ? EventTarget.SEARCH_PANEL : EventTarget.SEARCH_CONTENT_VIEW);
|
| +
|
| + mHasChangedEventTarget = mEventTarget != mPreviousEventTarget
|
| + && mPreviousEventTarget != EventTarget.UNDETERMINED;
|
| + }
|
| +
|
| + /**
|
| + * Sets the {@link EventTarget}.
|
| + * @param target The {@link EventTarget} to be set.
|
| + */
|
| + private void setEventTarget(EventTarget target) {
|
| + mEventTarget = target;
|
| +
|
| + mIsDeterminingEventTarget = false;
|
| + mHasDeterminedEventTarget = true;
|
| + }
|
| +
|
| + /**
|
| + * @param e1 The first down {@link MotionEvent} that started the scrolling.
|
| + * @param e2 The move {@link MotionEvent} that triggered the current scroll.
|
| + * @return Whether the distance is greater than the touch slop threshold.
|
| + */
|
| + private boolean isDistanceGreaterThanTouchSlop(MotionEvent e1, MotionEvent e2) {
|
| + float deltaX = e2.getX() - e1.getX();
|
| + float deltaY = e2.getY() - e1.getY();
|
| + // Check if the distance between the events |e1| and |e2| is greater than the touch slop.
|
| + return isDistanceGreaterThanTouchSlop(deltaX, deltaY);
|
| + }
|
| +
|
| + /**
|
| + * @param deltaX The delta X in pixels.
|
| + * @param deltaY The delta Y in pixels.
|
| + * @return Whether the distance is greater than the touch slop threshold.
|
| + */
|
| + private boolean isDistanceGreaterThanTouchSlop(float deltaX, float deltaY) {
|
| + return deltaX * deltaX + deltaY * deltaY > mTouchSlopSquarePx;
|
| + }
|
| +
|
| + /**
|
| + * Internal GestureDetector class that is responsible for determining the event target.
|
| + */
|
| + private class InternalGestureDetector extends GestureDetector.SimpleOnGestureListener {
|
| + @Override
|
| + public boolean onSingleTapUp(MotionEvent e) {
|
| + return handleSingleTapUp(e);
|
| + }
|
| +
|
| + @Override
|
| + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
| + return handleScroll(e1, e2, distanceY);
|
| + }
|
| + }
|
| +}
|
|
|