Add support for more Scroll Events to Android

Summary:
public
This adds support for
onScrollBeginDrag/End
onMomentumScrolBegin/End

Reviewed By: astreet

Differential Revision: D2739035

fb-gh-sync-id: 2a49d1df54e5f5cd82008bdb0ffde0881ba39aff
This commit is contained in:
Dave Miller
2015-12-16 00:47:43 -08:00
committed by facebook-github-bot-7
parent 1fabd86048
commit fcf0431d25
10 changed files with 214 additions and 16 deletions

View File

@@ -19,6 +19,7 @@ import com.facebook.react.common.annotations.VisibleForTesting;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
/**
* Wraps {@link RecyclerView} providing interface similar to `ScrollView.js` where each children
@@ -311,6 +312,7 @@ public class RecyclerViewBackedScrollView extends RecyclerView {
.dispatchEvent(ScrollEvent.obtain(
getId(),
SystemClock.uptimeMillis(),
ScrollEventType.SCROLL,
0, /* offsetX = 0, horizontal scrolling only */
calculateAbsoluteOffset(),
getWidth(),

View File

@@ -14,7 +14,7 @@ import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.views.scroll.ReactScrollViewCommandHelper;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
/**
* View manager for {@link RecyclerViewBackedScrollView}.
@@ -91,7 +91,7 @@ public class RecyclerViewBackedScrollViewManager extends
public @Nullable
Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.builder()
.put(ScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onScroll"))
.put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll"))
.put(
ContentSizeChangeEvent.EVENT_NAME,
MapBuilder.of("registrationName", "onContentSizeChange"))

View File

@@ -23,10 +23,19 @@ public class ReactHorizontalScrollView extends HorizontalScrollView {
private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper();
private boolean mSendMomentumEvents;
private boolean mDragging;
private boolean mFlinging;
private boolean mDoneFlinging;
public ReactHorizontalScrollView(Context context) {
super(context);
}
public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
@@ -47,7 +56,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView {
super.onScrollChanged(x, y, oldX, oldY);
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
ReactScrollViewHelper.emitScrollEvent(this, x, y);
if (mFlinging) {
mDoneFlinging = false;
}
ReactScrollViewHelper.emitScrollEvent(this);
}
}
@@ -55,9 +67,43 @@ public class ReactHorizontalScrollView extends HorizontalScrollView {
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
ReactScrollViewHelper.emitScrollEndDragEvent(this);
mDragging = false;
}
return super.onTouchEvent(ev);
}
@Override
public void fling(int velocityX) {
super.fling(velocityX);
if (mSendMomentumEvents) {
mFlinging = true;
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
Runnable r = new Runnable() {
@Override
public void run() {
if (mDoneFlinging) {
mFlinging = false;
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactHorizontalScrollView.this);
} else {
mDoneFlinging = true;
ReactHorizontalScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
};
postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
}

View File

@@ -43,6 +43,19 @@ public class ReactHorizontalScrollViewManager
view.setHorizontalScrollBarEnabled(value);
}
/**
* Computing momentum events is potentially expensive since we post a runnable on the UI thread
* to see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This
* is handled automatically in js by checking if there is a listener on the momentum events.
*
* @param view
* @param sendMomentumEvents
*/
@ReactProp(name = "sendMomentumEvents")
public void setSendMomentumEvents(ReactHorizontalScrollView view, boolean sendMomentumEvents) {
view.setSendMomentumEvents(sendMomentumEvents);
}
@Override
public void receiveCommand(
ReactHorizontalScrollView scrollView,

View File

@@ -36,11 +36,19 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
private boolean mRemoveClippedSubviews;
private @Nullable Rect mClippingRect;
private boolean mSendMomentumEvents;
private boolean mDragging;
private boolean mFlinging;
private boolean mDoneFlinging;
public ReactScrollView(Context context) {
super(context);
}
public void setSendMomentumEvents(boolean sendMomentumEvents) {
mSendMomentumEvents = sendMomentumEvents;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpecAssertions.assertExplicitMeasureSpec(widthMeasureSpec, heightMeasureSpec);
@@ -73,7 +81,11 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
updateClippingRect();
}
ReactScrollViewHelper.emitScrollEvent(this, x, y);
if (mFlinging) {
mDoneFlinging = false;
}
ReactScrollViewHelper.emitScrollEvent(this);
}
}
@@ -81,12 +93,24 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (super.onInterceptTouchEvent(ev)) {
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
ReactScrollViewHelper.emitScrollBeginDragEvent(this);
mDragging = true;
return true;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_UP && mDragging) {
ReactScrollViewHelper.emitScrollEndDragEvent(this);
mDragging = false;
}
return super.onTouchEvent(ev);
}
@Override
public void setRemoveClippedSubviews(boolean removeClippedSubviews) {
if (removeClippedSubviews && mClippingRect == null) {
@@ -120,4 +144,26 @@ public class ReactScrollView extends ScrollView implements ReactClippingViewGrou
public void getClippingRect(Rect outClippingRect) {
outClippingRect.set(Assertions.assertNotNull(mClippingRect));
}
@Override
public void fling(int velocityY) {
super.fling(velocityY);
if (mSendMomentumEvents) {
mFlinging = true;
ReactScrollViewHelper.emitScrollMomentumBeginEvent(this);
Runnable r = new Runnable() {
@Override
public void run() {
if (mDoneFlinging) {
mFlinging = false;
ReactScrollViewHelper.emitScrollMomentumEndEvent(ReactScrollView.this);
} else {
mDoneFlinging = true;
ReactScrollView.this.postOnAnimationDelayed(this, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
};
postOnAnimationDelayed(r, ReactScrollViewHelper.MOMENTUM_DELAY);
}
}
}

View File

@@ -21,18 +21,41 @@ import com.facebook.react.uimanager.UIManagerModule;
*/
public class ReactScrollViewHelper {
public static final long MOMENTUM_DELAY = 20;
/**
* Shared by {@link ReactScrollView} and {@link ReactHorizontalScrollView}.
*/
public static void emitScrollEvent(ViewGroup scrollView, int scrollX, int scrollY) {
public static void emitScrollEvent(ViewGroup scrollView) {
emitScrollEvent(scrollView, ScrollEventType.SCROLL);
}
public static void emitScrollBeginDragEvent(ViewGroup scrollView) {
emitScrollEvent(scrollView, ScrollEventType.BEGIN_DRAG);
}
public static void emitScrollEndDragEvent(ViewGroup scrollView) {
emitScrollEvent(scrollView, ScrollEventType.END_DRAG);
}
public static void emitScrollMomentumBeginEvent(ViewGroup scrollView) {
emitScrollEvent(scrollView, ScrollEventType.MOMENTUM_BEGIN);
}
public static void emitScrollMomentumEndEvent(ViewGroup scrollView) {
emitScrollEvent(scrollView, ScrollEventType.MOMENTUM_END);
}
private static void emitScrollEvent(ViewGroup scrollView, ScrollEventType scrollEventType) {
View contentView = scrollView.getChildAt(0);
ReactContext reactContext = (ReactContext) scrollView.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
ScrollEvent.obtain(
scrollView.getId(),
SystemClock.uptimeMillis(),
scrollX,
scrollY,
scrollEventType,
scrollView.getScrollX(),
scrollView.getScrollY(),
contentView.getWidth(),
contentView.getHeight(),
scrollView.getWidth(),

View File

@@ -52,6 +52,19 @@ public class ReactScrollViewManager
view.setRemoveClippedSubviews(removeClippedSubviews);
}
/**
* Computing momentum events is potentially expensive since we post a runnable on the UI thread
* to see when it is done. We only do that if {@param sendMomentumEvents} is set to true. This
* is handled automatically in js by checking if there is a listener on the momentum events.
*
* @param view
* @param sendMomentumEvents
*/
@ReactProp(name = "sendMomentumEvents")
public void setSendMomentumEvents(ReactScrollView view, boolean sendMomentumEvents) {
view.setSendMomentumEvents(sendMomentumEvents);
}
@Override
public @Nullable Map<String, Integer> getCommandsMap() {
return ReactScrollViewCommandHelper.getCommandsMap();
@@ -86,12 +99,12 @@ public class ReactScrollViewManager
public static Map createExportedCustomDirectEventTypeConstants() {
return MapBuilder.builder()
.put(ScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onScroll"))
.put("topScrollBeginDrag", MapBuilder.of("registrationName", "onScrollBeginDrag"))
.put("topScrollEndDrag", MapBuilder.of("registrationName", "onScrollEndDrag"))
.put("topScrollAnimationEnd", MapBuilder.of("registrationName", "onScrollAnimationEnd"))
.put("topMomentumScrollBegin", MapBuilder.of("registrationName", "onMomentumScrollBegin"))
.put("topMomentumScrollEnd", MapBuilder.of("registrationName", "onMomentumScrollEnd"))
.put(ScrollEventType.SCROLL.getJSEventName(), MapBuilder.of("registrationName", "onScroll"))
.put(ScrollEventType.BEGIN_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollBeginDrag"))
.put(ScrollEventType.END_DRAG.getJSEventName(), MapBuilder.of("registrationName", "onScrollEndDrag"))
.put(ScrollEventType.ANIMATION_END.getJSEventName(), MapBuilder.of("registrationName", "onScrollAnimationEnd"))
.put(ScrollEventType.MOMENTUM_BEGIN.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollBegin"))
.put(ScrollEventType.MOMENTUM_END.getJSEventName(), MapBuilder.of("registrationName", "onMomentumScrollEnd"))
.build();
}
}

View File

@@ -9,10 +9,13 @@
package com.facebook.react.views.scroll;
import javax.annotation.Nullable;
import java.lang.Override;
import android.support.v4.util.Pools;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.PixelUtil;
@@ -27,18 +30,18 @@ public class ScrollEvent extends Event<ScrollEvent> {
private static final Pools.SynchronizedPool<ScrollEvent> EVENTS_POOL =
new Pools.SynchronizedPool<>(3);
public static final String EVENT_NAME = "topScroll";
private int mScrollX;
private int mScrollY;
private int mContentWidth;
private int mContentHeight;
private int mScrollViewWidth;
private int mScrollViewHeight;
private @Nullable ScrollEventType mScrollEventType;
public static ScrollEvent obtain(
int viewTag,
long timestampMs,
ScrollEventType scrollEventType,
int scrollX,
int scrollY,
int contentWidth,
@@ -52,6 +55,7 @@ public class ScrollEvent extends Event<ScrollEvent> {
event.init(
viewTag,
timestampMs,
scrollEventType,
scrollX,
scrollY,
contentWidth,
@@ -72,6 +76,7 @@ public class ScrollEvent extends Event<ScrollEvent> {
private void init(
int viewTag,
long timestampMs,
ScrollEventType scrollEventType,
int scrollX,
int scrollY,
int contentWidth,
@@ -79,6 +84,7 @@ public class ScrollEvent extends Event<ScrollEvent> {
int scrollViewWidth,
int scrollViewHeight) {
super.init(viewTag, timestampMs);
mScrollEventType = scrollEventType;
mScrollX = scrollX;
mScrollY = scrollY;
mContentWidth = contentWidth;
@@ -89,7 +95,7 @@ public class ScrollEvent extends Event<ScrollEvent> {
@Override
public String getEventName() {
return EVENT_NAME;
return Assertions.assertNotNull(mScrollEventType).getJSEventName();
}
@Override
@@ -98,6 +104,15 @@ public class ScrollEvent extends Event<ScrollEvent> {
return 0;
}
@Override
public boolean canCoalesce() {
// Only SCROLL events can be coalesced, all others can not be
if (mScrollEventType == ScrollEventType.SCROLL) {
return true;
}
return false;
}
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.react.views.scroll;
/**
* Scroll event types that JS module RCTEventEmitter can understand
*/
public enum ScrollEventType {
BEGIN_DRAG("topScrollBeginDrag"),
END_DRAG("topScrollEndDrag"),
SCROLL("topScroll"),
MOMENTUM_BEGIN("topMomentumScrollBegin"),
MOMENTUM_END("topMomentumScrollEnd"),
ANIMATION_END("topScrollAnimationEnd");
private final String mJSEventName;
ScrollEventType(String jsEventName) {
mJSEventName = jsEventName;
}
public String getJSEventName() {
return mJSEventName;
}
}