Skip to content

Commit

Permalink
refactor: align Android implementation with iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillzyusko committed Oct 18, 2023
1 parent 3934a03 commit 6c10332
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 155 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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, {
interpolate,
scrollTo,
Expand Down Expand Up @@ -66,6 +66,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
const input = useReanimatedFocusedInput();
const layout = useSharedValue<FocusedInputLayoutChangedEvent>(null);

const { height } = useWindowDimensions();

Expand All @@ -85,7 +86,7 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
fakeViewHeight.value = e;

const visibleRect = height - keyboardHeight.value;
const point = (input.value?.layout.absoluteY || 0) + (input.value?.layout.height || 0);
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

if (visibleRect - point <= BOTTOM_OFFSET) {
const interpolatedScrollTo = interpolate(
Expand Down Expand Up @@ -130,21 +131,23 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
keyboardHeight.value = e.height;
}

if (focusWasChanged && e.target !== -1 && !keyboardWillAppear) {
console.log("focus was changed -> scrolling");
maybeScroll(e.height, true);
}

// focus was changed
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;
// 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 @@ -164,7 +167,11 @@ const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
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;
}
}, []);

Expand Down
29 changes: 19 additions & 10 deletions FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import React from 'react';
import { TextInputProps, TextInput as TextInputRN } from 'react-native';
import { randomColor } from '../../../utils';
import { StyleSheet, TextInputProps, TextInput as TextInputRN } from 'react-native';

const TextInput = (props: TextInputProps) => {
return (
<TextInputRN
placeholderTextColor="black"
style={{
width: '100%',
minHeight: 50,
maxHeight: 200,
backgroundColor: randomColor(),
marginTop: 50,
}}
placeholderTextColor="#6c6c6c"
style={styles.container}
multiline
numberOfLines={10}
{...props}
placeholder={`${props.placeholder} (${props.keyboardType === 'default' ? 'text' : 'numeric'})`}
/>
);
};

const styles = StyleSheet.create({
container: {
width: '100%',
minHeight: 50,
maxHeight: 200,
marginTop: 50,
borderColor: 'black',
borderWidth: 2,
marginRight: 160,
borderRadius: 10,
color: 'black',
paddingHorizontal: 12,
},
});

export default TextInput;
5 changes: 2 additions & 3 deletions FabricExample/src/screens/Examples/AwareScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import { useResizeMode } from 'react-native-keyboard-controller';
import KeyboardAwareScrollView from './KeyboardAwareScrollView';
import TextInput from './TextInput';
import { styles } from './styles';
import { Keyboard, ScrollView, View } from 'react-native';

export default function AwareScrollView() {
useResizeMode();

return (
<KeyboardAwareScrollView>
<KeyboardAwareScrollView style={styles.container}>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`${i}`}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? 'numeric' : 'default'}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 16,
},
});
51 changes: 12 additions & 39 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,22 @@
- (x) jest -> add new mocks + new unit tests
- (x) improve events precision (int -> float, pageY, width точно float или double)
- (x) console.log("focus was changed -> scrolling") (even when keyboard appears) <- `!keyboardWillAppear` condition added
- (x) focus on field -> type any symbol -> close keyboard => no back transition animation <- memoize layout in local `layout` variable
- (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)
- 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
- focus on field -> type any symbol -> close keyboard => no back transition animation
- android - dispatch event when keyboard changes size
- enable hook under feature flag?

```
KeyboardTransitionEventData(
event = "topKeyboardMoveStart",
height = this.persistentKeyboardHeight,
progress = 1.0,
duration = 0,
target = viewTagFocused
),

data class KeyboardTransitionEventData(
val event: String,
val height: Double,
val progress: Double,
val duration: Int,
val target: Int,
)

@Suppress("detekt:LongParameterList")
class KeyboardTransitionEvent(
surfaceId: Int,
viewId: Int,
private val data: KeyboardTransitionEventData,
) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
override fun getEventName() = data.event

// All events for a given view can be coalesced?
override fun getCoalescingKey(): Short = 0

override fun getEventData(): WritableMap? = Arguments.createMap().apply {
putDouble("progress", data.progress)
putDouble("height", data.height)
putInt("duration", data.duration)
putInt("target", data.target)
}
}
```
- 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
- focus on 3, then on 5 -> grow text input -> first grow scroll into incorrect position
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,37 @@ import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

data class FocusedInputLayoutChangedEventData(
val x: Double,
val y: Double,
val width: Double,
val height: Double,
val absoluteX: Double,
val absoluteY: Double,
val target: Int,
)

class FocusedInputLayoutChangedEvent(
surfaceId: Int,
viewId: Int,
private val x: Double,
private val y: Double,
private val width: Double,
private val height: Double,
private val absoluteX: Double,
private val absoluteY: Double,
private val target: Int,
private val event: FocusedInputLayoutChangedEventData,
) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
override fun getEventName() = "topFocusedInputLayoutChanged"

// All events for a given view can be coalesced
override fun getCoalescingKey(): Short = 0

override fun getEventData(): WritableMap? = Arguments.createMap().apply {
putInt("target", target)
putInt("target", event.target)
putMap(
"layout",
Arguments.createMap().apply {
putDouble("x", x)
putDouble("y", y)
putDouble("width", width)
putDouble("height", height)
putDouble("absoluteX", absoluteX)
putDouble("absoluteY", absoluteY)
putDouble("x", event.x)
putDouble("y", event.y)
putDouble("width", event.width)
putDouble("height", event.height)
putDouble("absoluteX", event.absoluteX)
putDouble("absoluteY", event.absoluteY)
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.reactnativekeyboardcontroller.listeners

import android.view.View.OnLayoutChangeListener
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.views.textinput.ReactEditText
import com.facebook.react.views.view.ReactViewGroup
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
import com.reactnativekeyboardcontroller.extensions.dispatchEvent
import com.reactnativekeyboardcontroller.extensions.dp
import com.reactnativekeyboardcontroller.extensions.screenLocation
Expand All @@ -16,44 +18,59 @@ class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context:

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

// listeners
private val layoutListener =
OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
this.updateJSValue()
this.syncUpLayout()
}
private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
// unfocused or focused was changed
if (newFocus == null || oldFocus != null) {
lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
lastFocusedInput = null
}
if (newFocus is ReactEditText) {
lastFocusedInput = newFocus
newFocus.addOnLayoutChangeListener(layoutListener)
this.syncUpLayout()
}
}

init {
view.viewTreeObserver.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
if (newFocus is ReactEditText) {
lastFocusedInput = newFocus
newFocus.addOnLayoutChangeListener(layoutListener)
this.updateJSValue()
}
if (newFocus == null) {
lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
lastFocusedInput = null
}
}
view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
}

private fun updateJSValue() {
fun syncUpLayout() {
val input = lastFocusedInput ?: return

val (x, y) = input.screenLocation
context.dispatchEvent(
view.id,
FocusedInputLayoutChangedEvent(
surfaceId,
view.id,
input.x.dp,
input.y.dp,
input.width.toFloat().dp,
input.height.toFloat().dp,
x.toFloat().dp,
y.toFloat().dp,
input.id,
),
val event = FocusedInputLayoutChangedEventData(
x = input.x.dp,
y = input.y.dp,
width = input.width.toFloat().dp,
height = input.height.toFloat().dp,
absoluteX = x.toFloat().dp,
absoluteY = y.toFloat().dp,
target = input.id,
)

if (event != lastEventDispatched) {
lastEventDispatched = event
context.dispatchEvent(
view.id,
FocusedInputLayoutChangedEvent(
surfaceId,
view.id,
event = event,
),
)
println("DISPATCH $id")
}
}

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

0 comments on commit 6c10332

Please sign in to comment.