Skip to content

Commit

Permalink
Implement sticky headers in JS using Native Animated
Browse files Browse the repository at this point in the history
Summary:
This re-implements sticky headers in JS to make it work on Android.

The only change that was needed was to expose a way to attach a an animated value to an event manually since we can't use the Animated wrapper and `Animated.event` to do it for us because this is implemented directly in the `ScrollView` component. Simply exposed `attachNativeEvent` that takes a ref, event name and event object mapping. This is what is used by `Animated.event`.

TODO:
- Need to check why momentum scrolling isn't triggering scroll events properly on Android.
- Remove native iOS implementation
- cleanup / fix flow

**Test plan**
Test the example list in UIExplorer, test the ListViewPaging example.
Closes #11315

Differential Revision: D4450278

Pulled By: sahrens

fbshipit-source-id: fec8da2cffce9807d74f8e518ebdefeb6a708667
  • Loading branch information
janicduplessis authored and facebook-github-bot committed Mar 2, 2017
1 parent da04a6b commit 77b8c09
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 63 deletions.
1 change: 1 addition & 0 deletions Examples/UIExplorer/js/UIExplorerExampleList.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ const styles = StyleSheet.create({
backgroundColor: '#eeeeee',
},
sectionHeader: {
backgroundColor: '#eeeeee',
padding: 5,
fontWeight: '500',
fontSize: 11,
Expand Down
18 changes: 14 additions & 4 deletions Libraries/Animated/src/Animated.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

module.exports = ((Animated: any): (typeof AnimatedImplementation) & typeof Animated);
86 changes: 53 additions & 33 deletions Libraries/Animated/src/AnimatedImplementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2151,9 +2151,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));
}
}
};

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 = ReactNative.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(
Expand All @@ -2162,6 +2206,7 @@ class AnimatedEvent {
) {
this._argMapping = argMapping;
this._listener = config.listener;
this._attachedEvent = null;
this.__isNative = shouldUseNativeDriver(config);

if (__DEV__) {
Expand All @@ -2172,44 +2217,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 = ReactNative.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() {
Expand Down Expand Up @@ -2582,5 +2596,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,
};
Loading

0 comments on commit 77b8c09

Please sign in to comment.