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

Android Mask not working

Open
#2,875 2 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Description

Here is my component that renders this preloader.
Figma screenshots:
Image

It works perfectly on iOS, but the mask doesn't work on Android.
iOS screenshots:

Image

Android screenshots:

Image
import React, { useEffect } from 'react'
import Svg, {
    Circle,
    Defs,
    G,
    Path,
    Mask,
    LinearGradient,
    Stop,
} from 'react-native-svg'
import Animated, {
    Easing,
    useAnimatedStyle,
    useSharedValue,
    withRepeat,
    withTiming,
} from 'react-native-reanimated'
import { LoaderProps } from '@/shared/ui/Loader/model'
import { rem } from '@/lib'

export function Loader({ size = 56, style }: LoaderProps) {
    const outerRadius = rem(size)
    const segments = rem(size) 
    const id = Date.now().toString() 

    const segmentsArray = []
    const gradientsArray = []

    for (let i = 0; i < segments; i++) {
        const startAngle = (i / segments) * 360 - 90
        const endAngle = ((i + 1) / segments) * 360 - 90
        const largeArc = endAngle - startAngle >= 180 ? 1 : 0

        const x1 =
            outerRadius + outerRadius * Math.cos((Math.PI * startAngle) / 180)
        const y1 =
            outerRadius + outerRadius * Math.sin((Math.PI * startAngle) / 180)

        const x2 =
            outerRadius + outerRadius * Math.cos((Math.PI * endAngle) / 180)
        const y2 =
            outerRadius + outerRadius * Math.sin((Math.PI * endAngle) / 180)

        segmentsArray.push(
            <Path
                key={i}
                d={`M ${outerRadius} ${outerRadius} L ${x1} ${y1} A ${outerRadius} ${outerRadius} 0 ${largeArc} 1 ${x2} ${y2} Z`}
                stroke="none"
                fill={`url(#${id}-grad-${i})`}
            />,
        )
        const fromOpacity = i / segments
        const toOpacity = (i + 1) / segments
        gradientsArray.push(
            <LinearGradient
                key={i}
                id={`${id}-grad-${i}`}
                x1="0%"
                y1="0%"
                x2="0%"
                y2="100%">
                <Stop
                    offset="0%"
                    stopColor="black"
                    stopOpacity={fromOpacity}
                />
                <Stop
                    offset="100%"
                    stopColor="black"
                    stopOpacity={toOpacity}
                />
            </LinearGradient>,
        )
    }

    const rotation = useSharedValue(0)

    const animatedStyle = useAnimatedStyle(() => ({
        transform: [{ rotate: `${rotation.value}deg` }],
    }))

    useEffect(() => {
        rotation.value = withRepeat(
            withTiming(360, { duration: 1000, easing: Easing.linear }),
            -1,
            false,
        )
    })

    return (
        <Animated.View
            style={[
                style,
                {
                    width: rem(size),
                    height: rem(size),
                },
                animatedStyle,
            ]}>
            <Svg
                width={rem(size)}
                height={rem(size)}
                viewBox={`0 0 ${rem(size)} ${rem(size)}`}>
                <Defs>
                    {gradientsArray}
                    <Mask id={id}>
                        <Circle
                            cx={rem(size) / 2}
                            cy={rem(size) / 2}
                            r={outerRadius}
                            fill="white"
                        />
                        <Circle
                            cx={rem(size) / 2}
                            cy={rem(size) / 2}
                            r={rem(size) / 2 - rem(size) * 0.9090909}
                            fill="black"
                        />
                    </Mask>
                </Defs>

                <G mask={`url(#${id})`}>{segmentsArray}</G>
            </Svg>
        </Animated.View>
    )
}

Steps to reproduce
  1. Just add in App layout
Snack or a link to a repository
  • Nope
SVG version

15.12.1

React Native version

0.81.5

Platforms

Android, iOS

JavaScript runtime

None

Workflow

Expo Go

Architecture

Fabric (New Architecture)

Build type

Debug app & dev bundle

Device

Android emulator

Device model

Pixel 9 Pro XL

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 the provided Loader component and reproduce it in Expo Go on the Android emulator using React Native 0.81.5 and react-native-svg 15.12.1. Compare the mask output with iOS and inspect the Android SVG mask behavior; done means the preloader renders the intended masked segments consistently on Android.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
frontend, mobile
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.