Skip to content

Commit

Permalink
refactor: resolved more todos
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillzyusko committed Oct 19, 2023
1 parent 6c10332 commit 6910099
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 108 deletions.
24 changes: 13 additions & 11 deletions FabricExample/__tests__/focused-input.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { render } from '@testing-library/react-native';
import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';

function RectangleWithFocusedInputLayout() {
const input = useReanimatedFocusedInput();
const { input } = useReanimatedFocusedInput();
const style = useAnimatedStyle(
() => {
const layout = input.value?.layout;
Expand Down Expand Up @@ -36,17 +36,19 @@ describe('`useReanimatedFocusedInput` mocking', () => {
});

(useReanimatedFocusedInput as jest.Mock).mockReturnValue({
value: {
target: 2,
layout: {
x: 10,
y: 100,
width: 190,
height: 80,
absoluteX: 100,
absoluteY: 200,
input: {
value: {
target: 2,
layout: {
x: 10,
y: 100,
width: 190,
height: 80,
absoluteX: 100,
absoluteY: 200,
},
},
},
}
});
update(<RectangleWithFocusedInputLayout />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,12 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
const fakeViewHeight = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const tag = useSharedValue(-1);
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
const input = useReanimatedFocusedInput();
const layout = useSharedValue<FocusedInputLayoutChangedEvent>(null);
const { input } = useReanimatedFocusedInput();
const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);

const { height } = useWindowDimensions();

Expand All @@ -82,9 +81,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
/**
* Function that will scroll a ScrollView as keyboard gets moving
*/
const maybeScroll = useWorkletCallback((e: number, animated = false) => {
fakeViewHeight.value = e;

const maybeScroll = useWorkletCallback((e: number, animated: boolean = false) => {
const visibleRect = height - keyboardHeight.value;
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

Expand Down
14 changes: 8 additions & 6 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@
- (x) android - dispatch event when keyboard changes size
- (x) be sure that after switching enabled/disabled several times only one event gets dispatched <- only single listener should be active (open app, disable/enable, focus input)
- (x) do not generate colors randomly (to cover by e2e tests later)
- (x) send `null` when there is no input in focus
- (x) replicate example to paper
- (-) open app -> focus on 8/9 fields -> content is not moving <- fabric specific? paper works like a charm
- (-) const {update} = useReanimatedFocusedInput(); <- change signature of the method in advance? requires REA3
- scrollResponderScrollNativeHandleToKeyboard
- think about renaming FocusedInputLayoutChangedObserver -> FocusedInputObserver
-
- send `null` when there is no input in focus
- const {update} = useReanimatedFocusedInput(); <- requires REA3
- crash on every file changes AwareScrollView/hot reload
- crash on every file changes AwareScrollView/hot reload <- only if body of worklet changed? fabric specific?
- enable hook under feature flag?
- test paper/fabric iOS/Android
- open app -> focus on 8/9 fields -> content is not moving
- 8 -> 9 transitions (9 field is covered by keyboard)
- android: keyboard resize pushes content significantly
- sometimes `paddingBottom` is getting kind of freezed (i. e. keyboard is hidden, but padding is still present)
- replicate example to paper
- `y` on Android and from measure are diffferent
- `y` on Android and from measure are different
- focus on 3, then on 5 -> grow text input -> first grow scroll into incorrect position
- remove all println/console.log/TODO/etc.
- paper: focus on 5, unfocus -> no back transition <- keyboardHeight back to `0` too early (should be in end handler)
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
import com.reactnativekeyboardcontroller.extensions.dp
import com.reactnativekeyboardcontroller.extensions.screenLocation

val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
x = 0.0,
y = 0.0,
width = 0.0,
height = 0.0,
absoluteX = 0.0,
absoluteY = 0.0,
target = -1,
)

class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
// constructor variables
private val surfaceId = UIManagerHelper.getSurfaceId(view)

// state variables
private var lastFocusedInput: ReactEditText? = null
private var lastEventDispatched: FocusedInputLayoutChangedEventData? = null
private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent

// listeners
private val layoutListener =
Expand All @@ -36,6 +46,10 @@ class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context:
newFocus.addOnLayoutChangeListener(layoutListener)
this.syncUpLayout()
}
// unfocused
if (newFocus == null) {
dispatchEventToJS(noFocusedInputEvent)
}
}

init {
Expand All @@ -56,6 +70,14 @@ class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context:
target = input.id,
)

dispatchEventToJS(event)
}

fun destroy() {
view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
}

private fun dispatchEventToJS(event: FocusedInputLayoutChangedEventData) {
if (event != lastEventDispatched) {
lastEventDispatched = event
context.dispatchEvent(
Expand All @@ -66,11 +88,6 @@ class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context:
event = event,
),
)
println("DISPATCH $id")
}
}

fun destroy() {
view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
}
}
6 changes: 3 additions & 3 deletions docs/docs/api/hooks/input/use-reanimated-focused-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ keywords: [react-native-keyboard-controller, useReanimatedFocusedInput, react-na

# useReanimatedFocusedInput

Hook that returns an information about `TextInput` that currently has a focus.
Hook that returns an information about `TextInput` that currently has a focus. Returns `null` if no input has focus.

Hook will update its value in next cases:

Expand All @@ -18,7 +18,7 @@ The value from `useReanimatedFocusedInput` will be always updated before keyboar

## Event structure

Value from this hook is returned as `SharedValue`. The returned data has next structure:
The `input` property from this hook is returned as `SharedValue`. The returned data has next structure:

```ts
type KeyboardEventData = {
Expand All @@ -37,7 +37,7 @@ type KeyboardEventData = {
## Example

```tsx
const input = useReanimatedFocusedInput();
const {input} = useReanimatedFocusedInput();
```

Also have a look on [example](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example) app for more comprehensive usage.
24 changes: 13 additions & 11 deletions example/__tests__/focused-input.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { render } from '@testing-library/react-native';
import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';

function RectangleWithFocusedInputLayout() {
const input = useReanimatedFocusedInput();
const { input } = useReanimatedFocusedInput();
const style = useAnimatedStyle(
() => {
const layout = input.value?.layout;
Expand Down Expand Up @@ -36,17 +36,19 @@ describe('`useReanimatedFocusedInput` mocking', () => {
});

(useReanimatedFocusedInput as jest.Mock).mockReturnValue({
value: {
target: 2,
layout: {
x: 10,
y: 100,
width: 190,
height: 80,
absoluteX: 100,
absoluteY: 200,
input: {
value: {
target: 2,
layout: {
x: 10,
y: 100,
width: 190,
height: 80,
absoluteX: 100,
absoluteY: 200,
},
},
},
}
});
update(<RectangleWithFocusedInputLayout />);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import React, { Component, FC } from 'react';
import React, { FC } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import Reanimated, {
MeasuredDimensions,
interpolate,
measure,
scrollTo,
useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
useWorkletCallback,
} from 'react-native-reanimated';
import { RefObjectFunction } from 'react-native-reanimated/lib/types/lib/reanimated2/hook/commonTypes';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';

const BOTTOM_OFFSET = 50;
Expand Down Expand Up @@ -62,13 +60,12 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
const layout = useSharedValue<MeasuredDimensions | null>(null);
const fakeViewHeight = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const tag = useSharedValue(-1);
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
const input = useReanimatedFocusedInput();
const { input } = useReanimatedFocusedInput();
const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);

const { height } = useWindowDimensions();

Expand All @@ -80,20 +77,13 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
},
[]
);
const measureByTag = useWorkletCallback((viewTag: number) => {
return measure(
(() => viewTag) as unknown as RefObjectFunction<Component<{}, {}, any>>
);
}, []);

/**
* Function that will scroll a ScrollView as keyboard gets moving
*/
const maybeScroll = useWorkletCallback((e: number, animated = false) => {
fakeViewHeight.value = e;

const maybeScroll = useWorkletCallback((e: number, animated: boolean = false) => {
const visibleRect = height - keyboardHeight.value;
const point = (layout.value?.absoluteY || 0) + (layout.value?.height || 0);
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

if (visibleRect - point <= BOTTOM_OFFSET) {
const interpolatedScrollTo = interpolate(
Expand All @@ -103,7 +93,10 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
);
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
console.log({ targetScrollY, interpolatedScrollTo, s: scrollPosition.value });
scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated);

return interpolatedScrollTo;
}
}, []);

Expand All @@ -116,6 +109,8 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
keyboardHeight.value !== e.height && e.height > 0;
const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0;
const keyboardWillHide = e.height === 0;
const focusWasChanged = tag.value !== e.target || keyboardWillChangeSize;

if (keyboardWillChangeSize) {
initialKeyboardSize.value = keyboardHeight.value;
}
Expand All @@ -126,26 +121,30 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
scrollPosition.value = scrollBeforeKeyboardMovement.value;
}

if (keyboardWillAppear || keyboardWillChangeSize) {
if (keyboardWillAppear || keyboardWillChangeSize || focusWasChanged) {
// persist scroll value
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
keyboardHeight.value = e.height;
}

// focus was changed
if (tag.value !== e.target || keyboardWillChangeSize) {
if (focusWasChanged) {
tag.value = e.target;

if (tag.value !== -1) {
// save position of focused text input when keyboard starts to move
layout.value = input.value?.layout;
console.log(121212, height, layout.value, measureByTag(e.target));
layout.value = input.value;
// save current scroll position - when keyboard will hide we'll reuse
// this value to achieve smooth hide effect
scrollBeforeKeyboardMovement.value = position.value;
}
}

if (focusWasChanged && e.target !== -1 && !keyboardWillAppear) {
console.log("focus was changed -> scrolling");
maybeScroll(e.height, true);
}
},
onMove: (e) => {
'worklet';
Expand All @@ -157,24 +156,26 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({

keyboardHeight.value = e.height;
scrollPosition.value = position.value;

if (e.target !== -1 && e.height !== 0) {
const prevLayout = layout.value;
// just be sure, that view is no overlapped (i.e. focus changed)
layout.value = measureByTag(e.target);
maybeScroll(e.height, true);
// do layout substitution back to assure there will be correct
// back transition when keyboard hides
layout.value = prevLayout;
}
},
},
[height]
);

useAnimatedReaction(() => input.value, (current, previous) => {
if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) {
console.log("TextInput grows");
const prevLayout = layout.value;

layout.value = input.value;
scrollPosition.value += maybeScroll(keyboardHeight.value, true) || 0;
layout.value = prevLayout;
}
}, []);

const view = useAnimatedStyle(
() => ({
height: fakeViewHeight.value,
// TODO: take `contentContainerStyle` into consideration?
paddingBottom: keyboardHeight.value,
}),
[]
);
Expand All @@ -186,8 +187,9 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
onScroll={onScroll}
scrollEventThrottle={16}
>
{children}
<Reanimated.View style={view} />
<Reanimated.View style={view}>
{children}
</Reanimated.View>
</Reanimated.ScrollView>
);
};
Expand Down
Loading

0 comments on commit 6910099

Please sign in to comment.