From 7f7144f6708042a8e4983a143ef2733ac81abea5 Mon Sep 17 00:00:00 2001 From: Di Da Date: Tue, 5 Nov 2019 11:30:41 -0800 Subject: [PATCH 1/4] Support refreshControl --- .../Samples/scrollViewSnapSample.tsx | 19 + .../windows/playground/HostingPane.xaml | 2 - .../windows/playground/HostingPane.xaml.cpp | 6 + vnext/ReactUWP/Base/UwpReactInstance.cpp | 2 + vnext/ReactUWP/ReactUWP.vcxproj | 2 + vnext/ReactUWP/ReactUWP.vcxproj.filters | 8 +- .../ReactUWP/Views/RefreshControlManager.cpp | 120 ++ vnext/ReactUWP/Views/RefreshControlManager.h | 29 + .../RefreshControl/RefreshControl.windows.js | 216 +++ .../ScrollView/ScrollView.windows.js | 1278 +++++++++++++++++ yarn.lock | 8 +- 11 files changed, 1682 insertions(+), 8 deletions(-) create mode 100644 vnext/ReactUWP/Views/RefreshControlManager.cpp create mode 100644 vnext/ReactUWP/Views/RefreshControlManager.h create mode 100644 vnext/src/Libraries/Components/RefreshControl/RefreshControl.windows.js create mode 100644 vnext/src/Libraries/Components/ScrollView/ScrollView.windows.js diff --git a/packages/playground/Samples/scrollViewSnapSample.tsx b/packages/playground/Samples/scrollViewSnapSample.tsx index 3ad5b32790b..5e814d91f72 100644 --- a/packages/playground/Samples/scrollViewSnapSample.tsx +++ b/packages/playground/Samples/scrollViewSnapSample.tsx @@ -13,8 +13,15 @@ import { Text, ScrollView, TouchableOpacity, + RefreshControl, } from 'react-native'; +function wait(timeout: number) { + return new Promise(resolve => { + setTimeout(resolve, timeout); + }); +} + export default class Bootstrap extends React.Component<{}, any> { state = { horizontalValue: true, @@ -22,6 +29,7 @@ export default class Bootstrap extends React.Component<{}, any> { snapToEndValue: false, zoomValue: false, alignToStartValue: true, + refreshing: false, }; toggleSwitch1 = (value: boolean) => { @@ -44,6 +52,11 @@ export default class Bootstrap extends React.Component<{}, any> { this.setState({alignToStartValue: value}); }; + onRefresh = () => { + this.setState({refreshing: true}); + wait(2000).then(() => this.setState({refreshing: false})); + }; + makeItems = (nItems: number, styles: Object): Array => { const items = []; for (let i = 0; i < nItems; i++) { @@ -153,6 +166,12 @@ export default class Bootstrap extends React.Component<{}, any> { ? styles.horizontalScrollViewStyle : styles.verticalScrollViewStyle } + refreshControl={ + + } snapToOffsets={[100.0, 500.0]} minimumZoomScale={0.1} maximumZoomScale={2.0} diff --git a/packages/playground/windows/playground/HostingPane.xaml b/packages/playground/windows/playground/HostingPane.xaml index 42d34bf2d78..f2c67473c08 100644 --- a/packages/playground/windows/playground/HostingPane.xaml +++ b/packages/playground/windows/playground/HostingPane.xaml @@ -33,7 +33,6 @@ (); x_ReactAppName->ItemsSource = m_ReactAppNames; + + try { + x_ReactAppName->IsEditable = true; + x_JavaScriptFilename->IsEditable = true; + } catch (...) { + } } void HostingPane::LoadKnownApps() { diff --git a/vnext/ReactUWP/Base/UwpReactInstance.cpp b/vnext/ReactUWP/Base/UwpReactInstance.cpp index 1e75604e25c..e5ef6bffdb3 100644 --- a/vnext/ReactUWP/Base/UwpReactInstance.cpp +++ b/vnext/ReactUWP/Base/UwpReactInstance.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -132,6 +133,7 @@ CreateUIManager( viewManagers.push_back(std::make_unique(instance)); viewManagers.push_back(std::make_unique(instance)); viewManagers.push_back(std::make_unique(instance)); + viewManagers.push_back(std::make_unique(instance)); // Polyester view managers viewManagers.push_back(std::make_unique(instance)); diff --git a/vnext/ReactUWP/ReactUWP.vcxproj b/vnext/ReactUWP/ReactUWP.vcxproj index 4e10c83302e..9571d7e9d17 100644 --- a/vnext/ReactUWP/ReactUWP.vcxproj +++ b/vnext/ReactUWP/ReactUWP.vcxproj @@ -260,6 +260,7 @@ + @@ -362,6 +363,7 @@ + diff --git a/vnext/ReactUWP/ReactUWP.vcxproj.filters b/vnext/ReactUWP/ReactUWP.vcxproj.filters index 7be5ca8dcef..1561e4b451b 100644 --- a/vnext/ReactUWP/ReactUWP.vcxproj.filters +++ b/vnext/ReactUWP/ReactUWP.vcxproj.filters @@ -302,7 +302,9 @@ Views - + + Views + @@ -645,7 +647,9 @@ Modules - + + Views + diff --git a/vnext/ReactUWP/Views/RefreshControlManager.cpp b/vnext/ReactUWP/Views/RefreshControlManager.cpp new file mode 100644 index 00000000000..732deb573f9 --- /dev/null +++ b/vnext/ReactUWP/Views/RefreshControlManager.cpp @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "pch.h" + +#include "RefreshControlManager.h" + +#include + +#include + +namespace winrt { +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::Foundation; +} // namespace winrt + +namespace react { +namespace uwp { + +class RefreshControlShadowNode : public ShadowNodeBase { + using Super = ShadowNodeBase; + + public: + RefreshControlShadowNode(){}; + void createView() override; + void updateProperties(const folly::dynamic &&props) override; + + private: + winrt::RefreshContainer::RefreshRequested_revoker m_refreshRequestedRevoker{}; + winrt::Deferral m_refreshDeferral{nullptr}; +}; + +void RefreshControlShadowNode::createView() { + Super::createView(); + if (auto refreshContainer = GetView().try_as()){ + m_refreshRequestedRevoker = + refreshContainer.RefreshRequested(winrt::auto_revoke, [this](auto &&, winrt::RefreshRequestedEventArgs args) { + auto wkinstance = GetViewManager()->GetReactInstance(); + if (auto instance = wkinstance.lock()) { + m_refreshDeferral = args.GetDeferral(); + folly::dynamic eventData = folly::dynamic::object(); + instance->DispatchEvent(m_tag, "topOnRefresh", std::move(eventData)); + } + }); + } +} + +void RefreshControlShadowNode::updateProperties(const folly::dynamic &&props) { + if (auto refreshContainer = GetView().try_as()) { + for (const auto &pair : props.items()) { + const std::string &propertyName = pair.first.getString(); + if (propertyName == "flexDirection") { + const folly::dynamic &propertyValue = pair.second; + if (propertyValue.isString() && propertyValue.asString() == "column") { // vertical scrollView + refreshContainer.PullDirection(winrt::RefreshPullDirection::TopToBottom); + } else { + refreshContainer.PullDirection(winrt::RefreshPullDirection::LeftToRight); + } + } else if (propertyName == "refreshing") { + const folly::dynamic &propertyValue = pair.second; + if (propertyValue.isBool()) { + bool refreshing = propertyValue.asBool(); + if (!refreshing && m_refreshDeferral) { + m_refreshDeferral.Complete(); + m_refreshDeferral = nullptr; + } + } + } + } + } + + Super::updateProperties(std::move(props)); +} + +RefreshControlViewManager::RefreshControlViewManager(const std::shared_ptr &reactInstance) + : Super(reactInstance) {} + +facebook::react::ShadowNode *RefreshControlViewManager::createShadow() const { + return new RefreshControlShadowNode(); +} + +const char *RefreshControlViewManager::GetName() const { + return "RCTRefreshControl"; +} + +XamlView RefreshControlViewManager::CreateViewCore(int64_t tag) { + try { + // refreshContainer is supported >= RS4 + auto refreshContainer = winrt::RefreshContainer(); + return refreshContainer; + } catch (...) { + // just return a grid if refreshContainer is not supported + return winrt::Grid(); + } +} + +void RefreshControlViewManager::AddView(XamlView parent, XamlView child, int64_t index) { + if (auto refreshContainer = parent.try_as()){ + refreshContainer.Content(child.as()); + } else if (auto grid = parent.try_as()) { + grid.Children().Append(child.as()); + } +} + +folly::dynamic RefreshControlViewManager::GetNativeProps() const { + auto props = Super::GetNativeProps(); + + props.update(folly::dynamic::object("refreshing", "boolean")); + + return props; +} + +folly::dynamic RefreshControlViewManager::GetExportedCustomDirectEventTypeConstants() const { + auto directEvents = Super::GetExportedCustomDirectEventTypeConstants(); + directEvents["topOnRefresh"] = folly::dynamic::object("registrationName", "onRefresh"); + return directEvents; +} +} // namespace uwp +} // namespace react diff --git a/vnext/ReactUWP/Views/RefreshControlManager.h b/vnext/ReactUWP/Views/RefreshControlManager.h new file mode 100644 index 00000000000..51f2dd054ce --- /dev/null +++ b/vnext/ReactUWP/Views/RefreshControlManager.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace react { +namespace uwp { + +class RefreshControlViewManager : public FrameworkElementViewManager { + using Super = FrameworkElementViewManager; + + public: + RefreshControlViewManager(const std::shared_ptr &reactInstance); + + facebook::react::ShadowNode *createShadow() const override; + + const char *GetName() const override; + folly::dynamic GetNativeProps() const override; + folly::dynamic GetExportedCustomDirectEventTypeConstants() const override; + + protected: + XamlView CreateViewCore(int64_t tag) override; + void AddView(XamlView parent, XamlView child, int64_t index) override; +}; + +} // namespace uwp +} // namespace react diff --git a/vnext/src/Libraries/Components/RefreshControl/RefreshControl.windows.js b/vnext/src/Libraries/Components/RefreshControl/RefreshControl.windows.js new file mode 100644 index 00000000000..94b5d386dd6 --- /dev/null +++ b/vnext/src/Libraries/Components/RefreshControl/RefreshControl.windows.js @@ -0,0 +1,216 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +const Platform = require('../../Utilities/Platform'); +const React = require('react'); +const {NativeComponent} = require('../../Renderer/shims/ReactNative'); + +const AndroidSwipeRefreshLayoutNativeComponent = require('./AndroidSwipeRefreshLayoutNativeComponent'); +const RCTRefreshControlNativeComponent = require('./RCTRefreshControlNativeComponent'); +const nullthrows = require('nullthrows'); + +import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; +import type {ViewProps} from '../View/ViewPropTypes'; + +let RefreshLayoutConsts; +if (Platform.OS === 'android') { + const AndroidSwipeRefreshLayout = require('../../ReactNative/UIManager').getViewManagerConfig( + 'AndroidSwipeRefreshLayout', + ); + RefreshLayoutConsts = AndroidSwipeRefreshLayout + ? AndroidSwipeRefreshLayout.Constants + : {SIZE: {}}; +} else { + RefreshLayoutConsts = {SIZE: {}}; +} + +type IOSProps = $ReadOnly<{| + /** + * The color of the refresh indicator. + */ + tintColor?: ?ColorValue, + /** + * Title color. + */ + titleColor?: ?ColorValue, + /** + * The title displayed under the refresh indicator. + */ + title?: ?string, +|}>; + +type AndroidProps = $ReadOnly<{| + /** + * Whether the pull to refresh functionality is enabled. + */ + enabled?: ?boolean, + /** + * The colors (at least one) that will be used to draw the refresh indicator. + */ + colors?: ?$ReadOnlyArray, + /** + * The background color of the refresh indicator. + */ + progressBackgroundColor?: ?ColorValue, + /** + * Size of the refresh indicator, see RefreshControl.SIZE. + */ + size?: ?( + | typeof RefreshLayoutConsts.SIZE.DEFAULT + | typeof RefreshLayoutConsts.SIZE.LARGE + ), + /** + * Progress view top offset + */ + progressViewOffset?: ?number, +|}>; + +export type RefreshControlProps = $ReadOnly<{| + ...ViewProps, + ...IOSProps, + ...AndroidProps, + + /** + * Called when the view starts refreshing. + */ + onRefresh?: ?() => mixed, + + /** + * Whether the view should be indicating an active refresh. + */ + refreshing: boolean, +|}>; + +/** + * This component is used inside a ScrollView or ListView to add pull to refresh + * functionality. When the ScrollView is at `scrollY: 0`, swiping down + * triggers an `onRefresh` event. + * + * ### Usage example + * + * ``` js + * class RefreshableList extends Component { + * constructor(props) { + * super(props); + * this.state = { + * refreshing: false, + * }; + * } + * + * _onRefresh() { + * this.setState({refreshing: true}); + * fetchData().then(() => { + * this.setState({refreshing: false}); + * }); + * } + * + * render() { + * return ( + * + * } + * ... + * > + * ... + * + * ); + * } + * ... + * } + * ``` + * + * __Note:__ `refreshing` is a controlled prop, this is why it needs to be set to true + * in the `onRefresh` function otherwise the refresh indicator will stop immediately. + */ +class RefreshControl extends React.Component { + static SIZE = RefreshLayoutConsts.SIZE; + + _setNativePropsOnRef: ?({refreshing: boolean}) => void; + _lastNativeRefreshing = false; + + componentDidMount() { + this._lastNativeRefreshing = this.props.refreshing; + } + + componentDidUpdate(prevProps: RefreshControlProps) { + // RefreshControl is a controlled component so if the native refreshing + // value doesn't match the current js refreshing prop update it to + // the js value. + if (this.props.refreshing !== prevProps.refreshing) { + this._lastNativeRefreshing = this.props.refreshing; + } else if ( + this.props.refreshing !== this._lastNativeRefreshing && + this._setNativePropsOnRef + ) { + this._setNativePropsOnRef({ + refreshing: this.props.refreshing, + }); + this._lastNativeRefreshing = this.props.refreshing; + } + } + + render() { + const setRef = ref => + (this._setNativePropsOnRef = ref ? ref.setNativeProps.bind(ref) : null); + if (Platform.OS === 'ios') { + const { + enabled, + colors, + progressBackgroundColor, + size, + progressViewOffset, + ...props + } = this.props; + return ( + + ); + } else if (Platform.OS === 'windows') { + const {...props} = this.props; + return ( + + ); + } else { + const {tintColor, titleColor, title, ...props} = this.props; + return ( + + ); + } + } + + _onRefresh = () => { + this._lastNativeRefreshing = true; + + this.props.onRefresh && this.props.onRefresh(); + + // The native component will start refreshing so force an update to + // make sure it stays in sync with the js component. + this.forceUpdate(); + }; +} + +module.exports = RefreshControl; diff --git a/vnext/src/Libraries/Components/ScrollView/ScrollView.windows.js b/vnext/src/Libraries/Components/ScrollView/ScrollView.windows.js new file mode 100644 index 00000000000..b5585efe087 --- /dev/null +++ b/vnext/src/Libraries/Components/ScrollView/ScrollView.windows.js @@ -0,0 +1,1278 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +'use strict'; + +const AnimatedImplementation = require('../../Animated/src/AnimatedImplementation'); +const Platform = require('../../Utilities/Platform'); +const React = require('react'); +const ReactNative = require('../../Renderer/shims/ReactNative'); +const ScrollResponder = require('../ScrollResponder'); +const ScrollViewStickyHeader = require('./ScrollViewStickyHeader'); +const StyleSheet = require('../../StyleSheet/StyleSheet'); +const View = require('../View/View'); + +const dismissKeyboard = require('../../Utilities/dismissKeyboard'); +const flattenStyle = require('../../StyleSheet/flattenStyle'); +const invariant = require('invariant'); +const processDecelerationRate = require('./processDecelerationRate'); +const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); +const resolveAssetSource = require('../../Image/resolveAssetSource'); +const splitLayoutProps = require('../../StyleSheet/splitLayoutProps'); + +import type { + PressEvent, + ScrollEvent, + LayoutEvent, + KeyboardEvent, +} from '../../Types/CoreEventTypes'; +import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; +import type {NativeMethodsMixinType} from '../../Renderer/shims/ReactNativeTypes'; +import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; +import type {ViewProps} from '../View/ViewPropTypes'; +import type {PointProp} from '../../StyleSheet/PointPropType'; + +import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; +import type {State as ScrollResponderState} from '../ScrollResponder'; + +let AndroidScrollView; +let AndroidHorizontalScrollContentView; +let AndroidHorizontalScrollView; +let RCTScrollView; +let RCTScrollContentView; + +if (Platform.OS === 'android') { + AndroidScrollView = requireNativeComponent('RCTScrollView'); + AndroidHorizontalScrollView = requireNativeComponent( + 'AndroidHorizontalScrollView', + ); + AndroidHorizontalScrollContentView = requireNativeComponent( + 'AndroidHorizontalScrollContentView', + ); +} else if (Platform.OS === 'ios') { + RCTScrollView = requireNativeComponent('RCTScrollView'); + RCTScrollContentView = requireNativeComponent('RCTScrollContentView'); +} else { + RCTScrollView = requireNativeComponent('RCTScrollView'); + RCTScrollContentView = requireNativeComponent('RCTScrollContentView'); +} + +export type ScrollResponderType = { + ...ScrollView, + ...typeof ScrollResponder.Mixin, +}; + +type TouchableProps = $ReadOnly<{| + onTouchStart?: (event: PressEvent) => void, + onTouchMove?: (event: PressEvent) => void, + onTouchEnd?: (event: PressEvent) => void, + onTouchCancel?: (event: PressEvent) => void, + onTouchEndCapture?: (event: PressEvent) => void, +|}>; + +type IOSProps = $ReadOnly<{| + /** + * Controls whether iOS should automatically adjust the content inset + * for scroll views that are placed behind a navigation bar or + * tab bar/ toolbar. The default value is true. + * @platform ios + */ + automaticallyAdjustContentInsets?: ?boolean, + /** + * The amount by which the scroll view content is inset from the edges + * of the scroll view. Defaults to `{top: 0, left: 0, bottom: 0, right: 0}`. + * @platform ios + */ + contentInset?: ?EdgeInsetsProp, + /** + * Used to manually set the starting scroll offset. + * The default value is `{x: 0, y: 0}`. + * @platform ios + */ + contentOffset?: ?PointProp, + /** + * When true, the scroll view bounces when it reaches the end of the + * content if the content is larger then the scroll view along the axis of + * the scroll direction. When false, it disables all bouncing even if + * the `alwaysBounce*` props are true. The default value is true. + * @platform ios + */ + bounces?: ?boolean, + /** + * By default, ScrollView has an active pan responder that hijacks panresponders + * deeper in the render tree in order to prevent accidental touches while scrolling. + * However, in certain occasions (such as when using snapToInterval) in a vertical scrollview + * You may want to disable this behavior in order to prevent the ScrollView from blocking touches + */ + disableScrollViewPanResponder?: ?boolean, + /** + * When true, gestures can drive zoom past min/max and the zoom will animate + * to the min/max value at gesture end, otherwise the zoom will not exceed + * the limits. + * @platform ios + */ + bouncesZoom?: ?boolean, + /** + * When true, the scroll view bounces horizontally when it reaches the end + * even if the content is smaller than the scroll view itself. The default + * value is true when `horizontal={true}` and false otherwise. + * @platform ios + */ + alwaysBounceHorizontal?: ?boolean, + /** + * When true, the scroll view bounces vertically when it reaches the end + * even if the content is smaller than the scroll view itself. The default + * value is false when `horizontal={true}` and true otherwise. + * @platform ios + */ + alwaysBounceVertical?: ?boolean, + /** + * When true, the scroll view automatically centers the content when the + * content is smaller than the scroll view bounds; when the content is + * larger than the scroll view, this property has no effect. The default + * value is false. + * @platform ios + */ + centerContent?: ?boolean, + /** + * The style of the scroll indicators. + * + * - `'default'` (the default), same as `black`. + * - `'black'`, scroll indicator is black. This style is good against a light background. + * - `'white'`, scroll indicator is white. This style is good against a dark background. + * + * @platform ios + */ + indicatorStyle?: ?('default' | 'black' | 'white'), + /** + * When true, the ScrollView will try to lock to only vertical or horizontal + * scrolling while dragging. The default value is false. + * @platform ios + */ + directionalLockEnabled?: ?boolean, + /** + * When false, once tracking starts, won't try to drag if the touch moves. + * The default value is true. + * @platform ios + */ + canCancelContentTouches?: ?boolean, + /** + * When set, the scroll view will adjust the scroll position so that the first child that is + * currently visible and at or beyond `minIndexForVisible` will not change position. This is + * useful for lists that are loading content in both directions, e.g. a chat thread, where new + * messages coming in might otherwise cause the scroll position to jump. A value of 0 is common, + * but other values such as 1 can be used to skip loading spinners or other content that should + * not maintain position. + * + * The optional `autoscrollToTopThreshold` can be used to make the content automatically scroll + * to the top after making the adjustment if the user was within the threshold of the top before + * the adjustment was made. This is also useful for chat-like applications where you want to see + * new messages scroll into place, but not if the user has scrolled up a ways and it would be + * disruptive to scroll a bunch. + * + * Caveat 1: Reordering elements in the scrollview with this enabled will probably cause + * jumpiness and jank. It can be fixed, but there are currently no plans to do so. For now, + * don't re-order the content of any ScrollViews or Lists that use this feature. + * + * Caveat 2: This simply uses `contentOffset` and `frame.origin` in native code to compute + * visibility. Occlusion, transforms, and other complexity won't be taken into account as to + * whether content is "visible" or not. + * + * @platform ios + */ + maintainVisibleContentPosition?: ?$ReadOnly<{| + minIndexForVisible: number, + autoscrollToTopThreshold?: ?number, + |}>, + /** + * The maximum allowed zoom scale. The default value is 1.0. + * @platform ios + */ + maximumZoomScale?: ?number, + /** + * The minimum allowed zoom scale. The default value is 1.0. + * @platform ios + */ + minimumZoomScale?: ?number, + /** + * When true, ScrollView allows use of pinch gestures to zoom in and out. + * The default value is true. + * @platform ios + */ + pinchGestureEnabled?: ?boolean, + /** + * This controls how often the scroll event will be fired while scrolling + * (as a time interval in ms). A lower number yields better accuracy for code + * that is tracking the scroll position, but can lead to scroll performance + * problems due to the volume of information being send over the bridge. + * + * Values between 0 and 17ms indicate 60fps updates are needed and throttling + * will be disabled. + * + * If you do not need precise scroll position tracking, set this value higher + * to limit the information being sent across the bridge. + * + * The default value is zero, which results in the scroll event being sent only + * once each time the view is scrolled. + * + * @platform ios + */ + scrollEventThrottle?: ?number, + /** + * The amount by which the scroll view indicators are inset from the edges + * of the scroll view. This should normally be set to the same value as + * the `contentInset`. Defaults to `{0, 0, 0, 0}`. + * @platform ios + */ + scrollIndicatorInsets?: ?EdgeInsetsProp, + /** + * When true, the scroll view can be programmatically scrolled beyond its + * content size. The default value is false. + * @platform ios + */ + scrollToOverflowEnabled?: ?boolean, + /** + * When true, the scroll view scrolls to top when the status bar is tapped. + * The default value is true. + * @platform ios + */ + scrollsToTop?: ?boolean, + /** + * Fires when the scroll view scrolls to top after the status bar has been tapped + * @platform ios + */ + onScrollToTop?: (event: ScrollEvent) => void, + /** + * When true, shows a horizontal scroll indicator. + * The default value is true. + */ + showsHorizontalScrollIndicator?: ?boolean, + /** + * When `snapToInterval` is set, `snapToAlignment` will define the relationship + * of the snapping to the scroll view. + * + * - `'start'` (the default) will align the snap at the left (horizontal) or top (vertical) + * - `'center'` will align the snap in the center + * - `'end'` will align the snap at the right (horizontal) or bottom (vertical) + * + * @platform ios + */ + snapToAlignment?: ?('start' | 'center' | 'end'), + /** + * The current scale of the scroll view content. The default value is 1.0. + * @platform ios + */ + zoomScale?: ?number, + /** + * This property specifies how the safe area insets are used to modify the + * content area of the scroll view. The default value of this property is + * "never". Available on iOS 11 and later. + * @platform ios + */ + contentInsetAdjustmentBehavior?: ?( + | 'automatic' + | 'scrollableAxes' + | 'never' + | 'always' + ), + /** + * Experimental: specifies how much to adjust the content view by when using + * the keyboard to scroll. This value adjusts the content's horizontal offset. + * + * @platform macos + */ + horizontalLineScroll?: number, // TODO(macOS ISS#2323203) + /** + * Experimental: specifies how much to adjust the content view by when using + * the keyboard to scroll. This value adjusts the content's vertical offset. + * + * @platform macos + */ + verticalLineScroll?: number, // TODO(macOS ISS#2323203) + /** + * When true, ScrollView will emit updateChildFrames data in scroll events, + * otherwise will not compute or emit child frame data. This only exists + * to support legacy issues, `onLayout` should be used instead to retrieve + * frame data. + * The default value is false. + * @platform ios + */ + DEPRECATED_sendUpdatedChildFrames?: ?boolean, +|}>; + +type AndroidProps = $ReadOnly<{| + /** + * Enables nested scrolling for Android API level 21+. + * Nested scrolling is supported by default on iOS + * @platform android + */ + nestedScrollEnabled?: ?boolean, + /** + * Sometimes a scrollview takes up more space than its content fills. When this is + * the case, this prop will fill the rest of the scrollview with a color to avoid setting + * a background and creating unnecessary overdraw. This is an advanced optimization + * that is not needed in the general case. + * @platform android + */ + endFillColor?: ?ColorValue, + /** + * Tag used to log scroll performance on this scroll view. Will force + * momentum events to be turned on (see sendMomentumEvents). This doesn't do + * anything out of the box and you need to implement a custom native + * FpsListener for it to be useful. + * @platform android + */ + scrollPerfTag?: ?string, + /** + * Used to override default value of overScroll mode. + * + * Possible values: + * + * - `'auto'` - Default value, allow a user to over-scroll + * this view only if the content is large enough to meaningfully scroll. + * - `'always'` - Always allow a user to over-scroll this view. + * - `'never'` - Never allow a user to over-scroll this view. + * + * @platform android + */ + overScrollMode?: ?('auto' | 'always' | 'never'), + /** + * Causes the scrollbars not to turn transparent when they are not in use. + * The default value is false. + * + * @platform android + */ + persistentScrollbar?: ?boolean, +|}>; + +type VRProps = $ReadOnly<{| + /** + * Optionally an image can be used for the scroll bar thumb. This will + * override the color. While the image is loading or the image fails to + * load the color will be used instead. Use an alpha of 0 in the color + * to avoid seeing it while the image is loading. + * + * - `uri` - a string representing the resource identifier for the image, which + * should be either a local file path or the name of a static image resource + * - `number` - Opaque type returned by something like + * `import IMAGE from './image.jpg'`. + * @platform vr + */ + scrollBarThumbImage?: ?($ReadOnly<{||}> | number), // Opaque type returned by import IMAGE from './image.jpg' +|}>; + +export type Props = $ReadOnly<{| + ...ViewProps, + ...TouchableProps, + ...IOSProps, + ...AndroidProps, + ...VRProps, + + /** + * These styles will be applied to the scroll view content container which + * wraps all of the child views. Example: + * + * ``` + * return ( + * + * + * ); + * ... + * const styles = StyleSheet.create({ + * contentContainer: { + * paddingVertical: 20 + * } + * }); + * ``` + */ + contentContainerStyle?: ?ViewStyleProp, + /** + * When true, the scroll view stops on the next index (in relation to scroll + * position at release) regardless of how fast the gesture is. This can be + * used for horizontal pagination when the page is less than the width of + * the ScrollView. The default value is false. + */ + disableIntervalMomentum?: ?boolean, + /** + * A floating-point number that determines how quickly the scroll view + * decelerates after the user lifts their finger. You may also use string + * shortcuts `"normal"` and `"fast"` which match the underlying iOS settings + * for `UIScrollViewDecelerationRateNormal` and + * `UIScrollViewDecelerationRateFast` respectively. + * + * - `'normal'`: 0.998 on iOS, 0.985 on Android (the default) + * - `'fast'`: 0.99 on iOS, 0.9 on Android + */ + decelerationRate?: ?('fast' | 'normal' | number), + /** + * When true, the scroll view's children are arranged horizontally in a row + * instead of vertically in a column. The default value is false. + */ + horizontal?: ?boolean, + /** + * If sticky headers should stick at the bottom instead of the top of the + * ScrollView. This is usually used with inverted ScrollViews. + */ + invertStickyHeaders?: ?boolean, + /** + * Determines whether the keyboard gets dismissed in response to a drag. + * + * *Cross platform* + * + * - `'none'` (the default), drags do not dismiss the keyboard. + * - `'on-drag'`, the keyboard is dismissed when a drag begins. + * + * *iOS Only* + * + * - `'interactive'`, the keyboard is dismissed interactively with the drag and moves in + * synchrony with the touch; dragging upwards cancels the dismissal. + * On android this is not supported and it will have the same behavior as 'none'. + */ + keyboardDismissMode?: ?( + | 'none' // default + | 'on-drag' // cross-platform + | 'interactive' + ), // ios only + /** + * Determines when the keyboard should stay visible after a tap. + * + * - `'never'` (the default), tapping outside of the focused text input when the keyboard + * is up dismisses the keyboard. When this happens, children won't receive the tap. + * - `'always'`, the keyboard will not dismiss automatically, and the scroll view will not + * catch taps, but children of the scroll view can catch taps. + * - `'handled'`, the keyboard will not dismiss automatically when the tap was handled by + * a children, (or captured by an ancestor). + * - `false`, deprecated, use 'never' instead + * - `true`, deprecated, use 'always' instead + */ + keyboardShouldPersistTaps?: ?('always' | 'never' | 'handled' | true | false), + /** + * Called when the momentum scroll starts (scroll which occurs as the ScrollView glides to a stop). + */ + onMomentumScrollBegin?: (event: ScrollEvent) => void, + /** + * Called when the momentum scroll ends (scroll which occurs as the ScrollView glides to a stop). + */ + onMomentumScrollEnd?: (event: ScrollEvent) => void, + + /** + * Fires at most once per frame during scrolling. The frequency of the + * events can be controlled using the `scrollEventThrottle` prop. + */ + onScroll?: (event: ScrollEvent) => void, + /** + * Called when the user begins to drag the scroll view. + */ + onScrollBeginDrag?: (event: ScrollEvent) => void, + /** + * Called when the user stops dragging the scroll view and it either stops + * or begins to glide. + */ + onScrollEndDrag?: (event: ScrollEvent) => void, + /** + * Called when scrollable content view of the ScrollView changes. + * + * Handler function is passed the content width and content height as parameters: + * `(contentWidth, contentHeight)` + * + * It's implemented using onLayout handler attached to the content container + * which this ScrollView renders. + */ + onContentSizeChange?: (contentWidth: number, contentHeight: number) => void, + onKeyboardDidShow?: (event: PressEvent) => void, + /** + * When true, the scroll view stops on multiples of the scroll view's size + * when scrolling. This can be used for horizontal pagination. The default + * value is false. + * + * Note: Vertical pagination is not supported on Android. + */ + pagingEnabled?: ?boolean, + + /** + * When false, the view cannot be scrolled via touch interaction. + * The default value is true. + * + * Note that the view can always be scrolled by calling `scrollTo`. + */ + scrollEnabled?: ?boolean, + /** + * When true, shows a vertical scroll indicator. + * The default value is true. + */ + showsVerticalScrollIndicator?: ?boolean, + /** + * An array of child indices determining which children get docked to the + * top of the screen when scrolling. For example, passing + * `stickyHeaderIndices={[0]}` will cause the first child to be fixed to the + * top of the scroll view. This property is not supported in conjunction + * with `horizontal={true}`. + */ + stickyHeaderIndices?: ?$ReadOnlyArray, + /** + * When set, causes the scroll view to stop at multiples of the value of + * `snapToInterval`. This can be used for paginating through children + * that have lengths smaller than the scroll view. Typically used in + * combination with `snapToAlignment` and `decelerationRate="fast"`. + * + * Overrides less configurable `pagingEnabled` prop. + */ + snapToInterval?: ?number, + /** + * When set, causes the scroll view to stop at the defined offsets. + * This can be used for paginating through variously sized children + * that have lengths smaller than the scroll view. Typically used in + * combination with `decelerationRate="fast"`. + * + * Overrides less configurable `pagingEnabled` and `snapToInterval` props. + */ + snapToOffsets?: ?$ReadOnlyArray, + /** + * Use in conjuction with `snapToOffsets`. By default, the beginning + * of the list counts as a snap offset. Set `snapToStart` to false to disable + * this behavior and allow the list to scroll freely between its start and + * the first `snapToOffsets` offset. + * The default value is true. + */ + snapToStart?: ?boolean, + /** + * Use in conjuction with `snapToOffsets`. By default, the end + * of the list counts as a snap offset. Set `snapToEnd` to false to disable + * this behavior and allow the list to scroll freely between its end and + * the last `snapToOffsets` offset. + * The default value is true. + */ + snapToEnd?: ?boolean, + /** + * Experimental: When true, offscreen child views (whose `overflow` value is + * `hidden`) are removed from their native backing superview when offscreen. + * This can improve scrolling performance on long lists. The default value is + * true. + */ + removeClippedSubviews?: ?boolean, + /** + * A RefreshControl component, used to provide pull-to-refresh + * functionality for the ScrollView. Only works for vertical ScrollViews + * (`horizontal` prop must be `false`). + * + * See [RefreshControl](docs/refreshcontrol.html). + */ + // $FlowFixMe - how to handle generic type without existential opereator? + refreshControl?: ?React.Element, + children?: React.Node, +|}>; + +type State = {| + layoutHeight: ?number, + ...ScrollResponderState, +|}; + +function createScrollResponder( + node: React.ElementRef, +): typeof ScrollResponder.Mixin { + const scrollResponder = {...ScrollResponder.Mixin}; + + for (const key in scrollResponder) { + if (typeof scrollResponder[key] === 'function') { + scrollResponder[key] = scrollResponder[key].bind(node); + } + } + + return scrollResponder; +} + +/** + * Component that wraps platform ScrollView while providing + * integration with touch locking "responder" system. + * + * Keep in mind that ScrollViews must have a bounded height in order to work, + * since they contain unbounded-height children into a bounded container (via + * a scroll interaction). In order to bound the height of a ScrollView, either + * set the height of the view directly (discouraged) or make sure all parent + * views have bounded height. Forgetting to transfer `{flex: 1}` down the + * view stack can lead to errors here, which the element inspector makes + * easy to debug. + * + * Doesn't yet support other contained responders from blocking this scroll + * view from becoming the responder. + * + * + * `` vs [``](/react-native/docs/flatlist.html) - which one to use? + * + * `ScrollView` simply renders all its react child components at once. That + * makes it very easy to understand and use. + * + * On the other hand, this has a performance downside. Imagine you have a very + * long list of items you want to display, maybe several screens worth of + * content. Creating JS components and native views for everything all at once, + * much of which may not even be shown, will contribute to slow rendering and + * increased memory usage. + * + * This is where `FlatList` comes into play. `FlatList` renders items lazily, + * just when they are about to appear, and removes items that scroll way off + * screen to save memory and processing time. + * + * `FlatList` is also handy if you want to render separators between your items, + * multiple columns, infinite scroll loading, or any number of other features it + * supports out of the box. + */ +class ScrollView extends React.Component { + /** + * Part 1: Removing ScrollResponder.Mixin: + * + * 1. Mixin methods should be flow typed. That's why we create a + * copy of ScrollResponder.Mixin and attach it to this._scrollResponder. + * Otherwise, we'd have to manually declare each method on the component + * class and assign it a flow type. + * 2. Mixin methods can call component methods, and access the component's + * props and state. So, we need to bind all mixin methods to the + * component instance. + * 3. Continued... + */ + _scrollResponder: typeof ScrollResponder.Mixin = createScrollResponder(this); + + constructor(props: Props) { + super(props); + + /** + * Part 2: Removing ScrollResponder.Mixin + * + * 3. Mixin methods access other mixin methods via dynamic dispatch using + * this. Since mixin methods are bound to the component instance, we need + * to copy all mixin methods to the component instance. This is also + * necessary because getScrollResponder() is a public method that returns + * an object that can be used to execute all scrollResponder methods. + * Since the object returned from that method is the ScrollView instance, + * we need to bind all mixin methods to the ScrollView instance. + */ + for (const key in ScrollResponder.Mixin) { + if ( + typeof ScrollResponder.Mixin[key] === 'function' && + key.startsWith('scrollResponder') + ) { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = ScrollResponder.Mixin[key].bind(this); + } + } + + /** + * Part 3: Removing ScrollResponder.Mixin + * + * 4. Mixins can initialize properties and use properties on the component + * instance. + */ + Object.keys(ScrollResponder.Mixin) + .filter(key => typeof ScrollResponder.Mixin[key] !== 'function') + .forEach(key => { + // $FlowFixMe - dynamically adding properties to a class + (this: any)[key] = ScrollResponder.Mixin[key]; + }); + } + + _scrollAnimatedValue: AnimatedImplementation.Value = new AnimatedImplementation.Value( + 0, + ); + _scrollAnimatedValueAttachment: ?{detach: () => void} = null; + _stickyHeaderRefs: Map = new Map(); + _headerLayoutYs: Map = new Map(); + + state = { + layoutHeight: null, + ...ScrollResponder.Mixin.scrollResponderMixinGetInitialState(), + }; + + UNSAFE_componentWillMount() { + this._scrollResponder.UNSAFE_componentWillMount(); + this._scrollAnimatedValue = new AnimatedImplementation.Value( + this.props.contentOffset ? this.props.contentOffset.y : 0, + ); + this._scrollAnimatedValue.setOffset( + /* $FlowFixMe(>=0.98.0 site=react_native_fb) This comment suppresses an + * error found when Flow v0.98 was deployed. To see the error delete this + * comment and run Flow. */ + this.props.contentInset ? this.props.contentInset.top : 0, + ); + this._stickyHeaderRefs = new Map(); + this._headerLayoutYs = new Map(); + } + + UNSAFE_componentWillReceiveProps(nextProps: Props) { + const currentContentInsetTop = this.props.contentInset + ? this.props.contentInset.top + : 0; + const nextContentInsetTop = nextProps.contentInset + ? nextProps.contentInset.top + : 0; + if (currentContentInsetTop !== nextContentInsetTop) { + this._scrollAnimatedValue.setOffset(nextContentInsetTop || 0); + } + } + + componentDidMount() { + this._updateAnimatedNodeAttachment(); + } + + componentDidUpdate() { + this._updateAnimatedNodeAttachment(); + } + + componentWillUnmount() { + this._scrollResponder.componentWillUnmount(); + if (this._scrollAnimatedValueAttachment) { + this._scrollAnimatedValueAttachment.detach(); + } + } + + setNativeProps(props: {[key: string]: mixed}) { + this._scrollViewRef && this._scrollViewRef.setNativeProps(props); + } + + /** + * Returns a reference to the underlying scroll responder, which supports + * operations like `scrollTo`. All ScrollView-like components should + * implement this method so that they can be composed while providing access + * to the underlying scroll responder's methods. + */ + getScrollResponder(): ScrollResponderType { + // $FlowFixMe - overriding type to include ScrollResponder.Mixin + return ((this: any): ScrollResponderType); + } + + getScrollableNode(): ?number { + return ReactNative.findNodeHandle(this._scrollViewRef); + } + + getInnerViewNode(): ?number { + return ReactNative.findNodeHandle(this._innerViewRef); + } + + getNativeScrollRef(): ?ScrollView { + return this._scrollViewRef; + } + + /** + * Scrolls to a given x, y offset, either immediately or with a smooth animation. + * + * Example: + * + * `scrollTo({x: 0, y: 0, animated: true})` + * + * Note: The weird function signature is due to the fact that, for historical reasons, + * the function also accepts separate arguments as an alternative to the options object. + * This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED. + */ + scrollTo( + options?: {x?: number, y?: number, animated?: boolean} | number, + deprecatedX?: number, + deprecatedAnimated?: boolean, + ) { + let x, y, animated; + if (typeof options === 'number') { + console.warn( + '`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, ' + + 'animated: true})` instead.', + ); + y = options; + x = deprecatedX; + animated = deprecatedAnimated; + } else if (options) { + y = options.y; + x = options.x; + animated = options.animated; + } + this._scrollResponder.scrollResponderScrollTo({ + x: x || 0, + y: y || 0, + animated: animated !== false, + }); + } + + /** + * If this is a vertical ScrollView scrolls to the bottom. + * If this is a horizontal ScrollView scrolls to the right. + * + * Use `scrollToEnd({animated: true})` for smooth animated scrolling, + * `scrollToEnd({animated: false})` for immediate scrolling. + * If no options are passed, `animated` defaults to true. + */ + scrollToEnd(options?: ?{animated?: boolean}) { + // Default to true + const animated = (options && options.animated) !== false; + this._scrollResponder.scrollResponderScrollToEnd({ + animated: animated, + }); + } + + /** + * Deprecated, use `scrollTo` instead. + */ + scrollWithoutAnimationTo(y: number = 0, x: number = 0) { + console.warn( + '`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead', + ); + this.scrollTo({x, y, animated: false}); + } + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + this._scrollResponder.scrollResponderFlashScrollIndicators(); + } + + _getKeyForIndex(index, childArray) { + const child = childArray[index]; + return child && child.key; + } + + _updateAnimatedNodeAttachment() { + if (this._scrollAnimatedValueAttachment) { + this._scrollAnimatedValueAttachment.detach(); + } + if ( + this.props.stickyHeaderIndices && + this.props.stickyHeaderIndices.length > 0 + ) { + this._scrollAnimatedValueAttachment = AnimatedImplementation.attachNativeEvent( + this._scrollViewRef, + 'onScroll', + [{nativeEvent: {contentOffset: {y: this._scrollAnimatedValue}}}], + ); + } + } + + _setStickyHeaderRef(key, ref) { + if (ref) { + this._stickyHeaderRefs.set(key, ref); + } else { + this._stickyHeaderRefs.delete(key); + } + } + + _onStickyHeaderLayout(index, event, key) { + const {stickyHeaderIndices} = this.props; + if (!stickyHeaderIndices) { + return; + } + const childArray = React.Children.toArray(this.props.children); + if (key !== this._getKeyForIndex(index, childArray)) { + // ignore stale layout update + return; + } + + const layoutY = event.nativeEvent.layout.y; + this._headerLayoutYs.set(key, layoutY); + + const indexOfIndex = stickyHeaderIndices.indexOf(index); + const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1]; + if (previousHeaderIndex != null) { + const previousHeader = this._stickyHeaderRefs.get( + this._getKeyForIndex(previousHeaderIndex, childArray), + ); + previousHeader && previousHeader.setNextHeaderY(layoutY); + } + } + + // [TODO(macOS ISS#2323203) + _handleKeyDown = (e: KeyboardEvent) => { + if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } else { + const event = e.nativeEvent; + const key = event.key; + const kMinScrollOffset = 10; + + if (Platform.OS === 'macos') { + if (key === 'PAGE_UP') { + this._handleScrollByKeyDown(event, { + x: event.contentOffset.x, + y: event.contentOffset.y + -event.layoutMeasurement.height, + }); + } else if (key === 'PAGE_DOWN') { + this._handleScrollByKeyDown(event, { + x: event.contentOffset.x, + y: event.contentOffset.y + event.layoutMeasurement.height, + }); + } else if (key === 'LEFT_ARROW') { + this._handleScrollByKeyDown(event, { + x: + event.contentOffset.x + + -(this.props.horizontalLineScroll === undefined + ? this.props.horizontalLineScroll + : kMinScrollOffset), + y: event.contentOffset.y, + }); + } else if (key === 'RIGHT_ARROW') { + this._handleScrollByKeyDown(event, { + x: + event.contentOffset.x + + (this.props.horizontalLineScroll === undefined + ? this.props.horizontalLineScroll + : kMinScrollOffset), + y: event.contentOffset.y, + }); + } else if (key === 'DOWN_ARROW') { + this._handleScrollByKeyDown(event, { + x: event.contentOffset.x, + y: + event.contentOffset.y + + (this.props.verticalLineScroll === undefined + ? this.props.verticalLineScroll + : kMinScrollOffset), + }); + } else if (key === 'UP_ARROW') { + this._handleScrollByKeyDown(event, { + x: event.contentOffset.x, + y: + event.contentOffset.y + + -(this.props.verticalLineScroll === undefined + ? this.props.verticalLineScroll + : kMinScrollOffset), + }); + } + } + } + }; + + _handleScrollByKeyDown = (e: ScrollEvent, newOffset) => { + const maxX = + e.nativeEvent.contentSize.width - e.nativeEvent.layoutMeasurement.width; + const maxY = + e.nativeEvent.contentSize.height - e.nativeEvent.layoutMeasurement.height; + this.scrollTo({ + x: Math.max(0, Math.min(maxX, newOffset.x)), + y: Math.max(0, Math.min(maxY, newOffset.y)), + }); + }; // ]TODO(macOS ISS#2323203) + + _handleScroll = (e: ScrollEvent) => { + if (__DEV__) { + if ( + this.props.onScroll && + this.props.scrollEventThrottle == null && + (Platform.OS === 'ios' || Platform.OS === 'macos') // TODO(macOS ISS#2323203) + ) { + console.log( + 'You specified `onScroll` on a but not ' + + '`scrollEventThrottle`. You will only receive one event. ' + + 'Using `16` you get all the events but be aware that it may ' + + "cause frame drops, use a bigger number if you don't need as " + + 'much precision.', + ); + } + } + if (Platform.OS === 'android') { + if ( + this.props.keyboardDismissMode === 'on-drag' && + this.state.isTouching + ) { + dismissKeyboard(); + } + } + this._scrollResponder.scrollResponderHandleScroll(e); + }; + + _handleLayout = (e: LayoutEvent) => { + if (this.props.invertStickyHeaders === true) { + this.setState({layoutHeight: e.nativeEvent.layout.height}); + } + if (this.props.onLayout) { + this.props.onLayout(e); + } + }; + + _handleContentOnLayout = (e: LayoutEvent) => { + const {width, height} = e.nativeEvent.layout; + this.props.onContentSizeChange && + this.props.onContentSizeChange(width, height); + }; + + _scrollViewRef: ?ScrollView = null; + _setScrollViewRef = (ref: ?ScrollView) => { + this._scrollViewRef = ref; + }; + + _innerViewRef: ?NativeMethodsMixinType = null; + _setInnerViewRef = (ref: ?NativeMethodsMixinType) => { + this._innerViewRef = ref; + }; + + render() { + let ScrollViewClass; + let ScrollContentContainerViewClass; + if (Platform.OS === 'android') { + if (this.props.horizontal === true) { + ScrollViewClass = AndroidHorizontalScrollView; + ScrollContentContainerViewClass = AndroidHorizontalScrollContentView; + } else { + ScrollViewClass = AndroidScrollView; + ScrollContentContainerViewClass = View; + } + } else { + ScrollViewClass = RCTScrollView; + ScrollContentContainerViewClass = RCTScrollContentView; + } + + invariant( + ScrollViewClass !== undefined, + 'ScrollViewClass must not be undefined', + ); + + invariant( + ScrollContentContainerViewClass !== undefined, + 'ScrollContentContainerViewClass must not be undefined', + ); + + const contentContainerStyle = [ + this.props.horizontal === true && styles.contentContainerHorizontal, + this.props.contentContainerStyle, + ]; + if (__DEV__ && this.props.style !== undefined) { + const style = flattenStyle(this.props.style); + const childLayoutProps = ['alignItems', 'justifyContent'].filter( + prop => style && style[prop] !== undefined, + ); + invariant( + childLayoutProps.length === 0, + 'ScrollView child layout (' + + JSON.stringify(childLayoutProps) + + ') must be applied through the contentContainerStyle prop.', + ); + } + + let contentSizeChangeProps = {}; + if (this.props.onContentSizeChange) { + contentSizeChangeProps = { + onLayout: this._handleContentOnLayout, + }; + } + + const {stickyHeaderIndices} = this.props; + let children = this.props.children; + + if (stickyHeaderIndices != null && stickyHeaderIndices.length > 0) { + const childArray = React.Children.toArray(this.props.children); + + children = childArray.map((child, index) => { + const indexOfIndex = child ? stickyHeaderIndices.indexOf(index) : -1; + if (indexOfIndex > -1) { + const key = child.key; + const nextIndex = stickyHeaderIndices[indexOfIndex + 1]; + return ( + this._setStickyHeaderRef(key, ref)} + nextHeaderLayoutY={this._headerLayoutYs.get( + this._getKeyForIndex(nextIndex, childArray), + )} + onLayout={event => this._onStickyHeaderLayout(index, event, key)} + scrollAnimatedValue={this._scrollAnimatedValue} + inverted={this.props.invertStickyHeaders} + scrollViewHeight={this.state.layoutHeight}> + {child} + + ); + } else { + return child; + } + }); + } + + const hasStickyHeaders = + Array.isArray(stickyHeaderIndices) && stickyHeaderIndices.length > 0; + + const contentContainer = ( + + {children} + + ); + + const alwaysBounceHorizontal = + this.props.alwaysBounceHorizontal !== undefined + ? this.props.alwaysBounceHorizontal + : this.props.horizontal; + + const alwaysBounceVertical = + this.props.alwaysBounceVertical !== undefined + ? this.props.alwaysBounceVertical + : !this.props.horizontal; + + const DEPRECATED_sendUpdatedChildFrames = !!this.props + .DEPRECATED_sendUpdatedChildFrames; + + const baseStyle = + this.props.horizontal === true + ? styles.baseHorizontal + : styles.baseVertical; + const props = { + ...this.props, + alwaysBounceHorizontal, + alwaysBounceVertical, + style: [baseStyle, this.props.style], + // Override the onContentSizeChange from props, since this event can + // bubble up from TextInputs + onContentSizeChange: null, + onKeyDown: this._handleKeyDown, // TODO(macOS ISS#2323203) + onLayout: this._handleLayout, + onMomentumScrollBegin: this._scrollResponder + .scrollResponderHandleMomentumScrollBegin, + onMomentumScrollEnd: this._scrollResponder + .scrollResponderHandleMomentumScrollEnd, + onResponderGrant: this._scrollResponder + .scrollResponderHandleResponderGrant, + onResponderReject: this._scrollResponder + .scrollResponderHandleResponderReject, + onResponderRelease: this._scrollResponder + .scrollResponderHandleResponderRelease, + // $FlowFixMe + onResponderTerminate: this._scrollResponder + .scrollResponderHandleTerminate, + onResponderTerminationRequest: this._scrollResponder + .scrollResponderHandleTerminationRequest, + onScrollBeginDrag: this._scrollResponder + .scrollResponderHandleScrollBeginDrag, + onScrollEndDrag: this._scrollResponder.scrollResponderHandleScrollEndDrag, + onScrollShouldSetResponder: this._scrollResponder + .scrollResponderHandleScrollShouldSetResponder, + onStartShouldSetResponder: this._scrollResponder + .scrollResponderHandleStartShouldSetResponder, + onStartShouldSetResponderCapture: this._scrollResponder + .scrollResponderHandleStartShouldSetResponderCapture, + onTouchEnd: this._scrollResponder.scrollResponderHandleTouchEnd, + onTouchMove: this._scrollResponder.scrollResponderHandleTouchMove, + onTouchStart: this._scrollResponder.scrollResponderHandleTouchStart, + onTouchCancel: this._scrollResponder.scrollResponderHandleTouchCancel, + onScroll: this._handleScroll, + scrollBarThumbImage: resolveAssetSource(this.props.scrollBarThumbImage), + scrollEventThrottle: hasStickyHeaders + ? 1 + : this.props.scrollEventThrottle, + sendMomentumEvents: + this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd + ? true + : false, + DEPRECATED_sendUpdatedChildFrames, + // default to true + snapToStart: this.props.snapToStart !== false, + // default to true + snapToEnd: this.props.snapToEnd !== false, + // pagingEnabled is overridden by snapToInterval / snapToOffsets + pagingEnabled: Platform.select({ + // on iOS, pagingEnabled must be set to false to have snapToInterval / snapToOffsets work + ios: + this.props.pagingEnabled === true && + this.props.snapToInterval == null && + this.props.snapToOffsets == null, + // [TODO(macOS ISS#2323203) + macos: + this.props.pagingEnabled === true && + this.props.snapToInterval == null && + this.props.snapToOffsets == null, // ]TODO(macOS ISS#2323203) + // on Android, pagingEnabled must be set to true to have snapToInterval / snapToOffsets work + android: + this.props.pagingEnabled === true || + this.props.snapToInterval != null || + this.props.snapToOffsets != null, + }), + }; + + const {decelerationRate} = this.props; + if (decelerationRate != null) { + props.decelerationRate = processDecelerationRate(decelerationRate); + } + + const refreshControl = this.props.refreshControl; + + if (refreshControl) { + if (Platform.OS === 'ios') { + // On iOS the RefreshControl is a child of the ScrollView. + // tvOS lacks native support for RefreshControl, so don't include it in that case + return ( + // $FlowFixMe + + {Platform.isTV ? null : refreshControl} + {contentContainer} + + ); + } else if (Platform.OS === 'android') { + // On Android wrap the ScrollView with a AndroidSwipeRefreshLayout. + // Since the ScrollView is wrapped add the style props to the + // AndroidSwipeRefreshLayout and use flex: 1 for the ScrollView. + // Note: we should split props.style on the inner and outer props + // however, the ScrollView still needs the baseStyle to be scrollable + const {outer, inner} = splitLayoutProps(flattenStyle(props.style)); + return React.cloneElement( + refreshControl, + {style: [baseStyle, outer]}, + + {contentContainer} + , + ); + } else if (Platform.OS === 'windows') { + const {outer, inner} = splitLayoutProps(flattenStyle(props.style)); + return React.cloneElement( + refreshControl, + {style: [baseStyle, outer]}, + + {contentContainer} + , + ); + } + } + return ( + // $FlowFixMe + + {contentContainer} + + ); + } +} + +const styles = StyleSheet.create({ + baseVertical: { + flexGrow: 1, + flexShrink: 1, + flexDirection: 'column', + overflow: 'scroll', + }, + baseHorizontal: { + flexGrow: 1, + flexShrink: 1, + flexDirection: 'row', + overflow: 'scroll', + }, + contentContainerHorizontal: { + flexDirection: 'row', + }, +}); + +module.exports = ScrollView; diff --git a/yarn.lock b/yarn.lock index 02f3ce94dde..0abf574c19c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9863,10 +9863,10 @@ react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw== -"react-native@https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.14.tar.gz": - version "0.60.0-microsoft.14" - uid fa964a148d696ef3913f414224695b9e34abf363 - resolved "https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.14.tar.gz#fa964a148d696ef3913f414224695b9e34abf363" +"react-native@https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.15.tar.gz": + version "0.60.0-microsoft.15" + uid "1eb5433a13b9bc133d545d2c1867a8547fe2cbba" + resolved "https://github.com/microsoft/react-native/archive/v0.60.0-microsoft.15.tar.gz#1eb5433a13b9bc133d545d2c1867a8547fe2cbba" dependencies: "@babel/core" "^7.4.0" "@babel/generator" "^7.4.0" From e1aa502905f765bf94d54f2158b5f1656e133e14 Mon Sep 17 00:00:00 2001 From: Di Da Date: Tue, 5 Nov 2019 11:31:02 -0800 Subject: [PATCH 2/4] Change files --- ...react-native-windows-2019-11-05-11-31-02-refresh2.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 change/react-native-windows-2019-11-05-11-31-02-refresh2.json diff --git a/change/react-native-windows-2019-11-05-11-31-02-refresh2.json b/change/react-native-windows-2019-11-05-11-31-02-refresh2.json new file mode 100644 index 00000000000..f585c26cdd1 --- /dev/null +++ b/change/react-native-windows-2019-11-05-11-31-02-refresh2.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "Support refreshControl", + "packageName": "react-native-windows", + "email": "dida@ntdev.microsoft.com", + "commit": "7f7144f6708042a8e4983a143ef2733ac81abea5", + "date": "2019-11-05T19:31:02.270Z" +} \ No newline at end of file From 2f7e92f8c4c0472a2c9e9c43a8c6fcc3035f12e5 Mon Sep 17 00:00:00 2001 From: Di Da Date: Tue, 5 Nov 2019 16:22:54 -0800 Subject: [PATCH 3/4] Address Comments --- .../windows/playground/HostingPane.xaml.cpp | 5 +++-- vnext/ReactUWP/Utils/Helpers.cpp | 21 +++++++++++++++++++ vnext/ReactUWP/Utils/Helpers.h | 2 ++ .../ReactUWP/Views/RefreshControlManager.cpp | 12 +++++------ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/playground/windows/playground/HostingPane.xaml.cpp b/packages/playground/windows/playground/HostingPane.xaml.cpp index 54aca84b96d..5f51e25beeb 100644 --- a/packages/playground/windows/playground/HostingPane.xaml.cpp +++ b/packages/playground/windows/playground/HostingPane.xaml.cpp @@ -429,10 +429,11 @@ void HostingPane::InitComboBoxes() { x_ReactAppName->ItemsSource = m_ReactAppNames; - try { + // IsEditable is only supported on RS4 or higher + if (Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent( + L"Windows.Foundation.UniversalApiContract", 6)) { x_ReactAppName->IsEditable = true; x_JavaScriptFilename->IsEditable = true; - } catch (...) { } } diff --git a/vnext/ReactUWP/Utils/Helpers.cpp b/vnext/ReactUWP/Utils/Helpers.cpp index 6ddd46f4084..9ba993345ec 100644 --- a/vnext/ReactUWP/Utils/Helpers.cpp +++ b/vnext/ReactUWP/Utils/Helpers.cpp @@ -4,11 +4,13 @@ #include "pch.h" #include +#include #include "Helpers.h" namespace winrt { using namespace Windows::UI::Xaml::Controls::Primitives; using namespace Windows::UI::Xaml::Media; +using namespace Windows::Foundation::Metadata; } // namespace winrt namespace react { @@ -43,5 +45,24 @@ std::int32_t CountOpenPopups() { return (int32_t)popups.Size(); } +template +bool IsAPIContractVxAvailable() { + static bool isAPIContractVxAvailableInitialized = false; + static bool isAPIContractVxAvailable = false; + if (!isAPIContractVxAvailableInitialized) { + isAPIContractVxAvailableInitialized = true; + isAPIContractVxAvailable = winrt::ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", APIVersion); + } + + return isAPIContractVxAvailable; +} + +bool IsAPIContractV6Available() { + return IsAPIContractVxAvailable<6>(); +} + +bool IsRS4OrHigher() { + return IsAPIContractV6Available(); +} } // namespace uwp }; // namespace react diff --git a/vnext/ReactUWP/Utils/Helpers.h b/vnext/ReactUWP/Utils/Helpers.h index 66b62c6c607..50c46a234b3 100644 --- a/vnext/ReactUWP/Utils/Helpers.h +++ b/vnext/ReactUWP/Utils/Helpers.h @@ -29,5 +29,7 @@ inline typename T asEnum(folly::dynamic const &obj) { ReactId getViewId(_In_ IReactInstance *instance, winrt::FrameworkElement const &fe); std::int32_t CountOpenPopups(); + +bool IsRS4OrHigher(); } // namespace uwp } // namespace react diff --git a/vnext/ReactUWP/Views/RefreshControlManager.cpp b/vnext/ReactUWP/Views/RefreshControlManager.cpp index 732deb573f9..14d8df1791f 100644 --- a/vnext/ReactUWP/Views/RefreshControlManager.cpp +++ b/vnext/ReactUWP/Views/RefreshControlManager.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace winrt { using namespace Windows::UI::Xaml; @@ -33,7 +34,7 @@ class RefreshControlShadowNode : public ShadowNodeBase { void RefreshControlShadowNode::createView() { Super::createView(); - if (auto refreshContainer = GetView().try_as()){ + if (auto refreshContainer = GetView().try_as()) { m_refreshRequestedRevoker = refreshContainer.RefreshRequested(winrt::auto_revoke, [this](auto &&, winrt::RefreshRequestedEventArgs args) { auto wkinstance = GetViewManager()->GetReactInstance(); @@ -85,18 +86,17 @@ const char *RefreshControlViewManager::GetName() const { } XamlView RefreshControlViewManager::CreateViewCore(int64_t tag) { - try { + if (IsRS4OrHigher()) { // refreshContainer is supported >= RS4 - auto refreshContainer = winrt::RefreshContainer(); - return refreshContainer; - } catch (...) { + return winrt::RefreshContainer(); + }else{ // just return a grid if refreshContainer is not supported return winrt::Grid(); } } void RefreshControlViewManager::AddView(XamlView parent, XamlView child, int64_t index) { - if (auto refreshContainer = parent.try_as()){ + if (auto refreshContainer = parent.try_as()) { refreshContainer.Content(child.as()); } else if (auto grid = parent.try_as()) { grid.Children().Append(child.as()); From 21bb00fffed800453d97a924fa13e956327a5b80 Mon Sep 17 00:00:00 2001 From: Di Da Date: Tue, 5 Nov 2019 16:24:20 -0800 Subject: [PATCH 4/4] format --- vnext/ReactUWP/Utils/Helpers.cpp | 3 ++- vnext/ReactUWP/Views/RefreshControlManager.cpp | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vnext/ReactUWP/Utils/Helpers.cpp b/vnext/ReactUWP/Utils/Helpers.cpp index 9ba993345ec..c6919e0b697 100644 --- a/vnext/ReactUWP/Utils/Helpers.cpp +++ b/vnext/ReactUWP/Utils/Helpers.cpp @@ -51,7 +51,8 @@ bool IsAPIContractVxAvailable() { static bool isAPIContractVxAvailable = false; if (!isAPIContractVxAvailableInitialized) { isAPIContractVxAvailableInitialized = true; - isAPIContractVxAvailable = winrt::ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", APIVersion); + isAPIContractVxAvailable = + winrt::ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", APIVersion); } return isAPIContractVxAvailable; diff --git a/vnext/ReactUWP/Views/RefreshControlManager.cpp b/vnext/ReactUWP/Views/RefreshControlManager.cpp index 14d8df1791f..d72248abecd 100644 --- a/vnext/ReactUWP/Views/RefreshControlManager.cpp +++ b/vnext/ReactUWP/Views/RefreshControlManager.cpp @@ -7,8 +7,8 @@ #include -#include #include +#include namespace winrt { using namespace Windows::UI::Xaml; @@ -89,7 +89,7 @@ XamlView RefreshControlViewManager::CreateViewCore(int64_t tag) { if (IsRS4OrHigher()) { // refreshContainer is supported >= RS4 return winrt::RefreshContainer(); - }else{ + } else { // just return a grid if refreshContainer is not supported return winrt::Grid(); }