-
Notifications
You must be signed in to change notification settings - Fork 24.3k
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
Implement sticky headers in JS using Native Animated #11315
Implement sticky headers in JS using Native Animated #11315
Conversation
By analyzing the blame information on this pull request, we identified @sahrens and @emilsjolander to be potential reviewers. |
}; | ||
|
||
Object.assign((Animated: Object), AnimatedImplementation); |
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.
Is there a way to express this properly in flow? I want to add props to an object using Object.assign
. Can't create a new object here because it breaks the getter.
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 not just include AnimatedImplementation
in the initial Object creation?
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.
@sahrens Using { ...AnimatedImplementation, ... }
will compile to Object.assign({}, AnimatedImplementation, ...)
but there seems to be an issue with getters on iOS, the getter doesn't get assigned but it's value is instead, losing the getter altogether.
This is why I used Object.assign(Animated, AnimatedImplementation)
instead to avoid having to copy Animated
props and getters get preserved properly. However doing that causes a bunch of flow issues because we are assigning props that don't exist on the initial object.
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.
I meant as a field, like Animated.AnimatedImplementation.createAnimatedComponent
. Not a big deal.
It's interesting you are into the JS way to implement |
There is no point in keeping the iOS implementation since that will replace it. I tested that it behaves the exact same way so you should not see any difference on iOS. The Android implementation required a lot of changes because the view hierachy in react is different from the one in native because of the view hierarchy optimization that we do on Android so working with view indices doesn't work well at all. It would have been possible but after thinking about it more and now that native animations are a lot more stable this makes more sense. |
Also it uses native animations so it won't really be slower than the native implementation if that was your concern. |
I know that and great thanks for your works on this 👍 , and I have a new issue for your 😄 |
@nihgwu - if it is very important to you, you're welcome to contribute too ;) no need to wait for other people to do things for you |
++ Also another way to contribute is to sponsor development of the feature. |
I'd love to contribute but I know nothing about native development though ?
So for myself the best way to contribute is to use and to test it even it hasn't be shipped into the master branch
I'm switching all the animations to be driven in my apps now, although there are still some issues but I'm trying my best to work them out
BTW, I did have a pure js version workaround for sticky header on Android on my own way before, but I thought it was not good enough so I dropped it out
? 2016?12?8??06:04?Brent Vatne <notifications@github.com<mailto:notifications@github.com>> ???
@nihgwu<https://github.com/nihgwu> - if it is very important to you, you're welcome to contribute too ;) no need to wait for other people to do things for you
-
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<#11315 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/ACeY8k0aeGkvAyV7M3EET3j6M39kaZDsks5rFy1YgaJpZM4LE_sl>.
|
any progress on this? |
Nice work @janicduplessis! 👍 I've implemented sticky headers with a similar technique as the one you used here (but not as generic as what you did) and noticed the issue with momentum scrolling on Android. No issue on iOS though. Any hint on where to start tracking this issue? I'm very confortable with iOS development, but quite a beginner on Android. |
Yea that is mainly what is blocking this PR at the moment, I will try to have a look at this this week. |
some bugs on iOS:
on Android:
|
Interesting @nihgwu. |
Yea I think the bug with layout animation is on Android only and has always been there, more or less related to this PR. |
sorry, my mistake, layout animation issue exists on Android only |
I've to say that I've tried to solve the momentum issue, but seems |
Node is still experimental and will eventually support z-index so I'm not too worried at the moment. |
@ide: are you or anyone else reviewing this? |
@sahrens I don't think anyone is reviewing this at the moment, right now iOS works really well but android has a few bugs with events during momentum scrolling and view clipping. The JS code shouldn't change so feel free to review if you want :) |
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 is looking sweet :)
How are those known bugs going? Are you planning on fixing them in this diff or followups? How much have you tested this?
}; | ||
|
||
Object.assign((Animated: Object), AnimatedImplementation); |
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 not just include AnimatedImplementation
in the initial Object creation?
View: AnimatedImplementation.createAnimatedComponent(View), | ||
Text: AnimatedImplementation.createAnimatedComponent(Text), | ||
Image: AnimatedImplementation.createAnimatedComponent(Image), | ||
ScrollView: AnimatedImplementation.createAnimatedComponent(ScrollView), | ||
get 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.
@@ -2152,6 +2196,7 @@ class AnimatedEvent { | |||
) { | |||
this._argMapping = argMapping; | |||
this._listener = config.listener; | |||
this._attachment = null; |
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.
_attachment
is a non-intuitive name for me - something like _attachedEvent
or _nativeEvent
might be better?
ref={(ref) => this._setStickyHeaderRef(index, ref)} | ||
onLayout={(event) => this._onStickyHeaderLayout(index, event)} | ||
scrollAnimatedValue={this._scrollAnimatedValue} | ||
> |
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.
nit: should stay on prev line
}, | ||
|
||
_onStickyHeaderLayout: function(index, event) { | ||
const previousHeaderIndice = this.props.stickyHeaderIndices[this.props.stickyHeaderIndices.indexOf(index) - 1]; |
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.
s/Indice/Index
inputRange.push(layoutY + 1); | ||
outputRange.push(1); | ||
} | ||
translateY = this.props.scrollAnimatedValue.interpolate({ |
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.
All this interpolate range computation is pretty complicated and non-intuitive - can you add some comments, maybe a comment block, explaining the logic?
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.
Added a comment, let me know if that makes sense.
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.
Very helpful.
|
||
const styles = StyleSheet.create({ | ||
header: { | ||
zIndex: 10, |
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.
Anything less arbitrary/more robust you can do 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.
I don't really have other ideas except maybe exporting this as a constant somewhere.
const hasStickyHeaders = this.props.stickyHeaderIndices && this.props.stickyHeaderIndices.length > 0; | ||
|
||
const children = hasStickyHeaders ? | ||
React.Children.toArray(this.props.children).map((child, index) => { |
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 not React.Children.map?
https://facebook.github.io/react/docs/react-api.html#react.children.map
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.
I did try using React.Children.map but it doesn't work, looks like indices are different. My guess is that toArray handles null / nested children differently and that's the behavior we want here.
8ee2482
to
26e8c5a
Compare
Ok, I figured out the lag during momentum scrolling on android, fix in #11994 |
@@ -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 comment
The 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
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
call of method indexOf
Method cannot be called on possibly undefined value undefined
@sahrens I noticed you talked about FlatList not supporting sections and creating a separate component that support this. I was thinking we could just hold off on this PR and keep the native iOS only impl for ListView and move to something like this for the new FlatList that support sections. |
I believe FlatList is orthogonal - it's still just going to set stickyHeaderIndices on the ScrollView, and it can work with the current impl or the Animated-based one here (I just wanted to make sure it works with this approach since this is probably the future). The main value of these diffs is to add Android support for sticky headers, which isn't that important to us internally. @astreet/ @mkonicek: any progess on those test failures? |
Summary: Native animated events sometimes end up lagging a frame behind on android because we perform the update in the normal animation loop instead of doing it immediately when we receive the event. We had the same issue on iOS and was fixed in a similar way. Moved some code around to have a method that updates a list of node that we can use to update the node in the animated event handler and also use it in the animation update loop. **Test plan** Tested that it did fix sticky headers lagging a frame behind during momentum scrolling in my PR #11315 and also tested the native animations examples still work properly. Closes #11994 Reviewed By: mkonicek Differential Revision: D4488977 Pulled By: sahrens fbshipit-source-id: 831a1565bc7b8fa88cadd5a8c1be876fbdefbf66
Summary: Native animated events sometimes end up lagging a frame behind on android because we perform the update in the normal animation loop instead of doing it immediately when we receive the event. We had the same issue on iOS and was fixed in a similar way. Moved some code around to have a method that updates a list of node that we can use to update the node in the animated event handler and also use it in the animation update loop. **Test plan** Tested that it did fix sticky headers lagging a frame behind during momentum scrolling in my PR facebook#11315 and also tested the native animations examples still work properly. Closes facebook#11994 Reviewed By: mkonicek Differential Revision: D4488977 Pulled By: sahrens fbshipit-source-id: 831a1565bc7b8fa88cadd5a8c1be876fbdefbf66
Summary: Native animated events sometimes end up lagging a frame behind on android because we perform the update in the normal animation loop instead of doing it immediately when we receive the event. We had the same issue on iOS and was fixed in a similar way. Moved some code around to have a method that updates a list of node that we can use to update the node in the animated event handler and also use it in the animation update loop. **Test plan** Tested that it did fix sticky headers lagging a frame behind during momentum scrolling in my PR facebook#11315 and also tested the native animations examples still work properly. Closes facebook#11994 Reviewed By: mkonicek Differential Revision: D4488977 Pulled By: sahrens fbshipit-source-id: 831a1565bc7b8fa88cadd5a8c1be876fbdefbf66
Summary: Native animated events sometimes end up lagging a frame behind on android because we perform the update in the normal animation loop instead of doing it immediately when we receive the event. We had the same issue on iOS and was fixed in a similar way. Moved some code around to have a method that updates a list of node that we can use to update the node in the animated event handler and also use it in the animation update loop. **Test plan** Tested that it did fix sticky headers lagging a frame behind during momentum scrolling in my PR facebook#11315 and also tested the native animations examples still work properly. Closes facebook#11994 Reviewed By: mkonicek Differential Revision: D4488977 Pulled By: sahrens fbshipit-source-id: 831a1565bc7b8fa88cadd5a8c1be876fbdefbf66
Finally trying to land this again...wish it luck! |
land to where? |
Fixed the internal test this was breaking so this should be in master soon! |
🎉 Thanks @sahrens for working on landing this! |
@sahrens I just recalled that when landing the first version of this that was implemented natively (and that was reverted because of some bugs), having sticky headers enabled by default in ListView caused some issues for product teams at FB, since it's a breaking change in term of UI behavior. I think a good solution would be to make sticky section headers disabled by default on Android and enabled on iOS (https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/ListView/ListView.js#L333). |
Ah yeah, totally forgot we were going to make that change. |
Change to the default should go out soon. |
@sahrens Cool, thanks! |
Hmm, this might be causing some crashes - the interpolation range isn't always monotonic, maybe if the scroll offset is negative? Need to investigate further, might have to revert...
On Mar 2, 2017, at 8:39 PM, Janic Duplessis <notifications@github.com<mailto:notifications@github.com>> wrote:
@sahrens<https://github.com/sahrens> Cool, thanks!
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<#11315 (comment)>, or mute the thread<https://github.com/notifications/unsubscribe-auth/ABcJx_HnMl3LcMaHx9Yy8gB0FerOGFsSks5rh5ljgaJpZM4LE_sl>.
|
Hmm there might be cases where headers overlap? We'll have to safeguard against negative interpolations. |
Seeing this in some tests:
|
Looks like some headers have negative y coords, the code currently doesn't handle these properly. https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/ScrollViewStickyHeader.js#L63 |
I'm not sure if just making sure the y coord is > 0 would work properly, let me know if you find what the failing test looks like. |
Summary: They aren't normal for android, so don't make that the default. some more context: #11315 (comment) Reviewed By: furdei Differential Revision: D4648714 fbshipit-source-id: 3232a6914e3db82c2b300409663ce63412d892a6
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 facebook/react-native#11315 Differential Revision: D4450278 Pulled By: sahrens fbshipit-source-id: fec8da2cffce9807d74f8e518ebdefeb6a708667
is the latest commit still failing test? Are sticky list section header working in android? |
This should be all fixed and landed in master slated for v44 release around April. Note that behavior is slightly changed for headers because we wrap them in an Animated.View, so for example onLayout inside your header may give different results after this change. |
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 facebook#11315 Differential Revision: D4450278 Pulled By: sahrens fbshipit-source-id: fec8da2cffce9807d74f8e518ebdefeb6a708667
Summary: They aren't normal for android, so don't make that the default. some more context: facebook#11315 (comment) Reviewed By: furdei Differential Revision: D4648714 fbshipit-source-id: 3232a6914e3db82c2b300409663ce63412d892a6
This appears to have been merged and released in 0.44. Is there any reason this (rewrite/Android support) was not mentioned in the release notes? |
Probably slipped through, let me see if I can edit release notes. |
Works perfectly on Android and iOS. |
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 theScrollView
component. Simply exposedattachNativeEvent
that takes a ref, event name and event object mapping. This is what is used byAnimated.event
.TODO:
Test plan
Test the example list in UIExplorer, test the ListViewPaging example.