Skip to content

Commit

Permalink
Emit hover pointer events on Android
Browse files Browse the repository at this point in the history
Summary: Changelog: [Internal] - Add logic to handle hover events, experimental

Reviewed By: vincentriemer

Differential Revision: D35116525

fbshipit-source-id: e47a94b5f04c14caadc08ad37e4d80adc1affc15
  • Loading branch information
Luna Wei authored and facebook-github-bot committed Mar 31, 2022
1 parent 4ce3914 commit 8adedfe
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 0 deletions.
12 changes: 12 additions & 0 deletions ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onInterceptHoverEvent(MotionEvent ev) {
dispatchJSPointerEvent(ev);
return super.onInterceptHoverEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
if (shouldDispatchJSTouchEvent(ev)) {
Expand All @@ -273,6 +279,12 @@ public boolean onTouchEvent(MotionEvent ev) {
return true;
}

@Override
public boolean onHoverEvent(MotionEvent ev) {
dispatchJSPointerEvent(ev);
return super.onHoverEvent(ev);
}

@Override
protected void dispatchDraw(Canvas canvas) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import com.facebook.react.uimanager.events.PointerEventHelper;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper;
import java.util.Collections;
import java.util.List;

/**
* JSPointerDispatcher handles dispatching pointer events to JS from RootViews. If you implement
Expand All @@ -34,6 +36,13 @@ public class JSPointerDispatcher {
private final TouchEventCoalescingKeyHelper mTouchEventCoalescingKeyHelper =
new TouchEventCoalescingKeyHelper();

private static final float ONMOVE_EPSILON = 1f;

// Set globally for hover interactions, referenced for coalescing hover events
private long mHoverInteractionKey = TouchEvent.UNSET;
private List<Integer> mLastHitState = Collections.emptyList();
private final float[] mLastEventCoordinates = new float[2];

public JSPointerDispatcher(ViewGroup viewGroup) {
mRootViewGroup = viewGroup;
}
Expand Down Expand Up @@ -71,6 +80,18 @@ public void handleMotionEvent(MotionEvent motionEvent, EventDispatcher eventDisp
int action = motionEvent.getActionMasked();
int targetTag = findTargetTagAndSetCoordinates(motionEvent);

if (supportsHover) {
if (action == MotionEvent.ACTION_HOVER_MOVE) {
handleHoverEvent(motionEvent, eventDispatcher, surfaceId);
return;
}

// Ignore hover enter/exit because it's handled in `handleHoverEvent`
if (action == MotionEvent.ACTION_HOVER_EXIT || action == MotionEvent.ACTION_HOVER_ENTER) {
return;
}
}

// First down pointer
if (action == MotionEvent.ACTION_DOWN) {

Expand Down Expand Up @@ -161,6 +182,101 @@ private int findTargetTagAndSetCoordinates(MotionEvent ev) {
ev.getX(), ev.getY(), mRootViewGroup, mTargetCoordinates, null);
}

// called on hover_move motion events only
private void handleHoverEvent(
MotionEvent motionEvent, EventDispatcher eventDispatcher, int surfaceId) {

int action = motionEvent.getActionMasked();
if (action != MotionEvent.ACTION_HOVER_MOVE) {
return;
}

float x = motionEvent.getX();
float y = motionEvent.getY();

boolean qualifiedMove =
(Math.abs(mLastEventCoordinates[0] - x) > ONMOVE_EPSILON
|| Math.abs(mLastEventCoordinates[1] - y) > ONMOVE_EPSILON);

// Early exit
if (!qualifiedMove) {
return;
}

// Set the interaction key if unset, to be used as a coalescing key for hover interactions
if (mHoverInteractionKey < 0) {
mHoverInteractionKey = motionEvent.getEventTime();
mTouchEventCoalescingKeyHelper.addCoalescingKey(mHoverInteractionKey);
}

List<Integer> currHitState =
TouchTargetHelper.findTargetPathAndCoordinatesForTouch(
x, y, mRootViewGroup, mTargetCoordinates);

// If child is handling, eliminate target tags under handling child
if (mChildHandlingNativeGesture > 0) {
int index = currHitState.indexOf(mChildHandlingNativeGesture);
if (index > 0) {
currHitState.subList(0, index).clear();
}
}

int targetTag = currHitState.size() > 0 ? currHitState.get(0) : -1;
// If targetTag is empty, we should bail?
if (targetTag == -1) {
return;
}

// hitState is list ordered from inner child -> parent tag
// Traverse hitState back-to-front to find the first divergence with mLastHitState
// FIXME: this may generate incorrect events when view collapsing changes the hierarchy
int firstDivergentIndex = 0;
while (firstDivergentIndex < Math.min(currHitState.size(), mLastHitState.size())
&& currHitState
.get(currHitState.size() - 1 - firstDivergentIndex)
.equals(mLastHitState.get(mLastHitState.size() - 1 - firstDivergentIndex))) {
firstDivergentIndex++;
}

boolean hasDiverged = firstDivergentIndex < Math.max(currHitState.size(), mLastHitState.size());

// Fire all relevant enter events
if (hasDiverged) {
// If something has changed in either enter/exit, let's start a new coalescing key
mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mHoverInteractionKey);

List<Integer> enterTargetTags =
currHitState.subList(0, currHitState.size() - firstDivergentIndex);
if (enterTargetTags.size() > 0) {
for (Integer enterTargetTag : enterTargetTags) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_ENTER, surfaceId, enterTargetTag, motionEvent));
}
}

// Fire all relevant exit events
List<Integer> exitTargetTags =
mLastHitState.subList(0, mLastHitState.size() - firstDivergentIndex);
if (exitTargetTags.size() > 0) {
for (Integer exitTargetTag : exitTargetTags) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_LEAVE, surfaceId, exitTargetTag, motionEvent));
}
}
}

int coalescingKey = mTouchEventCoalescingKeyHelper.getCoalescingKey(mHoverInteractionKey);
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_MOVE, surfaceId, targetTag, motionEvent, coalescingKey));

mLastHitState = currHitState;
mLastEventCoordinates[0] = x;
mLastEventCoordinates[1] = y;
}

private void dispatchCancelEvent(
int targetTag, MotionEvent motionEvent, EventDispatcher eventDispatcher) {
// This means the gesture has already ended, via some other CANCEL or UP event. This is not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,22 @@ public boolean onTouchEvent(MotionEvent event) {
return true;
}

@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
if (mJSPointerDispatcher != null) {
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher);
}
return super.onHoverEvent(event);
}

@Override
public boolean onHoverEvent(MotionEvent event) {
if (mJSPointerDispatcher != null) {
mJSPointerDispatcher.handleMotionEvent(event, mEventDispatcher);
}
return super.onHoverEvent(event);
}

@Override
public void onChildStartedNativeGesture(MotionEvent ev) {
this.onChildStartedNativeGesture(null, ev);
Expand Down

0 comments on commit 8adedfe

Please sign in to comment.