Skip to content

Commit

Permalink
feat: enabled (#227)
Browse files Browse the repository at this point in the history
## 📜 Description

Added `useKeyboardController` hook that returns `setEnabled` + `enabled`
(which allows to disable and enable module on demand).

## 💡 Motivation and Context

Fixes
#233
#198

## 📢 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 -->

### JS
- added `useKeyboardController` hook that gives an access to
`setEnabled` method and `enabled` variable (that indicates current
state);
- added `enabled` property to specs;
- manage `enabled` property via state (from react) and passing methods
via context to change the value of this variable;
- apply and revert monkey patch depending on `enabled` value.

### iOS
- paper: added property setter that calls `mount`/`unmount` depends on
the value that comes;
- fabric: detect prop changes in `updateProps` method and call
`mount`/`unmount` depends on the value that comes;

### Android
- go to `edge-to-edge` when prop is set (before we did it in
`onAttachedToWindow` method);
- enable `edge-to-edge` mode if `onAttachedToWindow` is called more than
one time;

## 🤔 How Has This Been Tested?

Tested on:
- Pixel 3a (Android 13, emulator)
- Pixel 6 Pro (Android 9, emulator)
- Pixel 7 Pro (Android 13, real device);
- Xiaomi Redmi Note 5 Pro (Android 9, real device);
- iPhone 14 Pro (iOS 16.4, iOS 17, simulator);
- iPhone 6S (iOS 15.6, real device).

## 📸 Screenshots (if appropriate):

|Android|iOS|
|--------|---|
|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/6fcece94-8252-4fc8-9ab5-6d9b946168e7">|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/9e91d79f-cf42-4074-aeed-0f063896e500">|

## 📝 Checklist

- [x] CI successfully passed
  • Loading branch information
kirillzyusko committed Sep 18, 2023
1 parent 91a06d0 commit ef0f625
Show file tree
Hide file tree
Showing 38 changed files with 366 additions and 88 deletions.
1 change: 1 addition & 0 deletions FabricExample/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export enum ScreenNames {
INTERACTIVE_KEYBOARD_IOS = 'INTERACTIVE_KEYBOARD_IOS',
NATIVE_STACK = 'NATIVE_STACK',
KEYBOARD_AVOIDING_VIEW = 'KEYBOARD_AVOIDING_VIEW',
ENABLED_DISABLED = 'ENABLED_DISABLED',
}
10 changes: 10 additions & 0 deletions FabricExample/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import InteractiveKeyboard from '../../screens/Examples/InteractiveKeyboard';
import InteractiveKeyboardIOS from '../../screens/Examples/InteractiveKeyboardIOS';
import NativeStack from '../NestedStack';
import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoidingView';
import EnabledDisabled from '../../screens/Examples/EnabledDisabled';

export type ExamplesStackParamList = {
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
Expand All @@ -27,6 +28,7 @@ export type ExamplesStackParamList = {
[ScreenNames.INTERACTIVE_KEYBOARD_IOS]: undefined;
[ScreenNames.NATIVE_STACK]: undefined;
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: undefined;
[ScreenNames.ENABLED_DISABLED]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -66,6 +68,9 @@ const options = {
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: {
title: 'KeyboardAvoidingView',
},
[ScreenNames.ENABLED_DISABLED]: {
title: 'Enabled/disabled',
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -125,6 +130,11 @@ const ExamplesStack = () => (
component={KeyboardAvoidingViewExample}
options={options[ScreenNames.KEYBOARD_AVOIDING_VIEW]}
/>
<Stack.Screen
name={ScreenNames.ENABLED_DISABLED}
component={EnabledDisabled}
options={options[ScreenNames.ENABLED_DISABLED]}
/>
</Stack.Navigator>
);

Expand Down
18 changes: 18 additions & 0 deletions FabricExample/src/screens/Examples/EnabledDisabled/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, View } from 'react-native';
import { useKeyboardController } from 'react-native-keyboard-controller';
import KeyboardAnimationTemplate from '../../../components/KeyboardAnimation';

export default function EnabledDisabled() {
const { enabled, setEnabled } = useKeyboardController();

return (
<View style={{ flex: 1, paddingTop: 50 }}>
<Button
title={enabled ? 'Enabled' : 'Disabled'}
onPress={() => setEnabled(!enabled)}
/>
<KeyboardAnimationTemplate />
</View>
);
}
5 changes: 5 additions & 0 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,9 @@ export const examples: Example[] = [
info: ScreenNames.KEYBOARD_AVOIDING_VIEW,
icons: '😶',
},
{
title: 'Enabled/disabled',
info: ScreenNames.ENABLED_DISABLED,
icons: '💡',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class KeyboardControllerViewManager(mReactContext: ReactApplicationContext) :
return manager.setNavigationBarTranslucent(view as EdgeToEdgeReactViewGroup, value)
}

@ReactProp(name = "enabled")
override fun setEnabled(view: ReactViewGroup, value: Boolean) {
return manager.setEnabled(view as EdgeToEdgeReactViewGroup, value)
}

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return manager.getExportedCustomDirectEventTypeConstants()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
return EdgeToEdgeReactViewGroup(reactContext)
}

fun setEnabled(view: EdgeToEdgeReactViewGroup, enabled: Boolean) {
view.setActive(enabled)
}

fun setStatusBarTranslucent(view: EdgeToEdgeReactViewGroup, isStatusBarTranslucent: Boolean) {
view.setStatusBarTranslucent(isStatusBarTranslucent)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.reactnativekeyboardcontroller.views

import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.widget.FitWindowsLinearLayout
Expand All @@ -19,71 +17,39 @@ import com.reactnativekeyboardcontroller.extensions.rootView

private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName

@Suppress("detekt:TooManyFunctions")
@SuppressLint("ViewConstructor")
class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
// props
private var isStatusBarTranslucent = false
private var isNavigationBarTranslucent = false
private var active = false

// internal class members
private var eventView: ReactViewGroup? = null
private var wasMounted = false

// region View lifecycles
override fun onAttachedToWindow() {
super.onAttachedToWindow()

val activity = reactContext.currentActivity
if (activity == null) {
Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
if (!wasMounted) {
// skip logic with callback re-creation if it was first render/mount
wasMounted = true
return
}

Handler(Looper.getMainLooper()).post(this::setupWindowInsets)
WindowCompat.setDecorFitsSystemWindows(
activity.window,
false,
)

eventView = ReactViewGroup(context)
val root = this.getContentView()
root?.addView(eventView)

val callback = KeyboardAnimationCallback(
view = this,
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
context = reactContext,
)

eventView?.let {
ViewCompat.setWindowInsetsAnimationCallback(it, callback)
ViewCompat.setOnApplyWindowInsetsListener(it, callback)
it.requestApplyInsetsWhenAttached()
}
this.setupKeyboardCallbacks()
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()

eventView.removeSelf()
}
// endregion

// region Props setters
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
this.isStatusBarTranslucent = isStatusBarTranslucent
}

fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
this.isNavigationBarTranslucent = isNavigationBarTranslucent
this.removeKeyboardCallbacks()
}
// endregion

// region Private functions/class helpers
private fun getContentView(): FitWindowsLinearLayout? {
return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
androidx.appcompat.R.id.action_bar_root,
)
}

// region State manager helpers
private fun setupWindowInsets() {
val rootView = reactContext.rootView
if (rootView != null) {
Expand All @@ -94,16 +60,20 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
FrameLayout.LayoutParams.MATCH_PARENT,
)

val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent
val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent
params.setMargins(
0,
if (this.isStatusBarTranslucent) {
if (shouldApplyZeroPaddingTop) {
0
} else {
insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
?: 0
(
insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
?: 0
)
},
0,
if (this.isNavigationBarTranslucent) {
if (shouldApplyZeroPaddingBottom) {
0
} else {
insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom
Expand All @@ -113,9 +83,95 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R

content?.layoutParams = params

insets
val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets)

defaultInsets.replaceSystemWindowInsets(
defaultInsets.systemWindowInsetLeft,
if (this.isStatusBarTranslucent) 0 else defaultInsets.systemWindowInsetTop,
defaultInsets.systemWindowInsetRight,
defaultInsets.systemWindowInsetBottom,
)
}
}
}

private fun goToEdgeToEdge(edgeToEdge: Boolean) {
reactContext.currentActivity?.let {
WindowCompat.setDecorFitsSystemWindows(
it.window,
!edgeToEdge,
)
}
}

private fun setupKeyboardCallbacks() {
val activity = reactContext.currentActivity

if (activity != null) {
eventView = ReactViewGroup(context)
val root = this.getContentView()
root?.addView(eventView)

val callback = KeyboardAnimationCallback(
view = this,
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
context = reactContext,
)

eventView?.let {
ViewCompat.setWindowInsetsAnimationCallback(it, callback)
ViewCompat.setOnApplyWindowInsetsListener(it, callback)
it.requestApplyInsetsWhenAttached()
}
} else {
Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
}
}

private fun removeKeyboardCallbacks() {
eventView.removeSelf()
}

private fun getContentView(): FitWindowsLinearLayout? {
return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
androidx.appcompat.R.id.action_bar_root,
)
}
// endregion

// region State managers
private fun enable() {
this.goToEdgeToEdge(true)
this.setupWindowInsets()
this.setupKeyboardCallbacks()
}

private fun disable() {
this.goToEdgeToEdge(false)
this.setupWindowInsets()
this.removeKeyboardCallbacks()
}
// endregion

// region Props setters
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
this.isStatusBarTranslucent = isStatusBarTranslucent
}

fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
this.isNavigationBarTranslucent = isNavigationBarTranslucent
}

fun setActive(active: Boolean) {
this.active = active

if (active) {
this.enable()
} else {
this.disable()
}
}
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class KeyboardControllerViewManager(mReactContext: ReactApplicationContext) : Re
return manager.createViewInstance(reactContext)
}

@ReactProp(name = "enabled")
fun setEnabled(view: EdgeToEdgeReactViewGroup, enabled: Boolean) {
manager.setEnabled(view, enabled)
}

@ReactProp(name = "statusBarTranslucent")
fun setStatusBarTranslucent(view: EdgeToEdgeReactViewGroup, isStatusBarTranslucent: Boolean) {
manager.setStatusBarTranslucent(view, isStatusBarTranslucent)
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/api/hooks/keyboard/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "⌨️ Keyboard",
"position": 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const onInteractiveCode = (
</div>

:::info Event availability
This event is available only on Android >= 11. To receive it you need to use [KeyboardGestureArea](./../keyboard-gesture-area).
This event is available only on Android >= 11. To receive it you need to use [KeyboardGestureArea](./../../keyboard-gesture-area).

On iOS you need to specify `keyboardDismissMode="interactive"` on your `ScrollView`.
:::
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/api/hooks/module/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "📂 Module",
"position": 2
}
53 changes: 53 additions & 0 deletions docs/docs/api/hooks/module/use-keyboard-controller.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
keywords: [react-native-keyboard-controller, useKeyboardController, enabled, disabled, setEnabled]
---

# useKeyboardController

`useKeyboardController` is a hook which gives an access to the state of the `react-native-keyboard-controller` library. It return two values:

- `enabled` - boolean value which indicates whether the library is enabled in app;
- `setEnabled` - function that changes state of `enabled` property.

This hook can be handy in situations when your app is relying on default window resizing behavior on Android. Once the module is enabled - it moves the app in [edge-to-edge](https://developer.android.com/training/gestures/edge-to-edge) and prevents window from being resized (works as iOS). However if you need default Android behavior you can disable this module where needed and make a gradual integration of this library into your application.

:::caution Use it only when you really need it
Nonetheless that you can fallback to default Android behavior I still strongly recommend you not to go with this approach just because you'll loose all attractiveness of smooth animated keyboard transitions and your app will not look as great as it possibly can.

Consider to use [KeyboardAvoidingView](../../components/keyboard-avoiding-view.mdx) which also resize the window, but does it with beautiful animated transitions that makes your interactions with app smooth and pleasant.
:::

## Example

```tsx
import { useKeyboardController } from "react-native-keyboard-controller";

const { enabled, setEnabled } = useKeyboardController();

setEnabled(false);
```

Also have a look on [example](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example) app for more comprehensive usage.

## Using with class component

```tsx
import {
KeyboardController,
KeyboardContext,
AndroidSoftInputModes,
} from "react-native-keyboard-controller";

class KeyboardAnimation extends React.PureComponent {
// 1. use context value
static contextType = KeyboardContext;

componentDidMount() {
// 2. get an access to `enabled` and `setEnabled` props
const { enabled, setEnabled } = this.context;

// 3. disable a module on demand in your app
setEnabled(false);
}
}
```

0 comments on commit ef0f625

Please sign in to comment.