Bible verse text goes through a multi-stage pipeline from binary storage to rendered SpannableStringBuilder displayed in a RecyclerView.
YES2 binary file
→ Yes2Reader.loadChapterText()
→ SingleChapterVerses (array of raw verse strings with formatting codes)
→ VersesDataModel (merges verses + pericope headers into display list)
→ VersesControllerImpl (RecyclerView adapter)
→ VerseRenderer.render() (applies formatting codes as spans)
→ VerseItem (custom RelativeLayout with background drawing)
Verse text uses inline formatting codes prefixed with @:
| Code | Meaning |
|---|---|
@@ |
Marks verse as having formatting (must be first) |
@0 |
Paragraph level 0 (no indent) |
@1–@4 |
Paragraph indent levels 1–4 |
@^ |
Continuation indent |
@6 |
Red letter start (Jesus' words) |
@5 |
Red letter end |
@9 |
Italic start |
@7 |
Italic end |
@8 |
Line break / blank line |
@<tag@> |
Start of special inline element (xref, footnote, ruby) |
@/ |
End of special inline element |
Special inline elements:
| Element | Meaning |
|---|---|
@<f1@>@/ |
Footnote link, field 1 (empty content) |
@<x1@>@/ |
Cross-reference link, field 1 (empty content) |
@<r=reading@>base@/ |
Ruby: reading is drawn above base. A one-letter kind may follow r (@<rf=…@> furigana, @<rp=…@> pinyin, @<rs=…@> Strong's, …); it is reported on RubyRange.kind and does not change rendering |
VerseRenderer.kt processes formatting codes and produces a SpannableStringBuilder:
- Detects
@@prefix to determine if verse has formatting - Iterates through characters, building spans for each formatting region
- Applies
ForegroundColorSpanfor red letters,StyleSpan(ITALIC)for italics - Handles indentation via
LeadingMarginSpan - Processes
@<tag@>blocks for cross-references and footnotes - Verse numbers are prepended as superscript spans
FormattedTextRenderer.kt is a lightweight production renderer — "a much simpler version of VerseRenderer" per its own doc comment — used where only a subset of the formatting codes matters (italics @9…@7, line break @8, @<tag@>…@/ inline elements). It builds a SpannableStringBuilder directly, without the verse-number/paragraph machinery of the full pipeline.
A Compose port of verse rendering also exists (VerseRendererCompose.kt) alongside the View-based VerseRenderer.kt.
@<r=reading@>base@/ keeps base inline in the AnnotatedString and records a VerseRendererCompose.RubyRange for it, so highlight offsets, dictionary links and TalkBack text are unaffected by the annotation. The verse composable then draws the reading above the base:
computeLineMetricsreserves a band above every line (LineMetrics.rubyBandPx) when the verse has ruby, and the text is laid out withLineHeightStyle.Alignment.Bottomso the whole surplus sits on top of the glyphs.widenRubyBasesmakes room for a reading wider than its base.rubySideSlackPxvalues the room already beside the base: the whitespace between two words counts, halved when the word beyond it is annotated too, a neighbouring character without ruby is worth one ruby em of overhang (RUBY_OVERHANG_RATIO), and a reading directly abutting the base costs a gap (RUBY_SIDE_GAP_RATIO). Whatever is still missing is added to the space characters flanking the base, half per side, so a Strong's number over a word as short asairwidens the gaps around it rather than spreading it intoa i r. Only a base with no space on a side that needs room, such as one Han character between two others, falls back to letter spacing inside the base.Modifier.rubyOverlaypaints each reading over its base using theTextLayoutResult. Placement is decided per line and perTextLayoutResult, not from the text, because only the laid-out geometry knows where the lines broke and how wide the padded gaps ended up: a base that starts a wrapped line has no room to its left however much whitespace precedes it in the text. Readings on a line are ordered by where they were laid out, so a right-to-left run is bounded by the neighbours a reader sees beside it.rubyLineLayoutPxthen places each one centred over its base (rubyCentredLeftPx), pushes right any that would collide with the one before, and pulls the line back from the right edge, so a reading far wider than its base stays whole by borrowing room a narrower neighbour is not using. Only a line holding more than it can fit ellipsises, and even then positions rise left to right so two readings never overlap. A base run broken across lines gets a proportional slice of the reading on each line (rubySliceFor). The reading takes the color of the innermost colored span under its centre character (rubyColorAt), so red-letter and highlighted, selected runs stay readable.- Reading size is
RUBY_FONT_SIZE_RATIO(0.5) of the verse size.
The View-based VerseRenderer ignores the r tag and shows only the base text, and FormattedVerseText.removeSpecialCodes strips the reading, so search, copy and share operate on the base text. VerseRubySnapshotTest renders sample sheets (basics, poetry, highlights, inline styles, typography variants) to Alkitab/build/snapshots/verse-ruby/ for visual review. See docs/features/ruby/design.md for the data format, sources and open items.
FormattedVerseText.removeSpecialCodes() strips all @-codes to produce plain text for:
- Clipboard copy operations
- Search indexing
- Share text generation
This is important — search and copy must use the stripped text, not raw formatted text.
Pericopes (section headers like "The Sermon on the Mount") are rendered as distinct items in the RecyclerView, interleaved with verses. The VersesDataModel.itemPointer array maps display positions:
- Negative values → pericope index (bitwise NOT)
- Non-negative values → verse index (0-based)
VerseItem.kt is a custom RelativeLayout that handles:
- Checked/selected state visual feedback (colored background)
- "Attention" animation (pulse highlight when navigating to a verse)
- Drag-and-drop for progress mark pins
- Accessibility (TalkBack support with verse number and text)
- Highlight color painting on the background canvas
A checked verse paints the selection color over the page at TextColorUtil.CHECKED_VERSE_OVERLAY_ALPHA. The host then forces the text to black or white via TextColorUtil.getForCheckedVerse, based on the selection color alone.
A highlight band is drawn on top of that overlay. So both renderers give a highlighted run its own color through TextColorUtil.getForCheckedVerseHighlight, which picks whichever of the reading color or the forced color has better contrast against what the band actually paints. Words of Jesus lose their red in a checked verse and follow the same per-run color.
Dictionary links (DictionaryLinkSpan, and the Compose addDictionaryLinks) only underline. They never set their own color, so they follow the run they sit in.
VerseTextColorSnapshotTest renders every theme, selection color, highlight color and selection state through both pipelines into Alkitab/build/snapshots/verse-text-color/ for visual review.
Font size is controlled by:
- Base
textSizepreference (user setting) - Per-version
textSizeMultmultiplier (fromPerVersionsettings) CalculatedDimensionsinS.applied()precomputes final sizes
Two-finger pinch gesture in IsiActivity adjusts the base text size in real time.