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 > - +