facebook / react-native

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

ScrollResponder & KeyboardEvents #3195

Closed yelled3 closed 8 years ago

yelled3 commented 9 years ago

I'm trying to implement a classic messenger UI; I want the scroll view to respond to the keyboard opening.

I tried a few solutions:

Keyboard Events

The first was using keyboardWillShow & keyboardWillHide event and Animated.timing to toggle the scrollview bottom padding. fairly simple.

state = {
    keyboardAnimationDuration: 250,
    keyboardSpace: new Animated.Value(0)
}

componentDidMount() {
    DeviceEventEmitter.addListener('keyboardWillShow', this.updateKeyboardSpace);
    DeviceEventEmitter.addListener('keyboardWillHide', this.resetKeyboardSpace);
  }

  componentWillUnmount() {
    DeviceEventEmitter.removeListener('keyboardWillShow', this.updateKeyboardSpace);
    DeviceEventEmitter.removeListener('keyboardWillHide', this.resetKeyboardSpace);
  }

updateKeyboardSpace = (keyboardEvent)=> {

    const {
      duration,
      endCoordinates: { height }
      } = keyboardEvent;

    this.setState({keyboardAnimationDuration: duration});
    Animated.timing(this.state.keyboardSpace, {
      easing: Easing.inOut(Easing.ease),
      duration: duration,
      toValue: height
    }).start();
  }

  resetKeyboardSpace = ()=> {
    Animated.timing(this.state.keyboardSpace, {
      easing: Easing.inOut(Easing.ease),
      duration: this.state.keyboardAnimationDuration,
      toValue: 0
    }).start();
  }
render() {
    return (
      <Animated.View style={[styles.container, this.contentStyle]}>
        <ScrollView>
               // ... 
        </ScrollView>
        {this._renderTextInput()}
      </Animated.View>
    );
  }

I also tried using keyboardWillChangeFrame to handle ScrollView''skeyboardDismissMode="interactive"` - but it will not be fire at all. after a short research it seems like it's an open issue since iOS 8. see: http://stackoverflow.com/questions/19118921/moving-a-bar-with-uiscrollviewkeyboarddismissmodeinteractive can someone confirm this?

Scroll Responder Hooks

Although, completely undocumented I was able to find scrollResponderScrollNativeHandleToKeyboard

render() {
  return (
    <ScrollView ref="myScrollView" keyboardDismissMode="interactive" >
      <TextInput
        ref="myInput"
        onFocus={this._scrollToInput}
      />
    </ScrollView>
  );
}

_scrollToInput {
  var scrollView = this.refs.myScrollView.getScrollResponder();
  var scrollResponder = scrollView.getScrollRef();

  scrollResponder.scrollResponderScrollNativeHandleToKeyboard(
    React.findNodeHandle(this.refs.myInput),
    0, // adjust depending on your contentInset
    /* preventNegativeScrollOffset */ true
  );
}

source: http://stackoverflow.com/a/30609066/2857906

I'm either not using this correctly, or this still has some issues (see: https://github.com/facebook/react-native/issues/355) but this seems to not be working consistently... also notice that in this approach, the TextInput must be embedded inside the ScrollView.

It would be great to have a working example of this. maybe add it to UIExplorer...

@ide @brentvatne /cc

alvaromb commented 9 years ago

@yelled3 I managed to get the input focus scroll. Let me know if I can help you with something :relaxed:

yelled3 commented 9 years ago

@alvaromb it would be great if you can share an example in https://rnplay.org/ or a gist. cheers :-)

alvaromb commented 9 years ago

@yelled3 I'll try to put an example this afternoon :)

aakashbapna commented 9 years ago

@yelled3 I am also building a similar messenger like UI. I am able to get scrollResponderScrollNativeHandleToKeyboard to scroll to input box, but by using ScrollView my TextInput goes below viewport when keyboard is not shown (I am using a ListView to render messages). How are you sticking the TextInput to device screen bottom? The code from here- http://stackoverflow.com/a/32593814 works for me for reacting to keyboard show. My Layout looks like-

<ScrollView>
    <ListView/>
   </TextInput>
</ScrollView>   
yelled3 commented 9 years ago

@aakashbapna

How are you sticking the TextInput to device screen bottom?

I wasn't able to make it work. I used the same layout as you did. I really don't like the fact that you must place the TextInput inside the ScrollView - seems very limiting. either the text input will not be position correctly or that the screen will not scroll to the input correctly. not sure what I'm doing wrong.

apparently @alvaromb has a working example :-)

alvaromb commented 9 years ago

My solution is a bit hacky. I have a custom scroll component:

import React, { ScrollView, PropTypes, DeviceEventEmitter } from 'react-native'
import StyleSheetPropType from 'react-native/Libraries/StyleSheet/StyleSheetPropType'
import ViewStylePropTypes from 'react-native/Libraries/Components/View/ViewStylePropTypes'

class KeyboardAwareScrollView extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      keyboardSpace: 0,
    }
    this.updateKeyboardSpace = this.updateKeyboardSpace.bind(this)
    this.resetKeyboardSpace = this.resetKeyboardSpace.bind(this)
  }

  // Keyboard actions
  // TODO: automatically handle TabBar height instead of using props
  updateKeyboardSpace (frames) {
    const keyboardSpace = (this.props.viewIsInsideTabBar) ? frames.endCoordinates.height - 49 : frames.endCoordinates.height
    this.setState({
      keyboardSpace: keyboardSpace,
    })
  }

  resetKeyboardSpace () {
    this.setState({
      keyboardSpace: 0,
    })
  }

  componentDidMount () {
    // Keyboard events
    DeviceEventEmitter.addListener('keyboardWillShow', this.updateKeyboardSpace)
    DeviceEventEmitter.addListener('keyboardWillHide', this.resetKeyboardSpace)
  }

  componentWillUnmount () {
    // TODO: figure out if removeAllListeners is the right thing to do
    DeviceEventEmitter.removeAllListeners('keyboardWillShow')
    DeviceEventEmitter.removeAllListeners('keyboardWillHide')
  }

  /**
   * @param extraHeight: takes an extra height in consideration.
   */
  scrollToFocusedInput (event, reactNode, extraHeight = 49) {
    const scrollView = this.refs.keyboardScrollView.getScrollResponder()
    scrollView.scrollResponderScrollNativeHandleToKeyboard(
      reactNode, extraHeight, true
    )
  }

  render () {
    return (
      <ScrollView
        ref='keyboardScrollView'
        keyboardDismissMode='interactive'
        contentInset={{bottom: this.state.keyboardSpace}}
        showsVerticalScrollIndicator={true}
        style={this.props.style}>
        {this.props.children}
      </ScrollView>
    )
  }
}

KeyboardAwareScrollView.propTypes = {
  style: StyleSheetPropType(ViewStylePropTypes),
  children: PropTypes.node,
  viewIsInsideTabBar: PropTypes.bool,
}

export default KeyboardAwareScrollView

Then I put the TextInput components inside this scroll. All of the inputs implement the onFocus method, and it exposes it's node handle. I'm using a library called tcomb to generate the Form component, so this is why you see the this.refs.form.refs.input.refs line below:

// Inside TextInput
onFocus: this._scrollToInput.bind(this, 'input_name'),

// 'private' method
_scrollToInput (inputName, event) {
  if (this.props.onFocus) {
    this.props.onFocus(
      event,
      React.findNodeHandle(this.refs.form.refs.input.refs[inputName])
    )
  }
}

Then, in the component that renders both the form and the custom list view I can call the scrollToFocusedInput method of the KeyboardAwareScrollView component with the node handle of the input:

<KeyboardAwareScrollView
  ref='scroll'
  style={styles.container}
  viewIsInsideTabBar={true}>
  <CustomForm ref='form'
    onFocus={(event, reactNode) => {
      this.refs.scroll.scrollToFocusedInput(event, reactNode)
    }}
  />
</KeyboardAwareScrollView>

This is a very hacky and dirty solution, but it's working so far at this moment. We're thinking about a better integration of this scroll component with the https://github.com/gcanti/tcomb-form-native library, which is quite awesome.

In the end, the key is to send the React node handle of the input to the scrollToFocusedInput of the custom scroll view. Then I have to admit that some times the input gets hidden behind the keyboard, this is why I have an extra parameter to add some height to the calculation.

I'm not very proud of this code at all, but if this can help somebody to achieve a better solution, I'll be more than happy.

Let me know if you need something else :+1:

yelled3 commented 9 years ago

@alvaromb thank you for the thorough answer!

Then I have to admit that some times the input gets hidden behind the keyboard

I still need to try this out - maybe I can solve this. @ide any idea why this happens...

alvaromb commented 9 years ago

@yelled3 if I put a timeout when calling scrollResponderScrollNativeHandleToKeyboard, the input doesn't get hidden when the keyboard appears.

This is now my new scroll function:

scrollToFocusedInput: function (event, reactNode, extraHeight = 49) {
  const scrollView = this.refs.keyboardScrollView.getScrollResponder()
  this.setTimeout(() => {
    scrollView.scrollResponderScrollNativeHandleToKeyboard(
      reactNode, extraHeight, true
    )
  }, 220)
},

I know it's a hack but... ¯(ツ)

alvaromb commented 9 years ago

It does make sense because the scroll responder is acting when the scroll view contentInset is being changed, so the handle needs to wait and act when the scroll view has the new inset set.

yelled3 commented 9 years ago

@alvaromb perhaps using runAfterInteractions will solve this (or maybe requestAnimationFrame

scrollToFocusedInput: function (event, reactNode, extraHeight = 49) {
  const scrollView = this.refs.keyboardScrollView.getScrollResponder()
  InteractionManager.runAfterInteractions(() => {
    scrollView.scrollResponderScrollNativeHandleToKeyboard(
      reactNode, extraHeight, true
    )
  })
},

see: http://facebook.github.io/react-native/docs/timers.html#interactionmanager

alvaromb commented 9 years ago

Thanks @yelled3! but unfortunately only setTimeout is working.

yelled3 commented 9 years ago

@alvaromb sorry for the delay, I finally got to thoroughly check your solution;

as far as I can tell, your solution doesn't handle keyboardDismissMode='interactive' correctly. when opening the keyboard, the TextInput jumps from original position to keyboard opened position. and when I scroll down on the ScrollView - you can see the keyboard is being slowing lowered until it reaches the bottom and only then the TextInput jumps to the bottom.

For this to work correctly, the TextInput should always be attached to the top of the keyboard.

from: https://github.com/datwelk/RDRStickyKeyboardView

perhaps, adding a RN wrapper for RDRStickyKeyboardView (or whatever works best) is the way to go? WDYT?

trymbill commented 9 years ago

@alvaromb I'm interested in implementing a similar solution to what you've already got. I'm also using tcomb for form building and think something similar to your implementation could be a nice feature toggle. Would you mind sharing what you've got so far in a gist?

awesomejerry commented 9 years ago

Any better solution for this issue?? Or maybe a completely custom keyboard written in JS? lol

nidzovito commented 8 years ago

@yelled3 _scrollToInput { var scrollView = this.refs.myScrollView.getScrollResponder(); var scrollResponder = scrollView.getScrollRef();

scrollResponder.scrollResponderScrollNativeHandleToKeyboard( React.findNodeHandle(this.refs.myInput), 0, // adjust depending on your contentInset /* preventNegativeScrollOffset */ true );

After you move the screen up how can you move it down after keyboard is hidden?

yelled3 commented 8 years ago

@georgi-kovachev not sure I understand...

yelled3 commented 8 years ago

@jrans although, https://github.com/jrans/react-native-smart-scroll-view/ seems like a very elegant solution - from what I can tell, it doesn't solve what we're talking about:

I'm trying to implement a classic messenger UI

see the gif in: https://github.com/facebook/react-native/issues/3195#issuecomment-147427391

jrans commented 8 years ago

@yelled3 my bad! Misread things! Yes I had exactly the same problem with interactive mode! So disabled it! Only thing keyboardWillChangeFrame does is time how long they take to scroll..

gastonmorixe commented 8 years ago

+1 any solutions?

alvaromb commented 8 years ago

This https://github.com/facebook/react-native/issues/3195#issuecomment-146518331 works for me @imton, but it's not the most elegant solution ever.

gastonmorixe commented 8 years ago

@alvaromb for interactive drag?

gastonmorixe commented 8 years ago

I tried your code but don't know how to make it work actually. I want a sticky field to the keyboard like shown in above.

Would you mind showing me how to archive that? Really appreciate it.

Best, Gaston

Sent from my iPhone

On Jan 14, 2016, at 12:56 PM, Álvaro Medina Ballester notifications@github.com wrote:

This #3195 (comment) works for me @imton, but it's not the most elegant solution ever.

— Reply to this email directly or view it on GitHub.

erikhermans3 commented 8 years ago

It worked for me. Thanks.

pjcabrera commented 8 years ago

Implementing this worked for me in react-native 0.18.1

https://rnplay.org/apps/P774EQ

yogiben commented 8 years ago

Looks like there's a real need for this.

Ideal solution IMO would be to add {position: fixed, bottom: 0} to an element.

tgoldenberg commented 8 years ago

@pjcabrera the example works great on Simulator but unfortunately not in my actual 4S device - it actually hides the View. Any suggestions?

alvaromb commented 8 years ago

Just a quick update, I've added TextInput auto scroll feature into my react-native-keyboard-aware-scroll-view component: https://github.com/APSL/react-native-keyboard-aware-scroll-view/releases/tag/v0.1.0

mkonicek commented 8 years ago

react-native-keyboard-aware-scroll-view looks great!

@facebook-github-bot answered

mkonicek commented 8 years ago

@facebook-github-bot answered

facebook-github-bot commented 8 years ago

Closing this issue as @mkonicek says the question asked has been answered. Please help us by asking questions on StackOverflow. StackOverflow is amazing for Q&A: it has a reputation system, voting, the ability to mark a question as answered. Because of the reputation system it is likely the community will see and answer your question there. This also helps us use the GitHub bug tracker for bugs only.