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

Shared style #1470

Merged
merged 42 commits into from
Jul 19, 2021
Merged

Shared style #1470

merged 42 commits into from
Jul 19, 2021

Conversation

piaskowyk
Copy link
Member

@piaskowyk piaskowyk commented Nov 23, 2020

Description

I added the possibility to use one instance of useAnimationStyle() for many components. AnimationStyle is connected with view by ViewDescriptor object. I changed this to a set of ViewDescriptors and now AnimationStyle can be shared between many components.

Fixes #1263

Example of code

code
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
} from 'react-native-reanimated';
import { Button, ScrollView } from 'react-native';
import React from 'react';

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

  const config = {
    duration: 500,
    easing: Easing.bezier(0.5, 0.01, 0, 1),
  };

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

  const renderList = () => {
    const items = [];
    for (let i = 0; i < 100; i++) {
      items.push(
        <Animated.View
          key={i}
          style={[
            { width: 100, height: 5, backgroundColor: 'black', margin: 1 },
            style,
          ]}
        />
      );
    }
    return items;
  };

  return (
    <ScrollView
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Button
        title="toggle"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
      {renderList()}
    </ScrollView>
  );
}
code2
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
} from 'react-native-reanimated';
import { View, Button } from 'react-native';
import React, { useState } from 'react';

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

  const [counter, setCounter] = useState(0);
  const [counter2, setCounter2] = useState(0);
  const [itemList, setItemList] = useState([]);
  const [toggleState, setToggleState] = useState(false);

  const config = {
    duration: 500,
    easing: Easing.bezier(0.5, 0.01, 0, 1),
  };

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

  const staticObject = <Animated.View
    style={[
      { width: 100, height: 8, backgroundColor: 'black', margin: 1 },
      style,
    ]}
  />

  const renderItems = () => {
    let output = []
    for(let i = 0; i < counter; i++) {
      output.push(
        <Animated.View
        key={i + 'a'}
          style={[
            { width: 100, height: 8, backgroundColor: 'blue', margin: 1 },
            style,
          ]}
        />
      )
    }
    return output
  }

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
      }}>
      <Button
        title="animate"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
      <Button
        title="increment counter"
        onPress={() => {
          setCounter(counter + 1)
        }}
      />
      <Button
        title="add item to static lists"
        onPress={() => {
          setCounter2(counter2 + 1)
          setItemList([...itemList, <Animated.View
            key={counter2 + 'b'}
            style={[
              { width: 100, height: 8, backgroundColor: 'green', margin: 1 },
              style,
            ]}
          />])
        }}
      />
      <Button
        title="toggle state"
        onPress={() => {
          setToggleState(!toggleState)
        }}
      />
      <Animated.View
        style={[
          { width: 100, height: 8, backgroundColor: 'orange', margin: 1 },
          style,
        ]}
      />
      {toggleState && <Animated.View
        style={[
          { width: 100, height: 8, backgroundColor: 'black', margin: 1 },
          style,
        ]}
      />}
      {toggleState && staticObject}
      {renderItems()}
      {itemList}
    </View>
  );
}

Change

before changes:
1

final result:
3

@piaskowyk piaskowyk changed the title Base for shared style Shared style Nov 24, 2020
@mrousavy
Copy link
Contributor

Awesome! I've been having huge performance problems with reanimated v2 since I am animating multiple views with a sort of parallax effect, I'm sure this is a big step towards solving those performance issues. 👍

@terrysahaidak
Copy link
Contributor

hey @mrousavy can you share us these performance problems in an issue? perhaps it's the same problem I had recently

@mrousavy
Copy link
Contributor

@terrysahaidak Yeah, here's a video demo (imgur.com). The structure of this view is the following:
I have a view that horizontally displays all my individual "story" screens, and you can "scroll" between them using a horizontal scroll gesture, kinda like an image carousel. I have implemented this using a PanGestureHandler instead of a ScrollView, since that gives me more control.

So the whole horizontal view which contains each individual story screen is translated along the X axis depending on the swipe gesture. Then, I've tried to implement some sort of cool parallax effect with the headers, by making them go into the next screen by a tiny bit, to reveal to the user what the next page has to offer. The problem here is, that I can only achieve that by translating each header individually, depending on the carousel's translateX. And that's causing huge performance problems, since I now have about 30 useAnimatedStyle hooks for 30 stories, just for the headers! In REA v1 this was no problem, since I just passed the nodes in there.

I tried to play around with virtualization and eventually came up with a way to unmount every header that's not in viewport (and used removeSubclippedViews for the other views to gain additional performance), but it was still terrible, even with just 5 headers rendered at once. (Even on my iPhone 11 it has terrible FPS, on an iPhone 8 it takes about 10 seconds to even open that screen)

@piaskowyk piaskowyk marked this pull request as ready for review December 17, 2020 11:51
src/reanimated2/UpdateProps.js Outdated Show resolved Hide resolved
src/reanimated2/js-reanimated/MutableSet.js Outdated Show resolved Hide resolved
src/reanimated2/js-reanimated/index.web.js Outdated Show resolved Hide resolved
Example/ios/Podfile.lock Outdated Show resolved Hide resolved
Common/cpp/SharedItems/ShareableValue.cpp Outdated Show resolved Hide resolved
Common/cpp/SharedItems/MutableSet.cpp Outdated Show resolved Hide resolved
Common/cpp/SharedItems/MutableSet.cpp Outdated Show resolved Hide resolved
@jakub-gonet
Copy link
Member

Can you resolve the hooks file conflict?

@kmagiera
Copy link
Member

Do we really need native set support for this one? Why can't we make an array of descriptors and just assign it in one go instead of making changes with every object being added.

I haven't checked the full source but it seem problematic that we use set.add and never use set.remove in this PR.

@piaskowyk piaskowyk marked this pull request as ready for review March 11, 2021 09:04
@piaskowyk
Copy link
Member Author

Finally, works and ready for review, I would be grateful for any suggestions.

@charles-goode
Copy link

When this is ready, will it also work for useAnimatedProps? I didn't see issues reporting that the same need for sharing is present with animated props, so I wasn't sure if that had also been considered.

@mrousavy
Copy link
Contributor

mrousavy commented Jun 7, 2021

Hate to "bump" this, but do you guys think this will make it into 2.3.0? @piaskowyk @jakub-gonet let me know if you need any help testing this change 🙏

@piaskowyk
Copy link
Member Author

The good news, this PR will be release in 2.3 🎉

@piaskowyk
Copy link
Member Author

When this is ready, will it also work for useAnimatedProps? I didn't see issues reporting that the same need for sharing is present with animated props, so I wasn't sure if that had also been considered.

Yes, in Reanimated 2 useAnimatedStyle and useAnimatedProps has the same implementation:

export const useAnimatedProps = useAnimatedStyle;

@piaskowyk piaskowyk merged commit 0c2f66f into master Jul 19, 2021
@piaskowyk piaskowyk deleted the shareable-useAnimatedStyle branch July 19, 2021 08:26
@bobsmits
Copy link
Contributor

@piaskowyk Any sight on a alpha.2 release for 2.3? I would love to get started with this.

@yanush
Copy link

yanush commented Nov 19, 2021

This is not working for me in 2.3.0-beta.3
Any suggestions?

piaskowyk added a commit that referenced this pull request Mar 18, 2022
## Description

We can't use just initial style as the default style in every render because these styles can be outdated. We can't change the default style after the first render, because after the second render we don't run mapper that's why the component can change the style to the initial value.

Related:
- #2580
- #2431
- #2406
- #1470

### code

Before

https://user-images.githubusercontent.com/36106620/158142874-a11191e7-c0d9-4c3f-8f18-e5b540a6f17c.mov

https://user-images.githubusercontent.com/36106620/158167177-81dfa334-db01-4e04-a234-e1069e8d715b.mov




After

https://user-images.githubusercontent.com/36106620/149799832-b0c0748d-2d9d-42b9-b9ba-f6492cc1fbf0.mov

<details>
<summary>code</summary>

```js
import Animated, {
  useSharedValue,
  withTiming,
  useAnimatedStyle,
  Easing,
} from 'react-native-reanimated';
import { View, Button } from 'react-native';
import React, { useState } from 'react';

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

  const [counter, setCounter] = useState(0);
  const [counter2, setCounter2] = useState(0);
  const [itemList, setItemList] = useState([]);
  const [toggleState, setToggleState] = useState(false);

  const config = {
    duration: 500,
    easing: Easing.bezier(0.5, 0.01, 0, 1),
  };

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

  const staticObject = <Animated.View
    style={[
      { width: 100, height: 3, backgroundColor: 'black', margin: 1 },
      style,
    ]}
  />

  const renderItems = () => {
    let output = []
    for(let i = 0; i < counter; i++) {
      output.push(
        <Animated.View
        key={i + 'a'}
          style={[
            { width: 100, height: 3, backgroundColor: 'blue', margin: 1 },
            style,
          ]}
        />
      )
    }
    return output
  }

  return (
    <View
      style={{
        flex: 1,
        flexDirection: 'column',
        marginTop: 30
      }}>
      <Button
        title="animate"
        onPress={() => {
          randomWidth.value = Math.random() * 350;
        }}
      />
      <Button
        title="increment counter"
        onPress={() => {
          setCounter(counter + 1)
        }}
      />
      <Button
        title="add item to static lists"
        onPress={() => {
          setCounter2(counter2 + 1)
          setItemList([...itemList, <Animated.View
            key={counter2 + 'b'}
            style={[
              { width: 100, height: 3, backgroundColor: 'green', margin: 1 },
              style,
            ]}
          />])
        }}
      />
      <Button
        title="toggle state"
        onPress={() => {
          setToggleState(!toggleState)
        }}
      />
      <Animated.View
        style={[
          { width: 100, height: 3, backgroundColor: 'orange', margin: 1 },
          style,
        ]}
      />
      {toggleState && <Animated.View
        style={[
          { width: 100, height: 3, backgroundColor: 'black', margin: 1 },
          style,
        ]}
      />}
      {toggleState && staticObject}
      {renderItems()}
      {itemList}
    </View>
  );
}

```

</details>

### code2

Still works

https://user-images.githubusercontent.com/36106620/149800303-4c4316aa-7765-4c66-a81a-74489d9f0215.mov

<details>
<summary>code2</summary>

```js
import React from 'react';
import { View } from 'react-native';
import Animated, {
  useSharedValue,
  withSpring,
  useAnimatedStyle,
  useAnimatedGestureHandler,
  interpolate,
  Extrapolate,
  runOnJS,
} from 'react-native-reanimated';
import {
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import { useEffect, useState } from 'react';

function DragAndSnap(): React.ReactElement {
  const translation = {
    x: useSharedValue(0),
    y: useSharedValue(0),
  };
  type AnimatedGHContext = {
    startX: number;
    startY: number;
  };

  // run a couple of updates when gesture starts
  const [counter, setCounter] = useState(0);
  const makeFewUpdates = () => {
    let countdown = 100;
    const doStuff = () => {
      setCounter(countdown);
      countdown--;
      if (countdown > 0) {
        requestAnimationFrame(doStuff);
      }
    };
    doStuff();
  };

  const gestureHandler = useAnimatedGestureHandler<
    PanGestureHandlerGestureEvent,
    AnimatedGHContext
  >({
    onStart: (_, ctx) => {
      ctx.startX = translation.x.value;
      ctx.startY = translation.y.value;
      runOnJS(makeFewUpdates)();
    },
    onActive: (event, ctx) => {
      translation.x.value = ctx.startX + event.translationX;
      translation.y.value = ctx.startY + event.translationY;
    },
    onEnd: (_) => {
      translation.x.value = withSpring(0);
      translation.y.value = withSpring(0);
    },
  });

  const stylez = useAnimatedStyle(() => {
    const H = Math.round(
      interpolate(translation.x.value, [0, 300], [0, 360], Extrapolate.CLAMP)
    );
    const S = Math.round(
      interpolate(translation.y.value, [0, 500], [100, 50], Extrapolate.CLAMP)
    );
    const backgroundColor = `hsl(${H},${S}%,50%)`;
    return {
      transform: [
        {
          translateX: translation.x.value,
        },
        {
          translateY: translation.y.value,
        },
      ],
      backgroundColor,
    };
  });

  // make render slower
  let f = 0;
  for (var i = 0; i < 1e8; i++) {
    f++;
  }

  return (
    <View style={{ flex: 1, margin: 50 }}>
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View
          style={[
            {
              width: 40,
              height: 40,
            },
            stylez,
          ]}
        />
      </PanGestureHandler>
    </View>
  );
}

export default DragAndSnap;

```

</details>
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.

unable to use same useAnimatedStyle to multiple views components
10 participants