Skip to content

Commit

Permalink
fix: android rotate (#550)
Browse files Browse the repository at this point in the history
## 📜 Description

Fixed incorrect paddings when android rotation happens.

## 💡 Motivation and Context

The fix contains from 2 main points:
- the bottom padding is getting kind of frozen because we don't get
`ApplyWindowInsets` when rotation happened, so at the bottom of the
screen we have our old padding which is equal to navBar height. To fix
that we need to `requestApplyInsets`. But simple request is not enough
for some reasons - our listener will not be fired, so we need to
reattach listenere and request insets again, after that we get:

<img width="712" alt="Screenshot 2024-08-19 at 11 07 00"
src="https://github.com/user-attachments/assets/47b68bea-4a12-471d-b509-05ae7b37e47f">

- to fix empty spacing from left side and resolve all conflicts with
`react-native-safe-area-context` we need to apply correct left and right
paddings (depends on rotation angle). We need to take navBarInsets and
apply insets accordingly. After that we get properly resized view:

<img width="711" alt="Screenshot 2024-08-19 at 11 01 39"
src="https://github.com/user-attachments/assets/acedcc2e-69e1-4231-9240-b2527e1bedf1">

To be sure it doesn't get broken in future I added e2e tests. On iOS I
had empty spaces and I fixed them by stretching `background` element to
all edges. Such fix makes it more complicated to notice a regression for
human on Android (because we will not have empty spaces in tab bar), but
all content will be shifted so `e2e` will spot a regression.

> using `background` element helps us to get cross-platform UI 🙃 

Closes
#547

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

- added e2e tests with rotation;

### JS

- added `BottomTabBar` example;

### Android

- override `onConfigurationChanged` method;
- create `reApplyWindowInsets` method;
- use `reApplyWindowInsets` in `onConfigurationChanged` and
`forceStatusBarTranslucent`;
- apply correct left/right insets based on `navBar` insets.

## 🤔 How Has This Been Tested?

Tested manually on Pixel 2 API 30.

## 📸 Screenshots (if appropriate):

|Before|After|
|-------|-----|
|<img width="711" alt="Screenshot 2024-08-19 at 11 03 20"
src="https://github.com/user-attachments/assets/ec3a2068-d5cd-4258-a9d6-8d80f319a28c">|<img
width="711" alt="Screenshot 2024-08-19 at 11 01 39"
src="https://github.com/user-attachments/assets/acedcc2e-69e1-4231-9240-b2527e1bedf1">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko authored Aug 19, 2024
1 parent 2c460d3 commit 0d7ef43
Show file tree
Hide file tree
Showing 35 changed files with 238 additions and 11 deletions.
1 change: 1 addition & 0 deletions FabricExample/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export enum ScreenNames {
FOCUSED_INPUT_HANDLERS = "FOCUSED_INPUT_HANDLERS",
TOOLBAR = "TOOLBAR",
MODAL = "MODAL",
BOTTOM_TAB_BAR = "BOTTOM_TAB_BAR",
}
69 changes: 69 additions & 0 deletions FabricExample/src/navigation/BottomTabBar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";
import { StyleSheet, Text, View } from "react-native";

import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation";

const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
const SettingsStack = createNativeStackNavigator();

const HomeStackScreens = () => {
return (
<HomeStack.Navigator>
<HomeStack.Screen component={KeyboardAnimation} name="Home" />
</HomeStack.Navigator>
);
};

const SettingsStackScreens = () => {
return (
<SettingsStack.Navigator>
<SettingsStack.Screen component={KeyboardAnimation} name="Settings" />
</SettingsStack.Navigator>
);
};

export default function BottomTabBar() {
return (
<Tab.Navigator
detachInactiveScreens={true}
screenOptions={({ route }) => ({
tabBarBackground: () => <View style={styles.tabBarBackground} />,
tabBarIcon: () =>
route.name === "HomeStack" ? (
<Text style={styles.icon}>🏠</Text>
) : (
<Text style={styles.icon}>⚙️</Text>
),
tabBarLabel: () =>
route.name === "HomeStack" ? (
<Text style={styles.label}>Home</Text>
) : (
<Text style={styles.label}>Settings</Text>
),
headerShown: false,
})}
>
<Tab.Screen component={HomeStackScreens} name="HomeStack" />
<Tab.Screen component={SettingsStackScreens} name="SettingsStack" />
</Tab.Navigator>
);
}

const styles = StyleSheet.create({
icon: {
color: "white",
fontSize: 20,
},
label: {
color: "white",
marginHorizontal: 20,
},
tabBarBackground: {
backgroundColor: "#2c2c2c",
width: "100%",
height: "100%",
},
});
10 changes: 10 additions & 0 deletions FabricExample/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ReanimatedChat from "../../screens/Examples/ReanimatedChat";
import ReanimatedChatFlatList from "../../screens/Examples/ReanimatedChatFlatList";
import StatusBar from "../../screens/Examples/StatusBar";
import ToolbarExample from "../../screens/Examples/Toolbar";
import BottomTabBar from "../BottomTabBar";
import NativeStack from "../NestedStack";

export type ExamplesStackParamList = {
Expand All @@ -40,6 +41,7 @@ export type ExamplesStackParamList = {
[ScreenNames.FOCUSED_INPUT_HANDLERS]: undefined;
[ScreenNames.TOOLBAR]: undefined;
[ScreenNames.MODAL]: undefined;
[ScreenNames.BOTTOM_TAB_BAR]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -100,6 +102,9 @@ const options = {
[ScreenNames.MODAL]: {
title: "Modal",
},
[ScreenNames.BOTTOM_TAB_BAR]: {
headerShown: false,
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -194,6 +199,11 @@ const ExamplesStack = () => (
name={ScreenNames.MODAL}
options={options[ScreenNames.MODAL]}
/>
<Stack.Screen
component={BottomTabBar}
name={ScreenNames.BOTTOM_TAB_BAR}
options={options[ScreenNames.BOTTOM_TAB_BAR]}
/>
</Stack.Navigator>
);

Expand Down
6 changes: 6 additions & 0 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,10 @@ export const examples: Example[] = [
info: ScreenNames.MODAL,
icons: "🌎",
},
{
title: "Bottom tab bar",
testID: "bottom_tab_bar",
info: ScreenNames.BOTTOM_TAB_BAR,
icons: "🧱",
},
];
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.reactnativekeyboardcontroller.views

import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Handler
import android.os.Looper
import android.util.Log
Expand Down Expand Up @@ -68,6 +69,10 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R

this.removeKeyboardCallbacks()
}

override fun onConfigurationChanged(newConfig: Configuration?) {
this.reApplyWindowInsets()
}
// endregion

// region State manager helpers
Expand All @@ -83,25 +88,23 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R

val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent
val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())

params.setMargins(
0,
navBarInsets.left,
if (shouldApplyZeroPaddingTop) {
0
} else {
(
insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
?: 0
)
systemBarInsets.top
},
0,
navBarInsets.right,
if (shouldApplyZeroPaddingBottom) {
0
} else {
insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom
?: 0
navBarInsets.bottom
},
)

content?.layoutParams = params

val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets)
Expand Down Expand Up @@ -164,6 +167,11 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
// for more details
Handler(Looper.getMainLooper()).post { view.removeSelf() }
}

private fun reApplyWindowInsets() {
this.setupWindowInsets()
this.requestApplyInsetsWhenAttached()
}
// endregion

// region State managers
Expand Down Expand Up @@ -206,8 +214,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
fun forceStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
if (active && this.isStatusBarTranslucent != isStatusBarTranslucent) {
this.isStatusBarTranslucent = isStatusBarTranslucent
this.setupWindowInsets()
this.requestApplyInsetsWhenAttached()
this.reApplyWindowInsets()
}
}
// endregion
Expand Down
48 changes: 48 additions & 0 deletions e2e/kit/010-bottom-tab-bar-rotation.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expectBitmapsToBeEqual } from "./asserts";
import {
closeKeyboard,
scrollDownUntilElementIsVisible,
waitAndTap,
waitForExpect,
} from "./helpers";

describe("Bottom tab bar", () => {
it("should navigate to `Bottom tab bar` screen", async () => {
await scrollDownUntilElementIsVisible("main_scroll_view", "bottom_tab_bar");
await waitAndTap("bottom_tab_bar");
});

it("should have expected state in portrait mode", async () => {
await waitForExpect(async () => {
await expectBitmapsToBeEqual("BottomTabBarPortrait", 0.25);
});
});

it("should have expected state in landscape mode", async () => {
await device.setOrientation("landscape");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("BottomTabBarLandscape");
});
});

it("should have expected state in portrait mode after rotation", async () => {
await device.setOrientation("portrait");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("BottomTabBarPortraitAgain");
});
});

it("should have expected state when keyboard open", async () => {
await waitAndTap("keyboard_animation_text_input");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("BottomTabBarKeyboardIsShown");
});
});

it("should have expected state when emoji keyboard closed", async () => {
await closeKeyboard("keyboard_animation_text_input");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("BottomTabBarKeyboardIsHidden");
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions example/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export enum ScreenNames {
FOCUSED_INPUT_HANDLERS = "FOCUSED_INPUT_HANDLERS",
TOOLBAR = "TOOLBAR",
MODAL = "MODAL",
BOTTOM_TAB_BAR = "BOTTOM_TAB_BAR",
}
69 changes: 69 additions & 0 deletions example/src/navigation/BottomTabBar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";
import { StyleSheet, Text, View } from "react-native";

import KeyboardAnimation from "../../screens/Examples/KeyboardAnimation";

const Tab = createBottomTabNavigator();
const HomeStack = createNativeStackNavigator();
const SettingsStack = createNativeStackNavigator();

const HomeStackScreens = () => {
return (
<HomeStack.Navigator>
<HomeStack.Screen component={KeyboardAnimation} name="Home" />
</HomeStack.Navigator>
);
};

const SettingsStackScreens = () => {
return (
<SettingsStack.Navigator>
<SettingsStack.Screen component={KeyboardAnimation} name="Settings" />
</SettingsStack.Navigator>
);
};

export default function BottomTabBar() {
return (
<Tab.Navigator
detachInactiveScreens={true}
screenOptions={({ route }) => ({
tabBarBackground: () => <View style={styles.tabBarBackground} />,
tabBarIcon: () =>
route.name === "HomeStack" ? (
<Text style={styles.icon}>🏠</Text>
) : (
<Text style={styles.icon}>⚙️</Text>
),
tabBarLabel: () =>
route.name === "HomeStack" ? (
<Text style={styles.label}>Home</Text>
) : (
<Text style={styles.label}>Settings</Text>
),
headerShown: false,
})}
>
<Tab.Screen component={HomeStackScreens} name="HomeStack" />
<Tab.Screen component={SettingsStackScreens} name="SettingsStack" />
</Tab.Navigator>
);
}

const styles = StyleSheet.create({
icon: {
color: "white",
fontSize: 20,
},
label: {
color: "white",
marginHorizontal: 20,
},
tabBarBackground: {
backgroundColor: "#2c2c2c",
width: "100%",
height: "100%",
},
});
10 changes: 10 additions & 0 deletions example/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ReanimatedChat from "../../screens/Examples/ReanimatedChat";
import ReanimatedChatFlatList from "../../screens/Examples/ReanimatedChatFlatList";
import StatusBar from "../../screens/Examples/StatusBar";
import ToolbarExample from "../../screens/Examples/Toolbar";
import BottomTabBar from "../BottomTabBar";
import NativeStack from "../NestedStack";

export type ExamplesStackParamList = {
Expand All @@ -40,6 +41,7 @@ export type ExamplesStackParamList = {
[ScreenNames.FOCUSED_INPUT_HANDLERS]: undefined;
[ScreenNames.TOOLBAR]: undefined;
[ScreenNames.MODAL]: undefined;
[ScreenNames.BOTTOM_TAB_BAR]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -100,6 +102,9 @@ const options = {
[ScreenNames.MODAL]: {
title: "Modal",
},
[ScreenNames.BOTTOM_TAB_BAR]: {
headerShown: false,
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -194,6 +199,11 @@ const ExamplesStack = () => (
name={ScreenNames.MODAL}
options={options[ScreenNames.MODAL]}
/>
<Stack.Screen
component={BottomTabBar}
name={ScreenNames.BOTTOM_TAB_BAR}
options={options[ScreenNames.BOTTOM_TAB_BAR]}
/>
</Stack.Navigator>
);

Expand Down
6 changes: 6 additions & 0 deletions example/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,10 @@ export const examples: Example[] = [
info: ScreenNames.MODAL,
icons: "🌎",
},
{
title: "Bottom tab bar",
testID: "bottom_tab_bar",
info: ScreenNames.BOTTOM_TAB_BAR,
icons: "🧱",
},
];

0 comments on commit 0d7ef43

Please sign in to comment.