92

I've seen this hack for native apps to auto scroll the window, but wondering best way to do it in React Native... When a <TextInput> field gets focus and is positioned low in the view, the keyboard will cover up the text field.

You can see this issue in example UIExplorer's TextInputExample.js view.

Does anyone have a good solution?

Abdeen
  • 922
  • 9
  • 30
McG
  • 4,859
  • 3
  • 18
  • 13
  • 3
    I'd suggest adding this as an issue on the Github tracker and see if anything comes of it, since this is going to be a very common complaint. – Colin Ramsay Mar 28 '15 at 12:18

14 Answers14

85

2017 Answer

The KeyboardAvoidingView is probably the best way to go now. Check out the docs here. It is really simple compared to Keyboard module which gives Developer more control to perform animations. Spencer Carli demonstrated all the possible ways on his medium blog.

2015 Answer

The correct way to do this in react-native does not require external libraries, takes advantage of native code, and includes animations.

First define a function that will handle the onFocus event for each TextInput (or any other component you would like to scroll to):

// Scroll a component into view. Just pass the component ref string.
inputFocused (refName) {
  setTimeout(() => {
    let scrollResponder = this.refs.scrollView.getScrollResponder();
    scrollResponder.scrollResponderScrollNativeHandleToKeyboard(
      React.findNodeHandle(this.refs[refName]),
      110, //additionalOffset
      true
    );
  }, 50);
}

Then, in your render function:

render () {
  return (
    <ScrollView ref='scrollView'>
        <TextInput ref='username' 
                   onFocus={this.inputFocused.bind(this, 'username')}
    </ScrollView>
  )
}

This uses the RCTDeviceEventEmitter for keyboard events and sizing, measures the position of the component using RCTUIManager.measureLayout, and calculates the exact scroll movement required in scrollResponderInputMeasureAndScrollToKeyboard.

You may want to play around with the additionalOffset parameter, to fit the needs of your specific UI design.

Ricoter
  • 665
  • 5
  • 17
Sherlock
  • 5,557
  • 6
  • 50
  • 78
  • 5
    This is a nice find, but for me it wasn't enough, because while the ScrollView will make sure the TextInput is on screen, the ScrollView was still showing content below the keyboard that the user couldn't scroll to. Setting the ScrollView property "keyboardDismissMode=on-drag" allows the user to dismiss the keyboard, but if there is not *enough* scroll content below the keyboard, the experience is a bit jarring. If the ScrollView only has to scroll because of the keyboard in the first place, and you disable bouncing then there seems to be no way to dismiss the keyboard & show the content below – miracle2k Sep 16 '15 at 21:44
  • 2
    @miracle2k - I have a function that resets the scroll view position when an input is blurred, i.e. when the keyboard closes. Maybe this might help in your case? – Sherlock Sep 30 '15 at 19:58
  • 2
    @Sherlock What does that blur scroll view reset function look like? Awesome solution by the way :) – Ryan McDermott Oct 09 '15 at 15:40
  • @sherlock i write the code as per you mentioned above,But i got an error : Argument 0 (NSNumber) of RCTUIManager.measureLayout must not be null – Hussian Shaik Nov 12 '15 at 10:48
  • @HussainShaik - I think you're hitting this issue https://github.com/facebook/react-native/issues/355 but i'm not too sure, maybe step into the scrollResponderScrollNativeHandleToKeyboard call to double check – Sherlock Nov 12 '15 at 14:29
  • 2
    this [blog](http://kpetrovi.ch/2015/09/30/react-native-ios-keyboard-events.html) has been really helpful – AbM Nov 26 '15 at 19:14
  • Works great in portrait mode, in landscape it scrolls off the screen. – Rocklan Dec 17 '15 at 05:11
  • When I click the "done" button on the keyboard the scrollview doesn't scroll back down even though the keyboard is no longer visible. Has anyone gotten around that? – Dev01 Mar 20 '16 at 01:30
  • 8
    In newer React Native versions you'll need to call: * import ReactNative from 'react-native'; * before calling * ReactNative.findNodeHandle() * Otherwise the app will crash – amirfl May 21 '16 at 15:06
  • @Sherlock I used your solution above and it works on the iPhone simulator but when I ported the app to an actual iPhone, the screen doesn't scroll up when I focus on a TextInput and it gets hidden behind the keyboard. I am using iPhone 5s running iOS 9.3.2. I am not sure how to debug the problem. Any hints? – Varun Gupta May 25 '16 at 11:47
  • 2
    @VarunGupta i ran into the same thing. Increasing the timeout (50 to 100) worked for me. Maybe that'll work for you too. – paulshen Jun 01 '16 at 22:06
  • @paulshen thanks for the comment. It has been pointed to me already in a separate question that I asked in stack ooverflow for the same thing but you are right nevertheless. – Varun Gupta Jun 02 '16 at 17:23
  • 1
    @Sherlock i got error as undefined is not a function(evaluating '_react2.default.findNodeHandle(_this2.refs[refName])') . i had applied it in android. Whats is my error is? – Chiranjhivi Ghimire Jun 14 '16 at 06:04
  • 6
    Now `import {findNodeHandle} from 'react-native'` http://stackoverflow.com/questions/37626851/undefined-is-not-a-functionreact-findnodehandle – Antoine Sep 29 '16 at 10:03
  • Is there an updated version of this solution? It does not work for me. Keep getting red screen. This is a bit frustrating. I'd image a mobile library would handle the freaking keyboard in their native components but I was wrong. – ODelibalta Oct 22 '17 at 19:55
  • Using 2017 solutions seems worst than 2015. In 2017, it seems when you move focus to the next field though, it doesnt scroll to adjust for that next field. So does this mean 2015 solution is the better one? – Noitidart May 29 '18 at 02:20
27

Facebook open sourced KeyboardAvoidingView in react native 0.29 to solve this problem. Documentation and usage example can be found here.

farwayer
  • 3,872
  • 3
  • 21
  • 23
  • 37
    Beware of KeyboardAvoidingView, it just ain't easy to use. It doesn't always behave as you expect it should. Documentation is practically nonexistent. – Renato Back Jul 25 '16 at 04:04
  • doc and behavior are getting better now – Antoine Sep 29 '16 at 08:47
  • The problem I have, is that the KeyboardAvoidingView measures the keyboard height as 65 on my iPhone 6 simulator and so my view is still hidden behind the keyboard. – Marc Oct 05 '16 at 22:01
  • Only way I could manager it was through a bottompadding approach triggered by `DeviceEventEmitter.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));` – Marc Oct 05 '16 at 22:31
  • In React Native v0.65, here is the [KeyboardAvoidingView](https://reactnative.dev/docs/keyboardavoidingview) – Cong Dan Luong Sep 05 '21 at 17:35
13

We combined some of the code form react-native-keyboard-spacer and the code from @Sherlock to create a KeyboardHandler component that can be wrapped around any View with TextInput elements. Works like a charm! :-)

/**
 * Handle resizing enclosed View and scrolling to input
 * Usage:
 *    <KeyboardHandler ref='kh' offset={50}>
 *      <View>
 *        ...
 *        <TextInput ref='username'
 *          onFocus={()=>this.refs.kh.inputFocused(this,'username')}/>
 *        ...
 *      </View>
 *    </KeyboardHandler>
 * 
 *  offset is optional and defaults to 34
 *  Any other specified props will be passed on to ScrollView
 */
'use strict';

var React=require('react-native');
var {
  ScrollView,
  View,
  DeviceEventEmitter,
}=React;


var myprops={ 
  offset:34,
}
var KeyboardHandler=React.createClass({
  propTypes:{
    offset: React.PropTypes.number,
  },
  getDefaultProps(){
    return myprops;
  },
  getInitialState(){
    DeviceEventEmitter.addListener('keyboardDidShow',(frames)=>{
      if (!frames.endCoordinates) return;
      this.setState({keyboardSpace: frames.endCoordinates.height});
    });
    DeviceEventEmitter.addListener('keyboardWillHide',(frames)=>{
      this.setState({keyboardSpace:0});
    });

    this.scrollviewProps={
      automaticallyAdjustContentInsets:true,
      scrollEventThrottle:200,
    };
    // pass on any props we don't own to ScrollView
    Object.keys(this.props).filter((n)=>{return n!='children'})
    .forEach((e)=>{if(!myprops[e])this.scrollviewProps[e]=this.props[e]});

    return {
      keyboardSpace:0,
    };
  },
  render(){
    return (
      <ScrollView ref='scrollView' {...this.scrollviewProps}>
        {this.props.children}
        <View style={{height:this.state.keyboardSpace}}></View>
      </ScrollView>
    );
  },
  inputFocused(_this,refName){
    setTimeout(()=>{
      let scrollResponder=this.refs.scrollView.getScrollResponder();
      scrollResponder.scrollResponderScrollNativeHandleToKeyboard(
        React.findNodeHandle(_this.refs[refName]),
        this.props.offset, //additionalOffset
        true
      );
    }, 50);
  }
}) // KeyboardHandler

module.exports=KeyboardHandler;
John kendall
  • 498
  • 4
  • 10
  • Anything easy/obvious that would keep the keyboard from showing in an iOS simulator? – seigel Nov 24 '15 at 14:40
  • 1
    Did you try Command+K (Hardware->Keyboard->Toggle Software Keboard) ? – John kendall Nov 25 '15 at 15:42
  • Try the modified version of this here: https://gist.github.com/dbasedow/f5713763802e27fbde3fc57a600adcd3 I believe this is better because it doesn't rely on any timeouts which I think is fragile imo. – CoderDave Jun 03 '16 at 18:34
10

First you need to install react-native-keyboardevents.

  1. In XCode, in the project navigator, right click Libraries ➜ Add Files to [your project's name] Go to node_modules ➜ react-native-keyboardevents and add the .xcodeproj file
  2. In XCode, in the project navigator, select your project. Add the lib*.a from the keyboardevents project to your project's Build Phases ➜ Link Binary With Libraries Click .xcodeproj file you added before in the project navigator and go the Build Settings tab. Make sure 'All' is toggled on (instead of 'Basic'). Look for Header Search Paths and make sure it contains both $(SRCROOT)/../react-native/React and $(SRCROOT)/../../React - mark both as recursive.
  3. Run your project (Cmd+R)

Then back in javascript land:

You need to import the react-native-keyboardevents.

var KeyboardEvents = require('react-native-keyboardevents');
var KeyboardEventEmitter = KeyboardEvents.Emitter;

Then in your view, add some state for the keyboard space and update from listening to the keyboard events.

  getInitialState: function() {
    KeyboardEventEmitter.on(KeyboardEvents.KeyboardDidShowEvent, (frames) => {
      this.setState({keyboardSpace: frames.end.height});
    });
    KeyboardEventEmitter.on(KeyboardEvents.KeyboardWillHideEvent, (frames) => {
      this.setState({keyboardSpace: 0});
    });

    return {
      keyboardSpace: 0,
    };
  },

Finally, add a spacer to your render function beneath everything so when it increases size it bumps your stuff up.

<View style={{height: this.state.keyboardSpace}}></View>

It is also possible to use the animation api, but for simplicity's sake we just adjust after the animation.

brysgo
  • 838
  • 1
  • 8
  • 20
  • 1
    It would be awesome to see a code example/some more info on how to do the animation. The jump is pretty janky, and working with only the "will show" and "did show" methods, I can't quite figure out how to guess how long the keyboard animation will be or how tall it is from "will show". – Stephen May 16 '15 at 16:13
  • 2
    react-native@0.11.0-rc now sends keyboard events (e.g., "keyboardWillShow") through the DeviceEventEmitter, so you can register listeners for these events. When dealing with a ListView, however, I found that calling scrollTo() on the ListView's scrollview worked better: `this.listView.getScrollResponder().scrollTo(rowID * rowHeight);` This gets called on a row's TextInput when it receives an onFocus event. – Jed Lau Sep 01 '15 at 17:54
  • 4
    This answer is no longer valid, as the RCTDeviceEventEmitter does the job. – Sherlock Sep 15 '15 at 19:14
7

react-native-keyboard-aware-scroll-view solved the problem for me. react-native-keyboard-aware-scroll-view on GitHub

amirfl
  • 1,634
  • 2
  • 18
  • 34
6

Try this:

import React, {
  DeviceEventEmitter,
  Dimensions
} from 'react-native';

...

getInitialState: function() {
  return {
    visibleHeight: Dimensions.get('window').height
  }
},

...

componentDidMount: function() {
  let self = this;

  DeviceEventEmitter.addListener('keyboardWillShow', function(e: Event) {
    self.keyboardWillShow(e);
  });

  DeviceEventEmitter.addListener('keyboardWillHide', function(e: Event) {
      self.keyboardWillHide(e);
  });
}

...

keyboardWillShow (e) {
  let newSize = Dimensions.get('window').height - e.endCoordinates.height;
  this.setState({visibleHeight: newSize});
},

keyboardWillHide (e) {
  this.setState({visibleHeight: Dimensions.get('window').height});
},

...

render: function() {
  return (<View style={{height: this.state.visibleHeight}}>your view code here...</View>);
}

...

It worked for me. The view basically shrinks when the keyboard is displayed, and grows back again when its hidden.

pomo
  • 2,251
  • 1
  • 21
  • 34
4

Just wanted to mention, now there is a KeyboardAvoidingView in RN. Just import it and use it as any other module in RN.

Here is the link to the commit on RN:

https://github.com/facebook/react-native/commit/8b78846a9501ef9c5ce9d1e18ee104bfae76af2e

It is available from 0.29.0

They have also included an example on UIExplorer.

Aakash Sigdel
  • 9,060
  • 5
  • 33
  • 38
4

Maybe is to late, but the best solution is to use a native library, IQKeyboardManager

Just drag and drop IQKeyboardManager directory from demo project to your iOS project. That's it. Also you can setup some valus, as isToolbar enabled, or the space between text input and keyboard in the AppDelegate.m file. More details about customisation are in the GitHub page link that I've added.

Adrian Zghibarta
  • 397
  • 2
  • 5
  • 17
  • 1
    This is an excellent option. Also see https://github.com/douglasjunior/react-native-keyboard-manager for a version wrapped for ReactNative - it's easy to install. – loevborg Aug 18 '17 at 18:05
3

I used TextInput.onFocus and ScrollView.scrollTo.

...
<ScrollView ref="scrollView">
...
<TextInput onFocus={this.scrolldown}>
...
scrolldown: function(){
  this.refs.scrollView.scrollTo(width*2/3);
},
shohey1226
  • 700
  • 2
  • 7
  • 16
2

@Stephen

If you don't mind not having the height animate at exactly the same rate that the keyboard appears, you can just use LayoutAnimation, so that at least the height doesn't jump into place. e.g.

import LayoutAnimation from react-native and add the following methods to your component.

getInitialState: function() {
    return {keyboardSpace: 0};
  },
   updateKeyboardSpace: function(frames) {
    LayoutAnimation.configureNext(animations.layout.spring);
    this.setState({keyboardSpace: frames.end.height});
  },

  resetKeyboardSpace: function() {
    LayoutAnimation.configureNext(animations.layout.spring);
    this.setState({keyboardSpace: 0});
  },

  componentDidMount: function() {
    KeyboardEventEmitter.on(KeyboardEvents.KeyboardDidShowEvent, this.updateKeyboardSpace);
    KeyboardEventEmitter.on(KeyboardEvents.KeyboardWillHideEvent, this.resetKeyboardSpace);
  },

  componentWillUnmount: function() {
    KeyboardEventEmitter.off(KeyboardEvents.KeyboardDidShowEvent, this.updateKeyboardSpace);
    KeyboardEventEmitter.off(KeyboardEvents.KeyboardWillHideEvent, this.resetKeyboardSpace);
  },

Some example animations are (I'm using the spring one above):

var animations = {
  layout: {
    spring: {
      duration: 400,
      create: {
        duration: 300,
        type: LayoutAnimation.Types.easeInEaseOut,
        property: LayoutAnimation.Properties.opacity,
      },
      update: {
        type: LayoutAnimation.Types.spring,
        springDamping: 400,
      },
    },
    easeInEaseOut: {
      duration: 400,
      create: {
        type: LayoutAnimation.Types.easeInEaseOut,
        property: LayoutAnimation.Properties.scaleXY,
      },
      update: {
        type: LayoutAnimation.Types.easeInEaseOut,
      },
    },
  },
};

UPDATE:

See @sherlock's answer below, as of react-native 0.11 the keyboard resizing can be solved using built in functionality.

deanmcpherson
  • 738
  • 5
  • 8
2

You can combine a few of the methods into something a little simpler.

Attach a onFocus listener on your inputs

<TextInput ref="password" secureTextEntry={true} 
           onFocus={this.scrolldown.bind(this,'password')}
/>

Our scroll down method looks something like :

scrolldown(ref) {
    const self = this;
    this.refs[ref].measure((ox, oy, width, height, px, py) => {
        self.refs.scrollView.scrollTo({y: oy - 200});
    });
}

This tells our scroll view (remember to add a ref) to scroll to down to the position of our focused input - 200 (it's roughly the size of the keyboard)

componentWillMount() {
    this.keyboardDidHideListener = Keyboard.addListener(
      'keyboardWillHide', 
      this.keyboardDidHide.bind(this)
    )
}

componentWillUnmount() {
    this.keyboardDidHideListener.remove()
}

keyboardDidHide(e) {
    this.refs.scrollView.scrollTo({y: 0});
}

Here we reset our scroll view back to the top,

enter image description here

SherylHohman
  • 16,580
  • 17
  • 88
  • 94
Nath
  • 6,774
  • 2
  • 28
  • 22
0

I'm using a simpler method, but it's not animated yet. I have a component state called "bumpedUp" which I default to 0, but set to 1 when the textInput gets focus, like this:

On my textInput:

onFocus={() => this.setState({bumpedUp: 1})}
onEndEditing={() => this.setState({bumpedUp: 0})}

I also have style that gives the wrapping container of everything on that screen a bottom margin and negative top margin, like this:

mythingscontainer: {
  flex: 1,
  justifyContent: "center",
  alignItems: "center",
  flexDirection: "column",
},
bumpedcontainer: {
  marginBottom: 210,
  marginTop: -210,
},

And then on the wrapping container, I set the styles like this:

<View style={[styles.mythingscontainer, this.state.bumpedUp && styles.bumpedcontainer]}>

So, when the "bumpedUp" state gets set to 1, the bumpedcontainer style kicks in and moves the content up.

Kinda hacky and the margins are hardcoded, but it works :)

Stirman
  • 1,574
  • 3
  • 11
  • 12
0

I use brysgo answer to raise the bottom of my scrollview. Then I use the onScroll to update the current position of the scrollview. I then found this React Native: Getting the position of an element to get the position of the textinput. I then do some simple math to figure out if the input is in the current view. Then I use scrollTo to move the minimum amount plus a margin. It's pretty smooth. Heres the code for the scrolling portion:

            focusOn: function(target) {
                return () => {
                    var handle = React.findNodeHandle(this.refs[target]);
                    UIManager.measureLayoutRelativeToParent( handle, 
                        (e) => {console.error(e)}, 
                        (x,y,w,h) => {
                            var offs = this.scrollPosition + 250;
                            var subHeaderHeight = (Sizes.width > 320) ? Sizes.height * 0.067 : Sizes.height * 0.077;
                            var headerHeight = Sizes.height / 9;
                            var largeSpace = (Sizes.height - (subHeaderHeight + headerHeight));
                            var shortSpace = largeSpace - this.keyboardOffset;
                            if(y+h >= this.scrollPosition + shortSpace) {
                                this.refs.sv.scrollTo(y+h - shortSpace + 20);
                            }
                            if(y < this.scrollPosition) this.refs.sv.scrollTo(this.scrollPosition - (this.scrollPosition-y) - 20 );
                        }
                     );
                };
            },
Community
  • 1
  • 1
aintnorest
  • 1,326
  • 2
  • 13
  • 20
0

I also meet this question. Finally, I resolve it by defining the height of each scene, such as:

<Navigator
    ...
    sceneStyle={{height: **}}
/>

And, I also use a third-party module https://github.com/jaysoo/react-native-extra-dimensions-android to get the real height.

geisterfurz007
  • 5,292
  • 5
  • 33
  • 54