Skip to content

Animated ClipPaths not re-rendering on Android #2473

Open
@esbenvb

Description

@esbenvb

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions