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 ClipPaths not re-rendering on Android #2473

Open
esbenvb opened this issue Oct 3, 2024 · 1 comment · May be fixed by #2474
Open

Animated ClipPaths not re-rendering on Android #2473

esbenvb opened this issue Oct 3, 2024 · 1 comment · May be fixed by #2474

Comments

@esbenvb
Copy link

esbenvb commented Oct 3, 2024

Description

Animated ClipPaths does not seem to update visually on Android.

If I change other SVG props, it seems to re-render based on the current animated value, the animation of a ClipPath property itself does not cause the SVG to re-render.

I have attached example code using the latest RN and RNSVG modules.

There's a related issue from 2022, but unfortunately I can't downgrade to the mentioned version as it won't build with React Native 0.75

#1719

Steps to reproduce

Clone repo from the attached link or do the following:

npx react-native init TestSVG
cd TestSVG
yarn add react-native-svg
yarn android

Replace App.tsx with the code below and try the different variations. All works on iOS, but animated ClipPath does not work on Android.

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 */

import React, {useEffect, useRef, useState} from 'react';
import {
  Animated,
  Button,
  Easing,
  SafeAreaView,
  StatusBar,
  Text,
  useColorScheme,
  View,
} from 'react-native';
import {Circle, ClipPath, G, Mask, Rect, Svg} from 'react-native-svg';

import {Colors} from 'react-native/Libraries/NewAppScreen';

const WIDTH = 300;
const HEIGHT = 140;

const AnimatedRect = Animated.createAnimatedComponent(Rect);
const AnimatedCircle = Animated.createAnimatedComponent(Circle);

const SlidingInClipPath: React.FC = () => {
  const animatedWidth = useRef(new Animated.Value(0)).current;
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (isVisible) {
      Animated.timing(animatedWidth, {
        toValue: WIDTH,
        duration: 1200,
        useNativeDriver: true,
        easing: Easing.inOut(Easing.ease),
      }).start();
    } else {
      animatedWidth.setValue(0);
    }
  }, [animatedWidth, isVisible]);
  return (
    <View>
      <Text>ClipPath sliding in</Text>
      <Svg width={WIDTH} height={HEIGHT} fill={'blue'}>
        <ClipPath id="clipPath">
          <AnimatedRect x={0} y={0} width={animatedWidth} height={HEIGHT} />
        </ClipPath>
        <G clipPath="url(#clipPath)">
          <Circle stroke={'green'} strokeWidth={3} cx={230} cy={50} r={30} />
          <Rect x={50} y={30} width={200} height={100} />
          <Circle stroke={'red'} strokeWidth={3} cx={80} cy={50} r={30} />
        </G>
      </Svg>
      <Button
        onPress={() => setIsVisible(current => !current)}
        title={isVisible ? 'Hide' : 'SHow'}
      />
    </View>
  );
};

const SlidingInMask: React.FC = () => {
  const animatedWidth = useRef(new Animated.Value(0)).current;
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (isVisible) {
      Animated.timing(animatedWidth, {
        toValue: WIDTH,
        duration: 1200,
        useNativeDriver: true,
        easing: Easing.inOut(Easing.ease),
      }).start();
    } else {
      animatedWidth.setValue(0);
    }
  }, [animatedWidth, isVisible]);
  return (
    <View>
      <Text>Mask sliding in</Text>
      <Svg width={WIDTH} height={HEIGHT} fill={'blue'}>
        <Mask id="mask">
          <Rect x={0} y={0} width={WIDTH} height={HEIGHT} fill="black" />
          <AnimatedRect
            x={0}
            y={0}
            width={animatedWidth}
            height={HEIGHT}
            fill="white"
          />
        </Mask>
        <G mask="url(#mask)">
          <Circle stroke={'green'} strokeWidth={3} cx={230} cy={50} r={30} />
          <Rect x={50} y={30} width={200} height={100} />
          <Circle stroke={'red'} strokeWidth={3} cx={80} cy={50} r={30} />
        </G>
      </Svg>
      <Button
        onPress={() => setIsVisible(current => !current)}
        title={isVisible ? 'Hide' : 'SHow'}
      />
    </View>
  );
};

const FadingIn: React.FC = () => {
  const animatedOpacity = useRef(new Animated.Value(0)).current;
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (isVisible) {
      Animated.timing(animatedOpacity, {
        toValue: 1,
        duration: 1200,
        useNativeDriver: true,
        easing: Easing.inOut(Easing.ease),
      }).start();
    } else {
      animatedOpacity.setValue(0);
    }
  }, [animatedOpacity, isVisible]);
  return (
    <View>
      <Text>Mask fading in</Text>
      <Svg width={WIDTH} height={HEIGHT} fill={'blue'}>
        <Mask id="mask">
          <AnimatedRect
            x={0}
            y={0}
            width={WIDTH}
            opacity={animatedOpacity}
            height={HEIGHT}
            fill="white"
          />
        </Mask>
        <G mask="url(#mask)">
          <Circle stroke={'green'} strokeWidth={3} cx={230} cy={50} r={30} />
          <Rect x={50} y={30} width={200} height={100} />
          <Circle stroke={'red'} strokeWidth={3} cx={80} cy={50} r={30} />
        </G>
      </Svg>
      <Button
        onPress={() => setIsVisible(current => !current)}
        title={isVisible ? 'Hide' : 'SHow'}
      />
    </View>
  );
};

const RADIUS = 80;
const PulsatingCircle: React.FC = () => {
  const animatedRadius = useRef(new Animated.Value(0)).current;
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    if (isVisible) {
      Animated.timing(animatedRadius, {
        toValue: RADIUS,
        duration: 1200,
        useNativeDriver: true,
        easing: Easing.inOut(Easing.ease),
      }).start();
    } else {
      Animated.timing(animatedRadius, {
        toValue: 0,
        duration: 1200,
        useNativeDriver: true,
        easing: Easing.inOut(Easing.ease),
      }).start();
    }
  }, [animatedRadius, isVisible]);
  return (
    <View>
      <Text>Pulsating Circle</Text>
      <Svg width={WIDTH} height={HEIGHT} fill={'blue'}>
        <G>
          <AnimatedCircle
            fill={'green'}
            strokeWidth={3}
            cx={80}
            cy={80}
            r={animatedRadius}
          />
        </G>
      </Svg>
      <Button
        onPress={() => setIsVisible(current => !current)}
        title={isVisible ? 'Hide' : 'SHow'}
      />
    </View>
  );
};

function App(): React.JSX.Element {
  const isDarkMode = useColorScheme() === 'dark';

  const backgroundStyle = {
    backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
  };

  return (
    <SafeAreaView style={backgroundStyle}>
      <StatusBar
        barStyle={isDarkMode ? 'light-content' : 'dark-content'}
        backgroundColor={backgroundStyle.backgroundColor}
      />
      <SlidingInClipPath />
      <SlidingInMask />
      <FadingIn />
      <PulsatingCircle />
    </SafeAreaView>
  );
}

export default App;

Snack or a link to a repository

https://github.com/esbenvb/rnsvg-android-animated-clippath-issue-reproduction

SVG version

15.7.1

React Native version

0.75.4

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native

Architecture

Paper (Old Architecture)

Build type

Release app & production bundle

Device

Real device

Device model

Pixel 8 pro - Android 14, Samsung A14 - Android 14

Acknowledgements

Yes

@LukasMod
Copy link

Not sure if it's related, but I found a similar issue in my app after upgrading React Native from 0.73 to 0.76.3. This code, which is a gradient component for modals, worked fine on both iOS and Android before:


                <Svg width="100%" height="100%" >
                    <Defs>
                        <LinearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
                            <Stop offset="14.83%" stopColor={fromColor} />
                            <Stop offset="83.89%" stopColor={toColor} />
                        </LinearGradient>
                    </Defs>
                    <Rect width="100%" height="100%" fill="url(#grad)" />
                </Svg>

With React Native 0.76 (old architecture), I encountered strange behavior with this:
.

Randomly, it started rendering with something like width="80%". However, after making changes in the code (e.g., updating the width from 100 to 90 and then back to 100 during hot reload), it rendered properly as it should have initially at full width (100%).

I found a solution by using onLayout to set the SVG dimensions correctly.

    const [svgWidth, setSvgWidth] = useState(0);
    const [svgHeight, setSvgHeight] = useState(0);

    const handleLayout = (event: LayoutChangeEvent) => {
        const { width, height } = event.nativeEvent.layout;
        setSvgWidth(width);
        setSvgHeight(height);
    };

    return (
        <View style={[styles.container, containerStyle]} onLayout={handleLayout}>
            <View style={styles.backgroundContainer}>
                <Svg width={svgWidth} height={svgHeight}>
                    <Defs>
                        <LinearGradient id="grad" x1="0%" y1="0%" x2="0%" y2="100%">
                            <Stop offset="14.83%" stopColor={fromColor} />
                            <Stop offset="83.89%" stopColor={toColor} />
                        </LinearGradient>
                    </Defs>
                    <Rect width="100%" height="100%" fill="url(#grad)" />
                </Svg>
            </View>
            {children}
        </View> 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants