Skip to content

Commit a57fa4b

Browse files
feat: KeyboardBackgroundView (#981)
## 📜 Description Added `KeyboardBackgroundView` - component designed to match keyboard backdrop UI. ## 💡 Motivation and Context This component can be useful when you want to continue keyboard surface and embed advanced elements (such as `TextInput` inside it). Using this component you can achieve a similar UI to Photos app or Safari app search on iOS. Additioanlly this component will be used as a polyfill implementation for `KeyboardExtender` on Android (just because `KeyboardExtender` can not be implemented natively on Android). > [!WARNING] > This component only matches default keyboard UI background. If user customizes it -> it will look differently and will not match keyboard design. There is no way to check displayed keyboard color on a native side, but some apps still use this approach, so I thought to export that possibility from `keyboard-controller` too. On iOS we can use system class `UIKBBackdropView` to match the keyboard background. On Android keyboard is a 3rd party process so there is no API at all, so I kind of reverse-engineered it: we have an access to keyboard package name and then based on pacakge name and phone preferences we can "guess" keyboard color. In reality it may vary from OS version o OS version and may be even dynamic and depend on wallpaper generic color. To solve it I Added a class that has a basic color (can be dynamic too) and then we have additional property called `tone`, which can slightly adjust the color. The idea is that color like `#3c3c3c` with tone `+2` will become `#3e3e3e`. For now it works and we can successfully match default keyboard color. ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### E2E - cover `KeyboardBackgroundView` by e2e tests; ### Docs - mention new feature in README; - new docs page `KeyboardBackgroundView`; - new chapter in Components Overview guide. ### JS - added `KeyboardBackgroundView`; - added unit-tests for `KeyboardBackgroundView`. ### iOS - added `KeyboardBackgroundView`; ### Android - added `KeyboardBackgroundView`; - move `isSystemDarkMode` extensions to `Context`; - create new `currentImePackage` extension; - create resource files; ## 🤔 How Has This Been Tested? Tested manually on: - Pixel 7 Pro (API 35, API 36 - real device); - Pixel 9 Pro (API 31, API 33, API 34, API 35, API 36 - emulator); - Xiaomi Redmi Note 5 Pro (API 28 - real device); - iPhone 11 (iOS 18.5 - real device); - iPhone 6s (iOS 15.8 - real device); - iPhone 14 Pro (iOS 18.4 - real device). - iPhone 16 Pro (iOS 18.5, 26.0 - simulator) - iPhone 15 Pro (iOS 17.5 - simulator) plus tested on all e2e devices (via e2e tests). ## 📸 Screenshots (if appropriate): ### Shared surface |Android|iOS| |--------|---| |<img width="300" alt="android" src="https://github.com/user-attachments/assets/1bbbbdf3-17af-4277-a33a-390a413c50e5" />|<img width="300" alt="android" src="https://github.com/user-attachments/assets/489ace49-8a68-4fdf-a98a-bd4bdc0a0418">| ### "Meatball"/Ask AI/Liquid keyboard |Android|iOS| |--------|---| |<video src="https://github.com/user-attachments/assets/ac3efba6-2a6a-43a1-b0ee-501e2b5be361">|<video src="https://github.com/user-attachments/assets/2f7d5c3e-4e32-43d9-b2d4-9658accc757a">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c2c40aa commit a57fa4b

File tree

65 files changed

+1403
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1403
-17
lines changed

FabricExample/__tests__/__snapshots__/components-rendering.spec.tsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ exports[`components rendering should render \`KeyboardAwareScrollView\` 1`] = `
4646
</RCTScrollView>
4747
`;
4848

49+
exports[`components rendering should render \`KeyboardBackgroundView\` 1`] = `<KeyboardBackgroundView />`;
50+
4951
exports[`components rendering should render \`KeyboardControllerView\` 1`] = `
5052
<KeyboardControllerView
5153
statusBarTranslucent={true}

FabricExample/__tests__/components-rendering.spec.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { View } from "react-native";
44
import {
55
KeyboardAvoidingView,
66
KeyboardAwareScrollView,
7+
KeyboardBackgroundView,
78
KeyboardControllerView,
89
KeyboardProvider,
910
KeyboardStickyView,
@@ -74,6 +75,10 @@ function OverKeyboardViewTest() {
7475
);
7576
}
7677

78+
function KeyboardBackgroundViewTest() {
79+
return <KeyboardBackgroundView />;
80+
}
81+
7782
describe("components rendering", () => {
7883
it("should render `KeyboardControllerView`", () => {
7984
expect(render(<KeyboardControllerViewTest />)).toMatchSnapshot();
@@ -102,4 +107,8 @@ describe("components rendering", () => {
102107
it("should render `OverKeyboardView`", () => {
103108
expect(render(<OverKeyboardViewTest />)).toMatchSnapshot();
104109
});
110+
111+
it("should render `KeyboardBackgroundView`", () => {
112+
expect(render(<KeyboardBackgroundViewTest />)).toMatchSnapshot();
113+
});
105114
});

FabricExample/ios/KeyboardControllerFabricExample/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>CADisableMinimumFrameDurationOnPhone</key>
6+
<true/>
57
<key>CFBundleDevelopmentRegion</key>
68
<string>en</string>
79
<key>CFBundleDisplayName</key>
@@ -47,7 +49,5 @@
4749
</array>
4850
<key>UIViewControllerBasedStatusBarAppearance</key>
4951
<false/>
50-
<key>CADisableMinimumFrameDurationOnPhone</key>
51-
<true/>
5252
</dict>
5353
</plist>

FabricExample/src/constants/screenNames.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ export enum ScreenNames {
2424
OVER_KEYBOARD_VIEW = "OVER_KEYBOARD_VIEW",
2525
IMAGE_GALLERY = "IMAGE_GALLERY",
2626
USE_KEYBOARD_STATE = "USE_KEYBOARD_STATE",
27+
LIQUID_KEYBOARD = "LIQUID_KEYBOARD",
28+
KEYBOARD_SHARED_TRANSITIONS = "KEYBOARD_SHARED_TRANSITIONS",
2729
}

FabricExample/src/navigation/ExamplesStack/index.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import InteractiveKeyboard from "../../screens/Examples/InteractiveKeyboard";
1313
import InteractiveKeyboardIOS from "../../screens/Examples/InteractiveKeyboardIOS";
1414
import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation";
1515
import KeyboardAvoidingViewExample from "../../screens/Examples/KeyboardAvoidingView";
16+
import KeyboardSharedTransitionExample from "../../screens/Examples/KeyboardSharedTransitions";
1617
import UseKeyboardState from "../../screens/Examples/KeyboardStateHook";
18+
import LiquidKeyboardExample from "../../screens/Examples/LiquidKeyboard";
1719
import LottieAnimation from "../../screens/Examples/Lottie";
1820
import ModalExample from "../../screens/Examples/Modal";
1921
import NonUIProps from "../../screens/Examples/NonUIProps";
@@ -48,6 +50,8 @@ export type ExamplesStackParamList = {
4850
[ScreenNames.OVER_KEYBOARD_VIEW]: undefined;
4951
[ScreenNames.IMAGE_GALLERY]: undefined;
5052
[ScreenNames.USE_KEYBOARD_STATE]: undefined;
53+
[ScreenNames.LIQUID_KEYBOARD]: undefined;
54+
[ScreenNames.KEYBOARD_SHARED_TRANSITIONS]: undefined;
5155
};
5256

5357
const Stack = createStackNavigator<ExamplesStackParamList>();
@@ -120,6 +124,14 @@ const options = {
120124
[ScreenNames.USE_KEYBOARD_STATE]: {
121125
title: "useKeyboardState",
122126
},
127+
[ScreenNames.LIQUID_KEYBOARD]: {
128+
title: "Liquid keyboard",
129+
headerShown: false,
130+
},
131+
[ScreenNames.KEYBOARD_SHARED_TRANSITIONS]: {
132+
title: "Keyboard shared transitions",
133+
headerShown: false,
134+
},
123135
};
124136

125137
const ExamplesStack = () => (
@@ -234,6 +246,16 @@ const ExamplesStack = () => (
234246
name={ScreenNames.USE_KEYBOARD_STATE}
235247
options={options[ScreenNames.USE_KEYBOARD_STATE]}
236248
/>
249+
<Stack.Screen
250+
component={LiquidKeyboardExample}
251+
name={ScreenNames.LIQUID_KEYBOARD}
252+
options={options[ScreenNames.LIQUID_KEYBOARD]}
253+
/>
254+
<Stack.Screen
255+
component={KeyboardSharedTransitionExample}
256+
name={ScreenNames.KEYBOARD_SHARED_TRANSITIONS}
257+
options={options[ScreenNames.KEYBOARD_SHARED_TRANSITIONS]}
258+
/>
237259
</Stack.Navigator>
238260
);
239261

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from "react";
2+
import { TextInput, View } from "react-native";
3+
import {
4+
KeyboardBackgroundView,
5+
KeyboardStickyView,
6+
useReanimatedKeyboardAnimation,
7+
} from "react-native-keyboard-controller";
8+
import Reanimated, {
9+
interpolateColor,
10+
useAnimatedStyle,
11+
} from "react-native-reanimated";
12+
import {
13+
SafeAreaView,
14+
useSafeAreaInsets,
15+
} from "react-native-safe-area-context";
16+
17+
const ReanimatedBackgroundView = Reanimated.createAnimatedComponent(
18+
KeyboardBackgroundView,
19+
);
20+
const ReanimatedTextInput = Reanimated.createAnimatedComponent(TextInput);
21+
22+
const KeyboardSharedTransitionExample = () => {
23+
const { bottom } = useSafeAreaInsets();
24+
const { progress } = useReanimatedKeyboardAnimation();
25+
26+
const opacity = useAnimatedStyle(
27+
() => ({
28+
height: 291 + 70,
29+
opacity: progress.value,
30+
}),
31+
[],
32+
);
33+
const inputColor = useAnimatedStyle(
34+
() => ({
35+
backgroundColor: interpolateColor(
36+
progress.value,
37+
[0, 1],
38+
["#323232", "#474747"],
39+
),
40+
}),
41+
[],
42+
);
43+
44+
return (
45+
<SafeAreaView
46+
style={{
47+
backgroundColor: "#000000",
48+
flex: 1,
49+
justifyContent: "flex-end",
50+
}}
51+
>
52+
<KeyboardStickyView offset={{ closed: 291, opened: 291 + bottom }}>
53+
<ReanimatedBackgroundView style={opacity} />
54+
<View
55+
style={{
56+
marginHorizontal: 30,
57+
marginVertical: 16,
58+
position: "absolute",
59+
left: 0,
60+
right: 0,
61+
bottom: 0,
62+
top: 0,
63+
}}
64+
>
65+
<ReanimatedTextInput
66+
placeholder="127.0.0.1"
67+
placeholderTextColor="#ecececec"
68+
style={[
69+
{
70+
width: "100%",
71+
padding: 10,
72+
borderRadius: 8,
73+
textAlign: "center",
74+
},
75+
inputColor,
76+
]}
77+
testID="shared_transition_input"
78+
/>
79+
</View>
80+
</KeyboardStickyView>
81+
</SafeAreaView>
82+
);
83+
};
84+
85+
export default KeyboardSharedTransitionExample;
1.3 MB
Loading
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, { useEffect } from "react";
2+
import { Image, StyleSheet, TextInput } from "react-native";
3+
import {
4+
KeyboardBackgroundView,
5+
KeyboardEvents,
6+
KeyboardStickyView,
7+
} from "react-native-keyboard-controller";
8+
import Reanimated, {
9+
interpolate,
10+
useAnimatedStyle,
11+
useSharedValue,
12+
withSpring,
13+
} from "react-native-reanimated";
14+
import {
15+
SafeAreaView,
16+
useSafeAreaInsets,
17+
} from "react-native-safe-area-context";
18+
19+
const ReanimatedBackgroundView = Reanimated.createAnimatedComponent(
20+
KeyboardBackgroundView,
21+
);
22+
23+
const LiquidKeyboardExample = () => {
24+
const progress = useSharedValue(0);
25+
const { bottom } = useSafeAreaInsets();
26+
27+
useEffect(() => {
28+
progress.set(0);
29+
/*progress.value = withRepeat(withSpring(1, {
30+
stiffness: 1000,
31+
damping: 500,
32+
mass: 3,
33+
}), -1, true);*/
34+
KeyboardEvents.addListener("keyboardDidShow", () => {
35+
progress.set(
36+
withSpring(1, {
37+
stiffness: 1000,
38+
damping: 500,
39+
mass: 3,
40+
}),
41+
);
42+
});
43+
KeyboardEvents.addListener("keyboardWillHide", () => {
44+
progress.set(
45+
withSpring(0, {
46+
stiffness: 1000,
47+
damping: 500,
48+
mass: 3,
49+
}),
50+
);
51+
});
52+
}, []);
53+
54+
const mainCircle = useAnimatedStyle(
55+
() => ({
56+
borderBottomRightRadius: interpolate(
57+
progress.value,
58+
[0, 0.8, 1],
59+
[0, 0, 25],
60+
),
61+
transform: [{ translateY: -progress.value * 70 }, { rotate: "45deg" }],
62+
}),
63+
[],
64+
);
65+
const secondCircle = useAnimatedStyle(
66+
() => ({
67+
transform: [
68+
// { scale: interpolate(progress.value, [0, 0.5, 1], [1, 1, 0.8]) },
69+
{
70+
// 27%
71+
translateY: interpolate(progress.value, [0, 0.35, 1], [12, -2, 12]),
72+
},
73+
],
74+
}),
75+
[],
76+
);
77+
78+
return (
79+
<SafeAreaView style={styles.container}>
80+
<TextInput keyboardType="default" style={styles.textInput} />
81+
<KeyboardStickyView>
82+
<ReanimatedBackgroundView
83+
style={[
84+
{
85+
width: 50,
86+
height: 50,
87+
borderTopLeftRadius: 25,
88+
borderTopRightRadius: 25,
89+
borderBottomLeftRadius: 25,
90+
right: 20,
91+
position: "absolute",
92+
justifyContent: "center",
93+
alignItems: "center",
94+
top: bottom,
95+
// zIndex: 2,
96+
},
97+
mainCircle,
98+
]}
99+
>
100+
<Image
101+
source={require("./ai.png")}
102+
style={{ transform: [{ rotate: "-45deg" }], width: 20, height: 20 }}
103+
/>
104+
</ReanimatedBackgroundView>
105+
<ReanimatedBackgroundView
106+
style={[
107+
{
108+
width: 100,
109+
height: 100,
110+
borderRadius: 9999,
111+
right: -5,
112+
bottom: -122,
113+
position: "absolute",
114+
},
115+
secondCircle,
116+
]}
117+
/>
118+
</KeyboardStickyView>
119+
</SafeAreaView>
120+
);
121+
};
122+
123+
const styles = StyleSheet.create({
124+
container: {
125+
flex: 1,
126+
backgroundColor: "#8A8A8C",
127+
justifyContent: "space-between",
128+
},
129+
textInput: {
130+
height: 50,
131+
paddingHorizontal: 10,
132+
margin: 10,
133+
borderWidth: 1,
134+
borderColor: "white",
135+
},
136+
});
137+
138+
export default LiquidKeyboardExample;

FabricExample/src/screens/Examples/Main/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,16 @@ export const examples: Example[] = [
135135
info: ScreenNames.USE_KEYBOARD_STATE,
136136
icons: "📝",
137137
},
138+
{
139+
title: "Liquid keyboard",
140+
testID: "liquid_keyboard",
141+
info: ScreenNames.LIQUID_KEYBOARD,
142+
icons: "💧",
143+
},
144+
{
145+
title: "Keyboard shared transitions",
146+
testID: "keyboard_shared_transitions",
147+
info: ScreenNames.KEYBOARD_SHARED_TRANSITIONS,
148+
icons: "🔄",
149+
},
138150
];

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A universal keyboard handling solution for React Native — lightweight, fully c
1616
- 📚 Prebuilt components (`KeyboardStickyView`, `KeyboardAwareScrollView`, reworked `KeyboardAvoidingView`)
1717
- 📐 `KeyboardToolbar` with customizable _**previous**_, _**next**_, and _**done**_ buttons
1818
- 🌐 Display anything over the keyboard (without dismissing it) using `OverKeyboardView`
19+
- 🎨 Match keyboard background with `KeyboardBackgroundView`
1920
- 📝 Easy retrieval of focused input info
2021
- 🧭 Compatible with any navigation library
2122
- ✨ More coming soon... stay tuned! 😊

0 commit comments

Comments
 (0)