software-mansion/react-native-reanimated

[ReText, animatedProps text] Incorrect behavior with numeric text in fitting containers. Some numbers end with an ellipsis, IOS only

gendalf-thug opened this issue · 4 comments

Description

Bug demo in my project: https://drive.google.com/file/d/1dP7aYLf2zNKQvo8odrSgdm9GzwNU-AqO/view?usp=share_link

Steps to reproduce

  1. Setup latest blank expo project(SDK 52) with reanimated 3.16.2
  2. Write this component or use ReText from 'react-native-redash' library
import {memo} from 'react'

import {StyleSheet, TextInput} from 'react-native'
import Animated, {
  SharedValue,
  AnimatedProps,
  useAnimatedProps,
} from 'react-native-reanimated'

import {Color} from 'src/themes'
import {IS_ANDROID} from 'src/variables'

import type {TextInputProps, TextProps as RNTextProps} from 'react-native'

interface TextProps extends Omit<TextInputProps, 'value' | 'style'> {
  text: SharedValue<string> | SharedValue<number>
  style?: AnimatedProps<RNTextProps>['style']
}
Animated.addWhitelistedNativeProps({text: true})

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)

export const AnimText = memo((props: TextProps) => {
  const {style, text, ...rest} = props

  const animatedProps = useAnimatedProps(() => {
    return {
      text: String(text.value),
    } as any
  })

  return (
    <AnimatedTextInput
      underlineColorAndroid="transparent"
      editable={false}
      value={String(text.value)}
      style={[styles.baseStyle, style || undefined]}
      {...rest}
      animatedProps={animatedProps}
    />
  )
})

const styles = StyleSheet.create({
  baseStyle: {
    color: Color.textPrimary,
    padding: IS_ANDROID ? 0 : undefined,
  },
})
  1. Component
import {useEffect} from 'react'
import {View} from 'react-native'
import {useDerivedValue, useSharedValue} from 'react-native-reanimated'
import {AnimText} from 'ui/AnimText'

const formatSecondsTimerWorklet = (sec_num: number) => {
  'worklet'
  let minutes, seconds, hours

  hours = Math.floor(sec_num / 3600)
  minutes = Math.floor((sec_num - hours * 3600) / 60)
  seconds = sec_num - hours * 3600 - minutes * 60

  if (minutes < 10) {
    minutes = '0' + minutes
  }
  if (seconds < 10) {
    seconds = '0' + seconds
  }
  if (hours < 10) {
    hours = '0' + hours
  }
  return hours + ':' + minutes + ':' + seconds
}

export default function App() {
  const timer = useSharedValue(1)
  const timerText = useDerivedValue(() => {
    return formatSecondsTimerWorklet(timer.value)
  }, [])

  useEffect(() => {
    const interval = setInterval(() => {
      timer.value += 1
    }, 1000)
    return () => {
      clearInterval(interval)
    }
  }, [])

  return (
    <View style={{alignItems: 'center', justifyContent: 'center', flex: 1}}>
      <AnimText style={{fontSize: 50}} text={timerText} defaultValue="00:00" />
    </View>
  )
}

Code preview
Снимок экрана 2024-11-23 в 20 26 11

Snack or a link to a repository

Reanimated version

3.16.2

React Native version

0.76.3

Platforms

iOS

JavaScript runtime

None

Workflow

Expo Go

Architecture

None

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

Hey! 👋

The issue doesn't seem to contain a minimal reproduction.

Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?

Hey! 👋

It looks like you've omitted a few important sections from the issue template.

Please complete Snack or a link to a repository section.

Help? Feedback? Human’s comment?

Facing Same issue

Here is minimal Reproducible

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-gesture-handler';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedProps,
  runOnJS,
} from 'react-native-reanimated';

const SLIDER_WIDTH = 300;

Animated.addWhitelistedNativeProps({ text: true });

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

interface RangePickerProps {
  initialValue?: number; // in meters
  maxValue?: number; // in meters
  unit?: string;
  onValueChange?: (value: number) => void;
}

const theme = {
  colors: {
    primary: '#b58df1',
  },
};

const RangePicker: React.FC<RangePickerProps> = ({
  initialValue = 0,
  maxValue = 100,
  unit = 'm',
  onValueChange,
}) => {
  const SLIDER_PADDING = 5;
  const HANDLE_WIDTH = 40;
  const USABLE_WIDTH = SLIDER_WIDTH - HANDLE_WIDTH - SLIDER_PADDING * 2;

  // Convert initial value to slider position
  const initialOffset = (initialValue / maxValue) * USABLE_WIDTH;
  const offset = useSharedValue(initialOffset);
  const currentValue = useSharedValue(initialValue);

  const pan = Gesture.Pan().onChange((event) => {
    const newOffset = Math.max(
      0,
      Math.min(USABLE_WIDTH, offset.value + event.changeX)
    );
    offset.value = newOffset;

    // Calculate value with better precision
    const rawValue = (newOffset / USABLE_WIDTH) * maxValue;
    const newValue = Math.round(rawValue);
    currentValue.value = newValue;
    onValueChange && runOnJS(onValueChange)(newValue);
  });

  const sliderStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }));

  const animatedProps = useAnimatedProps(() => {
    const displayValue = currentValue.value.toFixed(0);
    return {
      text: displayValue.toString(),
      defaultValue: displayValue.toString(),
    };
  });

  return (
    <View style={styles.container}>
      <View style={styles.rangePickerContainer}>
        <AnimatedTextInput
          animatedProps={animatedProps}
          style={[styles.boxWidthText, { color: theme.colors.primary }]}
          editable={false}
        />
        <Text style={[styles.boxWidthText, { color: theme.colors.primary }]}>
          {' ' + unit}
        </Text>
      </View>
      <View style={styles.sliderTrack}>
        <GestureDetector gesture={pan}>
          <Animated.View style={[styles.sliderHandle, sliderStyle]} />
        </GestureDetector>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    gap: 32,
  },
  sliderTrack: {
    width: SLIDER_WIDTH,
    height: 30,
    backgroundColor: theme.colors.primary,
    borderRadius: 25,
    justifyContent: 'center',
    padding: 5,
  },
  sliderHandle: {
    width: 25,
    aspectRatio: 1,
    backgroundColor: '#f8f9ff',
    borderRadius: 20,
    position: 'absolute',
    left: 5,
  },
  box: {
    height: 30,
    backgroundColor: '#b58df1',
    borderRadius: 10,
  },
  boxWidthText: {
    textAlign: 'center',
    fontSize: 24,
  },
  rangePickerContainer: {
    flexDirection: 'row',
    alignItems: 'center',
  },
});

export default RangePicker;

The issue I'm facing is the number are divided by 1000 and I don't know why

Simulator.Screen.Recording.-.iPhone.15.-.2024-12-12.at.17.37.33.online-video-cutter.com.mp4