From dda7f82261cc5684564e2c67071c13e379985308 Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Thu, 16 Apr 2020 01:30:48 -0700 Subject: [PATCH] RN: Shrinkwrap Text Layout (Android) Summary: When text is in a constrained parent view using `maxWidth`, long text may wrap. When the text wraps, the final width is dependent on the word breaking strategy and text content. This means that the text width is not necessarily `maxWidth`. However, the current way that we compute text layout does not shrinkwrap the text width as much as possible. This leads to visual gaps to the end-side of wrapped text. This changes the text layout slightly so that we use the length of the longest line. This bug only exists on Android. After this change, Android behaves like iOS. Changelog: [Android] [Fixed] - Fixed excessive space in Text view with word-wrapping Reviewed By: JoshuaGross, mdvacca Differential Revision: D21056031 fbshipit-source-id: e9b7793f2632caafcce69bc15bac61330b0ed958 --- .../react/views/text/ReactTextShadowNode.java | 20 ++++++++++++----- .../react/views/text/TextLayoutManager.java | 22 ++++++++++++------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 2a3a5b604dd..b9fc8e75a25 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -122,12 +122,22 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { } } - if (mNumberOfLines != UNSET && mNumberOfLines < layout.getLineCount()) { - return YogaMeasureOutput.make( - layout.getWidth(), layout.getLineBottom(mNumberOfLines - 1)); - } else { - return YogaMeasureOutput.make(layout.getWidth(), layout.getHeight()); + final int lineCount = + mNumberOfLines == UNSET + ? layout.getLineCount() + : Math.min(mNumberOfLines, layout.getLineCount()); + + // Instead of using `layout.getWidth()` (which may yield a significantly larger width for + // text that is wrapping), compute width using the longest line. + float layoutWidth = 0; + for (int lineIndex = 0; lineIndex < lineCount; lineIndex++) { + float lineWidth = layout.getLineWidth(lineIndex); + if (lineWidth > layoutWidth) { + layoutWidth = lineWidth; + } } + + return YogaMeasureOutput.make(layoutWidth, layout.getLineBottom(lineCount - 1)); } }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index 93aaf3d4866..d2a68fd4550 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -294,14 +294,20 @@ public class TextLayoutManager { ? paragraphAttributes.getInt("maximumNumberOfLines") : UNSET; - int calculatedWidth = layout.getWidth(); - int calculatedHeight; - if (maximumNumberOfLines != UNSET - && maximumNumberOfLines != 0 - && maximumNumberOfLines < layout.getLineCount()) { - calculatedHeight = layout.getLineBottom(maximumNumberOfLines - 1); - } else { - calculatedHeight = layout.getHeight(); + int calculatedLineCount = + maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + ? layout.getLineCount() + : Math.min(maximumNumberOfLines, layout.getLineCount()); + + int calculatedHeight = layout.getLineBottom(calculatedLineCount - 1); + // Instead of using `layout.getWidth()` (which may yield a significantly larger width for + // text that is wrapping), compute width using the longest line. + int calculatedWidth = 0; + for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) { + float lineWidth = layout.getLineWidth(lineIndex); + if (lineWidth > calculatedWidth) { + calculatedWidth = (int) Math.ceil(lineWidth); + } } // Calculate the positions of the attachments (views) that will be rendered inside the Spanned