From fcf0431d252a9cb98ba14c0cee74d07be5303635 Mon Sep 17 00:00:00 2001 From: Dave Miller Date: Wed, 16 Dec 2015 00:47:43 -0800 Subject: [PATCH] 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 --- Libraries/Components/ScrollView/ScrollView.js | 8 ++++ .../RecyclerViewBackedScrollView.java | 2 + .../RecyclerViewBackedScrollViewManager.java | 4 +- .../scroll/ReactHorizontalScrollView.java | 48 ++++++++++++++++++- .../ReactHorizontalScrollViewManager.java | 13 +++++ .../react/views/scroll/ReactScrollView.java | 48 ++++++++++++++++++- .../views/scroll/ReactScrollViewHelper.java | 29 +++++++++-- .../views/scroll/ReactScrollViewManager.java | 25 +++++++--- .../react/views/scroll/ScrollEvent.java | 21 ++++++-- .../react/views/scroll/ScrollEventType.java | 32 +++++++++++++ 10 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventType.java diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 5f6e08d86..a23b0a248 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -234,6 +234,13 @@ var ScrollView = React.createClass({ * @platform ios */ scrollsToTop: PropTypes.bool, + /** + * When true, momentum events will be sent from Android + * This is internal and set automatically by the framework if you have + * onMomentumScrollBegin or onMomentumScrollEnd set on your ScrollView + * @platform android + */ + sendMomentumEvents: PropTypes.bool, /** * When true, shows a horizontal scroll indicator. */ @@ -434,6 +441,7 @@ var ScrollView = React.createClass({ onResponderTerminate: this.scrollResponderHandleTerminate, onResponderRelease: this.scrollResponderHandleResponderRelease, onResponderReject: this.scrollResponderHandleResponderReject, + sendMomentumEvents: (this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd) ? true : false, }; var onRefreshStart = this.props.onRefreshStart; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java index 054a95222..6ebfb211c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollView.java @@ -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(), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java index c49c26742..4b3401980 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/recyclerview/RecyclerViewBackedScrollViewManager.java @@ -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")) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index ec528cedf..298512c6d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -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); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index e8ee9fff8..0d86a3346 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -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, diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index cc8098efc..a6a813db9 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -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); + } + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java index c8e7fa22e..1b9c3eb7d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java @@ -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(), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java index 6de7da6bc..b45fac0af 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.java @@ -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 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(); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java index fdf8cd5a2..dde0a6018 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.java @@ -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 { private static final Pools.SynchronizedPool 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 { event.init( viewTag, timestampMs, + scrollEventType, scrollX, scrollY, contentWidth, @@ -72,6 +76,7 @@ public class ScrollEvent extends Event { private void init( int viewTag, long timestampMs, + ScrollEventType scrollEventType, int scrollX, int scrollY, int contentWidth, @@ -79,6 +84,7 @@ public class ScrollEvent extends Event { 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 { @Override public String getEventName() { - return EVENT_NAME; + return Assertions.assertNotNull(mScrollEventType).getJSEventName(); } @Override @@ -98,6 +104,15 @@ public class ScrollEvent extends Event { 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()); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventType.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventType.java new file mode 100644 index 000000000..db37f20d5 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventType.java @@ -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; + } +}