diff --git a/app/src/main/kotlin/org/bspeice/minimalbible/activity/viewer/BookAdapter.kt b/app/src/main/kotlin/org/bspeice/minimalbible/activity/viewer/BookAdapter.kt index 0c6a64e..6057208 100644 --- a/app/src/main/kotlin/org/bspeice/minimalbible/activity/viewer/BookAdapter.kt +++ b/app/src/main/kotlin/org/bspeice/minimalbible/activity/viewer/BookAdapter.kt @@ -12,6 +12,11 @@ import org.crosswire.jsword.versification.BibleBook import org.bspeice.minimalbible.activity.viewer.BookAdapter.ChapterInfo import rx.subjects.PublishSubject import org.bspeice.minimalbible.service.lookup.VerseLookup +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan +import android.graphics.Typeface +import android.text.style.SuperscriptSpan +import android.text.style.RelativeSizeSpan /** * Adapter used for displaying a book @@ -25,8 +30,16 @@ class BookAdapter(val b: Book, val lookup: VerseLookup) val bookList = versification.getBooks() val chapterCount = bookList.map { versification.getLastChapter(it) - 1 }.sum() + /** + * Store information needed to decode the text of a chapter + * Book, chapter, and bibleBook should be pretty self-explanatory + * The vStart, vEnd, and vOffset are needed to map between verses relative to their chapter, + * and the actual verse ordinal needed for parsing the text. + * So Genesis 1:1 would be chapter 1, bibleBook Genesis, vStart 1, vOffset 2 + * since it actually starts at ordinal 3 + */ data class ChapterInfo(val book: Book, val chapter: Int, val bibleBook: BibleBook, - val vStart: Int, val vEnd: Int) + val vStart: Int, val vEnd: Int, val vOffset: Int) /** * A list of all ChapterInfo objects needed for displaying a book @@ -37,17 +50,22 @@ class BookAdapter(val b: Book, val lookup: VerseLookup) * } yield ChapterInfo(...) * * Also note that getLastVerse() returns the number of verses in a chapter, - * so we build the actual last verse by adding getFirstVerse and getLastVerse + * not the actual last verse's ordinal */ // TODO: Lazy compute values needed for this list val chapterList: List = bookList.flatMap { val currentBook = it (1..versification.getLastChapter(currentBook)).map { - val firstVerse = versification.getFirstVerse(currentBook, it) + val firstVerseOrdinal = versification.getFirstVerse(currentBook, it) + val verseOrdinalOffset = firstVerseOrdinal - 1 val verseCount = versification.getLastVerse(currentBook, it) - ChapterInfo(b, it, currentBook, firstVerse, firstVerse + verseCount) + val firstVerseRelative = 1 + val lastVerseRelative = firstVerseRelative + verseCount + + ChapterInfo(b, it, currentBook, + firstVerseRelative, lastVerseRelative, verseOrdinalOffset) } - }; + } /** * I'm not sure what the position argument actually represents, @@ -94,13 +112,82 @@ class BookAdapter(val b: Book, val lookup: VerseLookup) class PassageView(val v: TextView, val b: Book, val lookup: VerseLookup) : RecyclerView.ViewHolder(v) { - fun getVerseText(verseRange: Progression) = - verseRange.map { lookup.getText(b.getVersification().decodeOrdinal(it)) } + // Span to be applied to an individual verse - doesn't know about the sizes + // of other verses so that's why start and end are relative + /** + * A holder object that knows how apply itself to a SpannableStringBuilder + * Since we don't know ahead of time where this verse will end up relative to the + * entire TextView (since there is one chapter per TextView) we use a start and end + * relative to the verse text itself. That is, rStart of 0 indicates verse text start, + * and rEnd of (text.length - 1) indicates the end of verse text + * @param span The span object we should apply + * @param rStart When the span should begin, relative to the verse + * @param rEnd When the span should end, relative to the verse + */ + data class SpanHolder(val span: Any?, val rStart: Int, val rEnd: Int) { + // TODO: Is there a more case-class like way of doing this? + class object { + val EMPTY = SpanHolder(null, 0, 0) + } + /** + * Apply this span object to the specified builder + * Tries to be as close to immutable as possible. The offset is used to calculate + * the absolute position of when this span should start and end, since + * rStart and rEnd are relative to the verse text, and know nothing about the + * rest of the text in the TextView + */ + fun apply(builder: SpannableStringBuilder, offset: Int): SpannableStringBuilder { + if (span != null) + builder.setSpan(span, rStart + offset, rEnd + offset, 0) + return builder + } + } - fun reduceText(verses: List) = verses.join(" ") + // TODO: getRawVerse shouldn't know how to decode ints + fun getRawVerse(verse: Int): String = + lookup.getText(b.getVersification().decodeOrdinal(verse)) + + // TODO: This code is nasty, not sure how to refactor, but it needs doing + fun getProcessedVerse(verseOrdinal: Int, info: ChapterInfo): Pair> { + val rawText = getRawVerse(verseOrdinal) + // To be honest, I have no idea why I need to subtract one. But I do. + val relativeVerse = verseOrdinal - info.vOffset - 1 + val processedText = when (relativeVerse) { + 0 -> "" + 1 -> "${info.chapter} $rawText" + else -> "$relativeVerse$rawText" + } + val spans: List = listOf( + when (relativeVerse) { + 0 -> SpanHolder.EMPTY + 1 -> SpanHolder(StyleSpan(Typeface.BOLD), 0, info.chapter.toString().length) + else -> SpanHolder(SuperscriptSpan(), 0, relativeVerse.toString().length) + } + ) + val secondSpan = + if (relativeVerse > 1) + // TODO: No magic numbers! + spans plus SpanHolder(RelativeSizeSpan(0.6f), 0, relativeVerse.toString().length) + else + spans + + return Pair(processedText, secondSpan) + } + + fun getAllVerses(verses: Progression, info: ChapterInfo) = + verses.map { getProcessedVerse(it + info.vOffset, info) } + // For each verse, get the text + .fold(SpannableStringBuilder(), { initialBuilder, versePair -> + val offset = initialBuilder.length() + val builderWithText = initialBuilder append versePair.first + + // And apply all spans + versePair.second.fold(builderWithText, { postBuilder, span -> + span.apply(postBuilder, offset) + }) + }) - // Uses functional style, but those parentheses man... you'd think I was writing LISP fun bind(info: ChapterInfo) { - v.setText(reduceText(getVerseText(info.vStart..info.vEnd))) + v setText getAllVerses(info.vStart..info.vEnd, info) } }