software-mansion / software-mansion/react-native-svg

Animated ClipPaths not re-rendering on Android

Open
#2,473 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Repro provided
Dominant language
TypeScript
Stars
8k
Forks
1.2k
Avg merge
10d 9h
Merged PRs (30d)
2

Description

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

https://github.com/software-mansion/react-native-svg/issues/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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with App.tsx in the linked rnsvg-android-animated-clippath-issue-reproduction repository and run the Android reproduction on the stated React Native and react-native-svg versions. Compare the ClipPath animation with the working mask, opacity, and circle cases; done means the ClipPath visibly updates during animation on Android without breaking the other cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, react-native, typescript
Domain
mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.