Skip to content
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

[ScrollView] Scrolling ends prematurely in ScrollViews embedded in a parent ScrollView #41

Closed
jordanna opened this issue Feb 4, 2015 · 74 comments
Assignees
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@jordanna
Copy link
Contributor

jordanna commented Feb 4, 2015

You can see this behavior in the UIExplorer app's <ScrollView> example. Fling to initiate a scroll in an inner ScrollView and fling again (before scrolling ends) to increase scroll momentum. Often times, this causes the scrolling to stop.

I haven't had gone through all of the ResponderEventPlugin code yet, but I'm seeing that when ResponderEventPlugin.extractEvents is called, the topLevelTargetID is sometimes incorrect.

The touches that unintentionally stop scrolling appear to be associated with the parent ScrollView instead of the inner ScrollView's content. I traced this down to RTCScrollView._shouldDisableScrollInteraction where [self isDescendantOfView:JSResponder] is sometimes YES. Forcing it to always return NO fixes the issue, but I'm pretty sure that's not the intention of this method :)

@vjeux
Copy link
Contributor

vjeux commented Feb 4, 2015

Cc @jordwalke

@jordwalke
Copy link

Great investigation, @jordanna. The information is very helpful. Scroll views in native know to stop scrolling if something that contains them becomes the responder in JS (like if you were dragging an outer modal view down - that should stop any scroll views from scrolling). My question is, why is the outer scroll view becoming the JS responder? One thing that would cause this is an onScroll occurring on the outer scroll view.
(This is why I just wish all the scroll views were implemented in JS - all these problems go away).

@jordanna
Copy link
Contributor Author

jordanna commented Feb 7, 2015

Ok, I finally got around to looking more into this issue (was sidetracked by animation hacks!). During event extraction in the ResponderEventPlugin, I'm seeing JS responder being transferred from the inner scroller to the outer on a topScroll event.

function setResponderAndExtractTransfer(
    topLevelType,
    topLevelTargetID,
    nativeEvent) {

  // topLevelType === 'topScroll'
  // topLevelTargetID === ID of inner scroller
  ...

  // bubbleShouldSetFrom is true here, responderID === topLevelTargetID
  // i.e. current JS responder is still inner scroller
  var bubbleShouldSetFrom = !responderID ?
    topLevelTargetID :
    ReactInstanceHandles._getFirstCommonAncestorID(responderID, topLevelTargetID);

 ...

  if (skipOverBubbleShouldSetFrom) {
    EventPropagators.accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
    // shouldSetEvent._dispatchIDs === outer scroller (skipped over inner scroller?),
    // these were possibly from 'touchmove' that occurred immediately before this 'topScroll' event
  } else {
    EventPropagators.accumulateTwoPhaseDispatches(shouldSetEvent);
  }
  var wantsResponderID = executeDispatchesInOrderStopAtTrue(shouldSetEvent);

  // By the time it gets here, wantResponderID is for outer scroller

Could this be a timing issue with touch moves and scrolls? ScrollResponder.scrollResponderHandleTouchMove is consistently invoked for both inner and outer scrollers before ResponderEventPlugin.extractEvent for a scroll.

@ericf
Copy link

ericf commented Feb 7, 2015

I've also been running into this issue. The setup I have is an outer horizontal/paging <ScrollView> with several inner vertical <ListView>s. When the <ListView>s are nested in the outer <ScrollView> I'm seeing the following warning logged when dragging the inner one:

Warning: ScrollView doesn't take rejection well - scrolls anyway

The adverse affects I'm seeing on the expected scrolling behavior of the inner <ScrollView>s is they're not respecting tapping the status bar to scroll to top, and scrolling is "sticking" to my finger when trying to continuously flick the list — the same issue that @jordanna is running into.

@jordwalke
Copy link

I'll look more into this in the coming week, but I believe the correct way to correctly integrate is to do all enabling/disabling of the scroll view completely from JS (it should start out disabled and then become enabled when JS determines it should become the JS responder). This is actually incredibly difficult because UIKit's scroll view doesn't like to work this way and it might involve some trickery.

@milanvanschaik
Copy link

Any updates on this one?

@jordwalke
Copy link

@nicklockwood: Do you know of a way to have UIScrollViews be disabled by default and then reenabled?

@nicklockwood
Copy link
Contributor

You could set the scrollEnabled property to NO, or in the viewDidScrollDelegate you could reset the contentOffset to th previous value.

@jordwalke
Copy link

I tried both of those options. In the former, you cannot reenable it after touches have began, which is what would allow this issue to be fixed (and all future scroll view conflicts). I also tried the later suggestion, but I believe the momentum has issues in that case.

@nick
Copy link

nick commented Apr 28, 2015

Running into this issue too - I don't suppose there's been any progress here?

@jordwalke
Copy link

@vjeux, this is the kind of troubles we're having with native scroll views. The responder system solves it and I don't know how to solve it while using the UIScrollViews.

@nicholasstephan
Copy link

Any movement on this one? I have a similar example up on stack overflow (http://stackoverflow.com/questions/29756217/react-native-nested-scrollview-locking-up) that nobody has been able to answer.

@Shuangzuan
Copy link
Contributor

@nicholasstephan Modify node_modules/react-native/Libraries/Components/ScrollResponder.js: Line 136 (See UPDATE):

scrollResponderHandleScrollShouldSetResponder: function(): boolean {
  return this.state.isTouching;
},

UPDATE: I find if the scroll view is currently animating and wants to become the responder, then it will reject. Line 189 in ScrollResponder.js. So I modify Line 340 and it work for me:

scrollResponderIsAnimating: function(): boolean {
  // var now = Date.now();
  // var timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
  // var isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
  //   this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
  // return isAnimating;
    return false;
},

@brentvatne brentvatne changed the title Scrolling ends prematurely in ScrollViews embedded in a parent ScrollView [ScrollView] Scrolling ends prematurely in ScrollViews embedded in a parent ScrollView May 31, 2015
@magicismight
Copy link
Contributor

Is there any progress here?

@sophiebits
Copy link
Contributor

Maybe with subclassing, you can make a disableOffsetUpdates like in the gist linked here:

https://twitter.com/andy_matuschak/status/559796157241507841

Not sure if that introduces significant lag.

@sahrens
Copy link
Contributor

sahrens commented Jun 12, 2015

what do you think, @ide? You seem to have the most experience with nesting and composing scroll views...

@PhilippKrone
Copy link
Contributor

@ide @sahrens Any movement on this issue? I think i have exactly the same problem.

@ide
Copy link
Contributor

ide commented Jun 22, 2015

I'm not actively looking into this. Based on what I've heard from other people and from reading this discussion, the solution could be quite hard or non-obvious without advanced knowledge of gesture systems. I have a thought regarding some of the discussion above...

Do you know of a way to have UIScrollViews be disabled by default and then reenabled?

Might be possible through the gesture recognizers by adding a custom gesture recognizer that communicates with the JS layer. I think you can tell UIScrollView's private gesture recognizers to need this custom gesture recognizer to fail -- this might let JS allow/disallow the private gesture recognizers to become active.

@sahrens
Copy link
Contributor

sahrens commented Jun 23, 2015

I think @andreic has been looking at this a bit?

@sghiassy
Copy link

I'm having this issue as well. Any insights into which files to start looking into first?

@Shimaa-Hassan
Copy link

any solution?

@brentvatne
Copy link
Collaborator

ping @andreic 😄 we need you

@bradens
Copy link

bradens commented Jul 17, 2015

+1 Also hitting this nasty one. Would look into it if I had more experience with gesture systems.

@amormysh
Copy link

+1

1 similar comment
@felixakiragreen
Copy link
Contributor

👍

@mkonicek
Copy link
Contributor

Github issues are not working well for us - we are a very small team and and need a way to prioritize. A good way to prioritize is to let people vote on issues and Product Pains lets you do exactly that.

You can get an estimate by sorting github issues by the number of comments but it's not that reliable: https://github.com/facebook/react-native/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc

@toddw
Copy link

toddw commented Nov 17, 2015

@brentvatne @ide @mkonicek fair enough, thanks for the explanations

@brentvatne
Copy link
Collaborator

@toddw - no problem :) thanks for weighing in on ProductPains!

@arianitu
Copy link

This has been fixed in this commit 67bf0f1 and should be coming to 0.17. Yay!

https://productpains.com/post/react-native/scrolling-ends-prematurely-in-scrollviews-embedded-in-a-parent-scrollview/

@toddw
Copy link

toddw commented Dec 10, 2015

So stoked to see this fixed! Thanks so much!

@aksonov
Copy link

aksonov commented Feb 16, 2016

@arianitu I still see this issue in latest 0.19, why?

@Taakn
Copy link

Taakn commented Feb 16, 2016

+1 I still get it

@pppluto
Copy link

pppluto commented Mar 1, 2016

+1

1 similar comment
@jalliance
Copy link

+1

@maraujop
Copy link

I see this happenning in 0.20 too

@neverlan
Copy link

+1 seeing this issue too

@faceyspacey
Copy link

+1 still seeing this issue. is there really no workarounds? Disabling scrolling on the parent causes lots of jerkiness.

@HaviLee
Copy link

HaviLee commented Apr 6, 2016

+1,I see this issue in 0.22 too

@sandect
Copy link

sandect commented Apr 13, 2016

+1,I see this issue in 0.22 too

@YashYash
Copy link

+1 still seeing it in .20 as well.

dustturtle added a commit to dustturtle/react-native that referenced this issue Jul 6, 2016
…crash on simulator, on device I got nothing but app freezed)!

My app has an old version of JSONKit which is still using MRC. I think JSONKit is not needed if system version is available. Kicking out of JSONKit will make react native stronger.
Crash stack:
* thread facebook#11: tid = 0xbd672f, 0x000000010a10edeb imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=22 key/value pairs) + 16971 at JSONKit.m:2807, name = 'com.facebook.React.JavaScript', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000000010a10edeb imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=22 key/value pairs) + 16971 at JSONKit.m:2807
    frame facebook#1: 0x000000010a10ef67 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=2 key/value pairs) + 17351 at JSONKit.m:2811
    frame facebook#2: 0x000000010a10ef67 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=25 key/value pairs) + 17351 at JSONKit.m:2811
    frame facebook#3: 0x000000010a10e768 imobii-waiqin`jk_encode_add_atom_to_buffer(encodeState=0x00007f9b820a1000, objectPtr=@"3 elements") + 15304 at JSONKit.m:2778
  * frame facebook#4: 0x000000010a10a26a imobii-waiqin`-[JKSerializer serializeObject:options:encodeOption:block:delegate:selector:error:](self=0x00007f9b831fbc80, _cmd="serializeObject:options:encodeOption:block:delegate:selector:error:", object=@"3 elements", optionFlags=0, encodeOption=10, block=0x0000000000000000, delegate=0x0000000000000000, selector=<no value available>, error=domain: class name = NSInvocation - code: 0) + 2250 at JSONKit.m:2876
    frame facebook#5: 0x000000010a109992 imobii-waiqin`+[JKSerializer serializeObject:options:encodeOption:block:delegate:selector:error:](self=JKSerializer, _cmd="serializeObject:options:encodeOption:block:delegate:selector:error:", object=@"3 elements", optionFlags=0, encodeOption=10, block=0x0000000000000000, delegate=0x0000000000000000, selector=<no value available>, error=domain: class name = NSInvocation - code: 0) + 178 at JSONKit.m:2831
    frame facebook#6: 0x000000010a10f700 imobii-waiqin`-[NSArray(self=@"3 elements", _cmd="JSONStringWithOptions:error:", serializeOptions=0, error=domain: class name = NSInvocation - code: 0) JSONStringWithOptions:error:] + 112 at JSONKit.m:2985
    frame facebook#7: 0x000000010ac13c02 imobii-waiqin`_RCTJSONStringifyNoRetry(jsonObject=@"3 elements", error=domain: class name = NSInvocation - code: 0) + 338 at RCTUtils.m:49
    frame facebook#8: 0x000000010ac13990 imobii-waiqin`RCTJSONStringify(jsonObject=@"3 elements", error=0x0000000000000000) + 128 at RCTUtils.m:77
    frame facebook#9: 0x000000010ab5fdfa imobii-waiqin`__27-[RCTContextExecutor setUp]_block_invoke_2(.block_descriptor=<unavailable>, moduleName=@"UIManager") + 218 at RCTContextExecutor.m:363
    frame facebook#10: 0x00000001134495cc CoreFoundation`__invoking___ + 140
    frame facebook#11: 0x000000011344941e CoreFoundation`-[NSInvocation invoke] + 286
    frame facebook#12: 0x000000010db13db3 JavaScriptCore`JSC::ObjCCallbackFunctionImpl::call(JSContext*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) + 451
    frame facebook#13: 0x000000010db13926 JavaScriptCore`JSC::objCCallbackFunctionCallAsFunction(OpaqueJSContext const*, OpaqueJSValue*, OpaqueJSValue*, unsigned long, OpaqueJSValue const* const*, OpaqueJSValue const**) + 262
    frame facebook#14: 0x000000010db14bad JavaScriptCore`long long JSC::APICallbackFunction::call<JSC::ObjCCallbackFunction>(JSC::ExecState*) + 573
    frame facebook#15: 0x000000010dade340 JavaScriptCore`JSC::LLInt::setUpCall(JSC::ExecState*, JSC::Instruction*, JSC::CodeSpecializationKind, JSC::JSValue, JSC::LLIntCallLinkInfo*) + 528
    frame facebook#16: 0x000000010dae535d JavaScriptCore`llint_entry + 22900
    frame facebook#17: 0x000000010dadf7d9 JavaScriptCore`vmEntryToJavaScript + 326
    frame facebook#18: 0x000000010d9b1959 JavaScriptCore`JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 169
    frame facebook#19: 0x000000010d9985ad JavaScriptCore`JSC::Interpreter::executeCall(JSC::ExecState*, JSC::JSObject*, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 493
    frame facebook#20: 0x000000010d76cb7e JavaScriptCore`JSC::call(JSC::ExecState*, JSC::JSValue, JSC::CallType, JSC::CallData const&, JSC::JSValue, JSC::ArgList const&) + 62
    frame facebook#21: 0x000000010d929a55 JavaScriptCore`JSC::callGetter(JSC::ExecState*, JSC::JSValue, JSC::JSValue) + 149
    frame facebook#22: 0x000000010dad49fb JavaScriptCore`llint_slow_path_get_by_id + 2203
    frame facebook#23: 0x000000010dae22f0 JavaScriptCore`llint_entry + 10503
    frame facebook#24: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#25: 0x000000010dae52fd JavaScriptCore`llint_entry + 22804
    frame facebook#26: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#27: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#28: 0x000000010dae52fd JavaScriptCore`llint_entry + 22804
    frame facebook#29: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#30: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#31: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#32: 0x000000010dae552a JavaScriptCore`llint_entry + 23361
    frame facebook#33: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#34: 0x000000010dae5368 JavaScriptCore`llint_entry + 22911
    frame facebook#35: 0x000000010dadf7d9 JavaScriptCore`vmEntryToJavaScript + 326
    frame facebook#36: 0x000000010d9b1959 JavaScriptCore`JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) + 169
    frame facebook#37: 0x000000010d998264 JavaScriptCore`JSC::Interpreter::execute(JSC::ProgramExecutable*, JSC::ExecState*, JSC::JSObject*) + 10404
    frame facebook#38: 0x000000010d7a8786 JavaScriptCore`JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&) + 470
    frame facebook#39: 0x000000010d9f6fb8 JavaScriptCore`JSEvaluateScript + 424
    frame facebook#40: 0x000000010ab6379e imobii-waiqin`__68-[RCTContextExecutor executeApplicationScript:sourceURL:onComplete:]_block_invoke.264(.block_descriptor=<unavailable>) + 414 at RCTContextExecutor.m:589
    frame facebook#41: 0x000000010ab63262 imobii-waiqin`__68-[RCTContextExecutor executeApplicationScript:sourceURL:onComplete:]_block_invoke(.block_descriptor=<unavailable>) + 498 at RCTContextExecutor.m:589
    frame facebook#42: 0x000000010ab63df8 imobii-waiqin`-[RCTContextExecutor executeBlockOnJavaScriptQueue:](self=0x00007f9b832f6040, _cmd="executeBlockOnJavaScriptQueue:", block=0x00007f9b80c92970) + 248 at RCTContextExecutor.m:627
    frame facebook#43: 0x000000010eb1d7a7 Foundation`__NSThreadPerformPerform + 283
    frame facebook#44: 0x0000000113486301 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame facebook#45: 0x000000011347c22c CoreFoundation`__CFRunLoopDoSources0 + 556
    frame facebook#46: 0x000000011347b6e3 CoreFoundation`__CFRunLoopRun + 867
    frame facebook#47: 0x000000011347b0f8 CoreFoundation`CFRunLoopRunSpecific + 488
    frame facebook#48: 0x000000010ab5e41b imobii-waiqin`+[RCTContextExecutor runRunLoopThread](self=RCTContextExecutor, _cmd="runRunLoopThread") + 363 at RCTContextExecutor.m:284
    frame facebook#49: 0x000000010ebc012b Foundation`__NSThread__start__ + 1198
    frame facebook#50: 0x00000001140869b1 libsystem_pthread.dylib`_pthread_body + 131
    frame facebook#51: 0x000000011408692e libsystem_pthread.dylib`_pthread_start + 168
    frame facebook#52: 0x0000000114084385 libsystem_pthread.dylib`thread_start + 13
@dgurns
Copy link

dgurns commented Feb 14, 2017

Still seeing the same issue in 0.41!

@joaosilva05
Copy link

+1, also happening in 0.41

@bintoll
Copy link

bintoll commented Sep 2, 2017

+1, 0.46.1

@shukerullah
Copy link

+1, 0.48.3

@binchik
Copy link

binchik commented Dec 18, 2017

+1, 0.51.0

@lishoulong
Copy link

+1, 0.50.3, And I try the following way ,but not use.

UPDATE: I find if the scroll view is currently animating and wants to become the responder, then it will reject. Line 189 in ScrollResponder.js. So I modify Line 340 and it work for me:

scrollResponderIsAnimating: function(): boolean {
// var now = Date.now();
// var timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
// var isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
// this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
// return isAnimating;
return false;
},

@koloff
Copy link

koloff commented Feb 25, 2018

+1 0.53.0

@lumiasaki
Copy link

lumiasaki commented Mar 19, 2018

+1 0.52.0

My scenario is using a root view in a native view controller as a separated view on iOS platform, the root view contains a <ScrollView/> component as container view, the items which in scrollview are <TouchableWithoutFeedback/>. When I scroll the scrollview, before the scrollViewDidEndDecelerating: function of UIScrollView been invoked, I press some button pushing the current view controller to another one, the scrollViewDidEndDecelerating: function will never been invoked.

When I pop back to the previous view controller, I press down the <TouchableWithoutFeedback/> in <ScrollView/> will have no effect, because the scrollResponderIsAnimating function in ScrollResponder.js, will return true because of this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime( last momentum scroll end time didn't set properly because scrollViewDidEndDecelerating: in UIScrollView not been invoked ) , the gesture system thought that scrollview was still animating, and then the scrollview will capture the response event, not pass it to <TouchableWithoutFeedback/>.

My solution is return false from scrollResponderIsAnimating directly, but I'm still looking for a better solution.

@facebook facebook locked as resolved and limited conversation to collaborators Jul 23, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jul 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests