react-native-vietnam / react-native-search-box

A simple search box with animation, inspired from ios search bar. Lightweight, fast, flexible.
476 stars 132 forks source link

Keyboard Launch Loop #9

Open huberf opened 7 years ago

huberf commented 7 years ago

I'm trying to integrate the search element into the Android version of my React Native app, but when the view containing the search element is loaded, no matter where I navigate in the app when the keyboard attempts to launch, it immediately closes back. In regular text inputs, this is where the action ends, but in the search element, it creates an infinite loop where the beforeFocus, onFocus, and afterFocus events all fire one after the other in a never ending cycle. The app becomes unresponsive to touch, and the "Cancel" button can't be operated.

Does anyone know what might be causing this? Initially, I thought it might be that the search bar was inside a ListView, but the same behavior happened when it was placed outside of it. I've tried debugging it for the past couple hours, but can't seem to find the source of the issue.

Thanks in advance!

huberf commented 7 years ago

Here is a video of the app experiencing the issue: https://goo.gl/photos/jgekQ4R9Hhs3uKNb8

anhtuank7c commented 7 years ago

@huberf Can you show me your code? Ensure these function beforeFocus, onFocus, afterFocus return a Promise. Checkout details example: https://github.com/crabstudio/react-native-atoz-listview/blob/master/example/src/screens/Contacts/Home.js#L162

anhtuank7c commented 7 years ago

@huberf anything new?

huberf commented 7 years ago

The functions are executing and to test the search, I have had each one use the example code inside, so each one calls resolve(); after execution is finished. If one looks at the logs, after initially tapping the search bar, it looks as follows. [code] beforeFocus onFocus afterFocus beforeFocus onFocus afterFocus [/code] These infinitely repeat. One thing that differs from the example is my functions scope are inside of a function placed in the render method. I'll try making them functions of the view object later today, and report if that affects the outcome.

However, the problem does appear as soon as the view is loaded, and then prevents the keyboard from working across the entire app. Which makes it seem that the search module is instructing keyboard shutdown as soon as keyboard launch is initiated.

anhtuank7c commented 7 years ago

Can you show me your source code? My example don't get loop problem. Please take a look: https://github.com/crabstudio/react-native-atoz-listview/blob/master/example/src/screens/Contacts/Home.js#L162

huberf commented 7 years ago

Here is my source code. Basically, it initially renders the search bar and loading text, and then when the recommendations from the API arrive, it refreshes it with the search bar still at the top and the recommendation elements below it.

var React = require('react');
var ReactNative = require('react-native');
var t = require('tcomb-form-native');
import { Actions } from 'react-native-router-flux';

import Tabs from 'react-native-tabs';

var {
  AppRegistry,
  ScrollView,
  AsyncStorage,
  ListView,
  StyleSheet,
  Text,
  View,
  WebView,
  TouchableHighlight,
  RefreshControl,
  Alert,
  TabBarIOS,
  StatusBar,
} = ReactNative;

var {
  Component
} = React;

import {
  Divider,
  ListBasic,
} from 'react-native-uikit';

// Setup search form
/*
var Form = t.form.Form;

var Search = t.struct({
  query: t.String
});

const options = {
};
*/

// var ProfileRec = require('./elements/profileRec');
var profileCard = require('./elements/profileCard');
var careerCard = require('./elements/careerCard');

import AtoZListView from 'react-native-atoz-listview';
import SearchBar from 'react-native-search-box';

var api = require('./api');

// Components
var tabBar = require('./elements/tabBar');
var story = require('./elements/story');

// Series number
var series = 1;

export default class loginPage extends Component {
  constructor(props){
    super(props);
    this.state = {page:'explore', data: {
            "A": [
        {
          "name": "Anh Tuan Nguyen",
          "age": 28
        },
        {
          "name": "An Nhien",
          "age": 20
        },
      ],
      "Z": [
        {
          "name": "Zue Dang",
          "age": 22
        },
        {
          "name": "Zoom Jane",
          "age": 30
        },
      ]
        }};
    // Ensure recommendations load on launch
    this.props.justJumped = true;
    var tips = [];
  }

  componentDidMount() {
  }

  renderRow = (item, sectionId, index) => {
    return (
      <TouchableHightLight
        style={{
          height: rowHeight,
          justifyContent: 'center',
          alignItems: 'center'}}
      >
        <Text>{item.name}</Text>
      </TouchableHightLight>
    );
  }
  beforeFocus = () => {
      return new Promise((resolve, reject) => {
          console.log('beforeFocus');
          resolve();
      });
  }
  onFocus = (text) => {
      return new Promise((resolve, reject) => {
          console.log('onFocus', text);
          resolve();
      });
  }
  afterFocus = () => {
      return new Promise((resolve, reject) => {
          console.log('afterFocus');
          resolve();
      });
  }
  onSearch = (text) => {
    return new Promise((resolve, reject) => {
      Alert.alert(text);
      Actions.searchPage({justJumped: true, queryString: text});
      resolve();
    })
  }
  onCancel = () => {
    return new Promise((resolve, reject) => {
      console.log('onCancel');
      resolve();
    })
  }

  render() {
    var showRecs = (data) => {
      var toShowInfl = [];
      infl = data.influencers;
      for(var i = 0; i< infl.length;i ++) {
        toShowInfl.push({
          userId: infl[i].userId,
          uuid: infl[i].uuid,
          name: infl[i].name,
          description: infl[i].bio,
          profile: infl[i].profileImage,
          isFollowing: false,
        });
      }
      var toShowJobs = [];
      jobs = data.jobs;
      for(var i = 0; i < jobs.length;i ++) {
        toShowJobs.push({
          name: jobs[i].name,
          description: jobs[i].description,
          meanAnnualWage: jobs[i].meanAnnualWage,
          employment: jobs[i].employment,
          blsId: jobs[i].blsId,
        });
      }
      Actions.refresh({ recommendationList: { influencers: toShowInfl, careers: toShowJobs }, justJumped: false });
    }
    var getRecs = () => {
      if (this.props.justJumped != false || this.state.justJumped != false) {
        this.props.justJumped = false;
        this.state.justJumped = false;
        api.loadUserRecommendations(showRecs);
      }
    }
    var allItems = [];
    if (this.props.justJumped == false || this.state.justJumped == false) {
      if (this.props.recommendationList) {
        allItems = allItems.concat([{ type: 'search' }]);
        allItems = allItems.concat(this.props.recommendationList.careers.map((e, index) => {
          e.type = 'job';
          return e;
        }));
        allItems = allItems.concat(this.props.recommendationList.influencers.map((e, index) => {
          e.type = 'infl';
          return e;
        }));
      } else {
        getRecs();
      }
    } else {
      allItems = allItems.concat([{ type: 'search' }]);
      allItems = allItems.concat([{ type: 'loading' }]);
    }
    this.state.allRecommendations = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
    this.state.allRecommendations = this.state.allRecommendations.cloneWithRows(allItems);
    // Finally load recs
    getRecs();
    // Prevent recs from loading without explicit command
    this.props.justJumped = false;
    this.state.justJumped = false;
    var renderCareer = (data) => {
      Actions.careerPage({details: data});
    }
    var renderInfluencer = (data) => {
      Actions.influencerPage({details: data});
    }
    var renderJobRow = (rowData) => {
      return (
        <View onPress={() => renderCareer(rowData)}>
          <Text onPress={() => renderCareer(rowData)}>{rowData.name} - {rowData.description.substring(0, 60)}...</Text>
          <Divider style={{paddingTop: 20}} />
        </View>
      );
    }
    var renderInflRow = (rowData) => {
      return profileCard({rowData});
      //return ProfileRec(rowData.profile, rowData.name, rowData.description, () => renderInfluencer(rowData));
      /*return (<View onPress={() => renderInfluencer(rowData)}>
          <Text onPress={() => renderInfluencer(rowData)}>{rowData.name} - {rowData.description}</Text>
          <Divider style={{paddingTop: 20}} />
          </View>)*/
    }
    var renderRowAll = (rowData) => {
      if (rowData.type == 'job') {
        return careerCard(rowData);
        /*return (<View onPress={() => renderCareer(rowData)}>
          <Text onPress={() => renderCareer(rowData)}>{rowData.name} - {rowData.description.substring(0, 60)}...</Text>
          <Divider style={{paddingTop: 20}} />
        </View>)*/
      } else if (rowData.type === 'infl') {
        return profileCard({rowData});
        // return ProfileRec(rowData.profile, rowData.name, rowData.description, () => renderInfluencer(rowData));
      } else if (rowData.type === 'search') {
        return (<View style={{flex: 1}}>
          <SearchBar
            ref='search_box'
            placeholder='Search'
            beforeFocus={this.beforeFocus}
            onFocus={this.onFocus}
            afterFocus={this.afterFocus}
            onSearch={this.onSearch}
            onCancel={this.onCancel}
          />
        </View>);
      } else if (rowData.type === 'loading') {
        return (<Text style={{paddingTop: 0, alignSelf: 'center'}}>Loading...</Text>)
      }
    }
    this.state.refreshing = false;
    return (
      <View style={styles.container}>
        <View style={styles.container}>
          <ListView
            style={{flex: 1, alignSelf: 'stretch'}}
            dataSource={this.state.allRecommendations}
            renderRow={(rowData) => renderRowAll(rowData)}
            refreshControl={
              <RefreshControl
                refreshing={this.state.refreshing}
                onRefresh={() => {this.state.refreshing = true; Actions.refresh({justJumped: true}); this.state.refreshing = false;}}
              />
            }
          />
        </View>
        {tabBar(this.state.page)}
      </View>
    );
  }
};

var styles = StyleSheet.create({
/*
  container: {
    justifyContent: 'center',
    marginTop: 50,
    padding: 20,
    backgroundColor: '#ffffff',
  },
*/
  container: {
    padding: 0,
    flex: 1,
        alignSelf: 'stretch',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 30,
    alignSelf: 'center',
    marginBottom: 30
  },
  buttonText: {
    fontSize: 18,
    color: 'white',
    alignSelf: 'center'
  },
  button: {
    width: 200,
    height: 36,
    backgroundColor: '#48BBEC',
    borderColor: '#48BBEC',
    borderWidth: 1,
    borderRadius: 8,
    marginBottom: 10,
    alignSelf: 'stretch',
    justifyContent: 'center'
  },
});
huberf commented 7 years ago

I have isolated the module code causing the issue, and been able to prevent the catastrophic keyboard failure by commenting out this line https://github.com/crabstudio/react-native-search-box/blob/master/index.js#L217

Do you know what would cause the collapseAnimation function to autorun as soon as the view containing the search box is loaded? For now, I'll just use the locally patched module, but am happy to help with any module fixes or restructuring of my own code.

huberf commented 7 years ago

I wonder if this might be the module code causing the issue https://github.com/crabstudio/react-native-search-box/blob/master/index.js#L68. I can't test this right now but will look into it later unless you can get to it sooner.

Thanks for helping to assist!

ramsestom commented 7 years ago

I have the same issue. As soon as I click on the search box afterFocus, beforeFocus and onFocus are called in an infinite loop.... I am testing on android

huberf commented 7 years ago

@ramsestom Did commenting out the line of code at https://github.com/crabstudio/react-native-search-box/blob/master/index.js#L217 resolve the issue? You can do so in the node_modules/ directory. If this resolves it for you too, then it will help me diagnose the issue further and hopefully, submit a PR to fix it.

ramsestom commented 7 years ago

yes If I do

collapseAnimation = ( isForceAnim = false ) => {
        return new Promise((resolve, reject) => {
            /*
            Animated.parallel([
                ( ( this.props.keyboardShouldPersist === false ) ? Keyboard.dismiss() : null ),
                Animated.timing(
                    this.inputFocusWidthAnimated,
                    {
                        toValue: this.contentWidth - 10,
                        duration: 200
                    }
                ).start(),
                Animated.timing(
                    this.btnCancelAnimated,
                    {
                        toValue: this.contentWidth,
                        duration: 200
                    }
                ).start(),
                ( ( this.props.keyboardShouldPersist === false ) ?
                Animated.timing(
                    this.inputFocusPlaceholderAnimated,
                    {
                        toValue: this.middleWidth - this.props.placeholderCollapsedMargin,
                        duration: 200
                    }
                ).start() : null ),
                ( ( this.props.keyboardShouldPersist === false || isForceAnim === true ) ?
                Animated.timing(
                    this.iconSearchAnimated,
                    {
                        toValue: this.middleWidth - this.props.searchIconCollapsedMargin,
                        duration: 200
                    }
                ).start() : null ),
                Animated.timing(
                    this.iconDeleteAnimated,
                    {
                        toValue: 0,
                        duration: 200
                    }
                ).start(),
                Animated.timing(
                    this.shadowOpacityAnimated,
                    {
                        toValue: this.props.shadowOpacityCollapsed,
                        duration: 200
                    }
                ).start(),
            ]);
            this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
            */
            resolve();
        });
    }

That works

cyveros commented 7 years ago

@huberf @ramsestom @anhtuank7c

I find a fix for it.

around line 273

<Animated.View
       ref="searchContainer"
       style={
             [
                 styles.container,
                  his.props.backgroundColor && { backgroundColor: this.props.backgroundColor }
              ]}
        onLayout={this.onLayout}
>

simply remove the line:

onLayout={this.onLayout}

then, the glitch is gone. But you still have the expand/collapse animation.

btw, tested both on android and ios

dreamdev21 commented 5 years ago

Hi @huberf @anhtuank7c @ramsestom @cyveros @smtlaissezfaire I have same keyboard loop issue. Tried above solutions but never fixed.

        <Search
            inputHeight={Metrics.navbarHeight - 12}
            ref="search_box"
            onSearch={this.onSearch}
            onChangeText={(text) => {
              this.setState(
                {
                  searchText: text
                },
                () => this.reload()
              );
            }}
            value={this.state.searchText}
            afterCancel={(text) => {
              this.setState(
                {
                  searchText: '',
                  lastVisible: null
                },
                () => {
                  this.props.navigation.state.params.onGoBack();
                  this.props.navigation.goBack();
                }
              );
            }}
            afterDelete={() => {
              this.setState(
                {
                  searchText: ''
                },
                () => this.reload()
              );
            }}
            onFocus={this.onFocus}
            beforeFocus={this.beforeFocus}
            afterFocus={this.afterFocus}
            backgroundColor="#1c2127"
            returnKeyType="done"
            autoCapitalize="words"
            // autoFocus={Platform.OS === 'ios' ? true : false}
            autoFocus={true}
            keyboardShouldPersist={false}
          />

Here is my video what is happening. issue

Anyone have solution for me?