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

Better error when passing easing from 'react-native' instead of 'reanimated' #5639

Merged
merged 36 commits into from
Mar 22, 2024

Conversation

Latropos
Copy link
Contributor

@Latropos Latropos commented Feb 5, 2024

Summary

Function withTiming should throw an error when easing is not a worklet (or a bound function run from UI thread).
This is a common bug, because it happens when user is mixing imports from reanimated and animated.

Test plan

Before After
The actual names of all easing functions from react-native are all "fun"
image image
timing animation (example from the table) - code
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  // Easing, <- this should be the correct import
} from 'react-native-reanimated';
import { View, Button, StyleSheet, Easing } from 'react-native';
import React from 'react';

export default function AnimatedStyleUpdateExample() {
  const randomWidth = useSharedValue(10);

  const style = useAnimatedStyle(() => {
    return {
      width: withTiming(randomWidth.value, {
        easing: Easing.linear,
      }),
    };
  });

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, style]} />
      <Button
        title="toggle"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
  },
  box: {
    width: 100,
    height: 80,
    backgroundColor: 'black',
    margin: 30,
  },
});
Keyframe animation - code
import Animated, { Keyframe } from 'react-native-reanimated';
import { Button, Easing, View, StyleSheet } from 'react-native';
import React, { useState } from 'react';

export default function KeyframeAnimation() {
  const [show, setShow] = useState(false);
  const enteringAnimation = new Keyframe({
    from: {
      originX: 50,
      transform: [{ rotate: '45deg' }, { scale: 0.5 }],
    },
    30: {
      transform: [{ rotate: '-90deg' }, { scale: 2 }],
    },
    50: {
      originX: 70,
    },
    100: {
      originX: 0,
      transform: [{ rotate: '0deg' }, { scale: 1 }],
      easing: Easing.quad,
    },
  })
    .duration(2000)
    .withCallback((finished: boolean) => {
      'worklet';
      if (finished) {
        console.log('callback');
      }
    });
  const exitingAnimation = new Keyframe({
    0: {
      opacity: 1,
      originX: 0,
    },
    30: {
      originX: -50,
      easing: Easing.exp,
    },
    to: {
      opacity: 0,
      originX: 500,
    },
  }).duration(2000);
  return (
    <View style={styles.columnReverse}>
      <Button
        title="animate"
        onPress={() => {
          setShow((last) => !last);
        }}
      />
      <View style={styles.blueBoxContainer}>
        {show && (
          <Animated.View
            entering={enteringAnimation}
            exiting={exitingAnimation}
            style={styles.blueBox}
          />
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  columnReverse: { flexDirection: 'column-reverse' },
  blueBoxContainer: {
    height: 400,
    alignItems: 'center',
    justifyContent: 'center',
  },
  blueBox: {
    height: 100,
    width: 200,
    backgroundColor: 'blue',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
Curved transition animation - code
import Animated, {
  BounceOut,
  CurvedTransition,
  LightSpeedInRight,
} from 'react-native-reanimated';
import {
  Image,
  LayoutChangeEvent,
  Text,
  View,
  StyleSheet,
  Easing,
} from 'react-native';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ScrollView, TapGestureHandler } from 'react-native-gesture-handler';

const AnimatedImage = Animated.createAnimatedComponent(Image);
type Props = {
  columns: number;
  pokemons: number;
};
function getRandomColor() {
  const randomColor = Math.floor(Math.random() * 16777215).toString(16);
  return randomColor;
}
type PokemonData = {
  ratio: number;
  address: string;
  key: number;
  color: string;
};

export function WaterfallGrid({ columns = 3, pokemons = 100 }: Props) {
  const [poks, setPoks] = useState<Array<PokemonData>>([]);
  const [dims, setDims] = useState({ width: 0, height: 0 });
  const handleOnLayout = useCallback(
    (e: LayoutChangeEvent) => {
      const newLayout = e.nativeEvent.layout;
      if (
        dims.width !== +newLayout.width ||
        dims.height !== +newLayout.height
      ) {
        setDims({ width: newLayout.width, height: newLayout.height });
      }
    },
    [dims, setDims]
  );
  const margin = 10;
  const width = (dims.width - (columns + 1) * margin) / columns;
  useEffect(() => {
    if (dims.width === 0 || dims.height === 0) {
      return;
    }
    const poks: {
      ratio: number;
      address: string;
      key: number;
      color: string;
    }[] = [];

    for (let i = 0; i < pokemons; i++) {
      const ratio = 1 + Math.random();
      poks.push({
        ratio,
        address: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${i}.png`,
        key: i,
        color: `#${getRandomColor()}`,
      });
    }
    setPoks(poks);
  }, [dims, setPoks, pokemons]);
  const layoutTransition = CurvedTransition.delay(1000).easingX(Easing.linear);

  const [cardsMemo, height] = useMemo<[Array<JSX.Element>, number]>(() => {
    if (poks.length === 0) {
      return [[], 0];
    }
    const cardsResult: Array<JSX.Element> = [];
    const heights = new Array(columns).fill(0);
    for (const pok of poks) {
      const cur = Math.floor(Math.random() * (columns - 0.01));
      const pokHeight = width * pok.ratio;
      heights[cur] += pokHeight + margin / 2;
      cardsResult.push(
        <Animated.View
          entering={LightSpeedInRight.delay(cur * 200 * 2).springify()}
          exiting={BounceOut}
          layout={layoutTransition}
          key={pok.address}
          style={[
            {
              width: width,
              height: pokHeight,
              backgroundColor: pok.color,
              left: cur * width + (cur + 1) * margin,
              top: heights[cur] - pokHeight,
            },
            styles.pok,
          ]}>
          <TapGestureHandler
            onHandlerStateChange={() => {
              setPoks(poks.filter((it) => it.key !== pok.key));
            }}>
            <AnimatedImage
              layout={layoutTransition}
              source={{ uri: pok.address }}
              style={{ width: width, height: width }}
            />
          </TapGestureHandler>
        </Animated.View>
      );
    }
    return [cardsResult, Math.max(...heights) + margin / 2];
  }, [poks, columns, layoutTransition, width]);
  return (
    <View onLayout={handleOnLayout} style={styles.flexOne}>
      {cardsMemo.length === 0 && <Text> Loading </Text>}
      {cardsMemo.length !== 0 && (
        <ScrollView>
          <View style={{ height: height }}>{cardsMemo}</View>
        </ScrollView>
      )}
    </View>
  );
}
export default function WaterfallGridExample() {
  return (
    <View style={styles.flexOne}>
      <WaterfallGrid columns={3} pokemons={10} />
    </View>
  );
}

const styles = StyleSheet.create({
  flexOne: {
    flex: 1,
  },
  pok: {
    alignItems: 'center',
    justifyContent: 'center',
    position: 'absolute',
  },
});

@Latropos Latropos marked this pull request as ready for review February 5, 2024 12:42
src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/timing.ts Show resolved Hide resolved
src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
Latropos and others added 2 commits February 5, 2024 15:49
Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also add a check if this is a development build? We shouldn't be checking this in production I think.

Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add some test plan in the PR description (with before/after screenshots maybe?).

@Latropos Latropos requested a review from tjzel February 8, 2024 14:21
Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make the comment more accurate and we are good to go!

src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
Copy link
Member

@tomekzaw tomekzaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, left some questions in the comments.

Also, shouldn't we also perform the same check here:

If yes, it probably makes sense to move it to a utility function assertEasingIsWorklet and call it from all these places.

src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
Latropos and others added 3 commits February 20, 2024 13:04
Co-authored-by: Tomasz Żelawski <40713406+tjzel@users.noreply.github.com>
@tjzel
Copy link
Collaborator

tjzel commented Feb 20, 2024

@Latropos Can you check if it works on Web?

@Latropos
Copy link
Contributor Author

@tjzel I've tested, it works on web

@Latropos Latropos requested a review from tomekzaw March 5, 2024 11:33
src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
src/reanimated2/commonTypes.ts Outdated Show resolved Hide resolved
src/reanimated2/valueUnpacker.ts Outdated Show resolved Hide resolved
src/reanimated2/commonTypes.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
Latropos and others added 3 commits March 6, 2024 17:08
Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
@Latropos Latropos requested a review from tomekzaw March 8, 2024 13:14
Copy link
Member

@tomekzaw tomekzaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, let's just slightly improve the error message.

src/reanimated2/animation/util.ts Outdated Show resolved Hide resolved
Co-authored-by: Tomek Zawadzki <tomasz.zawadzki@swmansion.com>
src/reanimated2/animation/timing.ts Outdated Show resolved Hide resolved
Co-authored-by: Krzysztof Piaskowy <krzysztof.piaskowy@swmansion.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants