Skip to content

[Android][Fabric] Text re-wraps on every frame while LayoutAnimation changes its width (enableLayoutAnimationsOnAndroid) #59163

Open

Description

@tomekzaw

Description

With enableLayoutAnimationsOnAndroid turned on, a LayoutAnimation that changes the width of a <Text> on Android with the New Architecture makes the text re-wrap on every frame. While the box widens, the text goes from 7 lines to 6, 5, 4 and 3, and words keep moving between lines. Narrowing does the same in reverse.

With the old architecture (tested on 0.81.6 with newArchEnabled=false), the same animation keeps the final line breaks: the text is laid out once at its final width and is uncovered as the view grows.

Frames from the widen animation, top to bottom: New Architecture on 0.88.0 with the flag on (left) and old architecture on 0.81.6 (right):

Image

Height changes (for example numberOfLines={2} → no limit) aren't affected. The text only depends on its width there, so it unrolls correctly. The same reflow also happens on main.

I know LayoutAnimation is still off by default on Android with the New Architecture (#52921). I'm reporting this so it's known before the flag is enabled. The iOS counterpart of this problem (line breaks changing while a Text is resized) is #59161, with a fix in #59162.

Likely cause

Fabric's LayoutAnimation mounts an interpolated frame on every animation frame, and SurfaceMountingManager.updateLayout calls view.measure(EXACTLY width, EXACTLY height) before view.layout(...) for each of them. ReactTextView is an AppCompatTextView, and TextView rebuilds its text Layout for the new width in onMeasure, so the line breaks follow the intermediate width.

The old architecture measured the view once with its final size in NativeViewHierarchyManager.updateLayout and then animated only view.layout(...) in PositionAndSizeAnimation, so TextView kept the layout it had built for the final width.

Steps to reproduce

  1. Clone the reproducer: cd ReproducerApp && yarn install && yarn android. Its MainApplication.kt enables enableLayoutAnimationsOnAndroid.
  2. Tap Widen.
  3. Watch the text during the 3-second animation: the line breaks change on every frame. Tap Narrow for the reverse.

The repro:

const TEXT = Array.from({ length: 14 }, (_, i) => `word${i + 1}`).join(' ');

export default function App() {
  const [wide, setWide] = useState(false);

  const toggle = () => {
    LayoutAnimation.configureNext({
      duration: 3000,
      update: { type: LayoutAnimation.Types.linear },
    });
    setWide(value => !value);
  };

  return (
    <View style={{ flex: 1, paddingTop: 100, paddingHorizontal: 20 }}>
      <Pressable onPress={toggle}>
        <Text>{wide ? 'Narrow' : 'Widen'}</Text>
      </Pressable>
      <Text style={{ fontSize: 22, backgroundColor: 'gold', width: wide ? '100%' : '50%' }}>
        {TEXT}
      </Text>
    </View>
  );
}
// MainApplication.onCreate(), after loadReactNative(this)
ReactNativeFeatureFlags.dangerouslyForceOverride(
  object : ReactNativeFeatureFlagsProvider by ReactNativeFeatureFlagsOverrides_RNOSS_Stable_Android() {
    override fun enableLayoutAnimationsOnAndroid(): Boolean = true
  }
)

React Native Version

0.88.0

Affected Platforms

Runtime - Android

Areas

Fabric - The New Renderer

Output of npx @react-native-community/cli info

System:
  OS: macOS 27.0.1
  CPU: (15) arm64 Apple M5 Pro
  Memory: 58.41 MB / 48.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 24.19.0
    path: /Users/tomekzaw/.nvm/versions/node/v24.19.0/bin/node
  Yarn:
    version: 1.22.22
    path: /opt/homebrew/bin/yarn
  npm:
    version: 11.17.0
    path: /Users/tomekzaw/.nvm/versions/node/v24.19.0/bin/npm
  Watchman:
    version: 2026.10.12.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.17.0
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.0
      - iOS 27.1
      - macOS 27.0
      - tvOS 27.0
      - visionOS 27.0
      - watchOS 27.0
  Android SDK:
    API Levels:
      - "36"
      - "36"
      - "37"
    Build Tools:
      - 35.0.0
      - 36.0.0
      - 36.1.0
      - 37.0.0
    System Images:
      - android-37.0 | 16 KB Page Size Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2026.1 AI-261.26222.65.2614.16204760
  Xcode:
    version: 27.1/27A9275
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.20.1
    path: /usr/bin/javac
  Ruby:
    version: 2.7.6
    path: /Users/tomekzaw/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.2.0
    wanted: 20.2.0
  react:
    installed: 19.3.0
    wanted: 19.3.0
  react-native:
    installed: 0.88.0
    wanted: 0.88.0
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: Not found
  newArchEnabled: false

Stacktrace or Logs

N/A, visual issue only (no errors or warnings are logged).

MANDATORY Reproducer

https://github.com/tomekzaw/repro-layout-animations-android

Screenshots and Videos

Tested on the Pixel 10 Pro emulator (API 37). The old-architecture comparison used the same emulator.

Widen, then narrow, on 0.88.0 with the New Architecture and enableLayoutAnimationsOnAndroid on:

android-0.88-width-animation.mp4

Activity

  1. tomekzaw commented on Oct 17, 2026

    @tomekzaw
    ContributorAuthor

    Some more findings after looking into a fix.

    This only affects ReactTextView. With enablePreparedTextLayout also turned on, the same width animation keeps the final line breaks: the text is laid out at the final width and is uncovered as the view grows, the same as the old architecture. Height changes still unroll correctly too. I tested this with RNTester built from main (27e77216ef2) on the Pixel 10 Pro emulator (API 37), with enableLayoutAnimationsOnAndroid and enablePreparedTextLayout forced on. Frames from the widen animation, top to bottom:

    Image

    PreparedLayoutTextView draws the layout prepared in the shadow tree for the committed size, so the interpolated frames only clip it. ReactTextView builds its text layout in TextView.onMeasure from whatever width SurfaceMountingManager.updateLayout measures it with, and that's the interpolated width on every frame.

    Why I didn't send a patch for ReactTextView. To keep its line breaks stable, it would need the final size in Java, and only the C++ LayoutAnimation driver knows it during the animation. I see two ways to get it there, and neither seems worth it for a view that the TODO in ParagraphShadowNode.h says PreparedLayoutTextView will replace:

    • Put the committed width in Android's ParagraphState. Every width change of a Text would then cause a state update, which on Android means a JNI round trip, rebuilding the Spannable and calling setText.
    • Mark LayoutAnimation frames in the mounting layer and pass the final size, so Android measures once at the final size and only calls layout() for the interpolated frames. That's what the old architecture's PositionAndSizeAnimation did. It would fix this for every view, but it changes core mounting structures.

    So it might make sense to enable LayoutAnimation on Android together with, or after, the enablePreparedTextLayout rollout. I'm happy to work on either of the options above if you'd prefer to fix ReactTextView directly.

  2. alex-strae commented on Oct 18, 2026

    @alex-strae

    Fantastic, LayoutAnimation on Android is on its way back. Behaves a little weird for text though. On iOS, the entire text moves/animates to new position. On Android, the last word just appears after the rest of the text has animated into place, but only when the text animates/moves to the left, and not to the right. As in widens and narrows.

    Skarminspelning.2026-10-18.kl.15.35.41.mov

    Implementation:
    Called by:

     <TouchableOpacity style={pickerStyle} onPress={() => {
                LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
                handlePress(date, prodInfo);
            }}>
    ... </TouchableOpacity>
    

    and animated element is just a simple <Text></Text> element.

    Edit: and I realize this might be the same bug that you are reporting, it's just that only the first line is rendered in my view.

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

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