May 11, 2019 Adem Bilican No comments

How to deal with the keyboard appearance in React Native?

Photo by unsplash-logoVincent Yee on Unsplash

    Controlling the appearance of the keyboard is a big headache for mobile app developers. The keyboard should appear smoothly and fit in the user interface without blocking any element. Several solutions exist in React Native to deal with that issue: one can use the KeyboardAvoidingView component or one of the several npm packages that are doing the work for you. Finally, it is also possible to manually take care of the user interface adaptation to the keyboard appearance.
    In this tutorial, we will create a simple view that requires user input (thus keyboard appearance) and that will adapt its component sizes/positions based on the keyboard visibility. We will create a simple view asking the user to fill in the 12 secret words to access a cryptocurrency wallet, the mnemonic. Once the user has provided the mnemonic, the keyboard will disappear and the view components will adapt to their new positions/sizes.
So let’s get started.

1. Create a new React Native app

    Open your terminal and run the following command to create a new React Native project:

react-native init enterMnemonicView

2. The code

    Contrary to the other tutorials, I am giving the whole code here at once first and we’ll go to the details below. Go to your newly created React Native project and open the App.js file, remove its content and add the one below:

import React from 'react';
import {
  Text, View, TextInput, Image, Keyboard, LayoutAnimation, ImageBackground, TouchableHighlight, StyleSheet,
} from 'react-native';

export default class ImportWallet extends React.Component {

  // state
  state = {
    counter: 1,
    word1: '',
    word2: '',
    word3: '',
    word4: '',
    word5: '',
    word6: '',
    word7: '',
    word8: '',
    word9: '',
    word10: '',
    word11: '',
    word12: '',
    topHeight: 150,
    complete: false,
    marginTop: 100,
    marginBottom: 30,
  }

  keyboardDidShowListener = {}
  keyboardDidHideListener = {}

  // add listeners on componentWillMount()
  componentWillMount () {
    Keyboard.addListener('keyboardDidShow', (e) => this.keyboardDidShow(e));
    Keyboard.addListener('keyboardDidHide', (e) => this.keyboardDidHide(e));
  }

  // remove listeners on componentWillUnmont()
  componentWillUnmount () {
    this.keyboardDidShowListener.remove()
    this.keyboardDidHideListener.remove()
  }

  // fired when keybord appears
  keyboardDidShow (e) {
    let newSize = e.endCoordinates.height
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
    this.setState({
      marginTopMenu: 0,
      marginTop: 50,
      topHeight: 100,
      marginBottom: newSize
    })
  }

  // fired when keyboard disappears
  keyboardDidHide (e) {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
    this.setState({
      marginTopMenu: 100, 
      marginTop: 100,
      topHeight: 150,
    })
  } 

  // will check the user input and create a new block when the user presses the [space] button
  _onMnemonicChange = (text) => {
    const lastChar = text[text.length - 1];
    // check for [space]
    if (lastChar === ' ') {
      this.textInput.clear()
      // update the state accordingly
      switch (this.state.counter) {
        case 1:
          this.setState({ word1: text, counter: this.state.counter + 1 });
          break;
        case 2:
          this.setState({ word2: text, counter: this.state.counter + 1 });
          break;
        case 3:
          this.setState({ word3: text, counter: this.state.counter + 1 });
          break;
        case 4:
          this.setState({ word4: text, counter: this.state.counter + 1 });
          break;
        case 5:
          this.setState({ word5: text, counter: this.state.counter + 1 });
          break;
        case 6:
          this.setState({ word6: text, counter: this.state.counter + 1 });
          break;
        case 7:
          this.setState({ word7: text, counter: this.state.counter + 1 });
          break;
        case 8:
          this.setState({ word8: text, counter: this.state.counter + 1 });
          break;
        case 9:
          this.setState({ word9: text, counter: this.state.counter + 1 });
          break;
        case 10:
          this.setState({ word10: text, counter: this.state.counter + 1 });
          break;
        case 11:
          this.setState({ word11: text, counter: this.state.counter + 1 });
          break;
        case 12:
          this.setState({word12: text, counter: this.state.counter + 1 , complete: true});
          break;
        default:
          break;
      }
    }
  }

  // render
  render() {
    return (
      <ImageBackground source={require('./resources/images/main_background.png')} style={{flex:1, height:this.state.topHeight }}>
        <View style={{height: 100, width: 360, borderRadius: 10, borderColor: 'black', alignSelf: 'center', marginTop: this.state.marginTop, backgroundColor: 'white', shadowColor: '#000000', shadowRadius: 10, shadowOpacity: 1.0, justifyContent: 'center' }}>
          <Text style={{textAlign: 'center', fontSize: 24}}>ENTER MNEMONIC</Text>
        </View>

        <View style={{flex:1,  marginTop: this.state.marginTopMenu}}>

          <View style={{flexDirection:'row'}}>
            {this.state.word1 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word1}</Text>
              </View>
              :
              <View ></View>
            }

            {this.state.word2 != '' ?
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word2}</Text>
              </View>
              :
              <View></View>
            }

            {this.state.word3 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word3}</Text>
              </View>
              :
              <View></View>
            }
          </View>

          <View style={{flexDirection:'row', height:70}}>
            {this.state.word4 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word4}</Text>
              </View>
              :
              <View></View>
            }

            {this.state.word5 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word5}</Text>
              </View>
              :
              <View></View>
            }

            {this.state.word6 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word6}</Text>
              </View>
              :
              <View></View>
            }  
          </View>

          <View style={{flexDirection:'row', height:70 }}>
            {this.state.word7 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word7}</Text>
              </View>
              :
              <View></View>
            }

            {this.state.word8 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word8}</Text>
              </View>
              :
              <View></View>
            }

            {this.state.word9 != '' ? 
              <View style={styles.mnemonic_word}>
                <Text style={{color:'white'}}>{this.state.word9}</Text>
              </View>
              :
              <View></View>
            }
            </View>

            <View style={{flexDirection:'row', height:70}}>

              {this.state.word10 != '' ? 
                <View style={styles.mnemonic_word}>
                  <Text style={{color:'white'}}>{this.state.word10}</Text>
                </View>
                :
                null
              }

              {this.state.word11 != '' ? 
                <View style={styles.mnemonic_word}>
                  <Text style={{color:'white'}}>{this.state.word11}</Text>
                </View>
                :
                null
              }

              {this.state.word12 != '' ? 
                <View style={styles.mnemonic_word}>
                  <Text style={{color:'white'}}>{this.state.word12}</Text>
                </View>
                :
                null
              }
            </View>

          <View style={{flex:1, flexDirection:'row', alignItems:'flex-end'}}>
          {this.state.complete ? 
            <View style={{flex:1, bottom:50}}>
              <TouchableHighlight onPress={() => this.props.navigation.navigate("ProvidePwd") } underlayColor='#172D39' style={styles.action_button_above}>  
                <Text style={styles.button_text}>IMPORT</Text>
              </TouchableHighlight>

              <TouchableHighlight onPress={() => this.props.navigation.navigate("ProvidePwd") } underlayColor='#172D39' style={styles.action_button_second}> 
                <Text style={styles.button_text_second}>CLEAR</Text>
              </TouchableHighlight>
            </View>
          : 
            <View style={{flex:1, height:100, marginBottom: this.state.marginBottom}}>  
              <TextInput ref={input => { this.textInput = input }} onChangeText={ (text) => this._onMnemonicChange(text) } autoCapitalize='none'  style={{ marginTop: 10, backgroundColor: 'white', height: 70, borderColor: 'black', borderWidth:1, borderRadius:30}}/>
            </View>
          }
          </View>

        </View>
        </ImageBackground>
    );
  }
}


// styling
const styles = StyleSheet.create({
  action_button_above: {
    justifyContent: 'center',
    alignSelf: 'center',
    width: 270,
    height: 60,
    backgroundColor: '#172D39',
    borderRadius: 30,
    marginBottom: 20,
  },
  action_button_second: {
    justifyContent: 'center',
    alignSelf: 'center',
    width: 270,
    height: 60,
    backgroundColor: 'white',
    borderColor: '#172D39',
    borderWidth: 1,
    borderRadius: 30,
    marginBottom: 20,
  },
  button_text: {
    color: 'white',
    textAlign: 'center',
    fontSize: 24,
  },
  button_text_second: {
    color: '#172D39',
    textAlign: 'center',
    fontSize: 24,
  },
  mnemonic_word: {
    flex: 1,
    height: 50,
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 10,
    marginRight: 10,
    backgroundColor: '#6C5D81',
    borderRadius: 10,
    marginTop: 20
  },
});

3. Explanation

3.a. Show/hide keyboard listerners

    The first part of the code is taking care of the keyboard appearance. The code needs to be aware of the keyboard visibility to adapt the view accordingly, that’s why we need to instantiate and implement our listeners.
We instantiate our listeners and tell React Native to take care of them at mount and unmount as follow:

keyboardDidShowListener = {}
keyboardDidHideListener = {}

// add listeners on componentWillMount()
componentWillMount () {
  Keyboard.addListener('keyboardDidShow', (e) => this.keyboardDidShow(e));
  Keyboard.addListener('keyboardDidHide', (e) => this.keyboardDidHide(e));
}

// remove listeners on componentWillUnmont()
componentWillUnmount () {
  this.keyboardDidShowListener.remove()
  this.keyboardDidHideListener.remove()
}
3.a. Adapting the view on keyboard show/hide

    We can now check when the keyboard appears/disappears and therefore control the interface according to the visibility of the keyboard. On the render() function we adapted our main View to have some position related values defined in the local state (see L.119 for an example), so that we can play with them. We can now change these values according to the visibility of the keyboard using this.setState() function.

// fired when keybord appears
keyboardDidShow (e) {
  let newSize = e.endCoordinates.height
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
  // adapt component positions by updating the local state values
  this.setState({
    marginTopMenu: 0,
    marginTop: 50,
    topHeight: 100,
    marginBottom: newSize
  })
}

// fired when keyboard disappears
keyboardDidHide (e) {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
  // adapt component positions by updating the local state values
  this.setState({
    marginTopMenu: 100, 
    marginTop: 100,
    topHeight: 150,
  })
}

In the end the whole process looks like the image below:

The whole source code is also available on GitHub.

Sources:
React Native’s LayoutAnimation is Awesome
Avoiding the Keyboard in React Native
Let’s Get Moving: An Introduction to React Native Animations — Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *