Skip to content
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

Feat RN KeyboardAvoidingView #1857

Merged
merged 18 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/platform/createApp.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export default function createApp (options) {
if (headerBackImageSource) {
navScreenOpts.headerBackImageSource = headerBackImageSource
}

return createElement(SafeAreaProvider,
null,
createElement(NavigationContainer,
Expand Down
36 changes: 16 additions & 20 deletions packages/core/src/platform/patch/getDefaultOptions.ios.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useSyncExternalStore, useRef, useMemo, useState, useCallback, createElement, memo, forwardRef, useImperativeHandle, useContext, Fragment, cloneElement, createContext } from 'react'
import { useEffect, useLayoutEffect, useSyncExternalStore, useRef, useMemo, useCallback, createElement, memo, forwardRef, useImperativeHandle, useContext, Fragment, cloneElement, createContext } from 'react'
import * as ReactNative from 'react-native'
import { ReactiveEffect } from '../../observer/effect'
import { watch } from '../../observer/watch'
Expand All @@ -10,6 +10,7 @@ import mergeOptions from '../../core/mergeOptions'
import { queueJob, hasPendingJob } from '../../observer/scheduler'
import { createSelectorQuery, createIntersectionObserver } from '@mpxjs/api-proxy'
import { IntersectionObserverContext, RouteContext, KeyboardAvoidContext } from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/context'
import KeyboardAvoidingView from '@mpxjs/webpack-plugin/lib/runtime/components/react/dist/KeyboardAvoidingView'

const ProviderContext = createContext(null)

Expand Down Expand Up @@ -569,7 +570,6 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
const { PortalHost, useSafeAreaInsets, GestureHandlerRootView, useHeaderHeight } = global.__navigationHelper
const pageConfig = Object.assign({}, global.__mpxPageConfig, currentInject.pageConfig)
const Page = ({ navigation, route }) => {
const [enabled, setEnabled] = useState(false)
const currentPageId = useMemo(() => ++pageId, [])
const intersectionObservers = useRef({})
usePageStatus(navigation, currentPageId)
Expand All @@ -587,34 +587,30 @@ export function getDefaultOptions ({ type, rawOptions = {}, currentInject }) {
}, [])

const rootRef = useRef(null)
const keyboardAvoidRef = useRef({ cursorSpacing: 0, ref: null })
const onLayout = useCallback(() => {
rootRef.current?.measureInWindow((x, y, width, height) => {
navigation.layout = { x, y, width, height }
})
}, [])

const withKeyboardAvoidingView = (element) => {
if (__mpx_mode__ === 'ios') {
return createElement(KeyboardAvoidContext.Provider,
return createElement(KeyboardAvoidContext.Provider,
{
value: keyboardAvoidRef
},
createElement(KeyboardAvoidingView,
{
value: setEnabled
},
createElement(ReactNative.KeyboardAvoidingView,
{
style: {
flex: 1
},
contentContainerStyle: {
flex: 1
},
behavior: 'position',
enabled
style: {
flex: 1
},
element
)
contentContainerStyle: {
flex: 1
}
},
element
)
}
return element
)
}

navigation.insets = useSafeAreaInsets()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ module.exports = function ({ print }) {
qa: qaPropLog
},
{
test: /^(placeholder-style|placeholder-class|cursor-spacing|always-embed|hold-keyboard|safe-password-.+)$/,
test: /^(placeholder-style|placeholder-class|always-embed|hold-keyboard|safe-password-.+)$/,
ios: iosPropLog,
android: androidPropLog
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ module.exports = function ({ print }) {
}
},
{
test: /^(placeholder-style|placeholder-class|cursor-spacing|always-embed|hold-keyboard|disable-default-padding|adjust-keyboard-to|fixed|show-confirm-bar)$/,
test: /^(placeholder-style|placeholder-class|always-embed|hold-keyboard|disable-default-padding|adjust-keyboard-to|fixed|show-confirm-bar)$/,
ios: iosPropLog,
android: androidPropLog
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { ReactNode, useContext, useEffect } from 'react'
import { DimensionValue, EmitterSubscription, Keyboard, Platform, View, ViewStyle } from 'react-native'
import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated'
import { KeyboardAvoidContext } from './context'
import { extendObject } from './utils'

type KeyboardAvoidViewProps = {
children?: ReactNode
style?: ViewStyle
contentContainerStyle?: ViewStyle
}

const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: KeyboardAvoidViewProps) => {
const isIOS = Platform.OS === 'ios'
const duration = isIOS ? 250 : 300
const easing = isIOS ? Easing.inOut(Easing.ease) : Easing.out(Easing.quad)

const offset = useSharedValue(0)
const basic = useSharedValue('auto')
const keyboardAvoid = useContext(KeyboardAvoidContext)

const animatedStyle = useAnimatedStyle(() => {
return Object.assign(
{
transform: [{ translateY: -offset.value }]
},
isIOS ? {} : { flexBasis: basic.value as DimensionValue }
)
})

const resetKeyboard = () => {
keyboardAvoid?.current && extendObject(keyboardAvoid.current, {
cursorSpacing: 0,
ref: null
})
offset.value = withTiming(0, { duration, easing })
basic.value = 'auto'
}

useEffect(() => {
let subscriptions: EmitterSubscription[] = []

if (isIOS) {
subscriptions = [
Keyboard.addListener('keyboardWillShow', (evt: any) => {
if (!keyboardAvoid?.current) return
const { endCoordinates } = evt
const { ref, cursorSpacing = 0 } = keyboardAvoid.current
setTimeout(() => {
ref?.current?.measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
const aboveOffset = offset.value + pageY + height - endCoordinates.screenY
const aboveValue = -aboveOffset >= cursorSpacing ? 0 : aboveOffset + cursorSpacing
const belowValue = Math.min(endCoordinates.height, aboveOffset + cursorSpacing)
const value = aboveOffset > 0 ? belowValue : aboveValue
offset.value = withTiming(value, { duration, easing })
})
})
}),
Keyboard.addListener('keyboardWillHide', resetKeyboard)
]
} else {
subscriptions = [
Keyboard.addListener('keyboardDidShow', (evt: any) => {
if (!keyboardAvoid?.current) return
const { endCoordinates } = evt
const { ref, cursorSpacing = 0 } = keyboardAvoid.current
ref?.current?.measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
const aboveOffset = pageY + height - endCoordinates.screenY
const belowOffset = endCoordinates.height - aboveOffset
const aboveValue = -aboveOffset >= cursorSpacing ? 0 : aboveOffset + cursorSpacing
const belowValue = Math.min(belowOffset, cursorSpacing)
const value = aboveOffset > 0 ? belowValue : aboveValue
offset.value = withTiming(value, { duration, easing }, (finished) => {
if (finished) {
/**
* In the Android environment, the layout information is not synchronized after the animation,
* which results in the inability to correctly trigger element events.
* Here, we utilize flexBasic to proactively trigger a re-layout
*/
basic.value = '99.99%'
}
})
})
}),
Keyboard.addListener('keyboardDidHide', resetKeyboard)
]
}

return () => {
subscriptions.forEach(subscription => subscription.remove())
}
}, [keyboardAvoid])

return (
<View style={style}>
<Animated.View
style={[
contentContainerStyle,
animatedStyle
]}
>
{children}
</Animated.View>
</View>
)
}

export default KeyboardAvoidingView
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export type LabelContextValue = MutableRefObject<{
triggerChange: (evt: NativeSyntheticEvent<TouchEvent>) => void
}>

export type KeyboardAvoidContextValue = (enabled: boolean) => void
export type KeyboardAvoidContextValue = MutableRefObject<{
cursorSpacing: number
ref: MutableRefObject<any>
}>

export interface GroupValue {
[key: string]: { checked: boolean; setValue: Dispatch<SetStateAction<boolean>> }
Expand Down
58 changes: 44 additions & 14 deletions packages/webpack-plugin/lib/runtime/components/react/mpx-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* ✘ placeholder-class
* ✔ disabled
* ✔ maxlength
* cursor-spacing
* cursor-spacing
* ✔ auto-focus
* ✔ focus
* ✔ confirm-type
Expand Down Expand Up @@ -37,9 +37,8 @@
* ✘ bind:keyboardcompositionend
* ✘ bind:onkeyboardheightchange
*/
import { JSX, forwardRef, useMemo, useRef, useState, useContext, useEffect, createElement } from 'react'
import { JSX, forwardRef, useRef, useState, useContext, useEffect, createElement } from 'react'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

textarea也需要支持adjust-position

import {
KeyboardTypeOptions,
Platform,
TextInput,
TextStyle,
Expand Down Expand Up @@ -77,11 +76,12 @@ type Type = 'text' | 'number' | 'idcard' | 'digit'
export interface InputProps {
name?: string
style?: InputStyle & Record<string, any>
value?: string
value?: string | number
type?: Type
password?: boolean
placeholder?: string
disabled?: boolean
'cursor-spacing'?: number
maxlength?: number
'auto-focus'?: boolean
focus?: boolean
Expand Down Expand Up @@ -136,6 +136,7 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
'placeholder-style': placeholderStyle,
disabled,
maxlength = 140,
'cursor-spacing': cursorSpacing = 0,
'auto-focus': autoFocus,
focus,
'confirm-type': confirmType = 'done',
Expand All @@ -149,7 +150,7 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
'parent-font-size': parentFontSize,
'parent-width': parentWidth,
'parent-height': parentHeight,
'adjust-position': adjustPosition = false,
'adjust-position': adjustPosition = true,
bindinput,
bindfocus,
bindblur,
Expand All @@ -163,16 +164,27 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps

const formContext = useContext(FormContext)

const setKeyboardAvoidEnabled = useContext(KeyboardAvoidContext)
const keyboardAvoid = useContext(KeyboardAvoidContext)

let formValuesMap: Map<string, FormFieldValue> | undefined

if (formContext) {
formValuesMap = formContext.formValuesMap
}

const parseValue = (value: string | number | undefined): string => {
if (typeof value === 'string') {
if (value.length > maxlength && maxlength >= 0) {
return value.slice(0, maxlength)
}
return value
}
if (typeof value === 'number') return value + ''
return ''
}

const keyboardType = keyboardTypeMap[type]
const defaultValue = type === 'number' && value ? value + '' : value
const defaultValue = parseValue(value)
const placeholderTextColor = parseInlineStyle(placeholderStyle)?.color
const textAlignVertical = multiline ? 'top' : 'auto'

Expand Down Expand Up @@ -208,7 +220,7 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps

useEffect(() => {
if (inputValue !== value) {
setInputValue(value)
setInputValue(parseValue(value))
}
}, [value])

Expand Down Expand Up @@ -254,8 +266,23 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
}
}

const setKeyboardAvoidContext = () => {
if (adjustPosition && keyboardAvoid?.current) {
extendObject(keyboardAvoid.current, {
cursorSpacing,
ref: nodeRef
})
}
}

const onInputTouchStart = () => {
// sometimes the focus event occurs later than the keyboardWillShow event
setKeyboardAvoidContext()
}

const onInputFocus = (evt: NativeSyntheticEvent<TextInputFocusEventData>) => {
bindfocus!(
setKeyboardAvoidContext()
bindfocus && bindfocus(
getCustomEvent(
'focus',
evt,
Expand All @@ -271,7 +298,7 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
}

const onInputBlur = (evt: NativeSyntheticEvent<TextInputFocusEventData>) => {
bindblur!(
bindblur && bindblur(
getCustomEvent(
'blur',
evt,
Expand Down Expand Up @@ -389,8 +416,10 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
}, [])

useEffect(() => {
setKeyboardAvoidEnabled?.(adjustPosition)
}, [adjustPosition])
if (focus) {
setKeyboardAvoidContext()
}
}, [focus])

useUpdateEffect(() => {
if (!nodeRef?.current) {
Expand Down Expand Up @@ -426,8 +455,9 @@ const Input = forwardRef<HandlerRef<TextInput, FinalInputProps>, FinalInputProps
},
layoutProps,
{
onFocus: bindfocus && onInputFocus,
onBlur: bindblur && onInputBlur,
onTouchStart: onInputTouchStart,
onFocus: onInputFocus,
onBlur: onInputBlur,
onKeyPress: bindconfirm && onKeyPress,
onSubmitEditing: bindconfirm && multiline && onSubmitEditing,
onSelectionChange: onSelectionChange,
Expand Down
Loading
Loading