facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
117.98k stars 24.17k forks source link

LayoutAnimation crashes on Android #25832

Closed gdoudeng closed 4 years ago

gdoudeng commented 5 years ago

React Native version:0.60.4 phone:OPPO PAAM00 android 9.0 System: OS: Windows 10 CPU: (4) x64 Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz Memory: 7.47 GB / 19.89 GB Binaries: Node: 10.14.1 - C:\Program Files\nodejs\node.EXE Yarn: 1.13.0 - ~\AppData\Roaming\npm\yarn.CMD npm: 6.9.0 - ~\AppData\Roaming\npm\npm.CMD Watchman: 4.9.4 - G:\Users\ASUS\watchman\watchman.EXE SDKs: Android SDK: Android NDK: 19.0.5232133

Steps To Reproduce

1.UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); 2.LayoutAnimation.spring(); 3.this.setState to Update view

Describe what you expected to happen: Render layout animation

Not all layout animations are abnormal. Only some layout animations are abnormal. Sometimes some views disappear after executing the layout animation, and then the program crashes when executed again.But normal in version 0.59.10.

Snack, code example, screenshot, or link to a repository:

adb logcat show this

2019-07-26 14:02:00.744 26524-26524/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.reactproject, PID: 26524
    java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewParent android.view.View.getParent()' on a null object reference
        at com.facebook.react.uimanager.layoutanimation.LayoutAnimationController.shouldAnimateLayout(LayoutAnimationController.java:89)
        at com.facebook.react.uimanager.NativeViewHierarchyManager.manageChildren(NativeViewHierarchyManager.java:445)
        at com.facebook.react.uimanager.UIViewOperationQueue$ManageChildrenOperation.execute(UIViewOperationQueue.java:228)
        at com.facebook.react.uimanager.UIViewOperationQueue$1.run(UIViewOperationQueue.java:844)
        at com.facebook.react.uimanager.UIViewOperationQueue.flushPendingBatches(UIViewOperationQueue.java:952)
        at com.facebook.react.uimanager.UIViewOperationQueue.access$2200(UIViewOperationQueue.java:44)
        at com.facebook.react.uimanager.UIViewOperationQueue$DispatchUIFrameCallback.doFrameGuarded(UIViewOperationQueue.java:1012)
        at com.facebook.react.uimanager.GuardedFrameCallback.doFrame(GuardedFrameCallback.java:29)
        at com.facebook.react.modules.core.ReactChoreographer$ReactChoreographerDispatcher.doFrame(ReactChoreographer.java:172)
        at com.facebook.react.modules.core.ChoreographerCompat$FrameCallback$1.doFrame(ChoreographerCompat.java:84)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1022)
        at android.view.Choreographer.doCallbacks(Choreographer.java:836)
        at android.view.Choreographer.doFrame(Choreographer.java:760)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1010)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:232)
        at android.app.ActivityThread.main(ActivityThread.java:7154)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:576)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)
2019-07-26 14:02:00.885 8889-8889/? E/PhoneState: iconId is 0android.widget.ImageView{65ff7d0 G.ED..... ......I. 0,0-13,36 #7f0a0116 app:id/data_inout}
2019-07-26 14:02:00.886 8889-8889/? E/PhoneState: iconId is 0android.widget.ImageView{f3e13c9 G.ED..... ......I. 0,0-0,0 #7f0a0116 app:id/data_inout}
2019-07-26 14:02:00.927 768-768/? E/lowmemorykiller: Error writing /proc/26524/oom_score_adj; errno=22
2019-07-26 14:02:01.062 2044-2489/? E/InputDispatcher: channel 'ab4bf97 com.reactproject/com.reactproject.MainActivity (server)' ~ Channel is unrecoverably broken and will be disposed!
2019-07-26 14:02:01.365 2044-27130/? E/Environment: Path requests must specify a user by using UserEnvironment
    java.lang.Throwable
        at android.os.Environment.throwIfUserRequired(Environment.java:1076)
        at android.os.Environment.getExternalStorageDirectory(Environment.java:516)
        at com.android.server.am.ColorEapUtils.isLowStorage(ColorEapUtils.java:951)
        at com.android.server.am.ColorEapUtils.collectFile(ColorEapUtils.java:533)
        at com.android.server.DropBoxManagerService.add(DropBoxManagerService.java:404)
        at com.android.server.DropBoxManagerService$2.add(DropBoxManagerService.java:175)
        at android.os.DropBoxManager.addText(DropBoxManager.java:283)
        at com.android.server.am.ActivityManagerService$27.run(ActivityManagerService.java:18440)
2019-07-26 14:02:01.985 7457-11733/? E/SceneService.AdUploadResponseData: parseJson, ret = 1008, data = null; errorMsg = KX85ODP-1564120921457#nearbyInfoResponse is null

0.60.4 show this The bottom cell disappeared.Then the program crashes Aaron Swartz

0.59.10 is normal Aaron Swartz

msinghus96 commented 5 years ago

For hotfix just wrap your component inside parent view(If your are using conditional rendering make sure wrap that view outside that condition. This will fix that issue..

jomla97 commented 4 years ago

My project is suffering from this at the moment. The solution by @msinghus96 isn't working for me unfortunately.

For hotfix just wrap your component inside parent view(If your are using conditional rendering make sure wrap that view outside that condition. This will fix that issue..

Any other ideas?

msinghus96 commented 4 years ago

My project is suffering from this at the moment. The solution by @msinghus96 isn't working for me unfortunately.

For hotfix just wrap your component inside parent view(If your are using conditional rendering make sure wrap that view outside that condition. This will fix that issue..

Any other ideas?

Can you share your code.

jomla97 commented 4 years ago

@msinghus96 Here's the component using the experimental LayoutAnimation:

import Text from './Text.js';
import SectionButton from './ExpandableCard/SectionButton.js';
import SectionListRowButton from './ExpandableCard/SectionListRowButton.js';

type Props = {};
export default class ExpandableCard extends Component<Props>{
  constructor(props) {
    super(props);

    this.state = {
      expanded: false
    };

    if(Platform.OS === "android"){
      if(UIManager.setLayoutAnimationEnabledExperimental){
        UIManager.setLayoutAnimationEnabledExperimental(true);
      }
    }
  }

  componentDidUpdate(prevProps, prevState){
    if(this.props.expandable === false && this.state.expanded){
      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
      this.setState({expanded: false});
    }
  }

  toggleExpanded(){
    if(this.props.expandable !== false){
      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
      this.setState({expanded: !this.state.expanded});
    }
  }

  render(){
    return (
      <TouchableWithoutFeedback onPress={() => this.toggleExpanded()}>
        <View style={Styles.ExpandableCard.wrapper}>
          {
            !this.state.expanded ?
            <View style={Styles.ExpandableCard.retractedView}>
              <Text style={Styles.ExpandableCard.retractedViewTitle}>{this.props.retractedTitle}</Text>
              <View style={Styles.ExpandableCard.retractedViewIconsWrapper}>
                {
                  this.props.retractedIcons !== undefined && this.props.retractedIcons.length > 0 ?
                  this.props.retractedIcons.map((icon, key) => {
                    var IconLibrary = icon.library;

                    return (
                      <IconLibrary key={key} name={icon.name} style={Styles.ExpandableCard.retractedViewIcon}/>
                    );
                  })
                  : null
                }
              </View>
            </View>
            :
            <View style={{height: this.state.expanded ? null : 0}}>
              {
                this.props.sections.map((section, sectionKey) => (
                  <View key={sectionKey} style={Styles.ExpandableCard.expandedViewSection}>
                    <View style={Styles.ExpandableCard.expandedViewSectionTitleWrapper}>
                      <Text style={Styles.ExpandableCard.expandedViewSectionTitle}>{section.title}</Text>
                      <View style={Styles.ExpandableCard.expandedViewSectionButtonWrapper}>
                        {
                          section.buttons !== undefined && section.buttons !== null ?
                          section.buttons.map((button, key) => (<SectionButton key={key} {...button}/>))
                          : null
                        }
                      </View>
                    </View>
                    <View style={Styles.ExpandableCard.expandedViewSectionList}>
                      {
                        section.list.map((sectionListRow, sectionListKey) => (
                          <View key={sectionListKey} style={Styles.ExpandableCard.expandedViewSectionListRow}>
                            <Text style={Styles.ExpandableCard.expandedViewSectionListRowText}>{sectionListRow.value}</Text>
                            <View style={Styles.ExpandableCard.expandedViewSectionListRowButtonWrapper}>
                              {
                                sectionListRow.buttons !== undefined && sectionListRow.buttons !== null ?
                                sectionListRow.buttons.map((button, key) => (<SectionListRowButton key={key} {...button}/>))
                                : null
                              }
                            </View>
                          </View>
                        ))
                      }
                    </View>
                  </View>
                ))
              }
            </View>
          }
        </View>
      </TouchableWithoutFeedback>
    );
  }
}

It can pretty much be summed up with this:

export default class ExpandableCard extends Component<Props>{
  constructor(props) {
    super(props);

    this.state = {
      expanded: false
    };

    if(Platform.OS === "android"){
      if(UIManager.setLayoutAnimationEnabledExperimental){
        UIManager.setLayoutAnimationEnabledExperimental(true);
      }
    }
  }

  toggleExpanded(){
    if(this.props.expandable !== false){
      LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
      this.setState({expanded: !this.state.expanded});
    }
  }

  render(){
    return (
      <TouchableWithoutFeedback onPress={() => this.toggleExpanded()}>
        <View style={Styles.ExpandableCard.wrapper}>
          {
            !this.state.expanded ?
            <View style={Styles.ExpandableCard.retractedView}>

            </View>
            :
            <View style={{height: this.state.expanded ? null : 0}}>

            </View>
          }
        </View>
      </TouchableWithoutFeedback>
    );
  }
}
jomla97 commented 4 years ago

If anyone needs it I've found a viable alternative:

I've successfully implemented Transitioning from react-native-reanimated, which works much in the same way. A quite important fix was merged into master a couple of days ago though, which version 1.4.0 does not include. I installed it directly from the master github branch.

jomla97 commented 4 years ago

Hey @gdoudeng why did you close the issue?

TheSavior commented 4 years ago

If anyone is able to provide a simplified, standalone repro that would be super helpful to investigate this. Ideally the repro should use Expo's Snack. We know there are some crashes with Layout Animations on Android but have had trouble tracking them all down. If you could create a repro for this we'd greatly appreciate it.

mjstelly commented 4 years ago

Unfortunately, @msinghus96, your solution did not resolve my similar issue. The called method

  const expandItem = () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
    setExpanded(!expanded)
  }

executed in component property onPress={expandItem} from here: this.props.orderHistoryData.map(userData => <UserOrders data={userData} key={userData.id} />) is already wrapped in a ScrollView which itself is wrapped in a parent View. I continue to receive the same error unknown:ReactNative: java.lang.NullPointerException: Attempt to invoke virtual method 'android.view.ViewParent android.view.View.getParent()' on a null object reference

@TheSavior I'll see if I can replicate this in minimal POC app. UPDATE: Unfortunately, I am unable to reproduce the error using this expo example. Anyone else have a workaround for this bug?

k3nda commented 4 years ago

Yes, same issue here. Env:

This code:

if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
  UIManager.setLayoutAnimationEnabledExperimental(true)
}

together with:

  onAccountDetailToggle = () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.spring)
    this.setState((state) => ({ isSliderVisible: !state.isSliderVisible }))
  }

makes app crash. For first click when panel is expanded it is ok but for the second click when it should hide then app crash.

TheSavior commented 4 years ago

@k3nada, can you put that into a complete example into an expo snack that crashes. See my comment above.

stale[bot] commented 4 years ago

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as a "Discussion" or add it to the "Backlog" and I will leave it open. Thank you for your contributions.

stale[bot] commented 4 years ago

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.