Skip to content

fix(android): use ceil for split-segment width to match commonmark path and rendered TextView#282

Merged
hryhoriiK97 merged 1 commit intosoftware-mansion-labs:mainfrom
FylerSearch:fix-android-text-measurement
Apr 30, 2026
Merged

fix(android): use ceil for split-segment width to match commonmark path and rendered TextView#282
hryhoriiK97 merged 1 commit intosoftware-mansion-labs:mainfrom
FylerSearch:fix-android-text-measurement

Conversation

@istarkov
Copy link
Copy Markdown
Contributor

@istarkov istarkov commented Apr 30, 2026

Fixes #281

measureAndCacheSplit truncated the Yoga width with width.toInt(),
while the commonmark paths

https://github.com/software-mansion-labs/react-native-enriched-markdown/blob/main/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt#L523

https://github.com/FylerSearch/react-native-enriched-markdown/blob/cd484331e51a1e937fada0431e80ab29943dc68f/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt#L559-L561

already use ceil(maxWidth).toInt().

Yoga rounds the rendered TextView frame UP to the next pixel.

Locally I tested with the following code, and wasn't able to reproduce, (BTW it's rare bug on random ;-))

import { ScrollView, View } from "react-native";
import { EnrichedMarkdownText } from "react-native-enriched-markdown";

const WORDS = [
  "fields",
  ",",
  "it",
  "consistently",
  "emphasizes",
  "that",
  "reaching",
  "higher",
  "goals",
  "requires",
  "a",
  "fundamental",
  "change",
  "in",
  "thinking",
  "and",
  "effort",
  "rather",
  "than",
  "simply",
  "working",
  "more",
  "hours",
  ".",
];

function mulberry32(seed: number) {
  let state = seed;
  return () => {
    state = (state + 0x6d2b79f5) | 0;
    let t = state;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
  };
}

function randomBlock(seed: number): string {
  const rand = mulberry32(seed * Math.random());
  const count = 20 + Math.floor(rand() * 20);
  const words: string[] = [];
  for (let i = 0; i < count; i++) {
    words.push(WORDS[Math.floor(rand() * WORDS.length)]);
  }
  return `${words.join(" ")}.`;
}

const BLOCKS = Array.from({ length: 300 }, (_, i) => randomBlock(i * 112 + 1));

export default function PartsPlayground() {
  return (
    <ScrollView className="flex-1 bg-background p-4 pt-safe">
      {BLOCKS.map((markdown, i) => (
        <View key={i}>
          <EnrichedMarkdownText
            containerStyle={{
              backgroundColor: "red",
            }}
            flavor="github"
            markdown={markdown}
          />
          <View className="h-4" />
        </View>
      ))}
    </ScrollView>
  );
}

Copy link
Copy Markdown
Collaborator

@hryhoriiK97 hryhoriiK97 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I've left one comment 🙂

@istarkov
Copy link
Copy Markdown
Contributor Author

Finally nor round nor ceil is right
Example below on android 9a fails with round, with ceil and with pre PR.

Calculations definitely depend on left position of the text
Changing
marginLeft: 17, to marginLeft: 16, fixes issue when ceil(width) is set

Seems like real calculation is ceil(right) - floor(left) and not any operation around width only

cc @hryhoriiK97

import { PixelRatio, View } from "react-native";
import { EnrichedMarkdownText } from "react-native-enriched-markdown";

const markdownB = `, it emphasizes higher goals requires a fundamental change in thinking and effort rather than working more odntly emphasizes that reaching higher goals  a fundamental change in thinking and effort rather than simply working more hours. fields, it consistently that reaching higher goals fundamental change in thinking and effort rather than simply working more hours.`;

const counterCeilExample = `33333 aa xx 1 rrrrrr rrrrrr 1 www rrrrrr 111 www www 1 e , 111 1 e rrrrrr , aa www w e xx www aa 33333 111 1 rrrrrr aa e 33333 , e aa 111 w xx e , e 111 rrrrrr aa xx xx rrrrrr , aa xx e rrrrrr w 111 33333 w 33333 , w 111.`;

export default function PartsPlayground() {
  console.log("rendering", PixelRatio.get(), PixelRatio.roundToNearestPixel(379));
  return (
    <View className="flex-1 bg-background p-0 pt-safe">
      <EnrichedMarkdownText
        containerStyle={{
          backgroundColor: "red",
          width: 379,
          marginLeft: 16,
        }}
        flavor="github"
        markdown={markdownB}
      />
      <View className="h-6" />
      <EnrichedMarkdownText
        containerStyle={{
          backgroundColor: "red",
          width: 103.5,
          marginLeft: 17,
        }}
        flavor="github"
        markdown={counterCeilExample}
      />
    </View>
  );
}

@istarkov
Copy link
Copy Markdown
Contributor Author

It seems like the ceil is the best strategy at least now.

If

yogaWidth = ceil(right) - floor(left)

ceilWidth = ceil(right - left) then yogaWidth - ceilWidth ∈ {0, 1}
roundWidth = round(right - left) then yogaWidth - roundWidth ∈ {0, 1, 2}
and same for floor ∈ {0, 1, 2}

So for ceilWidth we have that max error is just 1, and 2 for others. At least seriously less probability to get the issue.

Even if some re-measurement will be done in the future (or now but I have no idea how) ceil seems like the best choice.

@hryhoriiK97
Copy link
Copy Markdown
Collaborator

It seems like the ceil is the best strategy at least now.

If

yogaWidth = ceil(right) - floor(left)

ceilWidth = ceil(right - left) then yogaWidth - ceilWidth ∈ {0, 1} roundWidth = round(right - left) then yogaWidth - roundWidth ∈ {0, 1, 2} and same for floor ∈ {0, 1, 2}

So for ceilWidth we have that max error is just 1, and 2 for others. At least seriously less probability to get the issue.

Even if some re-measurement will be done in the future (or now but I have no idea how) ceil seems like the best choice.

Nice analysis, ceil is indeed the best option here 👍 Thank you for the PR 🙌

Copy link
Copy Markdown
Collaborator

@hryhoriiK97 hryhoriiK97 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@istarkov thank you for the fix! It will be available in tomorrow’s nightly release 🙂

@hryhoriiK97 hryhoriiK97 merged commit dec4a37 into software-mansion-labs:main Apr 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect text measurement on Android.

2 participants