Como deslizar automaticamente a janela para fora do teclado quando o TextInput está em foco?

90

Já vi esse hack para aplicativos nativos rolarem automaticamente a janela, mas me perguntando a melhor maneira de fazer isso no React Native ... Quando um <TextInput>campo recebe o foco e é posicionado baixo na visualização, o teclado cobre o campo de texto.

Você pode ver esse problema no exemplo de TextInputExample.jsvisualização do UIExplorer .

Alguém tem uma boa solução?

McG
fonte
3
Eu sugiro adicionar isso como um problema no rastreador do Github e ver se acontece alguma coisa, já que essa será uma reclamação muito comum.
Colin Ramsay

Respostas:

83

Resposta de 2017

Este KeyboardAvoidingViewé provavelmente o melhor caminho a seguir agora. Confira a documentação aqui . É realmente simples em comparação com o Keyboardmódulo, o que dá ao desenvolvedor mais controle para realizar animações. Spencer Carli demonstrou todas as maneiras possíveis em seu blog médio .

Resposta de 2015

A maneira correta de fazer isso react-nativenão requer bibliotecas externas, aproveita o código nativo e inclui animações.

Primeiro defina uma função que tratará o onFocusevento para cada um TextInput(ou qualquer outro componente para o qual você gostaria de rolar):

// 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);
}

Então, em sua função de renderização:

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

Isso usa RCTDeviceEventEmitterpara eventos e dimensionamento do teclado, mede a posição do componente usando RCTUIManager.measureLayoute calcula o movimento de rolagem exato necessário em scrollResponderInputMeasureAndScrollToKeyboard.

Você pode querer brincar com o additionalOffsetparâmetro, para se adequar às necessidades de seu design de IU específico.

Sherlock
fonte
5
Este é um bom achado, mas para mim não foi suficiente, porque enquanto o ScrollView irá garantir que o TextInput esteja na tela, o ScrollView ainda estava mostrando o conteúdo abaixo do teclado que o usuário não conseguia rolar. Definir a propriedade ScrollView "keyboardDismissMode = on-drag" permite ao usuário dispensar o teclado, mas se não houver conteúdo de rolagem suficiente abaixo do teclado, a experiência será um pouco chocante. Se o ScrollView só precisa rolar por causa do teclado em primeiro lugar, e você desativa o salto, então parece não haver maneira de descartar o teclado e mostrar o conteúdo abaixo
miracle2k
2
@ miracle2k - Tenho uma função que redefine a posição da visualização de rolagem quando uma entrada está borrada, ou seja, quando o teclado fecha. Talvez isso possa ajudar no seu caso?
Sherlock
2
@Sherlock Com o que se parece a função de redefinição da visualização desfocada da rolagem? A propósito, solução incrível :)
Ryan McDermott,
8
Nas versões mais recentes do React Native, você precisará chamar: * import ReactNative from 'react-native'; * antes de chamar * ReactNative.findNodeHandle () * Caso contrário, o aplicativo irá travar
amirfl
6
Agora import {findNodeHandle} from 'react-native' stackoverflow.com/questions/37626851/…
antoine129
26

O Facebook abriu o código-fonte KeyboardAvoidingView em react nativos 0,29 para resolver este problema. A documentação e o exemplo de uso podem ser encontrados aqui .

distante
fonte
32
Cuidado com o KeyboardAvoidingView, ele simplesmente não é fácil de usar. Nem sempre se comporta como você espera que deveria. A documentação é praticamente inexistente.
Renato Volta
doc e comportamento estão melhorando agora
antoine129
O problema que tenho é que o KeyboardAvoidingView mede a altura do teclado como 65 no meu simulador do iPhone 6 e, portanto, minha visão ainda está escondida atrás do teclado.
Marc
A única maneira de gerenciar isso foi por meio de uma abordagem de encaixe acionada porDeviceEventEmitter.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
Marc
12

Combinamos parte da forma de código react-native-keyboard-spacer e o código de @Sherlock para criar um componente KeyboardHandler que pode ser agrupado em qualquer View com elementos TextInput. Funciona como um encanto! :-)

/**
 * 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
fonte
Algo fácil / óbvio que impediria o teclado de ser exibido em um simulador de iOS?
seigel
1
Você tentou Command + K (Hardware-> Teclado-> Alternar Keboard de Software)?
John kendall
Tente a versão modificada disso aqui: gist.github.com/dbasedow/f5713763802e27fbde3fc57a600adcd3 Eu acredito que isso é melhor porque não depende de nenhum tempo limite que eu considero frágil.
CoderDave
10

Primeiro você precisa instalar os eventos react-native-keyboard .

  1. No XCode, no navegador do projeto, clique com o botão direito em Bibliotecas ➜ Adicionar arquivos a [nome do seu projeto] Vá para node_modules ➜ react-native-keyboardevents e adicione o arquivo .xcodeproj
  2. No XCode, no navegador de projetos, selecione seu projeto. Adicione o lib * .a do projeto keyboardevents às fases de construção do seu projeto ➜ Vincular binário às bibliotecas Clique no arquivo .xcodeproj que você adicionou antes no navegador do projeto e vá para a guia Configurações de construção. Certifique-se de que 'Tudo' está ativado (em vez de 'Básico'). Procure por Caminhos de Pesquisa de Cabeçalho e certifique-se de que contém $ (SRCROOT) /../ react-native / React e $ (SRCROOT) /../../ React - marque ambos como recursivos.
  3. Execute seu projeto (Cmd + R)

Em seguida, de volta à terra do javascript:

Você precisa importar os eventos react-native-keyboard.

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

Em seguida, em sua visualização, adicione algum estado para o espaço do teclado e atualize a partir da escuta dos eventos do teclado.

  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,
    };
  },

Finalmente, adicione um espaçador à sua função de renderização embaixo de tudo para que, quando aumentar o tamanho, bata no seu material.

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

Também é possível usar a API de animação, mas para simplificar, apenas ajustamos após a animação.

Brysgo
fonte
1
Seria incrível ver um exemplo de código / mais algumas informações sobre como fazer a animação. O salto é bem desajeitado, e trabalhando apenas com os métodos "mostrará" e "mostrarei", não consigo descobrir como adivinhar quanto tempo a animação do teclado terá ou qual a altura de "mostrará".
Stephen
2
[email protected] agora envia eventos de teclado (por exemplo, "keyboardWillShow") por meio do DeviceEventEmitter, para que você possa registrar ouvintes para esses eventos. Ao lidar com um ListView, no entanto, descobri que chamar scrollTo () no scrollview do ListView funcionou melhor: this.listView.getScrollResponder().scrollTo(rowID * rowHeight); isso é chamado em TextInput de uma linha quando recebe um evento onFocus.
Jed Lau
4
Essa resposta não é mais válida, pois o RCTDeviceEventEmitter faz o trabalho.
Sherlock de
6

Experimente isto:

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>);
}

...

Funcionou para mim A visualização basicamente diminui quando o teclado é exibido e volta a crescer quando está oculto.

pomo
fonte
Além disso, esta solução funciona bem (RN 0.21.0) stackoverflow.com/a/35874233/3346628
pomo
use this.keyboardWillHide.bind (this) em vez de self
animekun
Use o teclado em vez do DeviceEventEmitter
Madura Pradeep
4

Talvez seja tarde demais , mas a melhor solução é usar uma biblioteca nativa, IQKeyboardManager

Basta arrastar e soltar o diretório IQKeyboardManager do projeto de demonstração para o seu projeto iOS. É isso aí. Além disso, você pode configurar alguns valores, como isToolbar habilitado, ou o espaço entre a entrada de texto e o teclado no arquivo AppDelegate.m. Mais detalhes sobre a personalização estão no link da página GitHub que adicionei.

Adrian Zghibarta
fonte
1
Esta é uma excelente opção. Veja também github.com/douglasjunior/react-native-keyboard-manager para uma versão empacotada para ReactNative - é fácil de instalar.
loevborg
3

Usei TextInput.onFocus e ScrollView.scrollTo.

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

@Stephen

Se você não se importa em não ter a animação de altura exatamente na mesma taxa que o teclado aparece, você pode apenas usar LayoutAnimation, para que pelo menos a altura não pule no lugar. por exemplo

importar LayoutAnimation de react-native e adicionar os seguintes métodos ao seu componente.

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);
  },

Alguns exemplos de animações são (estou usando a mola acima):

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,
      },
    },
  },
};

ATUALIZAR:

Veja a resposta de @sherlock abaixo, a partir do react-native 0.11, o redimensionamento do teclado pode ser resolvido usando a funcionalidade embutida.

decanmcpherson
fonte
2

Você pode combinar alguns dos métodos em algo um pouco mais simples.

Anexe um ouvinte onFocus em suas entradas

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

Nosso método de rolagem para baixo se parece com:

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

Isso diz à nossa visualização de rolagem (lembre-se de adicionar um ref) para rolar para baixo até a posição de nossa entrada focada - 200 (é aproximadamente do tamanho do teclado)

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

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

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

Aqui, redefinimos nossa visualização de rolagem de volta ao topo

insira a descrição da imagem aqui

Nath
fonte
2
@ você poderia fornecer seu método render ()?
valerybodak
0

Estou usando um método mais simples, mas ainda não está animado. Eu tenho um estado de componente chamado "bumpedUp", que é definido como 0 por padrão, mas definido como 1 quando o textInput obtém o foco, assim:

No meu textInput:

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

Eu também tenho um estilo que dá ao contêiner de embalagem de tudo na tela uma margem inferior e uma margem superior negativa, assim:

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

E então, no contêiner de embalagem, defino os estilos assim:

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

Portanto, quando o estado "bumpedUp" é definido como 1, o estilo bumpedcontainer entra em ação e move o conteúdo para cima.

Meio hacky e as margens são codificadas, mas funciona :)

Stirman
fonte
0

Eu uso a resposta brysgo para aumentar a parte inferior do meu scrollview. Então eu uso o onScroll para atualizar a posição atual do scrollview. Então, encontrei este React Native: Obtendo a posição de um elemento para obter a posição da entrada de texto. Em seguida, faço algumas contas simples para descobrir se a entrada está na visualização atual. Em seguida, uso scrollTo para mover a quantidade mínima mais uma margem. É muito bom. Aqui está o código para a parte de rolagem:

            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 );
                        }
                     );
                };
            },
Aintnorest
fonte
0

Eu também encontro esta questão. Por fim, resolvo isso definindo a altura de cada cena, como:

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

E também uso um módulo de terceiros https://github.com/jaysoo/react-native-extra-dimensions-android para obter a altura real.

Renguang Dong
fonte