-
Notifications
You must be signed in to change notification settings - Fork 24.7k
Implement sticky headers in JS using Native Animated #11315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,12 +16,22 @@ var AnimatedImplementation = require('AnimatedImplementation'); | |
var Image = require('Image'); | ||
var Text = require('Text'); | ||
var View = require('View'); | ||
var ScrollView = require('ScrollView'); | ||
|
||
module.exports = { | ||
...AnimatedImplementation, | ||
let AnimatedScrollView; | ||
|
||
const Animated = { | ||
View: AnimatedImplementation.createAnimatedComponent(View), | ||
Text: AnimatedImplementation.createAnimatedComponent(Text), | ||
Image: AnimatedImplementation.createAnimatedComponent(Image), | ||
ScrollView: AnimatedImplementation.createAnimatedComponent(ScrollView), | ||
get ScrollView() { | ||
// Make this lazy to avoid circular reference. | ||
if (!AnimatedScrollView) { | ||
AnimatedScrollView = AnimatedImplementation.createAnimatedComponent(require('ScrollView')); | ||
} | ||
return AnimatedScrollView; | ||
}, | ||
}; | ||
|
||
Object.assign((Animated: Object), AnimatedImplementation); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to express this properly in flow? I want to add props to an object using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just include There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sahrens Using This is why I used There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant as a field, like |
||
|
||
module.exports = ((Animated: any): (typeof AnimatedImplementation) & typeof Animated); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2148,9 +2148,53 @@ type EventConfig = { | |
useNativeDriver?: bool, | ||
}; | ||
|
||
function attachNativeEvent(viewRef: any, eventName: string, argMapping: Array<?Mapping>) { | ||
// Find animated values in `argMapping` and create an array representing their | ||
// key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. | ||
const eventMappings = []; | ||
|
||
const traverse = (value, path) => { | ||
if (value instanceof AnimatedValue) { | ||
value.__makeNative(); | ||
|
||
eventMappings.push({ | ||
nativeEventPath: path, | ||
animatedValueTag: value.__getNativeTag(), | ||
}); | ||
} else if (typeof value === 'object') { | ||
for (const key in value) { | ||
traverse(value[key], path.concat(key)); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. array type This type is incompatible with the expected param type of object type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No there were some flow errors at some point, doesn't show as outdated diff because I changed some other line to fix it. |
||
}; | ||
|
||
invariant( | ||
argMapping[0] && argMapping[0].nativeEvent, | ||
'Native driven events only support animated values contained inside `nativeEvent`.' | ||
); | ||
|
||
// Assume that the event containing `nativeEvent` is always the first argument. | ||
traverse(argMapping[0].nativeEvent, []); | ||
|
||
const viewTag = findNodeHandle(viewRef); | ||
|
||
eventMappings.forEach((mapping) => { | ||
NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping); | ||
}); | ||
|
||
return { | ||
detach() { | ||
NativeAnimatedAPI.removeAnimatedEventFromView(viewTag, eventName); | ||
}, | ||
}; | ||
} | ||
|
||
class AnimatedEvent { | ||
_argMapping: Array<?Mapping>; | ||
_listener: ?Function; | ||
_attachedEvent: ?{ | ||
detach: () => void, | ||
}; | ||
__isNative: bool; | ||
|
||
constructor( | ||
|
@@ -2159,6 +2203,7 @@ class AnimatedEvent { | |
) { | ||
this._argMapping = argMapping; | ||
this._listener = config.listener; | ||
this._attachedEvent = null; | ||
this.__isNative = shouldUseNativeDriver(config); | ||
|
||
if (this.__isNative) { | ||
|
@@ -2173,44 +2218,13 @@ class AnimatedEvent { | |
__attach(viewRef, eventName) { | ||
invariant(this.__isNative, 'Only native driven events need to be attached.'); | ||
|
||
// Find animated values in `argMapping` and create an array representing their | ||
// key path inside the `nativeEvent` object. Ex.: ['contentOffset', 'x']. | ||
const eventMappings = []; | ||
|
||
const traverse = (value, path) => { | ||
if (value instanceof AnimatedValue) { | ||
value.__makeNative(); | ||
|
||
eventMappings.push({ | ||
nativeEventPath: path, | ||
animatedValueTag: value.__getNativeTag(), | ||
}); | ||
} else if (typeof value === 'object') { | ||
for (const key in value) { | ||
traverse(value[key], path.concat(key)); | ||
} | ||
} | ||
}; | ||
|
||
invariant( | ||
this._argMapping[0] && this._argMapping[0].nativeEvent, | ||
'Native driven events only support animated values contained inside `nativeEvent`.' | ||
); | ||
|
||
// Assume that the event containing `nativeEvent` is always the first argument. | ||
traverse(this._argMapping[0].nativeEvent, []); | ||
|
||
const viewTag = findNodeHandle(viewRef); | ||
|
||
eventMappings.forEach((mapping) => { | ||
NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping); | ||
}); | ||
this._attachedEvent = attachNativeEvent(viewRef, eventName, this._argMapping); | ||
} | ||
|
||
__detach(viewTag, eventName) { | ||
invariant(this.__isNative, 'Only native driven events need to be detached.'); | ||
|
||
NativeAnimatedAPI.removeAnimatedEventFromView(viewTag, eventName); | ||
this._attachedEvent && this._attachedEvent.detach(); | ||
} | ||
|
||
__getHandler() { | ||
|
@@ -2470,5 +2484,11 @@ module.exports = { | |
*/ | ||
createAnimatedComponent, | ||
|
||
/** | ||
* Imperative API to attach an animated value to an event on a view. Prefer using | ||
* `Animated.event` with `useNativeDrive: true` if possible. | ||
*/ | ||
attachNativeEvent, | ||
|
||
__PropsOnlyForTests: AnimatedProps, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,13 +11,15 @@ | |
*/ | ||
'use strict'; | ||
|
||
const Animated = require('Animated'); | ||
const ColorPropType = require('ColorPropType'); | ||
const EdgeInsetsPropType = require('EdgeInsetsPropType'); | ||
const Platform = require('Platform'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. access of computed property/element Computed property/element cannot be accessed on possibly undefined value undefined There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. call of method |
||
const PointPropType = require('PointPropType'); | ||
const React = require('React'); | ||
const ReactNative = require('ReactNative'); | ||
const ScrollResponder = require('ScrollResponder'); | ||
const ScrollViewStickyHeader = require('ScrollViewStickyHeader'); | ||
const StyleSheet = require('StyleSheet'); | ||
const StyleSheetPropType = require('StyleSheetPropType'); | ||
const View = require('View'); | ||
|
@@ -353,10 +355,33 @@ const ScrollView = React.createClass({ | |
|
||
mixins: [ScrollResponder.Mixin], | ||
|
||
_scrollAnimatedValue: (new Animated.Value(0): Animated.Value), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there another way to declare instance variables with createClass that are not nullable? I can't just initialize these values here because otherwise the same object gets used for every instance so I have to do it in componentWillMount. As a workaround I'm just creating these values to be able to make the type not nullable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that is a limitation of |
||
_scrollAnimatedValueAttachment: (null: ?{detach: () => void}), | ||
_stickyHeaderRefs: (new Map(): Map<number, ScrollViewStickyHeader>), | ||
|
||
getInitialState: function() { | ||
return this.scrollResponderMixinGetInitialState(); | ||
}, | ||
|
||
componentWillMount: function() { | ||
this._scrollAnimatedValue = new Animated.Value(0); | ||
this._stickyHeaderRefs = new Map(); | ||
}, | ||
|
||
componentDidMount: function() { | ||
this._updateAnimatedNodeAttachment(); | ||
}, | ||
|
||
componentDidUpdate: function() { | ||
this._updateAnimatedNodeAttachment(); | ||
}, | ||
|
||
componentWillUnmount: function() { | ||
if (this._scrollAnimatedValueAttachment) { | ||
this._scrollAnimatedValueAttachment.detach(); | ||
} | ||
}, | ||
|
||
setNativeProps: function(props: Object) { | ||
this._scrollViewRef && this._scrollViewRef.setNativeProps(props); | ||
}, | ||
|
@@ -411,6 +436,40 @@ const ScrollView = React.createClass({ | |
this.scrollTo({x, y, animated: false}); | ||
}, | ||
|
||
_updateAnimatedNodeAttachment: function() { | ||
if (this.props.stickyHeaderIndices && this.props.stickyHeaderIndices.length > 0) { | ||
if (!this._scrollAnimatedValueAttachment) { | ||
this._scrollAnimatedValueAttachment = Animated.attachNativeEvent( | ||
this._scrollViewRef, | ||
'onScroll', | ||
[{nativeEvent: {contentOffset: {y: this._scrollAnimatedValue}}}] | ||
); | ||
} | ||
} else { | ||
if (this._scrollAnimatedValueAttachment) { | ||
this._scrollAnimatedValueAttachment.detach(); | ||
} | ||
} | ||
}, | ||
|
||
_setStickyHeaderRef: function(index, ref) { | ||
this._stickyHeaderRefs.set(index, ref); | ||
}, | ||
|
||
_onStickyHeaderLayout: function(index, event) { | ||
if (!this.props.stickyHeaderIndices) { | ||
return; | ||
} | ||
|
||
const previousHeaderIndex = this.props.stickyHeaderIndices[this.props.stickyHeaderIndices.indexOf(index) - 1]; | ||
if (previousHeaderIndex != null) { | ||
const previousHeader = this._stickyHeaderRefs.get(previousHeaderIndex); | ||
previousHeader && previousHeader.setNextHeaderY( | ||
event.nativeEvent.layout.y - event.nativeEvent.layout.height, | ||
); | ||
} | ||
}, | ||
|
||
_handleScroll: function(e: Object) { | ||
if (__DEV__) { | ||
if (this.props.onScroll && this.props.scrollEventThrottle == null && Platform.OS === 'ios') { | ||
|
@@ -470,14 +529,38 @@ const ScrollView = React.createClass({ | |
}; | ||
} | ||
|
||
const {stickyHeaderIndices} = this.props; | ||
const hasStickyHeaders = stickyHeaderIndices && stickyHeaderIndices.length > 0; | ||
|
||
const children = stickyHeaderIndices && hasStickyHeaders ? | ||
React.Children.toArray(this.props.children).map((child, index) => { | ||
const stickyHeaderIndex = stickyHeaderIndices.indexOf(index); | ||
if (child && stickyHeaderIndex >= 0) { | ||
return ( | ||
<ScrollViewStickyHeader | ||
key={index} | ||
ref={(ref) => this._setStickyHeaderRef(index, ref)} | ||
onLayout={(event) => this._onStickyHeaderLayout(index, event)} | ||
scrollAnimatedValue={this._scrollAnimatedValue}> | ||
{child} | ||
</ScrollViewStickyHeader> | ||
); | ||
} else { | ||
return child; | ||
} | ||
}) : | ||
this.props.children; | ||
|
||
const contentContainer = | ||
<View | ||
{...contentSizeChangeProps} | ||
ref={this._setInnerViewRef} | ||
style={contentContainerStyle} | ||
removeClippedSubviews={this.props.removeClippedSubviews} | ||
removeClippedSubviews={ | ||
hasStickyHeaders && Platform.OS === 'android' ? false : this.props.removeClippedSubviews | ||
} | ||
collapsable={false}> | ||
{this.props.children} | ||
{children} | ||
</View>; | ||
|
||
const alwaysBounceHorizontal = | ||
|
@@ -499,23 +582,25 @@ const ScrollView = React.createClass({ | |
// Override the onContentSizeChange from props, since this event can | ||
// bubble up from TextInputs | ||
onContentSizeChange: null, | ||
onTouchStart: this.scrollResponderHandleTouchStart, | ||
onTouchMove: this.scrollResponderHandleTouchMove, | ||
onTouchEnd: this.scrollResponderHandleTouchEnd, | ||
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag, | ||
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag, | ||
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin, | ||
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd, | ||
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder, | ||
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture, | ||
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder, | ||
onScroll: this._handleScroll, | ||
onResponderGrant: this.scrollResponderHandleResponderGrant, | ||
onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest, | ||
onResponderTerminate: this.scrollResponderHandleTerminate, | ||
onResponderRelease: this.scrollResponderHandleResponderRelease, | ||
onResponderReject: this.scrollResponderHandleResponderReject, | ||
onResponderRelease: this.scrollResponderHandleResponderRelease, | ||
onResponderTerminate: this.scrollResponderHandleTerminate, | ||
onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest, | ||
onScroll: this._handleScroll, | ||
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag, | ||
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag, | ||
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder, | ||
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder, | ||
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture, | ||
onTouchEnd: this.scrollResponderHandleTouchEnd, | ||
onTouchMove: this.scrollResponderHandleTouchMove, | ||
onTouchStart: this.scrollResponderHandleTouchStart, | ||
scrollEventThrottle: hasStickyHeaders ? 1 : this.props.scrollEventThrottle, | ||
sendMomentumEvents: (this.props.onMomentumScrollBegin || this.props.onMomentumScrollEnd) ? true : false, | ||
stickyHeaderIndices: null, | ||
}; | ||
|
||
const { decelerationRate } = this.props; | ||
|
@@ -597,10 +682,10 @@ if (Platform.OS === 'android') { | |
sendMomentumEvents: true, | ||
} | ||
}; | ||
AndroidScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps); | ||
AndroidScrollView = requireNativeComponent('RCTScrollView', (ScrollView: ReactClass<any>), nativeOnlyProps); | ||
AndroidHorizontalScrollView = requireNativeComponent( | ||
'AndroidHorizontalScrollView', | ||
ScrollView, | ||
(ScrollView: ReactClass<any>), | ||
nativeOnlyProps | ||
); | ||
} else if (Platform.OS === 'ios') { | ||
|
@@ -612,7 +697,7 @@ if (Platform.OS === 'android') { | |
onScrollEndDrag: true, | ||
} | ||
}; | ||
RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView, nativeOnlyProps); | ||
RCTScrollView = requireNativeComponent('RCTScrollView', (ScrollView: ReactClass<any>), nativeOnlyProps); | ||
} | ||
|
||
module.exports = ScrollView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you even need to create and export this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Native animated events do need a wrapper for ScrollView / ListView this is the reason it is exported here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When is this getter accessed though? I don't see any code in this diff that would call it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't used in this diff but we export it as part of the public api like Animated.View. Had to make this change just because this diff creates a circular reference since ScrollView now imports animated.