如何在 React Native 中处理键盘遮挡 |
您所在的位置:网站首页 › h5移动端键盘弹出挡住内容 › 如何在 React Native 中处理键盘遮挡 |
tag: [React, React Native, expo, KeyboardAvoidingView] 前言在编写移动端 App 中, 一个经常处理的问题就是如何避免键盘遮挡页面, 在 H5 中可以通过 scrollIntoView或者手动设置 scrollTop的值来处理, 今天让我们把目光聚焦于 React Native, 看看 RN 是如何产生和处理键盘遮挡视图的问题. 本文相关代码 github: github.com/MonchiLin/K… Android:windowSoftInputModeAndroid 自身可以通过 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 被顶上去了. Pan 模式观看 gif 图可以发现, Fixed Header 在输入框也被顶上去了. 总结对于笔者来说, 最大的区别就在于两者对于键盘弹出时的处理, 除此之外还有一些其他区别, 但是本文的重点不在于此, 如果有兴趣深入了解的小伙伴可以自行搜索 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, 来避免被键盘遮挡. 聊天框式视图效果预览, 头部导航栏和底部输入框是固定的, 中间内容区域可滚动. 主要需要注意的是 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被设置为了键盘的高度. 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 跟随输入框内容区域滚动, 而不是固定 伪代码如下: 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 的子元素 此时整个页面已经表现的很好了, 但是问题往往不是这么简单, 让我们看看现在的效果 可以发现, 在 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 |