-
-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: port
KeyboardAwareScrollView
to Fabric (#220)
## 📜 Description Ported `KeyboardAwareScrollView` example to `FabricExample` app. ## 💡 Motivation and Context Basically I ported paper example to fabric. However there is two main differences between implementations. ### `measure` expect shadow node instance You can not call `measure(() => tag)` as in paper, because it expects to see an associated instance of `ShadowNode`. There was two ways how to achieve that: - use `findShadowNodeByTag_DEPRECATED` - but it's deprecated and it's obviously that better not to use (basically this method go through all views until it finds a view for a given `tag` - I think it may hit performance issues as well); - take `ShadowNode` from a `ref` of `TextInput`. I've decided to stick with 2nd approach (just because it's not deprecated). For that purposes I added context where I store a map like: ```ts { [tag]: () => shadowNodeOrTag } ``` In `KeyboardAwarescrollView` I consume this context and extract function by `viewTag`. After that this construction can be passed to `measure` function. > The implementation is cross-platform (i. e. it can be run on paper too, but I've decided not to have identical code for now - I think the approach will be changed anyway and in example I want to keep as simple as possible example). After some tie I hope these examples will be unified and will be less complex than now. ### `measure` doesn't measure layout in window It seems like `measure` works differently across architectures. On Paper it measures element within window boundaries, however on Fabric architecture it measures relatively to ViewContainer (doesn't take header height into consideration). I handled it by adding a simple check, but in future we'll need to rework it, because right now it's not very convenient to depend on other libraries. > I know all this stuff looks complicated, but I've already did some research on how to simplify the integration across architectures. I'm just merging this PR as an example on how to use new API. Most likely later the approach will be changed and we'll not need to add additional context/conditionally add height of other UI elements etc. ## 📢 Changelog ### JS - added `AwareScrollViewContext` context where I store map described above; - added internal `useAwareScrollView` hook that incapsulate TextInput registration in provider + provides `measure` method; - copied/pasted code from `example` to `FabricExample` ## 🤔 How Has This Been Tested? Tested only on iPhone 14 Pro (iOS 16.4), Android lead to ANR (most likely because of `scrollTo` on Fabric). ## 📸 Screenshots (if appropriate): https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/8d7a7bac-783e-44b2-a316-b30c3ef43900 ## 📝 Checklist - [x] CI successfully passed
- Loading branch information
1 parent
fc7dee9
commit 2cf490a
Showing
4 changed files
with
237 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from 'react'; | ||
import { TextInputProps, TextInput as TextInputRN } from 'react-native'; | ||
import { randomColor } from '../../../utils'; | ||
import { useAwareScrollView } from './context'; | ||
|
||
const TextInput = React.forwardRef((props: TextInputProps, forwardRef) => { | ||
const { onRef } = useAwareScrollView(); | ||
|
||
return ( | ||
<TextInputRN | ||
ref={(ref) => { | ||
onRef(ref); | ||
if (typeof forwardRef === 'function') { | ||
forwardRef(ref); | ||
} | ||
}} | ||
placeholderTextColor="black" | ||
style={{ | ||
width: '100%', | ||
height: 50, | ||
backgroundColor: randomColor(), | ||
marginTop: 50, | ||
}} | ||
{...props} | ||
/> | ||
); | ||
}); | ||
|
||
export default TextInput; |
102 changes: 102 additions & 0 deletions
102
FabricExample/src/screens/Examples/AwareScrollView/context.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
declare const _IS_FABRIC: boolean; | ||
import React, { | ||
Component, | ||
RefObject, | ||
useContext, | ||
useMemo, | ||
useRef, | ||
} from 'react'; | ||
import { TextInput, findNodeHandle } from 'react-native'; | ||
import { | ||
useWorkletCallback, | ||
measure as measureREA, | ||
useSharedValue, | ||
} from 'react-native-reanimated'; | ||
import { useHeaderHeight } from '@react-navigation/elements'; | ||
|
||
type KeyboardAwareContext = { | ||
handlersRef: { | ||
current: Record<string, RefObject<Component>>; | ||
}; | ||
handlers: { | ||
value: Record<string, RefObject<Component>>; | ||
}; | ||
}; | ||
|
||
const defaultValue: KeyboardAwareContext = { | ||
handlersRef: { current: {} }, | ||
handlers: { value: {} }, | ||
}; | ||
|
||
const AwareScrollViewContext = React.createContext(defaultValue); | ||
|
||
export const useAwareScrollView = () => { | ||
const ctx = useContext(AwareScrollViewContext); | ||
const headerHeight = useHeaderHeight(); | ||
|
||
const onRef = (ref: TextInput | null) => { | ||
const viewTag = findNodeHandle(ref); | ||
if (viewTag) { | ||
const viewTagOrShadowNode = _IS_FABRIC | ||
? // @ts-expect-error this API doesn't have any types | ||
ref._internalInstanceHandle.stateNode.node | ||
: viewTag; | ||
ctx.handlersRef.current = { | ||
...ctx.handlersRef.current, | ||
[viewTag]: () => { | ||
'worklet'; | ||
|
||
return viewTagOrShadowNode; | ||
}, | ||
}; | ||
ctx.handlers.value = ctx.handlersRef.current; | ||
} | ||
}; | ||
|
||
const measure = useWorkletCallback( | ||
(tag: number) => { | ||
const ref = ctx.handlers.value[tag]; | ||
|
||
if (ref) { | ||
const layout = measureREA(ref); | ||
if (layout) { | ||
return { | ||
...layout, | ||
pageY: _IS_FABRIC ? layout.pageY + headerHeight : layout.pageY, | ||
}; | ||
} else { | ||
return null; | ||
} | ||
} else { | ||
return null; | ||
} | ||
}, | ||
[headerHeight] | ||
); | ||
|
||
return { | ||
onRef, | ||
measure, | ||
}; | ||
}; | ||
|
||
export const AwareScrollViewProvider: React.FC<React.PropsWithChildren<{}>> = ({ | ||
children, | ||
}) => { | ||
const handlersRef = useRef({}); | ||
const handlers = useSharedValue({}); | ||
|
||
const value = useMemo( | ||
() => ({ | ||
handlersRef, | ||
handlers, | ||
}), | ||
[] | ||
); | ||
|
||
return ( | ||
<AwareScrollViewContext.Provider value={value}> | ||
{children} | ||
</AwareScrollViewContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters