diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 7e1870e8b30b..2eab8de1eb7b 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
+
-
+
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 808babdec779..761117105f75 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -24,12 +24,11 @@ import CONST from '../../CONST';
import * as Expensicons from '../Icon/Expensicons';
import iouReportPropTypes from '../../pages/iouReportPropTypes';
import * as CurrencyUtils from '../../libs/CurrencyUtils';
-import EmptyStateBackgroundImage from '../../../assets/images/empty-state_background-fade.png';
import useLocalize from '../../hooks/useLocalize';
+import AnimatedEmptyStateBackground from '../../pages/home/report/AnimatedEmptyStateBackground';
import * as ReceiptUtils from '../../libs/ReceiptUtils';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import transactionPropTypes from '../transactionPropTypes';
-import Image from '../Image';
import Text from '../Text';
import Switch from '../Switch';
import ReportActionItemImage from './ReportActionItemImage';
@@ -138,11 +137,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
return (
-
+
{hasReceipt && (
diff --git a/src/libs/NumberUtils.ts b/src/libs/NumberUtils.ts
index 0d2b4135c5b6..477ac8464d57 100644
--- a/src/libs/NumberUtils.ts
+++ b/src/libs/NumberUtils.ts
@@ -47,6 +47,18 @@ function generateHexadecimalValue(num: number): string {
return result.join('').toUpperCase();
}
+/**
+ * Clamp a number in a range.
+ * This is a worklet so it should be used only from UI thread.
+
+ * @returns clamped value between min and max
+ */
+function clampWorklet(num: number, min: number, max: number): number {
+ 'worklet';
+
+ return Math.min(Math.max(num, min), max);
+}
+
/**
* Generates a random integer between a and b
* It's and equivalent of _.random(a, b)
@@ -59,4 +71,4 @@ function generateRandomInt(a: number, b: number): number {
return Math.floor(lower + Math.random() * (upper - lower + 1));
}
-export {rand64, generateHexadecimalValue, generateRandomInt};
+export {rand64, generateHexadecimalValue, generateRandomInt, clampWorklet};
diff --git a/src/pages/home/report/AnimatedEmptyStateBackground.js b/src/pages/home/report/AnimatedEmptyStateBackground.js
new file mode 100644
index 000000000000..ecc37a2b785f
--- /dev/null
+++ b/src/pages/home/report/AnimatedEmptyStateBackground.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import Animated, {SensorType, useAnimatedSensor, useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated';
+import useWindowDimensions from '../../../hooks/useWindowDimensions';
+import * as NumberUtils from '../../../libs/NumberUtils';
+import EmptyStateBackgroundImage from '../../../../assets/images/empty-state_background-fade.png';
+import * as StyleUtils from '../../../styles/StyleUtils';
+
+const IMAGE_OFFSET_Y = 75;
+
+function AnimatedEmptyStateBackground() {
+ const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+ const IMAGE_OFFSET_X = windowWidth / 2;
+
+ // Get data from phone rotation sensor and prep other variables for animation
+ const animatedSensor = useAnimatedSensor(SensorType.GYROSCOPE);
+ const xOffset = useSharedValue(0);
+ const yOffset = useSharedValue(0);
+
+ // Apply data to create style object
+ const animatedStyles = useAnimatedStyle(() => {
+ if (!isSmallScreenWidth) {
+ return {};
+ }
+ /*
+ * We use x and y gyroscope velocity and add it to position offset to move background based on device movements.
+ * Position the phone was in while entering the screen is the initial position for background image.
+ */
+ const {x, y} = animatedSensor.sensor.value;
+ // The x vs y here seems wrong but is the way to make it feel right to the user
+ xOffset.value = NumberUtils.clampWorklet(xOffset.value + y, -IMAGE_OFFSET_X, IMAGE_OFFSET_X);
+ yOffset.value = NumberUtils.clampWorklet(yOffset.value - x, -IMAGE_OFFSET_Y, IMAGE_OFFSET_Y);
+ return {
+ transform: [{translateX: withSpring(-IMAGE_OFFSET_X - xOffset.value)}, {translateY: withSpring(yOffset.value)}],
+ };
+ });
+
+ return (
+
+ );
+}
+
+AnimatedEmptyStateBackground.displayName = 'AnimatedEmptyStateBackground';
+export default AnimatedEmptyStateBackground;
diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js
index 4ae4fe81e4ac..a5df1c37e769 100644
--- a/src/pages/home/report/ReportActionItemCreated.js
+++ b/src/pages/home/report/ReportActionItemCreated.js
@@ -1,5 +1,5 @@
import React, {memo} from 'react';
-import {View, Image} from 'react-native';
+import {View} from 'react-native';
import lodashGet from 'lodash/get';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
@@ -11,7 +11,6 @@ import styles from '../../../styles/styles';
import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import * as Report from '../../../libs/actions/Report';
import reportPropTypes from '../../reportPropTypes';
-import EmptyStateBackgroundImage from '../../../../assets/images/empty-state_background-fade.png';
import * as StyleUtils from '../../../styles/StyleUtils';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import compose from '../../../libs/compose';
@@ -19,6 +18,7 @@ import withLocalize from '../../../components/withLocalize';
import PressableWithoutFeedback from '../../../components/Pressable/PressableWithoutFeedback';
import MultipleAvatars from '../../../components/MultipleAvatars';
import CONST from '../../../CONST';
+import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
const propTypes = {
/** The id of the report */
@@ -64,11 +64,7 @@ function ReportActionItemCreated(props) {
needsOffscreenAlphaCompositing
>
-
+