Skip to content

iOS height calculation is wrong? #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
cristianoccazinsp opened this issue Sep 20, 2019 · 9 comments
Open

iOS height calculation is wrong? #389

cristianoccazinsp opened this issue Sep 20, 2019 · 9 comments

Comments

@cristianoccazinsp
Copy link

Can you guys review this? https://github.com/APSL/react-native-keyboard-aware-scroll-view/blob/master/lib/KeyboardAwareHOC.js#L391

First, you are using a default extra height of 75 (probably to account for some height of an input). This is already a bit odd.

However, on the above code you are using the input's bottom location to decide if the screen should scroll or not. This seems fine, but you are also adding the extra height (which is also fine). What is wrong, however, is that you are not using the input's bottom location to actually scroll. You should be also adding the input height to the extra height calculation at the scroll code here (https://github.com/APSL/react-native-keyboard-aware-scroll-view/blob/master/lib/KeyboardAwareHOC.js#L463). If you did this, you wouldn't need a 75 default for extra height and a 0 would scroll just fine to the bottom of the element.

With the above, the final result is that you are adding some extra height to correctly scroll to the bottom of the input, but this also causes the checks to fire earlier (because you are using the bottom of the input + extra height to decide if scrolling or not). So you are basically checking to scroll for 1 value (input bottom + extra height), but only scrolling to input top + extra height. This makes scrolling very odd.

I can't tell if this is an issue also for Android since I'm not using it in this case.

@gldev
Copy link

gldev commented Oct 24, 2019

This doesn't seem to be happening on android but on ios i can see an extra overflow happening.

@andrey-tsaplin
Copy link

andrey-tsaplin commented Nov 14, 2019

My case probably related. When focus moves to TextInput nearby keyboard then autoscroll works completely wrong.

Demo (Note: when I focus bottom input, autoscroll makes it hidden):
appVideo

@cristianoccazinsp
Copy link
Author

cristianoccazinsp commented Nov 14, 2019 via email

@rgoldiez
Copy link

My fix was not to use the library at all, but make a reduced version of this component that doesn't use any of the RN undocumented APIs to scroll and stuff. RN will correctly scroll to the input (at least version 0.60). So all you have to do is adjust some of the component insets. This also has the advantage of working with nested scroll elements. Let me know if you need some code samples.

@cristianoccazinsp - code samples would be great!

@cristianoccazinsp
Copy link
Author

Alright, here it goes. This is my version of the keyboard aware scrollview without all the internal code usage. The only caveat is that it won't work with multine text inputs unless they have scrollEnabled={false} ( that is, no internal scroll for text areas).

On the other hand, it has the advantage that nested ScrollViews will work just fine and restore their scroll position for both iOS and Android.

Note: I've been using this on a few projects, but I can't guarantee it will work as expected. Let me know if you find any issues.

KeyboardShiftView.js

import React, { Component } from 'react';
import { Keyboard, ScrollView, Platform } from 'react-native';
import {runAfterInteractions} from './utils';


// helper that calls interaction manager run after interactions
// but with a tiny timeout to also give time to other code to run
// some RN change broke runAfterInteractions in a way that it no longer has a huge delay as it used to
// so we add one here
// returns a cancellable object
function runAfterInteractions(fun, to=25){
 
  let prom = null;
  let timeout = setTimeout(() => {
    prom = InteractionManager.runAfterInteractions(fun);
  }, to);

  return () => {
    clearTimeout(timeout);
    if(prom){
      prom.cancel();
      prom = null;
    }
  }
}

// Component similar to react-native-keyboard-aware-scroll-view but with nested scrolling support and no internal APIs usage
// Input fields will be auto scrolled automatically. For multi line, scrollEnabled={false} must be used.
// props:
//    Component: FlatList | ScrollView
//    extraHeight
//    innerRef

const IS_IOS = Platform.OS == 'ios';


const showEvent = IS_IOS ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = IS_IOS ? 'keyboardWillHide' : 'keyboardDidHide';


// using native props to avoid re-renders


class KeyboardShift extends Component {
  constructor(props){
    super(props);
    this.state = {
    }
    this.scroll = null;
    this.lastScroll = null;
  }

  componentDidMount() {
    this.keyboardShowSub = Keyboard.addListener(showEvent, this.handleKeyboardShow);
    this.keyboardHideSub = Keyboard.addListener(hideEvent, this.handleKeyboardHide);
    this.mounted = true;
  }

  componentWillUnmount() {
    this.mounted = false;
    this.lastScroll = null;
    this.keyboardShowSub.remove();
    this.keyboardHideSub.remove();

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    if(this.cancelShow){
      this.cancelShow();
      this.cancelShow = null;
    }
  }

  onLayout = (event) => {
    this.layout = event.nativeEvent.layout
  }

  onScroll = (event) => {
    if(event.nativeEvent.contentOffset){
      this.lastScroll = event.nativeEvent.contentOffset.y;
    }
    this.props.onScroll && this.props.onScroll(event);
  }

  onScrollEndDrag = (event) =>{
    // if user manually scrolled, do not restore scroll
    this.scroll = null;
    this.props.onScrollEndDrag && this.props.onScrollEndDrag(event);
  }

  setRef = (r) => {
    this.ref = r;
    if(this.props.innerRef){
      this.props.innerRef(r);
    }
  }

  render() {
    const { Component, innerRef, onScroll, onScrollEndDrag, keyboardDismissMode, ...rest } = this.props;
    return (
      <Component
        ref={this.setRef}
        onLayout={this.onLayout}
        keyboardDismissMode={keyboardDismissMode}
        automaticallyAdjustContentInsets={false}
        scrollEventThrottle={16}
        onScroll={this.onScroll}
        onScrollEndDrag={this.onScrollEndDrag}
        {...rest} />
    );
  }

  scrollTo = (scroll) => {
    if(this.ref.scrollToOffset){
      this.ref.scrollToOffset({
        offset: scroll,
        animated: true
      })
    }
    else{
      this.ref.scrollTo({
        animated: true,
        y: scroll
      })
    }
  }

  handleKeyboardShow = (event) => {

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    if(this.cancelShow){
      this.cancelShow();
    }

    // need to give time to other stuff to hide if any
    // also set last scroll to 0 if it wasn't defined since we always must scroll on restore
    this.scroll = this.lastScroll || (this.lastScroll = 0);

    this.cancelShow = runAfterInteractions(()=>{
      if(this.layout && this.ref && this.mounted){

        const keyboardHeight = event.endCoordinates.height;
        //const keyboardPosition = event.endCoordinates.screenY;

        let gap = keyboardHeight + this.props.extraHeight;

        // inset is also added on timeout so it doesn't look too awkward
        this.ref.setNativeProps({contentInset: { bottom: gap }});
        this.cancelShow = null;

      }
    }, 250);

  }

  handleKeyboardHide = () => {

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    if(this.cancelShow){
      this.cancelShow();
      this.cancelShow = null;
    }

    // only fire this if we actually did something
    if(this.lastScroll != null && this.ref){

      // update inset right away to remove visible area as soon as possible
      this.ref.setNativeProps({contentInset: { bottom: 0 }});

      this.cancelHide = runAfterInteractions(()=>{
        if(this.ref && this.mounted){

          let scroll = this.scroll != null ? this.scroll : this.lastScroll;
          this.scroll = null;

          if(scroll != null && this.props.enableResetScrollToCoords){
            this.scrollTo(scroll + 0.001);
          }
          this.cancelHide = null;
        }

      }, 150);

    }


  }
}


// very similar component with a few differences
// made to work with android's android:windowSoftInputMode="adjustResize"
// other modes *might* might work, but test it.
class KeyboardShiftAndroid extends Component {
  constructor(props){
    super(props);
    this.state = {
    }
    this.scroll = null;
    this.lastScroll = null;
  }

  componentDidMount() {
    // Android events are far more limited
    this.keyboardShowSub = Keyboard.addListener(showEvent, this.handleKeyboardShow);
    this.keyboardHideSub = Keyboard.addListener(hideEvent, this.handleKeyboardHide);
    this.mounted = true;
  }

  componentWillUnmount() {
    this.mounted = false;
    this.lastScroll = null;
    this.keyboardShowSub.remove();
    this.keyboardHideSub.remove();

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    if(this.cancelShow){
      this.cancelShow();
      this.cancelShow = null;
    }
  }

  onLayout = (event) => {
    this.layout = event.nativeEvent.layout
  }

  onScroll = (event) => {
    if(event.nativeEvent.contentOffset){
      // update this with a timeout since scrolling might happen
      // before keyboard show event
      let scroll = event.nativeEvent.contentOffset.y;
      if(this.scrollTimeout){
        clearTimeout(this.scrollTimeout);
      }
      this.scrollTimeout = setTimeout(()=>{
        this.lastScroll = scroll;
      }, 260); // just a little higher than keyboard show time

    }
    this.props.onScroll && this.props.onScroll(event);
  }

  onScrollEndDrag = (event) =>{
    // if user manually scrolled, do not restore scroll
    this.scroll = null;
    this.props.onScrollEndDrag && this.props.onScrollEndDrag(event);
  }

  setRef = (r) => {
    this.ref = r;
    if(this.props.innerRef){
      this.props.innerRef(r);
    }
  }

  render() {
    const { Component, innerRef, onScroll, onScrollEndDrag, ...rest } = this.props;
    return (
      <Component
        ref={this.setRef}
        onLayout={this.onLayout}
        automaticallyAdjustContentInsets={false}
        scrollEventThrottle={16}
        onScroll={this.onScroll}
        onScrollEndDrag={this.onScrollEndDrag}
        {...rest} />
    );
  }

  scrollTo = (scroll) => {
    if(this.ref.scrollToOffset){
      this.ref.scrollToOffset({
        offset: scroll,
        animated: true
      })
    }
    else{
      this.ref.scrollTo({
        animated: true,
        y: scroll
      })
    }
  }


  // this relies on the fact that keyboard did show
  // happens before everything scrolls up due to height changes
  handleKeyboardShow = (event) => {

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    // need to give time to other stuff to hide if any
    // also set last scroll to 0 if it wasn't defined since we always must scroll on restore
    this.scroll = this.lastScroll || (this.lastScroll = 0);


  }

  handleKeyboardHide = () => {

    if(this.cancelHide){
      this.cancelHide();
      this.cancelHide = null;
    }

    // only fire this if we actually did something
    if(this.lastScroll != null && this.ref){

      this.cancelHide = runAfterInteractions(()=>{
        if(this.ref && this.mounted && this.scroll != null && this.props.enableResetScrollToCoords){
          this.scrollTo(this.scroll);
          this.cancelHide = null;
        }

      }, 150);

    }


  }
}


export default class KeyboardShiftView extends React.Component{

  render(){
    let {Component, extraHeight, innerRef, androidEnabled, ...rest} = this.props;
    return IS_IOS ?
      <KeyboardShift Component={Component} extraHeight={extraHeight} innerRef={innerRef} {...rest}/>
      :
      (androidEnabled ? <KeyboardShiftAndroid Component={Component} extraHeight={extraHeight} innerRef={innerRef} {...rest}/> : <Component ref={innerRef} {...rest}/>)
  }
}

KeyboardShiftView.defaultProps = {
  Component: ScrollView,
  extraHeight: 10,
  keyboardDismissMode: 'interactive',
  enableResetScrollToCoords: true,
  keyboardShouldPersistTaps: 'handled',
  androidEnabled: true
}

@andrey-tsaplin
Copy link

andrey-tsaplin commented Nov 26, 2019

I've added insetOnly option. It disables autoScroll and only adds keyboard padding. That is all the app needs if only onscreen text inputs can be focused. PR: #403

@danhnguyeen
Copy link

I had same problem. Is there any way to fix this issue please?

@LauraBeatris
Copy link

A lot of problems with this lib... My solution was just to use the KeyboardAvoidingView from RN

@abhay-sqh
Copy link

Still facing the same issue, Any update on this thread?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants