如何在 React Native 中处理键盘遮挡

您所在的位置:网站首页 h5移动端键盘弹出挡住内容 如何在 React Native 中处理键盘遮挡

如何在 React Native 中处理键盘遮挡

2023-10-28 00:36| 来源: 网络整理| 查看: 265

tag: [React, React Native, expo, KeyboardAvoidingView]

前言

在编写移动端 App 中, 一个经常处理的问题就是如何避免键盘遮挡页面, 在 H5 中可以通过 scrollIntoView或者手动设置 scrollTop的值来处理, 今天让我们把目光聚焦于 React Native, 看看 RN 是如何产生和处理键盘遮挡视图的问题.

本文相关代码 github: github.com/MonchiLin/K…

Android:windowSoftInputMode

Android 自身可以通过 android:windowSoftInputMode来设置软键盘的方式, 这里就多余的就不做赘述了, 只介绍最常用的两种 adjustResize和 adjustPan. 如果你也使用 expo, 可以通过修改此选项来检验结果. docs.expo.dev/versions/la… 测试代码如下:

import { SafeAreaView, ScrollView, Text, TextInput, View } from 'react-native'; function Boxes() { return { [...Array(30).keys()] .map(i => { return ; }) } ; } export default function ResizeVSPan() { return Fixed Header Inner Header ; }

Fixed Header: 始终固定于屏幕顶部 Inner Header: 在内容区域滚动时跟随滚动

Resize 模式

观看 gif 图可以发现, Fixed Header 在输入框弹出时是固定的, 只有 Inner Header 被顶上去了. resize_mode.gif

Pan 模式

观看 gif 图可以发现, Fixed Header 在输入框也被顶上去了. pan_mode.gif

总结

对于笔者来说, 最大的区别就在于两者对于键盘弹出时的处理, 除此之外还有一些其他区别, 但是本文的重点不在于此, 如果有兴趣深入了解的小伙伴可以自行搜索 android windowSoftInputMode关键词. iOS 并没有此选项, 要依靠 KeyboardAvoidingView组件, 所以大家可能在 google 如何解决才会看到类似的回答:

笔者当初看到这个解决方案的时候也觉得很困惑(为什么 android 传 undefined, 参见源码解析和 windowSoftInputMode 用法), 这次深入研究后才明白其本质.

KeyboardAvoidingView 源码解析 /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @flow strict-local */ import Keyboard from './Keyboard'; import LayoutAnimation from '../../LayoutAnimation/LayoutAnimation'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; import StyleSheet from '../../StyleSheet/StyleSheet'; import View from '../View/View'; import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; import {type EventSubscription} from '../../vendor/emitter/EventEmitter'; import type { ViewProps, ViewLayout, ViewLayoutEvent, } from '../View/ViewPropTypes'; import type {KeyboardEvent, KeyboardEventCoordinates} from './Keyboard'; type Props = $ReadOnly; type State = {| bottom: number, |}; /** * View that moves out of the way when the keyboard appears by automatically * adjusting its height, position, or bottom padding. */ class KeyboardAvoidingView extends React.Component { _frame: ?ViewLayout = null; _keyboardEvent: ?KeyboardEvent = null; _subscriptions: Array = []; viewRef: {current: React.ElementRef | null, ...}; _initialFrameHeight: number = 0; constructor(props: Props) { super(props); this.state = {bottom: 0}; this.viewRef = React.createRef(); } _relativeKeyboardHeight(keyboardFrame: KeyboardEventCoordinates): number { const frame = this._frame; if (!frame || !keyboardFrame) { return 0; } const keyboardY = keyboardFrame.screenY - (this.props.keyboardVerticalOffset ?? 0); // Calculate the displacement needed for the view such that it // no longer overlaps with the keyboard return Math.max(frame.y + frame.height - keyboardY, 0); } _onKeyboardChange = (event: ?KeyboardEvent) => { this._keyboardEvent = event; this._updateBottomIfNecessary(); }; _onLayout = (event: ViewLayoutEvent) => { const wasFrameNull = this._frame == null; this._frame = event.nativeEvent.layout; if (!this._initialFrameHeight) { // save the initial frame height, before the keyboard is visible this._initialFrameHeight = this._frame.height; } if (wasFrameNull) { this._updateBottomIfNecessary(); } if (this.props.onLayout) { this.props.onLayout(event); } }; _updateBottomIfNecessary = () => { if (this._keyboardEvent == null) { this.setState({bottom: 0}); return; } const {duration, easing, endCoordinates} = this._keyboardEvent; const height = this._relativeKeyboardHeight(endCoordinates); if (this.state.bottom === height) { return; } if (duration && easing) { LayoutAnimation.configureNext({ // We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m duration: duration > 10 ? duration : 10, update: { duration: duration > 10 ? duration : 10, type: LayoutAnimation.Types[easing] || 'keyboard', }, }); } this.setState({bottom: height}); }; componentDidMount(): void { if (Platform.OS === 'ios') { this._subscriptions = [ Keyboard.addListener('keyboardWillChangeFrame', this._onKeyboardChange), ]; } else { this._subscriptions = [ Keyboard.addListener('keyboardDidHide', this._onKeyboardChange), Keyboard.addListener('keyboardDidShow', this._onKeyboardChange), ]; } } componentWillUnmount(): void { this._subscriptions.forEach(subscription => { subscription.remove(); }); } render(): React.Node { const { behavior, children, contentContainerStyle, enabled = true, // eslint-disable-next-line no-unused-vars keyboardVerticalOffset = 0, style, onLayout, ...props } = this.props; const bottomHeight = enabled === true ? this.state.bottom : 0; switch (behavior) { case 'height': let heightStyle; if (this._frame != null && this.state.bottom > 0) { // Note that we only apply a height change when there is keyboard present, // i.e. this.state.bottom is greater than 0. If we remove that condition, // this.frame.height will never go back to its original value. // When height changes, we need to disable flex. heightStyle = { height: this._initialFrameHeight - bottomHeight, flex: 0, }; } return ( {children} ); case 'position': return ( {children} ); case 'padding': return ( {children} ); default: return ( {children} ); } } } export default KeyboardAvoidingView;

可以看到, KeyboardAvoidingView 的源码还是相当简洁的, 它的实现目标就体现在 behavior参数. 这里提两个源码里的要点

keyboardDidShow 事件在 iOS 上还有会有一个 startCoordinates对象, 它可以区分键盘是收起还是展开, 参考, 源码位置. 键盘高度的算法为(仅为笔者推测): 视图(KeyboardAvoidingView 组件)所占用的高度 - 键盘弹出时屏幕可视高度

首先通过 Keyboard.addListener监听键盘弹出和隐藏, 然后通过键盘弹出事件得到 键盘高度最后根据 behavior参数来决定如何处理键盘高度.

对于如何处理键盘高度, 有三种方式

height

这种方式是通过减少 View 的高度, 来避免被键盘遮挡, 实际使用中作用不大, 应用场景也比较少.

position

这种方式是通过设置 View 的 bottom, 来避免被键盘遮挡.

padding

这种方式是通过设置 View 的 paddingBottom, 来避免被键盘遮挡.

聊天框式视图

效果预览, 头部导航栏和底部输入框是固定的, 中间内容区域可滚动.

image.png 主要需要注意的是 KeyboardAvoidingView 应包裹输入框, 但不应包含导航栏, 如果包含导航栏, 则会在键盘显示时将导航栏顶出可视视图外. 注: 笔者的代码并不会, 但是有些情况会, 例如这种情况: Flatlist 包裹了 Header 和 Footer, 不过这又是另一种情况了, 但还是建议大家可以良好的组织视图代码的结构.

Fixed Header } ListFooterComponent={ } ref={listRef} data={data} renderItem={({ item }) => } /> {}

注意 behavior={Platform.select({ ios: "padding", default: undefined })}这里 android 是不处理的, 因为笔者为 Android 设置了 ajustResize, 通过 ajustResize的特性, 使得在键盘弹出时, 视图自动向上滚动的特性来处理键盘遮挡. 如有已经了解了 KeyboardAvoidingView 的原理同学, 不难猜出, 其实在这种场景下键盘弹出时, 视图就变成了这样, 即 paddingBottom被设置为了键盘的高度. image.png

import { Animated, Button, Easing, FlatList, Keyboard, KeyboardAvoidingView, Platform, Text, TextInput, View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import { useEffect, useRef, useState } from "react"; function ItemView({ item }: { item: number }) { return Chart Message {item} ; } const MoreView = ({ visible }: { visible: boolean }) => { const v = useRef(new Animated.Value(visible ? 200 : 0)); useEffect(() => { Animated.timing(v.current, { toValue: visible ? 200 : 0, useNativeDriver: false, duration: 1000, easing: Easing.bounce }).start(); }, [visible]); return ; }; export default function KeyboardAvoidingChart() { const insets = useSafeAreaInsets(); const listRef = useRef(null); const inputRef = useRef(null); const [data, setData] = useState([...Array(30).keys()]); const [moreViewVisible, setMoreViewVisible] = useState(false); const onSendPress = () => { setData(state => [...state, state.length]); setImmediate(() => listRef.current?.scrollToEnd()); }; const onFocus = () => { setMoreViewVisible(false); }; const onMorePress = () => { Keyboard.dismiss(); setMoreViewVisible(state => !state); }; return ( Fixed Header } /> {} ); } 登录页面视图

我们的页面布局大概是这样

需求有几点 一. Fixed Header 任何时候都固定在顶部 二. 当弹出软键盘时, 输入框不要被软键盘遮挡 三. 当弹出软键盘时, Logo 跟随输入框内容区域滚动, 而不是固定 image.png 伪代码如下:

RootView Fixed Header Logo KeyboardAvoidingView ScrollView 若干个输入框 View 注册, 登录按钮

首先是第一个需求, 已经决定了我们不能设置 android:windowSoftInputMode 为 ajustPan因为这个模式在软键盘弹出时会平移整个视图从而导致 Fixed Header 不被固定 第二个需求已经被 KeyboardAvoidingView 自动处理了. 为了解决第三个需求, 我们将 Logo 移动至 KeyboardAvoidingView/ScrollView 内部即可解决, 即如下伪代码

RootView Fixed Header KeyboardAvoidingView ScrollView Logo 若干个输入框 View 注册, 登录按钮

它的秘诀在于, KeyboardAvoidingView 只能影响到自身的子元素, 要控制别的元素就要让别的元素成为 KeyboardAvoidingView 的子元素 此时整个页面已经表现的很好了, 但是问题往往不是这么简单, 让我们看看现在的效果 sign-up-1.gif 可以发现, 在 iOS 上表现已经很好了, 但是在 Android 上面会将键盘自动顶起来, 要解决这个问题有两种方案

将 android:windowSoftInputMode设置为为 ajustPan, 但是上面提到, ajustPan 会破坏布局, 所以不能使用. 将 按钮组也移动到 KeyboardAvoidingView/ScrollView 内部, 其原理也是依赖 ajustResize 会自动滚动视图的特性.

最终结构如下:

RootView Fixed Header KeyboardAvoidingView ScrollView Logo 若干个输入框 View 注册, 登录按钮

源码分享

import { Button, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, Text, TextInput, View } from 'react-native'; function StyledInput() { return ; } function Logo() { return Logo ; } export default function SignUp() { return Sign Up Header ; } 总结

KeyboardAvoiding + ScrollView 已经为我们解决了大多数问题, 但是实际使用仍然有些问题, 这里笔者推荐大家直接使用 react-native-keyboard-aware-scroll-view 来一劳永逸的解决大多数问题, 它的实现和官方的KeyboardAvoiding实现并不相同, 这个库会通过计算元素位置, 然后滚动至该元素位置.



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3