react-native-universal-keyboard-aware-scrollview 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/android/app/build.gradle +182 -0
- package/android/app/debug.keystore +0 -0
- package/android/app/proguard-rules.pro +14 -0
- package/android/app/src/debug/AndroidManifest.xml +7 -0
- package/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
- package/android/app/src/main/AndroidManifest.xml +25 -0
- package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainActivity.kt +61 -0
- package/android/app/src/main/java/com/anonymous/reactnativeuniversalkeyboardawarescrollview/MainApplication.kt +56 -0
- package/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- package/android/app/src/main/res/values/colors.xml +6 -0
- package/android/app/src/main/res/values/strings.xml +5 -0
- package/android/app/src/main/res/values/styles.xml +11 -0
- package/android/app/src/main/res/values-night/colors.xml +1 -0
- package/android/build.gradle +89 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +65 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle +39 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/universalkeyboard/UniversalKeyboardModule.kt +349 -0
- package/android/src/main/java/com/universalkeyboard/UniversalKeyboardPackage.kt +21 -0
- package/ios/.xcode.env +11 -0
- package/ios/Podfile +60 -0
- package/ios/Podfile.lock +2001 -0
- package/ios/Podfile.properties.json +5 -0
- package/ios/UniversalKeyboard.h +24 -0
- package/ios/UniversalKeyboard.m +413 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/AppDelegate.swift +70 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/Contents.json +6 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Info.plist +76 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/PrivacyInfo.xcprivacy +48 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/SplashScreen.storyboard +48 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/Supporting/Expo.plist +12 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview-Bridging-Header.h +3 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview/reactnativeuniversalkeyboardawarescrollview.entitlements +5 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/project.pbxproj +540 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcodeproj/xcshareddata/xcschemes/reactnativeuniversalkeyboardawarescrollview.xcscheme +88 -0
- package/ios/reactnativeuniversalkeyboardawarescrollview.xcworkspace/contents.xcworkspacedata +10 -0
- package/package.json +61 -0
- package/react-native-universal-keyboard-aware-scrollview.podspec +32 -0
- package/react-native.config.js +18 -0
- package/src/NativeModule.ts +61 -0
- package/src/components/KeyboardAwareScrollView.tsx +388 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useKeyboard.ts +360 -0
- package/src/index.ts +27 -0
- package/src/types.ts +87 -0
- package/src/utils/KeyboardController.ts +112 -0
- package/src/utils/index.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vijay Kishan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# react-native-universal-keyboard-aware-scrollview
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-universal-keyboard-aware-scrollview)
|
|
4
|
+
[](https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview/blob/main/LICENSE)
|
|
5
|
+
[](https://reactnative.dev/)
|
|
6
|
+
|
|
7
|
+
A **universal keyboard-aware ScrollView** for React Native that **works correctly** in:
|
|
8
|
+
- ✅ Normal screens
|
|
9
|
+
- ✅ React Native Modal
|
|
10
|
+
- ✅ BottomSheet components
|
|
11
|
+
- ✅ Any overlay or dialog scenario
|
|
12
|
+
|
|
13
|
+
This package uses **native keyboard listeners** on both Android and iOS for reliable keyboard detection, solving the common issues that plague other keyboard-aware libraries.
|
|
14
|
+
|
|
15
|
+
## 🎯 Why This Package?
|
|
16
|
+
|
|
17
|
+
Existing keyboard-aware solutions often fail in these scenarios:
|
|
18
|
+
|
|
19
|
+
| Issue | Other Libraries | This Package |
|
|
20
|
+
|-------|----------------|--------------|
|
|
21
|
+
| Keyboard overlaps input in Modal | ❌ | ✅ |
|
|
22
|
+
| Glitchy animations on Android | ❌ | ✅ |
|
|
23
|
+
| Incorrect height inside BottomSheet | ❌ | ✅ |
|
|
24
|
+
| Race conditions with keyboard events | ❌ | ✅ |
|
|
25
|
+
| Inconsistent behavior iOS vs Android | ❌ | ✅ |
|
|
26
|
+
|
|
27
|
+
### How We Solve These Issues
|
|
28
|
+
|
|
29
|
+
1. **Native Keyboard Detection**: Instead of relying solely on React Native's JavaScript keyboard events, we use platform-native APIs:
|
|
30
|
+
- **iOS**: `UIKeyboardWillChangeFrameNotification` for accurate keyboard frame tracking
|
|
31
|
+
- **Android**: `ViewTreeObserver.OnGlobalLayoutListener` with visible window frame measurement
|
|
32
|
+
|
|
33
|
+
2. **Modal-Aware Architecture**: Our native implementations measure keyboard height relative to the actual visible window, not the root activity/view controller. This means keyboard detection works correctly even when views are presented modally.
|
|
34
|
+
|
|
35
|
+
3. **Unified Event System**: Events are emitted from native → JavaScript through a consistent interface, eliminating timing issues and race conditions.
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Using npm
|
|
41
|
+
npm install react-native-universal-keyboard-aware-scrollview
|
|
42
|
+
|
|
43
|
+
# Using yarn
|
|
44
|
+
yarn add react-native-universal-keyboard-aware-scrollview
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### iOS Setup
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd ios && pod install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Android Setup
|
|
54
|
+
|
|
55
|
+
No additional setup required. The native module is automatically linked.
|
|
56
|
+
|
|
57
|
+
## 🚀 Quick Start
|
|
58
|
+
|
|
59
|
+
### Basic Usage
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import React from 'react';
|
|
63
|
+
import { TextInput, View, StyleSheet } from 'react-native';
|
|
64
|
+
import { KeyboardAwareScrollView } from 'react-native-universal-keyboard-aware-scrollview';
|
|
65
|
+
|
|
66
|
+
function MyForm() {
|
|
67
|
+
return (
|
|
68
|
+
<KeyboardAwareScrollView style={styles.container}>
|
|
69
|
+
<TextInput placeholder="Name" style={styles.input} />
|
|
70
|
+
<TextInput placeholder="Email" style={styles.input} />
|
|
71
|
+
<TextInput placeholder="Phone" style={styles.input} />
|
|
72
|
+
<TextInput
|
|
73
|
+
placeholder="Message"
|
|
74
|
+
style={[styles.input, styles.multiline]}
|
|
75
|
+
multiline
|
|
76
|
+
/>
|
|
77
|
+
</KeyboardAwareScrollView>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const styles = StyleSheet.create({
|
|
82
|
+
container: {
|
|
83
|
+
flex: 1,
|
|
84
|
+
},
|
|
85
|
+
input: {
|
|
86
|
+
borderWidth: 1,
|
|
87
|
+
borderColor: '#ccc',
|
|
88
|
+
borderRadius: 8,
|
|
89
|
+
padding: 12,
|
|
90
|
+
marginBottom: 16,
|
|
91
|
+
marginHorizontal: 16,
|
|
92
|
+
},
|
|
93
|
+
multiline: {
|
|
94
|
+
height: 100,
|
|
95
|
+
textAlignVertical: 'top',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Usage Inside Modal
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import React, { useState } from 'react';
|
|
104
|
+
import { Modal, TextInput, View, Button, StyleSheet } from 'react-native';
|
|
105
|
+
import { KeyboardAwareScrollView } from 'react-native-universal-keyboard-aware-scrollview';
|
|
106
|
+
|
|
107
|
+
function ModalForm() {
|
|
108
|
+
const [visible, setVisible] = useState(false);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<Button title="Open Form" onPress={() => setVisible(true)} />
|
|
113
|
+
|
|
114
|
+
<Modal visible={visible} animationType="slide">
|
|
115
|
+
<View style={styles.modalContainer}>
|
|
116
|
+
<KeyboardAwareScrollView
|
|
117
|
+
insideModal={true}
|
|
118
|
+
extraScrollHeight={20}
|
|
119
|
+
>
|
|
120
|
+
<TextInput placeholder="Name" style={styles.input} />
|
|
121
|
+
<TextInput placeholder="Email" style={styles.input} />
|
|
122
|
+
<TextInput
|
|
123
|
+
placeholder="Message"
|
|
124
|
+
style={[styles.input, styles.multiline]}
|
|
125
|
+
multiline
|
|
126
|
+
/>
|
|
127
|
+
</KeyboardAwareScrollView>
|
|
128
|
+
|
|
129
|
+
<Button title="Close" onPress={() => setVisible(false)} />
|
|
130
|
+
</View>
|
|
131
|
+
</Modal>
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Using the Hook Directly
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import React from 'react';
|
|
141
|
+
import { View, TextInput, StyleSheet } from 'react-native';
|
|
142
|
+
import { useKeyboard } from 'react-native-universal-keyboard-aware-scrollview';
|
|
143
|
+
|
|
144
|
+
function CustomKeyboardHandling() {
|
|
145
|
+
const {
|
|
146
|
+
keyboardHeight,
|
|
147
|
+
isKeyboardVisible,
|
|
148
|
+
dismissKeyboard
|
|
149
|
+
} = useKeyboard({
|
|
150
|
+
onKeyboardDidShow: (event) => {
|
|
151
|
+
console.log('Keyboard shown with height:', event.height);
|
|
152
|
+
},
|
|
153
|
+
onKeyboardDidHide: () => {
|
|
154
|
+
console.log('Keyboard hidden');
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<View style={[styles.container, { paddingBottom: keyboardHeight }]}>
|
|
160
|
+
<TextInput placeholder="Type here..." style={styles.input} />
|
|
161
|
+
{isKeyboardVisible && (
|
|
162
|
+
<Button title="Dismiss Keyboard" onPress={dismissKeyboard} />
|
|
163
|
+
)}
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## 📖 API Reference
|
|
170
|
+
|
|
171
|
+
### `KeyboardAwareScrollView`
|
|
172
|
+
|
|
173
|
+
A ScrollView that automatically adjusts for the keyboard.
|
|
174
|
+
|
|
175
|
+
#### Props
|
|
176
|
+
|
|
177
|
+
| Prop | Type | Default | Description |
|
|
178
|
+
|------|------|---------|-------------|
|
|
179
|
+
| `enableOnAndroid` | `boolean` | `true` | Enable keyboard handling on Android |
|
|
180
|
+
| `enableOnIOS` | `boolean` | `true` | Enable keyboard handling on iOS |
|
|
181
|
+
| `extraScrollHeight` | `number` | `20` | Extra scroll space above keyboard |
|
|
182
|
+
| `extraHeight` | `number` | `75` | Extra space for focused element |
|
|
183
|
+
| `enableAnimation` | `boolean` | `true` | Animate height changes |
|
|
184
|
+
| `animationDuration` | `number` | `250` | Animation duration in ms |
|
|
185
|
+
| `enableAutoScrollToFocused` | `boolean` | `true` | Auto-scroll when keyboard shows |
|
|
186
|
+
| `resetScrollToCoords` | `{x, y} \| null` | `null` | Reset scroll position on keyboard hide |
|
|
187
|
+
| `keyboardShouldPersistTaps` | `string` | `'handled'` | Keyboard dismiss behavior |
|
|
188
|
+
| `insideModal` | `boolean` | `false` | Optimize for modal usage |
|
|
189
|
+
| `enableKeyboardSpacer` | `boolean` | `true` | Use spacer view approach |
|
|
190
|
+
| `useContentInset` | `boolean` | `true` (iOS) | Use content inset vs padding |
|
|
191
|
+
|
|
192
|
+
#### Ref Methods
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
const scrollViewRef = useRef<KeyboardAwareScrollViewRef>(null);
|
|
196
|
+
|
|
197
|
+
// Available methods:
|
|
198
|
+
scrollViewRef.current?.scrollTo({ x: 0, y: 100, animated: true });
|
|
199
|
+
scrollViewRef.current?.scrollToEnd({ animated: true });
|
|
200
|
+
scrollViewRef.current?.scrollToFocusedInput(inputRef);
|
|
201
|
+
scrollViewRef.current?.getScrollResponder();
|
|
202
|
+
scrollViewRef.current?.dismissKeyboard();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `useKeyboard` Hook
|
|
206
|
+
|
|
207
|
+
A hook for tracking keyboard state.
|
|
208
|
+
|
|
209
|
+
#### Options
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
const keyboard = useKeyboard({
|
|
213
|
+
enableOnAndroid: true, // Enable on Android
|
|
214
|
+
enableOnIOS: true, // Enable on iOS
|
|
215
|
+
useNativeEvents: true, // Use native module events
|
|
216
|
+
animated: true, // Animate height changes
|
|
217
|
+
onKeyboardWillShow: (e) => {}, // iOS only
|
|
218
|
+
onKeyboardWillHide: (e) => {}, // iOS only
|
|
219
|
+
onKeyboardDidShow: (e) => {},
|
|
220
|
+
onKeyboardDidHide: (e) => {},
|
|
221
|
+
onKeyboardHeightChange: (h) => {},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Return Value
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
interface UseKeyboardReturn {
|
|
229
|
+
keyboardHeight: number; // Current keyboard height
|
|
230
|
+
isKeyboardVisible: boolean; // Is keyboard visible
|
|
231
|
+
isAnimating: boolean; // Is keyboard animating
|
|
232
|
+
dismissKeyboard: () => Promise<void>;
|
|
233
|
+
screenHeight: number; // Screen height
|
|
234
|
+
animationDuration: number; // Animation duration
|
|
235
|
+
safeAreaBottom: number; // Safe area inset (iOS)
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `KeyboardController`
|
|
240
|
+
|
|
241
|
+
Static utility class for keyboard control.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { KeyboardController } from 'react-native-universal-keyboard-aware-scrollview';
|
|
245
|
+
|
|
246
|
+
// Dismiss keyboard
|
|
247
|
+
await KeyboardController.dismiss();
|
|
248
|
+
|
|
249
|
+
// Get keyboard height
|
|
250
|
+
const height = await KeyboardController.getHeight();
|
|
251
|
+
|
|
252
|
+
// Check visibility
|
|
253
|
+
const isVisible = await KeyboardController.isVisible();
|
|
254
|
+
|
|
255
|
+
// Check if native module is available
|
|
256
|
+
const hasNative = KeyboardController.isNativeModuleAvailable();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## 🔧 Expo Support
|
|
260
|
+
|
|
261
|
+
This package works with Expo, but **requires a development build** (not Expo Go).
|
|
262
|
+
|
|
263
|
+
### Expo Bare Workflow
|
|
264
|
+
|
|
265
|
+
Works out of the box. Install and use normally.
|
|
266
|
+
|
|
267
|
+
### Expo Managed Workflow
|
|
268
|
+
|
|
269
|
+
1. Install the package:
|
|
270
|
+
```bash
|
|
271
|
+
npx expo install react-native-universal-keyboard-aware-scrollview
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
2. Create a development build:
|
|
275
|
+
```bash
|
|
276
|
+
npx expo prebuild
|
|
277
|
+
npx expo run:ios # or run:android
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
3. Use in your app as normal.
|
|
281
|
+
|
|
282
|
+
> **Note**: This package contains native code, so it cannot run in Expo Go. You must use a development build or EAS Build.
|
|
283
|
+
|
|
284
|
+
## 🏗 Architecture
|
|
285
|
+
|
|
286
|
+
### Native Implementation Details
|
|
287
|
+
|
|
288
|
+
#### Android (Kotlin)
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
┌─────────────────────────────────────┐
|
|
292
|
+
│ UniversalKeyboardModule │
|
|
293
|
+
├─────────────────────────────────────┤
|
|
294
|
+
│ ViewTreeObserver │
|
|
295
|
+
│ OnGlobalLayoutListener │
|
|
296
|
+
│ ↓ │
|
|
297
|
+
│ getWindowVisibleDisplayFrame() │
|
|
298
|
+
│ ↓ │
|
|
299
|
+
│ Calculate keyboard height │
|
|
300
|
+
│ ↓ │
|
|
301
|
+
│ Emit via DeviceEventEmitter │
|
|
302
|
+
└─────────────────────────────────────┘
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The Android implementation uses `ViewTreeObserver.OnGlobalLayoutListener` to detect layout changes. When the keyboard appears, the visible display frame shrinks. We calculate the keyboard height by comparing the visible frame with the full screen height.
|
|
306
|
+
|
|
307
|
+
This approach works in modals because we attach the listener to the content view, which is always affected by keyboard appearance regardless of the view hierarchy.
|
|
308
|
+
|
|
309
|
+
#### iOS (Objective-C)
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
┌─────────────────────────────────────┐
|
|
313
|
+
│ UniversalKeyboard │
|
|
314
|
+
├─────────────────────────────────────┤
|
|
315
|
+
│ NSNotificationCenter │
|
|
316
|
+
│ UIKeyboardWillChangeFrameNotification│
|
|
317
|
+
│ ↓ │
|
|
318
|
+
│ Extract keyboard frame from userInfo│
|
|
319
|
+
│ ↓ │
|
|
320
|
+
│ Calculate height from screen bottom │
|
|
321
|
+
│ ↓ │
|
|
322
|
+
│ Emit via RCTEventEmitter │
|
|
323
|
+
└─────────────────────────────────────┘
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The iOS implementation uses `UIKeyboardWillChangeFrameNotification`, which provides the most accurate keyboard frame information. This notification fires for all keyboard changes including interactive dismiss gestures.
|
|
327
|
+
|
|
328
|
+
We calculate keyboard height as `screenHeight - keyboardFrame.origin.y`, which works correctly in all presentation contexts including modals.
|
|
329
|
+
|
|
330
|
+
## 🐛 Troubleshooting
|
|
331
|
+
|
|
332
|
+
### "Native module not found" Error
|
|
333
|
+
|
|
334
|
+
This usually means the native module isn't linked:
|
|
335
|
+
|
|
336
|
+
**iOS:**
|
|
337
|
+
```bash
|
|
338
|
+
cd ios && pod install --repo-update
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Android:**
|
|
342
|
+
Clean and rebuild:
|
|
343
|
+
```bash
|
|
344
|
+
cd android && ./gradlew clean
|
|
345
|
+
cd .. && npx react-native run-android
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Keyboard still overlaps in Modal
|
|
349
|
+
|
|
350
|
+
Make sure you set `insideModal={true}`:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
<KeyboardAwareScrollView insideModal={true}>
|
|
354
|
+
{/* content */}
|
|
355
|
+
</KeyboardAwareScrollView>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Animation is jumpy on Android
|
|
359
|
+
|
|
360
|
+
Try adjusting the animation settings:
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
<KeyboardAwareScrollView
|
|
364
|
+
enableAnimation={true}
|
|
365
|
+
animationDuration={300}
|
|
366
|
+
>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Or disable animation entirely:
|
|
370
|
+
|
|
371
|
+
```tsx
|
|
372
|
+
<KeyboardAwareScrollView enableAnimation={false}>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## 📄 License
|
|
376
|
+
|
|
377
|
+
MIT © [Vijay Kishan](https://github.com/AetherTechDev)
|
|
378
|
+
|
|
379
|
+
## 🤝 Contributing
|
|
380
|
+
|
|
381
|
+
Contributions are welcome! Please read our [contributing guidelines](CONTRIBUTING.md) first.
|
|
382
|
+
|
|
383
|
+
## 📞 Support
|
|
384
|
+
|
|
385
|
+
- 🐛 [Report bugs](https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview/issues)
|
|
386
|
+
- 💡 [Request features](https://github.com/AetherTechDev/react-native-universal-keyboard-aware-scrollview/issues)
|
|
387
|
+
- 📧 Email: vijay@aethertech.dev
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
apply plugin: "com.android.application"
|
|
2
|
+
apply plugin: "org.jetbrains.kotlin.android"
|
|
3
|
+
apply plugin: "com.facebook.react"
|
|
4
|
+
|
|
5
|
+
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is the configuration block to customize your React Native Android app.
|
|
9
|
+
* By default you don't need to apply any configuration, just uncomment the lines you need.
|
|
10
|
+
*/
|
|
11
|
+
react {
|
|
12
|
+
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
|
|
13
|
+
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
|
14
|
+
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
|
|
15
|
+
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
|
|
16
|
+
|
|
17
|
+
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
|
|
18
|
+
// Use Expo CLI to bundle the app, this ensures the Metro config
|
|
19
|
+
// works correctly with Expo projects.
|
|
20
|
+
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
|
|
21
|
+
bundleCommand = "export:embed"
|
|
22
|
+
|
|
23
|
+
/* Folders */
|
|
24
|
+
// The root of your project, i.e. where "package.json" lives. Default is '../..'
|
|
25
|
+
// root = file("../../")
|
|
26
|
+
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
|
|
27
|
+
// reactNativeDir = file("../../node_modules/react-native")
|
|
28
|
+
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
|
|
29
|
+
// codegenDir = file("../../node_modules/@react-native/codegen")
|
|
30
|
+
|
|
31
|
+
/* Variants */
|
|
32
|
+
// The list of variants to that are debuggable. For those we're going to
|
|
33
|
+
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
|
|
34
|
+
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
|
|
35
|
+
// debuggableVariants = ["liteDebug", "prodDebug"]
|
|
36
|
+
|
|
37
|
+
/* Bundling */
|
|
38
|
+
// A list containing the node command and its flags. Default is just 'node'.
|
|
39
|
+
// nodeExecutableAndArgs = ["node"]
|
|
40
|
+
|
|
41
|
+
//
|
|
42
|
+
// The path to the CLI configuration file. Default is empty.
|
|
43
|
+
// bundleConfig = file(../rn-cli.config.js)
|
|
44
|
+
//
|
|
45
|
+
// The name of the generated asset file containing your JS bundle
|
|
46
|
+
// bundleAssetName = "MyApplication.android.bundle"
|
|
47
|
+
//
|
|
48
|
+
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
|
|
49
|
+
// entryFile = file("../js/MyApplication.android.js")
|
|
50
|
+
//
|
|
51
|
+
// A list of extra flags to pass to the 'bundle' commands.
|
|
52
|
+
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
|
53
|
+
// extraPackagerArgs = []
|
|
54
|
+
|
|
55
|
+
/* Hermes Commands */
|
|
56
|
+
// The hermes compiler command to run. By default it is 'hermesc'
|
|
57
|
+
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
|
|
58
|
+
//
|
|
59
|
+
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
|
|
60
|
+
// hermesFlags = ["-O", "-output-source-map"]
|
|
61
|
+
|
|
62
|
+
/* Autolinking */
|
|
63
|
+
autolinkLibrariesWithApp()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set this to true in release builds to optimize the app using [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization).
|
|
68
|
+
*/
|
|
69
|
+
def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The preferred build flavor of JavaScriptCore (JSC)
|
|
73
|
+
*
|
|
74
|
+
* For example, to use the international variant, you can use:
|
|
75
|
+
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
|
76
|
+
*
|
|
77
|
+
* The international variant includes ICU i18n library and necessary data
|
|
78
|
+
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
|
79
|
+
* give correct results when using with locales other than en-US. Note that
|
|
80
|
+
* this variant is about 6MiB larger per architecture than default.
|
|
81
|
+
*/
|
|
82
|
+
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
|
83
|
+
|
|
84
|
+
android {
|
|
85
|
+
ndkVersion rootProject.ext.ndkVersion
|
|
86
|
+
|
|
87
|
+
buildToolsVersion rootProject.ext.buildToolsVersion
|
|
88
|
+
compileSdk rootProject.ext.compileSdkVersion
|
|
89
|
+
|
|
90
|
+
namespace 'com.anonymous.reactnativeuniversalkeyboardawarescrollview'
|
|
91
|
+
defaultConfig {
|
|
92
|
+
applicationId 'com.anonymous.reactnativeuniversalkeyboardawarescrollview'
|
|
93
|
+
minSdkVersion rootProject.ext.minSdkVersion
|
|
94
|
+
targetSdkVersion rootProject.ext.targetSdkVersion
|
|
95
|
+
versionCode 1
|
|
96
|
+
versionName "1.0.0"
|
|
97
|
+
|
|
98
|
+
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
|
99
|
+
}
|
|
100
|
+
signingConfigs {
|
|
101
|
+
debug {
|
|
102
|
+
storeFile file('debug.keystore')
|
|
103
|
+
storePassword 'android'
|
|
104
|
+
keyAlias 'androiddebugkey'
|
|
105
|
+
keyPassword 'android'
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
buildTypes {
|
|
109
|
+
debug {
|
|
110
|
+
signingConfig signingConfigs.debug
|
|
111
|
+
}
|
|
112
|
+
release {
|
|
113
|
+
// Caution! In production, you need to generate your own keystore file.
|
|
114
|
+
// see https://reactnative.dev/docs/signed-apk-android.
|
|
115
|
+
signingConfig signingConfigs.debug
|
|
116
|
+
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
|
|
117
|
+
shrinkResources enableShrinkResources.toBoolean()
|
|
118
|
+
minifyEnabled enableMinifyInReleaseBuilds
|
|
119
|
+
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
|
120
|
+
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
|
|
121
|
+
crunchPngs enablePngCrunchInRelease.toBoolean()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
packagingOptions {
|
|
125
|
+
jniLibs {
|
|
126
|
+
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
|
|
127
|
+
useLegacyPackaging enableLegacyPackaging.toBoolean()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
androidResources {
|
|
131
|
+
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
|
136
|
+
// Accepts values in comma delimited lists, example:
|
|
137
|
+
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
|
|
138
|
+
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
|
|
139
|
+
// Split option: 'foo,bar' -> ['foo', 'bar']
|
|
140
|
+
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
|
|
141
|
+
// Trim all elements in place.
|
|
142
|
+
for (i in 0..<options.size()) options[i] = options[i].trim();
|
|
143
|
+
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
|
|
144
|
+
options -= ""
|
|
145
|
+
|
|
146
|
+
if (options.length > 0) {
|
|
147
|
+
println "android.packagingOptions.$prop += $options ($options.length)"
|
|
148
|
+
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
|
|
149
|
+
options.each {
|
|
150
|
+
android.packagingOptions[prop] += it
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
dependencies {
|
|
156
|
+
// The version of react-native is set by the React Native Gradle Plugin
|
|
157
|
+
implementation("com.facebook.react:react-android")
|
|
158
|
+
|
|
159
|
+
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
|
|
160
|
+
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
|
|
161
|
+
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
|
|
162
|
+
|
|
163
|
+
if (isGifEnabled) {
|
|
164
|
+
// For animated gif support
|
|
165
|
+
implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isWebpEnabled) {
|
|
169
|
+
// For webp support
|
|
170
|
+
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
|
|
171
|
+
if (isWebpAnimatedEnabled) {
|
|
172
|
+
// Animated webp support
|
|
173
|
+
implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (hermesEnabled.toBoolean()) {
|
|
178
|
+
implementation("com.facebook.react:hermes-android")
|
|
179
|
+
} else {
|
|
180
|
+
implementation jscFlavor
|
|
181
|
+
}
|
|
182
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Add project specific ProGuard rules here.
|
|
2
|
+
# By default, the flags in this file are appended to flags specified
|
|
3
|
+
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
|
|
4
|
+
# You can edit the include path and order by changing the proguardFiles
|
|
5
|
+
# directive in build.gradle.
|
|
6
|
+
#
|
|
7
|
+
# For more details, see
|
|
8
|
+
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
9
|
+
|
|
10
|
+
# react-native-reanimated
|
|
11
|
+
-keep class com.swmansion.reanimated.** { *; }
|
|
12
|
+
-keep class com.facebook.react.turbomodule.** { *; }
|
|
13
|
+
|
|
14
|
+
# Add any project specific keep options here:
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
5
|
+
|
|
6
|
+
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
|
7
|
+
</manifest>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
5
|
+
|
|
6
|
+
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
|
7
|
+
</manifest>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.INTERNET"/>
|
|
3
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
4
|
+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
|
5
|
+
<uses-permission android:name="android.permission.VIBRATE"/>
|
|
6
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
|
7
|
+
<queries>
|
|
8
|
+
<intent>
|
|
9
|
+
<action android:name="android.intent.action.VIEW"/>
|
|
10
|
+
<category android:name="android.intent.category.BROWSABLE"/>
|
|
11
|
+
<data android:scheme="https"/>
|
|
12
|
+
</intent>
|
|
13
|
+
</queries>
|
|
14
|
+
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
|
15
|
+
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
|
16
|
+
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
|
17
|
+
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
|
18
|
+
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
|
19
|
+
<intent-filter>
|
|
20
|
+
<action android:name="android.intent.action.MAIN"/>
|
|
21
|
+
<category android:name="android.intent.category.LAUNCHER"/>
|
|
22
|
+
</intent-filter>
|
|
23
|
+
</activity>
|
|
24
|
+
</application>
|
|
25
|
+
</manifest>
|