Keyboard is dismissed on Android when Animated View finishes its animation.

Hello, I’ve been at a loss so far with this. Im writing also to see if someone can give me more hindsight in this problem or what is going on. I’ve been developing an app on Expo sdk36 for at least 4 months now and normally when introducing text on an Input the app didn’t try to resize the views so the keyboard doesn’t cover them.

I implemented an animated view to hanlde such cases in a long form I have. Following articles I’ve found it was working perfectly on both platforms. On IOS the animation was more smooth because the keyboard events returned a somewhat decent duration meanwhile on Android the duration defaults to 0 from the event itself, but at least it only resized the View where I had the scroll with the Form.

Yesterday, I’m not sure if it’s due to the new version of expo client and sdk or what but on Android it suddenly started resizing the whole view when inputing a text. When that happened I did a rollback of the proyect back to sdk 36 but the issue persisted, I attribute to the fact that on my phone I already updated my expo client to the last version. On the android simulator, where the expo client is not yet updated to handle sdk37 it still works as before. From what I’ve read that’s the normal behavior on Android but the problem I have, after all this context, is the following:

On IOS it works as expected, of course because IOS doesn’t try to resize the view whenever an input is shown but on Android something weird is happening, the animation fires but when the view is finally resized, the keyboard gets dismissed immediately, I tested this by giving Animated.timing() a much longer duration and it was indeed executing it but when it finished, the keyboard gets dismissed and as such the view goes back immediately, not allowing the user to input any text.

Right now I added a validation so it doesn’t execute the animation on Android to avoid the issue, but I hope someone can see my problem and be able to explain to me what I’m doing wrong or if this is the expected behaviour on Android, or if I’m using a wrong library or a wrong implementation, I’ve been looking everywhere but I haven’t found anything of the sort and the only solution that could work in my case would be ejecting from Expo and changing android:windowSoftInputMode=“adjustResize”, which I can’t because of the requirements of the proyect I’m currently in.

This is the implementation I’m using for the view with the registration form:

import React, { Component, Fragment } from 'react';
import { TouchableOpacity, Text, View, ImageBackground, Animated, Keyboard, Platform, ScrollView, ActivityIndicator } from 'react-native';
import styles from './styles';
import moment from 'moment';
import DateTimePickerModal from "react-native-modal-datetime-picker";
import FormInput from '../../../components/formInput';
import { Formik } from 'formik';
import { Icon } from 'react-native-elements';
import { firstFormValidation } from './formValidations';
import { moderateScale } from '../../../styles/globalStyles';

export class RegisterOne extends Component {

    constructor(props) {
        super(...arguments);

        this.state = {
            firstForm: {
                nombre: '',
                apellidos: '',
                cedula: '',
                fechaNacimiento: '',
                NIT: '',
                cargo: '',
            },
            show: false,
            keyboardHeight: new Animated.Value(0)
        }
    };

    componentDidMount() {
        // Creamos Listeners para detectar cuando aparece o desaparece el teclado
       // Listeners to subscribe to keyboard events according to the platform and execute the resizing animation
        this.keyboardWillShowSub = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', this.keyboardWillShow);
        this.keyboardWillHideSub = Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', this.keyboardWillHide);
    };

    componentWillUnmount() {
        // Limpiamos los Listeners de los eventos del teclado
       // Remove subscriptions on component unmount
        this.keyboardWillShowSub.remove();
        this.keyboardWillHideSub.remove();
    };

    keyboardWillShow = (event) => {
        if (Platform.OS === 'ios') {
            Animated.timing(this.state.keyboardHeight, {
                duration: event.duration,
                toValue: event.endCoordinates.height,
            }).start();
        }
    };

    keyboardWillHide = (event) => {
        Animated.timing(this.state.keyboardHeight, {
            duration: event.duration,
            toValue: 0,
        }).start();
    };

    show = () => {
        this.setState({
            ...this.state,
            show: true,
        });
    };
    
    closePicker = () => {
        this.setState({
            ...this.state,
            show: false
        })
    };

    handleConfirm = (date,formikProps,name) => {
        this.closePicker();
        formikProps.setFieldValue(name, moment(date).format('YYYY-MM-DD'));
    };

    renderFormDateSelector(formikProps, name, placeholder, password = false) {
        return (
            <View style={styles.formInput}>
                <FormInput
                    secureTextEntry={password}
                    placeholder={placeholder}
                    editable={false}
                    rightIcon={
                        <TouchableOpacity onPress={() => this.show()}>
                            <Icon size={moderateScale(14)} type='feather' name={this.state.show ? 'chevron-up' : 'chevron-down'} color='grey'/>
                        </TouchableOpacity>
                    }
                >
                    { formikProps.values[name] !== '' ? moment(formikProps.values[name]).format('YYYY-MM-DD') : ''}
                </FormInput>
                {this.state.show &&
                    <DateTimePickerModal
                        mode={'date'}
                        isVisible={this.state.show}
                        maximumDate={moment().toDate()}
                        onConfirm={(date) => this.handleConfirm(date,formikProps,name)}
                        onCancel={this.closePicker}
                    />
                }
            </View>
        );
    };

    renderFormInput(formikProps, name, placeholder, password = false) {
        return (
            <View style={styles.formInput}>
                <FormInput
                    name={name}
                    secureTextEntry={password}
                    value={formikProps.values[name]}
                    placeholder={placeholder}
                    onBlur={formikProps.handleBlur(name)}
                    onChangeText={formikProps.handleChange(name)}
                    errorMessage={formikProps.touched[name] && formikProps.errors[name]}
                />
            </View>
        );
    };

    render() {
        return (
            <ImageBackground source={require("../../../assets/splashScreens/fondo_login.png")} style={{width: '100%', height: '100%'}}>
                <View style={styles.container}>
                    <View style={styles.titleContainer}>
                                <Text style={styles.title}>SOLICITUD DE REGISTRO</Text>
                            </View>
                            <View style={styles.formContainer}>
                                <View style={styles.formTitleContainer}>
                                    <Icon type='material-community' name='account-key' color='white' size={moderateScale(50)}/>
                                    <Text style={styles.formTitle}>Paso 1 de 2. Información Personal</Text>
                                </View>
                                <View style={styles.formInputs}>
                                    <Formik
                                        initialValues={{
                                            nombre: '',
                                            apellidos: '',
                                            cedula: '',
                                            fechaNacimiento: '',
                                            NIT: '',
                                            cargo: ''
                                        }}
                                        onSubmit={async (values, actions) => {
                                            // Guardamos los valores en el estado para poder enviarlos a la segunda parte
                                            this.setState({
                                                ...this.state,
                                                firstForm: {
                                                    ...values
                                                },
                                            }, () => {
                                                this.props.navigation.navigate('RegisterTwo', {firstForm: {...this.state.firstForm}})
                                            });
                                        }}
                                        validationSchema={firstFormValidation}
                                    >
                                {
                                    formikProps => (
                                            <Fragment>
                                                <Animated.View style={{ flex: 0.8, margin: moderateScale(10), paddingBottom: this.state.keyboardHeight}}>
                                                    <ScrollView style={{height: 300}}>
                                                        {this.renderFormInput(formikProps,'nombre','Nombre')}
                                                        {this.renderFormInput(formikProps,'apellidos','Apellidos')}
                                                        {this.renderFormInput(formikProps,'cedula','Cédula o Doc. Identidad')}
                                                        {this.renderFormDateSelector(formikProps,'fechaNacimiento','Fecha de Nacimiento')}
                                                        {this.renderFormInput(formikProps,'NIT','NIT de la Empresa (Sin digito de verificación)')}
                                                        {this.renderFormInput(formikProps,'cargo','Cargo en la Empresa')}
                                                    </ScrollView>
                                                </Animated.View>
                                                <View style={styles.formButtonContainer}>
                                                    <TouchableOpacity disabled={formikProps.isSubmitting} style={styles.button} onPress={() => formikProps.handleSubmit()}>
                                                        {!formikProps.isSubmitting ? (
                                                                <Text style={styles.buttonText}>CONTINUAR</Text>
                                                            ):(
                                                                <ActivityIndicator style={{flex: 1, alignSelf: 'center'}} size='large'/>
                                                            )
                                                        }
                                                    </TouchableOpacity>
                                                </View>
                                            </Fragment>
                                            )
                                        }
                                    </Formik>
                                </View>
                    </View>
                </View>
            </ImageBackground>
        )
    }
};
1 Like

The bit I’ve said about adding more duration worked to see what was happening. If someone has a problem like this one, as of now, what has been happening apparently is, the view that contains the scrollView is being resized in such a way that it’s paddingBottom is becoming bigger than the height itself, and as such it resets when the animation finishes.

The issue happens in android maybe because the whole container is being resized and pushed upwards and thus the height of the scrollview becomes less than what was defined, and as such when the Animation is triggered and the paddingBottom is augmented, it becomes higher than the ScrollView height itself.

So for now to have the animation on Android I would need to specify a static toValue in Animated.timing() and on ios keep using the endCoordinates I get from the event.

Still, it would be cool if I could find a way as to only allow one part of the view to be pushed instead of the whole screen but I think at that point I would need to eject and edit the android manifest for that.

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.