Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animated.modulo can return negative number (Where is Animated.min/Animated.max?) #37447

Open
Jackman3005 opened this issue May 16, 2023 · 6 comments

Comments

@Jackman3005
Copy link

Jackman3005 commented May 16, 2023

Description

Hello, I have a situation where I want to do Math.max between two different animated values. I have a chat bubble that floats on the bottom of the screen. Some screens it needs to float higher to be out of the way of nav bars, in this case, we animate it up, then back down again when navigating away.

We also need to animate the chat bubble up above the keyboard when the keyboard is opened. We started with the naive implementation:

const animatedValue = Animated.add(screenSpecificOffsetAnim, keyboardHeightAnim)

But this is no good, because when we are on the screens with a nav bar and the keyboard is open, the chat bubble is too high above the keyboard. Ideally I want this:

const animatedValue = Animated.max(screenSpecificOffsetAnim, keyboardHeightAnim)

But it does not seem to exist... So I decided to try to build it myself. This answer on stack overflow shows how to rebuild Math.max without any logical operators. In shorthand the formula looks like this: max = ( (a+b) + |a-b| ) / 2.

After reviewing all the Animated.* operators I'm given, it looked like I was stuck because there is no absolute value operator. BUT WAIT, there's more! I noticed the documentation on Animated.modulo states the following:

Creates a new Animated value that is the (non-negative) modulo of the provided Animated value
🤔 the non-negative modulo? Well... I think I could make that behave like Math.abs. So I tried this crazy mess:

const TWO = new Animated.Value(2);
export function AnimatedMaximum(
  a: Animated.Value,
  b: Animated.Value
): Animated.AnimatedInterpolation<number> {
  return Animated.divide(
    Animated.add(
      Animated.add(a, b),
      Animated.modulo(Animated.subtract(a, b), Infinity)
    ),
    TWO
  );
}

Which does produce the desired results when a > b. But when a < b I get strange behavior and my animated value disappears completely. So I thought maybe Infinity was too aggressive. Must be, because when using 10000 instead, I found out that I get a large positive number when a < b which appeared to simply be 10000 + (a -b). In an attempt to get around this situation I tried using a negative modulo.

Animated.modulo(
    Animated.subtract(Animated.subtract(a, b), 10000),
    -10000
)

Which for values: a: 0 and b: 336 the result is -336

Which goes against the documentation above.

In general this has been really rather confusing because Animated.modulo does not perform a traditional % modulo operation and does not seem to even hold to the documented exception of being always positive.

React Native Version

0.70.5

Output of npx react-native info

System:
    OS: macOS 13.3.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 72.30 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 16.14.0 - ~/.nvm/versions/node/v16.14.0/bin/node
    Yarn: 1.22.17 - /opt/homebrew/bin/yarn
    npm: 9.6.0 - ~/.nvm/versions/node/v16.14.0/bin/npm
    Watchman: Not Found
  Managers:
    CocoaPods: Not Found
  SDKs:
    iOS SDK:
      Platforms: DriverKit 22.4, iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4
    Android SDK: Not Found
  IDEs:
    Android Studio: 2021.2 AI-212.5712.43.2112.8512546
    Xcode: 14.3/14E222b - /usr/bin/xcodebuild
  Languages:
    Java: 18.0.2 - /Users/jack/.sdkman/candidates/java/current/bin/javac
  npmPackages:
    @react-native-community/cli: Not Found
    react: 18.1.0 => 18.1.0
    react-native: 0.70.5 => 0.70.5
    react-native-macos: Not Found
  npmGlobalPackages:
    *react-native*: Not Found

Steps to reproduce

const a = new Animated.value(0);
const b = new Animated.value(336);
Animated.modulo(
    Animated.subtract(Animated.subtract(a, b), 10000),
    -10000
)

Snack, code example, screenshot, or link to a repository

https://snack.expo.dev/@jcoy/ashamed-truffle

@github-actions
Copy link

⚠️ Newer Version of React Native is Available!
ℹ️ You are on a supported minor version, but it looks like there's a newer patch available - 0.70.9. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.

@Jackman3005
Copy link
Author

⚠️ Newer Version of React Native is Available!
ℹ️ You are on a supported minor version, but it looks like there's a newer patch available - 0.70.9. Please upgrade to the highest patch for your minor or latest and verify if the issue persists (alternatively, create a new project and repro the issue in it). If it does not repro, please let us know so we can close out this issue. This helps us ensure we are looking at issues that still exist in the most recent releases.

The reproduction I linked is using the latest Expo.

@Jackman3005
Copy link
Author

Jackman3005 commented May 16, 2023

Update (functional homegrown Animated.max)

I have found an alternative to using Animated.modulo to accomplish Math.abs functionality. Here is my updated and working Animated.max function:

import { Animated } from "react-native";

const TWO = new Animated.Value(2);
export function AnimatedMaximum(
  a: Animated.Value,
  b: Animated.Value
): Animated.AnimatedInterpolation<number> {
  return Animated.divide(
    Animated.add(
      Animated.add(a, b),
      Animated.subtract(a, b).interpolate({
        inputRange: [-1, 0, 1],
        outputRange: [1, 0, 1],
      })
    ),
    TWO
  );
}

This works because .interpolate will extrapolate beyond the input range you provide. (Note, you can prevent that by adding extrapolate, extrapolateRight, or extrapolateLeft to the interpolate config with the value "clamp" if needed for another situation.)

@Jackman3005
Copy link
Author

Jackman3005 commented May 16, 2023

Based on this success I imagine there are a few more Math-like helper functions that could be derived such as:

Note: These have not been tested, but I imagine they will work just fine based on the success I had with AnimatedMaximum. If you use them and find them valuable, please report back and let me know!

Animated.min

import { Animated } from "react-native";

const TWO = new Animated.Value(2);

export function AnimatedMinimum(
  a: Animated.Value,
  b: Animated.Value
): Animated.AnimatedInterpolation<number> {
  return Animated.divide(
    Animated.subtract(
      Animated.add(a, b),
      Animated.subtract(a, b).interpolate({
        inputRange: [-1, 0, 1],
        outputRange: [1, 0, 1],
      })
    ),
    TWO
  );
}

Animated.abs

import { Animated } from "react-native";

export function AnimatedAbsoluteValue(
  val: Animated.Value
): Animated.AnimatedInterpolation<number> {
  return val.interpolate({
    inputRange: [-1, 0, 1],
    outputRange: [1, 0, 1],
  });
}

@Jackman3005 Jackman3005 changed the title Animated.modulo can return negative number (Where is Animated.min/Animated.max?) Animated.modulo can return negative number (Where is Animated.min/Animated.max?) Jul 12, 2023
Copy link

github-actions bot commented Jan 9, 2024

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Jan 9, 2024
@Jackman3005
Copy link
Author

Maybe this comment could be updated/improved to better explain how it works and at least not claim that the modulo result is non-negative.

/**
  * Creates a new Animated value that is the (non-negative) modulo of the
  * provided Animated value
  */

@github-actions github-actions bot removed the Stale There has been a lack of activity on this issue and it may be closed soon. label Jan 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant