Skip to content

feat(android): honor textDecorationStyle on Text decorations#56768

Open
quantizor wants to merge 2 commits into
facebook:mainfrom
quantizor:feat-text-decoration-style-android
Open

feat(android): honor textDecorationStyle on Text decorations#56768
quantizor wants to merge 2 commits into
facebook:mainfrom
quantizor:feat-text-decoration-style-android

Conversation

@quantizor
Copy link
Copy Markdown

@quantizor quantizor commented May 11, 2026

Summary:

textDecorationStyle is declared on TextStyleAndroid in the public types but TA_KEY_TEXT_DECORATION_STYLE was a no-op handler: every value silently rendered as a solid line, and wavy was additionally rejected at the Fabric C++ enum boundary with an Unsupported value log. This PR wires the prop through the existing C++ → Kotlin pipeline and implements solid, double, dotted, dashed, and wavy for both underlines and strikethroughs.

Android's Layout.draw paints the underline produced by setUnderlineText(true) using paint.color only and offers no native way to draw a dotted / dashed / wavy decoration. ReactUnderlineSpan and ReactStrikethroughSpan now extend DrawCommandSpan and paint the decoration themselves in onDraw via Canvas.drawLine / Canvas.drawPath, dispatching by style. As a side effect this also makes textDecorationColor reach the paint, closing the separate long-standing gap filed as #4579 in 2015 — the companion color-focused PR #56767 isolates that fix for reviewers who only want the color change.

TextDecorationStyle::Wavy is added to the Fabric C++ primitives / conversions so the wavy JS value flows through; the same enum is shared with iOS (see companion iOS PR #56769).

The wavy curve uses Chromium/Blink's formula from decoration_line_painter.cc (wavelength = 1 + 2 * round(2 * thickness + 0.5), controlPointDistance = 0.5 + round(3 * thickness + 0.5), one cubic Bezier per wavelength with both control points at the midpoint, one above and one below the y-axis). The minimum stroke thickness is density-aware (1.5 dp) so decorations read consistently across display densities. The drawing loop iterates while x < x2 so the final cycle continues through the last character (including trailing punctuation that would otherwise be visually uncovered when the run width is not an integer multiple of the wavelength).

ReactTextView.onDraw invokes DrawCommandSpan.onDraw after super.onDraw, mirroring what PreparedLayoutTextView.onDraw already did. Without this, the new spans have no effect on the older view class, which is what some Text components on the new architecture still route through.

Companion PRs (independent, also targeting main):

Changelog:

[GENERAL] [ADDED] - textDecorationStyle: 'wavy' for <Text> (see corresponding iOS PR for the iOS counterpart)
[ANDROID] [ADDED] - Text decorations honor textDecorationStyle (solid, double, dotted, dashed, wavy)

Test Plan:

Rendered <Text> components with textDecorationLine set to "underline" or "line-through" and textDecorationStyle cycling through solid / double / dotted / dashed / wavy. On stock 0.85.2 every value renders as a solid line and wavy logs an Unsupported value warning; with this patch each style renders with the requested stroke geometry. Verified single-line and wrapped multi-line cases on an Android API 36 emulator: each visual line within a wrapped block receives its own correctly-styled decoration that starts and ends at the line's content boundaries.

<Text style={{
  color: 'black',
  textDecorationLine: 'underline',
  textDecorationStyle: 'wavy',
}}>
  Hello
</Text>

`textDecorationStyle` is declared on `TextStyleAndroid` in the public
types but `TA_KEY_TEXT_DECORATION_STYLE` was a no-op handler: every
value silently rendered as a solid line. This PR wires the prop through
the existing C++ → Kotlin pipeline and implements `solid`, `double`,
`dotted`, `dashed`, and `wavy` for both underlines and strikethroughs.

Background: Android's `Layout.draw` paints the underline produced by
`setUnderlineText(true)` using `paint.color`, ignoring
`paint.underlineColor` on every API level, and offers no native way to
draw a dotted / dashed / wavy decoration. The same applies to
strikethrough. `ReactUnderlineSpan` and `ReactStrikethroughSpan` now
extend `DrawCommandSpan` and paint the decoration themselves in
`onDraw` via `Canvas.drawLine` / `Canvas.drawPath`, dispatching by
style. This also makes `textDecorationColor` reach the paint as a side
effect, closing a separate long-standing gap (see facebook#4579 from 2015).

`TextDecorationStyle::Wavy` is added to the Fabric C++ primitives /
conversions so the JS value flows through instead of being rejected
with an `Unsupported value` log; the same enum is shared with iOS.

The wavy curve uses Chromium/Blink's formula from
`decoration_line_painter.cc` (`wavelength = 1 + 2 * round(2 * thickness
+ 0.5)`, `controlPointDistance = 0.5 + round(3 * thickness + 0.5)`,
one cubic Bezier per wavelength with both control points at the
midpoint, one above and one below the y-axis). The minimum stroke
thickness is density-aware (1.5 dp) so decorations read consistently
across display densities. The drawing loop iterates `while x < x2` so
the final cycle continues through the last character (including
trailing punctuation that would otherwise be visually uncovered when
the run width is not an integer multiple of the wavelength).

`ReactTextView.onDraw` invokes `DrawCommandSpan.onDraw` after
`super.onDraw`, mirroring what `PreparedLayoutTextView.onDraw` already
did. Without this, the new spans have no effect on the older view
class, which is what some Text components on the new architecture
still route through.

## Changelog:

[GENERAL] [ADDED] - `textDecorationStyle: 'wavy'` for `<Text>` (see corresponding iOS PR for the iOS counterpart)
[ANDROID] [ADDED] - Text decorations honor `textDecorationStyle` (`solid`, `double`, `dotted`, `dashed`, `wavy`)

## Test Plan:

Rendered `<Text>` components with `textDecorationLine` set to
`"underline"` or `"line-through"` and `textDecorationStyle` cycling
through `solid` / `double` / `dotted` / `dashed` / `wavy`. On stock
0.85.2 every value renders as a solid line and `wavy` logs an
`Unsupported value` warning; with this patch each style renders with
the requested stroke geometry. Verified single-line and wrapped
multi-line cases on an Android API 36 emulator: each visual line
within a wrapped block receives its own correctly-styled decoration
that starts and ends at the line's content boundaries.

```tsx
<Text style={{
  color: 'black',
  textDecorationLine: 'underline',
  textDecorationStyle: 'wavy',
}}>
  Hello
</Text>
```
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label May 11, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented May 11, 2026

@CalixTang has imported this pull request. If you are a Meta employee, you can view this in D104680895.

@github-actions
Copy link
Copy Markdown

Warning

JavaScript API change detected

This PR commits an update to ReactNativeApi.d.ts, indicating a change to React Native's public JavaScript API.

  • Please include a clear changelog message.
  • This change will be subject to additional review.

This change was flagged as: POTENTIALLY_BREAKING

Three CI breakages were introduced by changes that landed on `main`
between the original PR push and now:

1. PR facebook#56705 renamed `DrawCommandSpan` to `CanvasEffectSpan` and dropped
   the `ReactSpan` / `UpdateAppearance` interfaces from the base class.
   `ReactUnderlineSpan` and `ReactStrikethroughSpan` were extending the
   old name; rename them and re-declare `ReactSpan` so they remain
   valid `SetSpanOperation` arguments. `ReactTextView.onDraw` updated
   to import the new name.

2. Adding `Wavy` to `facebook::react::TextDecorationStyle` (this PR)
   left `RCTNSUnderlineStyleFromTextDecorationStyle` non-exhaustive,
   tripping `-Werror,-Wreturn-type` on iOS builds. Add a `Wavy` case
   that falls back to a solid underline (the actual wavy rendering
   ships in companion PR facebook#56769).

3. `validate_cxx_api_snapshots` flagged the missing `Wavy` entry in
   all six snapshots under `scripts/cxx-api/api-snapshots/`.
   Regenerate.

No behavior change beyond what was already in the feature commit; this
is purely "rebase the implementation onto current `main`."
@quantizor quantizor force-pushed the feat-text-decoration-style-android branch from 3b58384 to 676ed17 Compare May 11, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Issue with textDecoration

1 participant