Skip to content

Commit

Permalink
[RNKC-045] - status bar management new API (#30)
Browse files Browse the repository at this point in the history
* [RNKC-045] - fixed a bug with frozen animated values when user manages status bar

* [RNKC-045] - ktlint fixes

* [RNKC-045] - attempt to fix CI

* [RNKC-045] - added example app for StatusBar

* [RNKC-045] - resolve last TODOs
  • Loading branch information
kirillzyusko committed Jun 9, 2022
1 parent 7c8cb84 commit 684a110
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.facebook.react.uimanager.ViewManager

class KeyboardControllerPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(KeyboardControllerModule(reactContext))
return listOf(KeyboardControllerModule(reactContext), StatusBarManagerCompatModule(reactContext))
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class KeyboardControllerViewManager(reactContext: ReactApplicationContext) : Rea

override fun createViewInstance(reactContext: ThemedReactContext): ReactViewGroup {
val view = EdgeToEdgeReactViewGroup(reactContext)
val window = mReactContext.currentActivity!!.window
val decorView = window.decorView

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val content =
Expand All @@ -35,7 +37,7 @@ class KeyboardControllerViewManager(reactContext: ReactApplicationContext) : Rea
}

ViewCompat.setWindowInsetsAnimationCallback(
reactContext.currentActivity!!.window!!.decorView,
decorView,
TranslateDeferringInsetsAnimationCallback(
view = view,
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.reactnativekeyboardcontroller

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.UiThreadUtil

class StatusBarManagerCompatModule(private val mReactContext: ReactApplicationContext) : ReactContextBaseJavaModule(mReactContext) {
private var controller: WindowInsetsControllerCompat? = null

override fun getName(): String = "StatusBarManagerCompat"

@ReactMethod
private fun setHidden(hidden: Boolean) {
UiThreadUtil.runOnUiThread {
if (hidden) {
getController()?.hide(WindowInsetsCompat.Type.statusBars())
} else {
getController()?.show(WindowInsetsCompat.Type.statusBars())
}
}
}

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@ReactMethod
private fun setColor(color: Int, animated: Boolean) {
UiThreadUtil.runOnUiThread {
val window = mReactContext.currentActivity!!.window

if (animated) {
val curColor: Int = window.statusBarColor
val colorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), curColor, color)
colorAnimation.addUpdateListener { animator ->
window.statusBarColor = animator.animatedValue as Int
}
colorAnimation.setDuration(300).startDelay = 0
colorAnimation.start()
} else {
window.statusBarColor = color
}
}
}

@ReactMethod
private fun setTranslucent(translucent: Boolean) {
// the status bar is translucent by default (once you wrapped App in Provider,
// and EdgeToEdgeReactViewGroup has been mounted and called
// `setDecorFitsSystemWindows(window, false)`. By default this library applies default padding
// which equal to StatusBar height, so it will have a default RN app behavior. Though once you
// need to set StatusBar as translucent, you will need to use `statusBarTranslucent` prop on
// `KeyboardProvider` (it will preventing of applying additional padding, and status bar will be
// translucent. Though it's important to note, that this value is not reactive (i. e. if you change
// `statusBarTranslucent` in runtime it will not have any effect. Just theoretically I could make
// it reactive, but I know, that most of apps or don't use StatusBar translucency at all or they are
// specifying it for entire app, so I don't see a lot of sense to make it reactive as of now. If your
// app requires to dynamically manage it - just shoot an issue and I will try to add a support fot that.
}

@ReactMethod
private fun setStyle(style: String) {
UiThreadUtil.runOnUiThread {
getController()?.isAppearanceLightStatusBars = style == "dark-content"
}
}

private fun getController(): WindowInsetsControllerCompat? {
if (this.controller == null) {
val window = mReactContext.currentActivity!!.window

this.controller = WindowInsetsControllerCompat(window, window.decorView)
}

return this.controller
}
}
5 changes: 5 additions & 0 deletions example/android/app/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>

<!-- Optional: set to transparent if your app is drawing behind the status bar. -->
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>

</resources>
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import RootStack from './navigation/RootStack';
export default function App() {
return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<KeyboardProvider>
<KeyboardProvider statusBarTranslucent>
<NavigationContainer>
<RootStack />
</NavigationContainer>
Expand Down
61 changes: 61 additions & 0 deletions example/src/components/KeyboardAnimation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { Animated, TextInput, View } from 'react-native';
import {
useKeyboardAnimation,
useKeyboardAnimationReplica,
} from 'react-native-keyboard-controller';
import styles from './styles';

export default function KeyboardAnimation() {
const { height, progress } = useKeyboardAnimation();
const { height: heightReplica } = useKeyboardAnimationReplica();

return (
<View style={styles.container}>
<View style={styles.row}>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'red',
borderRadius: 25,
transform: [{ translateY: height }],
}}
/>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'green',
borderRadius: 25,
transform: [
{
translateX: progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
}),
},
],
}}
/>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'blue',
borderRadius: 25,
transform: [{ translateY: heightReplica }],
}}
/>
</View>
<TextInput
style={{
width: 200,
marginTop: 50,
height: 50,
backgroundColor: 'yellow',
}}
/>
</View>
);
}
17 changes: 17 additions & 0 deletions example/src/components/KeyboardAnimation/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StyleSheet } from 'react-native';

export default StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 60,
height: 60,
marginVertical: 20,
},
row: {
flexDirection: 'row',
},
});
1 change: 1 addition & 0 deletions example/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum ScreenNames {
REANIMATED_CHAT = 'REANIMATED_CHAT',
EVENTS = 'EVENTS',
AWARE_SCROLL_VIEW = 'AWARE_SCROLL_VIEW',
STATUS_BAR = 'STATUS_BAR',
EXAMPLES_STACK = 'EXAMPLES_STACK',
EXAMPLES = 'EXAMPLES',
}
11 changes: 11 additions & 0 deletions example/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import KeyboardAnimation from '../../screens/Examples/KeyboardAnimation';
import ReanimatedChat from '../../screens/Examples/ReanimatedChat';
import Events from '../../screens/Examples/Events';
import AwareScrollView from '../../screens/Examples/AwareScrollView';
import StatusBar from '../../screens/Examples/StatusBar';

type ExamplesStackParamList = {
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
[ScreenNames.REANIMATED_CHAT]: undefined;
[ScreenNames.EVENTS]: undefined;
[ScreenNames.AWARE_SCROLL_VIEW]: undefined;
[ScreenNames.STATUS_BAR]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand All @@ -30,6 +32,10 @@ const options = {
[ScreenNames.AWARE_SCROLL_VIEW]: {
title: 'Aware scroll view',
},
[ScreenNames.STATUS_BAR]: {
headerShown: false,
title: 'Status bar manipulation',
},
};

const ExamplesStack = () => (
Expand All @@ -54,6 +60,11 @@ const ExamplesStack = () => (
component={AwareScrollView}
options={options[ScreenNames.AWARE_SCROLL_VIEW]}
/>
<Stack.Screen
name={ScreenNames.STATUS_BAR}
component={StatusBar}
options={options[ScreenNames.STATUS_BAR]}
/>
</Stack.Navigator>
);

Expand Down
5 changes: 1 addition & 4 deletions example/src/screens/Examples/AwareScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import Reanimated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';

function randomColor() {
return '#' + Math.random().toString(16).slice(-6);
}
import { randomColor } from '../../../utils';

const screenHeight = Dimensions.get('window').height;

Expand Down
60 changes: 3 additions & 57 deletions example/src/screens/Examples/KeyboardAnimation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,7 @@
import React from 'react';
import { Animated, TextInput, View } from 'react-native';
import {
useKeyboardAnimation,
useKeyboardAnimationReplica,
} from 'react-native-keyboard-controller';
import styles from './styles';

export default function KeyboardAnimation() {
const { height, progress } = useKeyboardAnimation();
const { height: heightReplica } = useKeyboardAnimationReplica();
import KeyboardAnimationTemplate from '../../../components/KeyboardAnimation';

return (
<View style={styles.container}>
<View style={styles.row}>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'red',
borderRadius: 25,
transform: [{ translateY: height }],
}}
/>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'green',
borderRadius: 25,
transform: [
{
translateX: progress.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
}),
},
],
}}
/>
<Animated.View
style={{
width: 50,
height: 50,
backgroundColor: 'blue',
borderRadius: 25,
transform: [{ translateY: heightReplica }],
}}
/>
</View>
<TextInput
style={{
width: 200,
marginTop: 50,
height: 50,
backgroundColor: 'yellow',
}}
/>
</View>
);
export default function KeyboardAnimation() {
return <KeyboardAnimationTemplate />;
}
5 changes: 5 additions & 0 deletions example/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ export const examples: Example[] = [
info: ScreenNames.AWARE_SCROLL_VIEW,
icons: '🤓',
},
{
title: 'Status Bar',
info: ScreenNames.STATUS_BAR,
icons: '🔋',
},
];
45 changes: 45 additions & 0 deletions example/src/screens/Examples/StatusBar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { View, StatusBar, StatusBarStyle, Button } from 'react-native';

import KeyboardAnimationTemplate from '../../../components/KeyboardAnimation';
import { randomColor } from '../../../utils';

export default function StatusBarManipulation() {
const [color, setColor] = useState('#00FF0000');
const [barStyle, setBarStyle] = useState<StatusBarStyle>('light-content');
const [hidden, setHidden] = useState(false);
const [animated, setAnimated] = useState(true);

return (
<View style={{ flex: 1, backgroundColor: 'pink' }}>
<StatusBar
backgroundColor={color}
barStyle={barStyle}
hidden={hidden}
animated={animated}
translucent
/>
<KeyboardAnimationTemplate />
<Button
title={`Set ${hidden ? 'shown' : 'hidden'}`}
onPress={() => setHidden(!hidden)}
/>
<Button
title="Update color"
onPress={() => setColor(`${randomColor()}`)}
/>
<Button
title={`Set ${!animated ? '' : 'not'} animated`}
onPress={() => setAnimated(!animated)}
/>
<Button
title={`Change ${barStyle}`}
onPress={() =>
setBarStyle(
barStyle === 'light-content' ? 'dark-content' : 'light-content'
)
}
/>
</View>
);
}
3 changes: 3 additions & 0 deletions example/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function randomColor() {
return '#' + Math.random().toString(16).slice(-6);
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './monkey-patch';

export * from './native';
export * from './animated';
export * from './replicas';
Loading

0 comments on commit 684a110

Please sign in to comment.