1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303 |
- /**
- * 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');
- require('../../Renderer/shims/ReactNative'); // Force side effects to prevent T55744311
- 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 resolveAssetSource = require('../../Image/resolveAssetSource');
- const splitLayoutProps = require('../../StyleSheet/splitLayoutProps');
- const setAndForwardRef = require('../../Utilities/setAndForwardRef');
- import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType';
- import type {PointProp} from '../../StyleSheet/PointPropType';
- import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
- import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
- import type {
- PressEvent,
- ScrollEvent,
- LayoutEvent,
- } from '../../Types/CoreEventTypes';
- import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
- import type {State as ScrollResponderState} from '../ScrollResponder';
- import type {ViewProps} from '../View/ViewPropTypes';
- import type {Props as ScrollViewStickyHeaderProps} from './ScrollViewStickyHeader';
- import ScrollViewNativeComponent from './ScrollViewNativeComponent';
- import ScrollContentViewNativeComponent from './ScrollContentViewNativeComponent';
- import AndroidHorizontalScrollViewNativeComponent from './AndroidHorizontalScrollViewNativeComponent';
- import AndroidHorizontalScrollContentViewNativeComponent from './AndroidHorizontalScrollContentViewNativeComponent';
- let AndroidScrollView;
- let AndroidHorizontalScrollContentView;
- let AndroidHorizontalScrollView;
- let RCTScrollView;
- let RCTScrollContentView;
- if (Platform.OS === 'android') {
- AndroidScrollView = ScrollViewNativeComponent;
- AndroidHorizontalScrollView = AndroidHorizontalScrollViewNativeComponent;
- AndroidHorizontalScrollContentView = AndroidHorizontalScrollContentViewNativeComponent;
- } else {
- RCTScrollView = ScrollViewNativeComponent;
- RCTScrollContentView = ScrollContentViewNativeComponent;
- }
- // Public methods for ScrollView
- export type ScrollViewImperativeMethods = $ReadOnly<{|
- getScrollResponder: $PropertyType<ScrollView, 'getScrollResponder'>,
- getScrollableNode: $PropertyType<ScrollView, 'getScrollableNode'>,
- getInnerViewNode: $PropertyType<ScrollView, 'getInnerViewNode'>,
- getInnerViewRef: $PropertyType<ScrollView, 'getInnerViewRef'>,
- getNativeScrollRef: $PropertyType<ScrollView, 'getNativeScrollRef'>,
- scrollTo: $PropertyType<ScrollView, 'scrollTo'>,
- scrollToEnd: $PropertyType<ScrollView, 'scrollToEnd'>,
- flashScrollIndicators: $PropertyType<ScrollView, 'flashScrollIndicators'>,
- // ScrollResponder.Mixin public methods
- scrollResponderZoomTo: $PropertyType<
- typeof ScrollResponder.Mixin,
- 'scrollResponderZoomTo',
- >,
- scrollResponderScrollNativeHandleToKeyboard: $PropertyType<
- typeof ScrollResponder.Mixin,
- 'scrollResponderScrollNativeHandleToKeyboard',
- >,
- |}>;
- export type ScrollResponderType = ScrollViewImperativeMethods;
- 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'
- ),
- /**
- * 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,
- /**
- * Fades out the edges of the the scroll content.
- *
- * If the value is greater than 0, the fading edges will be set accordingly
- * to the current scroll direction and position,
- * indicating if there is more content to show.
- *
- * The default value is 0.
- *
- * @platform android
- */
- fadingEdgeLength?: ?number,
- |}>;
- 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'
- |}>;
- type StickyHeaderComponentType = React.AbstractComponent<
- ScrollViewStickyHeaderProps,
- $ReadOnly<{setNextHeaderY: number => void, ...}>,
- >;
- export type Props = $ReadOnly<{|
- ...ViewProps,
- ...IOSProps,
- ...AndroidProps,
- ...VRProps,
- /**
- * These styles will be applied to the scroll view content container which
- * wraps all of the child views. Example:
- *
- * ```
- * return (
- * <ScrollView contentContainerStyle={styles.contentContainer}>
- * </ScrollView>
- * );
- * ...
- * 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 pagination when the page is less than the width of the
- * horizontal ScrollView or the height of the vertical 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?: ?// default
- (| 'none' // cross-platform
- | 'on-drag'
- | '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<number>,
- /**
- * A React Component that will be used to render sticky headers.
- * To be used together with `stickyHeaderIndices` or with `SectionList`, defaults to `ScrollViewStickyHeader`.
- * You may need to set this if your sticky header uses custom transforms (eg. translation),
- * for example when you want your list to have an animated hidable header.
- */
- StickyHeaderComponent?: StickyHeaderComponentType,
- /**
- * 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<number>,
- /**
- * Use in conjunction 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 conjunction 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 operator?
- refreshControl?: ?React.Element<any>,
- children?: React.Node,
- /**
- * A ref to the inner View element of the ScrollView. This should be used
- * instead of calling `getInnerViewRef`.
- */
- innerViewRef?: React.Ref<typeof View>,
- /**
- * A ref to the Native ScrollView component. This ref can be used to call
- * all of ScrollView's public methods, in addition to native methods like
- * measure, measureLayout, etc.
- */
- scrollViewRef?: React.Ref<
- typeof ScrollViewNativeComponent & ScrollViewImperativeMethods,
- >,
- |}>;
- type State = {|
- layoutHeight: ?number,
- ...ScrollResponderState,
- |};
- function createScrollResponder(
- node: React.ElementRef<typeof ScrollView>,
- ): 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;
- }
- type ContextType = {|horizontal: boolean|} | null;
- const Context: React.Context<ContextType> = React.createContext(null);
- const standardHorizontalContext: ContextType = Object.freeze({
- horizontal: true,
- });
- const standardVerticalContext: ContextType = Object.freeze({horizontal: false});
- type ScrollViewComponentStatics = $ReadOnly<{|
- Context: typeof Context,
- |}>;
- /**
- * 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.
- *
- *
- * `<ScrollView>` vs [`<FlatList>`](https://reactnative.dev/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<Props, State> {
- static Context: React$Context<ContextType> = Context;
- /**
- * 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<
- string,
- React.ElementRef<StickyHeaderComponentType>,
- > = new Map();
- _headerLayoutYs: Map<string, number> = new Map();
- state: State = {
- layoutHeight: null,
- ...ScrollResponder.Mixin.scrollResponderMixinGetInitialState(),
- };
- UNSAFE_componentWillMount() {
- this._scrollResponder.UNSAFE_componentWillMount();
- this._scrollAnimatedValue = new AnimatedImplementation.Value(
- this.props.contentOffset?.y ?? 0,
- );
- this._scrollAnimatedValue.setOffset(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();
- }
- }
- _setNativeRef = setAndForwardRef({
- getForwardedRef: () => this.props.scrollViewRef,
- setLocalRef: ref => {
- this._scrollViewRef = ref;
- /*
- This is a hack. Ideally we would forwardRef to the underlying
- host component. However, since ScrollView has it's own methods that can be
- called as well, if we used the standard forwardRef then these
- methods wouldn't be accessible and thus be a breaking change.
- Therefore we edit ref to include ScrollView's public methods so that
- they are callable from the ref.
- */
- if (ref) {
- ref.getScrollResponder = this.getScrollResponder;
- ref.getScrollableNode = this.getScrollableNode;
- ref.getInnerViewNode = this.getInnerViewNode;
- ref.getInnerViewRef = this.getInnerViewRef;
- ref.getNativeScrollRef = this.getNativeScrollRef;
- ref.scrollTo = this.scrollTo;
- ref.scrollToEnd = this.scrollToEnd;
- ref.flashScrollIndicators = this.flashScrollIndicators;
- // $FlowFixMe - This method was manually bound from ScrollResponder.mixin
- ref.scrollResponderZoomTo = this.scrollResponderZoomTo;
- // $FlowFixMe - This method was manually bound from ScrollResponder.mixin
- ref.scrollResponderScrollNativeHandleToKeyboard = this.scrollResponderScrollNativeHandleToKeyboard;
- }
- },
- });
- /**
- * 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);
- }
- getInnerViewRef(): ?React.ElementRef<typeof View> {
- return this._innerViewRef;
- }
- getNativeScrollRef: () => ?React.ElementRef<HostComponent<mixed>> = () => {
- 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,
- ) => void = (
- 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, ...}) => void = (
- options?: ?{animated?: boolean, ...},
- ) => {
- // Default to true
- const animated = (options && options.animated) !== false;
- this._scrollResponder.scrollResponderScrollToEnd({
- animated: animated,
- });
- };
- /**
- * Displays the scroll indicators momentarily.
- *
- * @platform ios
- */
- flashScrollIndicators: () => void = () => {
- 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: string,
- ref: ?React.ElementRef<StickyHeaderComponentType>,
- ) {
- 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 &&
- previousHeader.setNextHeaderY(layoutY);
- }
- }
- _handleScroll = (e: ScrollEvent) => {
- if (__DEV__) {
- if (
- this.props.onScroll &&
- this.props.scrollEventThrottle == null &&
- Platform.OS === 'ios'
- ) {
- console.log(
- 'You specified `onScroll` on a <ScrollView> 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: ?React.ElementRef<HostComponent<mixed>> = null;
- _innerViewRef: ?React.ElementRef<typeof View> = null;
- _setInnerViewRef = setAndForwardRef({
- getForwardedRef: () => this.props.innerViewRef,
- setLocalRef: ref => {
- this._innerViewRef = ref;
- },
- });
- render(): React.Node | React.Element<string> {
- 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];
- const StickyHeaderComponent =
- this.props.StickyHeaderComponent || ScrollViewStickyHeader;
- return (
- <StickyHeaderComponent
- key={key}
- ref={ref => 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}
- </StickyHeaderComponent>
- );
- } else {
- return child;
- }
- });
- }
- children = (
- <Context.Provider
- value={
- this.props.horizontal === true
- ? standardHorizontalContext
- : standardVerticalContext
- }>
- {children}
- </Context.Provider>
- );
- const hasStickyHeaders =
- Array.isArray(stickyHeaderIndices) && stickyHeaderIndices.length > 0;
- const contentContainer = (
- /* $FlowFixMe(>=0.112.0 site=react_native_fb) This comment suppresses an
- * error found when Flow v0.112 was deployed. To see the error, delete
- * this comment and run Flow. */
- <ScrollContentContainerViewClass
- {...contentSizeChangeProps}
- ref={this._setInnerViewRef}
- style={contentContainerStyle}
- removeClippedSubviews={
- // Subview clipping causes issues with sticky headers on Android and
- // would be hard to fix properly in a performant way.
- Platform.OS === 'android' && hasStickyHeaders
- ? false
- : this.props.removeClippedSubviews
- }
- collapsable={false}>
- {children}
- </ScrollContentContainerViewClass>
- );
- 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,
- onLayout: this._handleLayout,
- onMomentumScrollBegin: this._scrollResponder
- .scrollResponderHandleMomentumScrollBegin,
- onMomentumScrollEnd: this._scrollResponder
- .scrollResponderHandleMomentumScrollEnd,
- onResponderGrant: this._scrollResponder
- .scrollResponderHandleResponderGrant,
- onResponderReject: this._scrollResponder
- .scrollResponderHandleResponderReject,
- onResponderRelease: this._scrollResponder
- .scrollResponderHandleResponderRelease,
- 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,
- // 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(>=0.117.0 site=react_native_fb) This comment suppresses
- * an error found when Flow v0.117 was deployed. To see the error,
- * delete this comment and run Flow. */
- <ScrollViewClass {...props} ref={this._setNativeRef}>
- {Platform.isTV ? null : refreshControl}
- {contentContainer}
- </ScrollViewClass>
- );
- } 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]},
- <ScrollViewClass
- {...props}
- style={[baseStyle, inner]}
- ref={this._setNativeRef}>
- {contentContainer}
- </ScrollViewClass>,
- );
- }
- }
- return (
- <ScrollViewClass {...props} ref={this._setNativeRef}>
- {contentContainer}
- </ScrollViewClass>
- );
- }
- }
- 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',
- },
- });
- function Wrapper(props, ref) {
- return <ScrollView {...props} scrollViewRef={ref} />;
- }
- Wrapper.displayName = 'ScrollView';
- const ForwardedScrollView = React.forwardRef(Wrapper);
- // $FlowFixMe Add static context to ForwardedScrollView
- ForwardedScrollView.Context = Context;
- ForwardedScrollView.displayName = 'ScrollView';
- module.exports = ((ForwardedScrollView: $FlowFixMe): React.AbstractComponent<
- React.ElementConfig<typeof ScrollView>,
- $ReadOnly<{|
- ...$Exact<React.ElementRef<HostComponent<mixed>>>,
- ...ScrollViewImperativeMethods,
- |}>,
- > &
- ScrollViewComponentStatics);
|