-
-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: interactive keyboard (android) (#133)
## 📜 Description Added Interactive Keyboard dismissing on Android 11+. ## 💡 Motivation and Context Initially I planned to control the keyboard properties (`opacity` and `position`) via animated properties (`Animated.Value`). But it turned out, that every update of animated value is not scheduled on UI thread and since we need to call `controller.setInsetsAndAlpha` on UI thread we need to wrap it in `UiThreadUtil.runOnUiThread`. It works, but since it creates a new thread too frequently CPU is overused (CPU usage is too high, as a result battery gets drained faster and animation is not very smooth). The second idea was to add JSI `interpolate` function, which can be called directly on UI thread. I thought about usage of worklets (`react-native-worklets`/`react-native-reanimated`) or writing pure JSI function. But in the end I decided to postpone it, since right now I need to support both (paper and fabric) architectures and it seems like JSI is changing time to time (depends on a react-native version) and I thought that it may be complicated to support such code. Taking everything from above into consideration I've decided to implement `interpolate` function in a native language (kotlin). It works well for both architectures, since there is no direct communication between JS and Native thread, though it brings own restrictions, such as limited customization (you have only two pre-defined variants of the function and you can not write your own implementation in JS code). However I think to rewrite it to JSI in the future to make it more customizable. ## 📢 Changelog ### JS - added specs for new `KeyboardGestureArea` component; - added example (paper&fabric) screen; - added `onKeyboardMoveInteractive` view callback and `onInteractive` handler for `useKeyboardHandler` hook; ### Android - added `KeyboardGestureArea` view; - changed target api to 33; - added `androidx.dynamicanimation:dynamicanimation-ktx` library; - added `InteractiveKeyboardProvider` which acts as a singleton to store keyboard state; - added `Interpolator` interface and `IosInterpolator` and `LinearInterpolator` implementation; ## 🤔 How Has This Been Tested? Tested manually on: - Pixel 7 Pro (Android 13, real device); - Pixel 3A (Android 13, emulator); ## 📸 Screenshots (if appropriate): |linear|iOS| |------|---| |<video src="https://user-images.githubusercontent.com/22820318/229305276-fe666f30-90c3-4cf0-9197-16bf8d7c7c89.mp4" />|<video src="https://user-images.githubusercontent.com/22820318/229305352-9927ff93-7c9a-4fd3-95cb-73627cee77f2.mp4" />| ## 📝 Checklist - [x] CI successfully passed
- Loading branch information
1 parent
f117cf9
commit b2e70e5
Showing
33 changed files
with
1,153 additions
and
17 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
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
108 changes: 108 additions & 0 deletions
108
FabricExample/src/screens/Examples/InteractiveKeyboard/index.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,108 @@ | ||
import { StackScreenProps } from '@react-navigation/stack'; | ||
import React, { useEffect, useState } from 'react'; | ||
import { Text, TextInput, View } from 'react-native'; | ||
import { | ||
KeyboardGestureArea, | ||
useKeyboardHandler, | ||
} from 'react-native-keyboard-controller'; | ||
import Reanimated, { | ||
useAnimatedStyle, | ||
useSharedValue, | ||
} from 'react-native-reanimated'; | ||
|
||
import Message from '../../../components/Message'; | ||
import { history } from '../../../components/Message/data'; | ||
import { ExamplesStackParamList } from '../../../navigation/ExamplesStack'; | ||
import styles from './styles'; | ||
|
||
const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput); | ||
|
||
const useKeyboardAnimation = () => { | ||
const progress = useSharedValue(0); | ||
const height = useSharedValue(0); | ||
useKeyboardHandler({ | ||
onMove: (e) => { | ||
'worklet'; | ||
|
||
progress.value = e.progress; | ||
height.value = e.height; | ||
}, | ||
onInteractive: (e) => { | ||
'worklet'; | ||
|
||
progress.value = e.progress; | ||
height.value = e.height; | ||
}, | ||
}); | ||
|
||
return { height, progress }; | ||
}; | ||
|
||
type Props = StackScreenProps<ExamplesStackParamList>; | ||
|
||
function InteractiveKeyboard({ navigation }: Props) { | ||
const [interpolator, setInterpolator] = useState<'ios' | 'linear'>('linear'); | ||
const { height } = useKeyboardAnimation(); | ||
|
||
useEffect(() => { | ||
navigation.setOptions({ | ||
headerRight: () => ( | ||
<Text | ||
style={styles.header} | ||
onPress={() => | ||
setInterpolator(interpolator === 'ios' ? 'linear' : 'ios') | ||
} | ||
> | ||
{interpolator} | ||
</Text> | ||
), | ||
}); | ||
}, [interpolator]); | ||
|
||
const scrollViewStyle = useAnimatedStyle( | ||
() => ({ | ||
transform: [{ translateY: -height.value }, ...styles.inverted.transform], | ||
}), | ||
[] | ||
); | ||
const textInputStyle = useAnimatedStyle( | ||
() => ({ | ||
height: 50, | ||
width: '100%', | ||
backgroundColor: '#BCBCBC', | ||
transform: [{ translateY: -height.value }], | ||
}), | ||
[] | ||
); | ||
const fakeView = useAnimatedStyle( | ||
() => ({ | ||
height: height.value, | ||
}), | ||
[] | ||
); | ||
|
||
return ( | ||
<View style={styles.container}> | ||
<KeyboardGestureArea | ||
style={styles.content} | ||
interpolator={interpolator} | ||
allowToShowKeyboardFromHiddenStateBySwipeUp | ||
> | ||
<Reanimated.ScrollView | ||
showsVerticalScrollIndicator={false} | ||
style={scrollViewStyle} | ||
> | ||
<View style={styles.inverted}> | ||
<Reanimated.View style={fakeView} /> | ||
{history.map((message, index) => ( | ||
<Message key={index} {...message} /> | ||
))} | ||
</View> | ||
</Reanimated.ScrollView> | ||
</KeyboardGestureArea> | ||
<AnimatedTextInput style={textInputStyle} /> | ||
</View> | ||
); | ||
} | ||
|
||
export default InteractiveKeyboard; |
22 changes: 22 additions & 0 deletions
22
FabricExample/src/screens/Examples/InteractiveKeyboard/styles.ts
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,22 @@ | ||
import { StyleSheet } from 'react-native'; | ||
|
||
export default StyleSheet.create({ | ||
container: { | ||
justifyContent: 'flex-end', | ||
flex: 1, | ||
}, | ||
header: { | ||
color: 'black', | ||
marginRight: 12, | ||
}, | ||
inverted: { | ||
transform: [ | ||
{ | ||
rotate: '180deg', | ||
}, | ||
], | ||
}, | ||
content: { | ||
flex: 1, | ||
}, | ||
}); |
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
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
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
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 |
---|---|---|
@@ -1,5 +1,5 @@ | ||
KeyboardController_kotlinVersion=1.6.21 | ||
KeyboardController_compileSdkVersion=29 | ||
KeyboardController_targetSdkVersion=29 | ||
KeyboardController_compileSdkVersion=33 | ||
KeyboardController_targetSdkVersion=33 | ||
|
||
android.useAndroidX=true |
47 changes: 47 additions & 0 deletions
47
android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt
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,47 @@ | ||
package com.reactnativekeyboardcontroller | ||
|
||
import com.facebook.react.bridge.ReactApplicationContext | ||
import com.facebook.react.uimanager.ThemedReactContext | ||
import com.facebook.react.uimanager.ViewManagerDelegate | ||
import com.facebook.react.uimanager.annotations.ReactProp | ||
import com.facebook.react.viewmanagers.KeyboardGestureAreaManagerDelegate | ||
import com.facebook.react.viewmanagers.KeyboardGestureAreaManagerInterface | ||
import com.facebook.react.views.view.ReactViewGroup | ||
import com.facebook.react.views.view.ReactViewManager | ||
import com.reactnativekeyboardcontroller.managers.KeyboardGestureAreaViewManagerImpl | ||
import com.reactnativekeyboardcontroller.views.KeyboardGestureAreaReactViewGroup | ||
|
||
class KeyboardGestureAreaViewManager(mReactContext: ReactApplicationContext) : | ||
ReactViewManager(), | ||
KeyboardGestureAreaManagerInterface<ReactViewGroup> { | ||
private val manager = KeyboardGestureAreaViewManagerImpl(mReactContext) | ||
private val mDelegate = KeyboardGestureAreaManagerDelegate(this) | ||
|
||
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup?> { | ||
return mDelegate | ||
} | ||
|
||
override fun getName(): String = KeyboardGestureAreaViewManagerImpl.NAME | ||
|
||
override fun createViewInstance(context: ThemedReactContext): KeyboardGestureAreaReactViewGroup { | ||
return manager.createViewInstance(context) | ||
} | ||
|
||
@ReactProp(name = "interpolator") | ||
override fun setInterpolator(view: ReactViewGroup, value: String?) { | ||
manager.setInterpolator(view as KeyboardGestureAreaReactViewGroup, value ?: "linear") | ||
} | ||
|
||
@ReactProp(name = "allowToShowKeyboardFromHiddenStateBySwipeUp") | ||
override fun setAllowToShowKeyboardFromHiddenStateBySwipeUp( | ||
view: ReactViewGroup, | ||
value: Boolean, | ||
) { | ||
manager.setScrollKeyboardOnScreenWhenNotVisible(view as KeyboardGestureAreaReactViewGroup, value) | ||
} | ||
|
||
@ReactProp(name = "allowToDragKeyboardFromShownStateBySwipes") | ||
override fun setAllowToDragKeyboardFromShownStateBySwipes(view: ReactViewGroup?, value: Boolean) { | ||
manager.setScrollKeyboardOffScreenWhenVisible(view as KeyboardGestureAreaReactViewGroup, value) | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
android/src/main/java/com/reactnativekeyboardcontroller/InteractiveKeyboardProvider.kt
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,6 @@ | ||
package com.reactnativekeyboardcontroller | ||
|
||
object InteractiveKeyboardProvider { | ||
var shown = false | ||
var isInteractive = false | ||
} |
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
Oops, something went wrong.