Add spans for chapter start and verse superscript

Needs serious refactoring, but quickly approaching a minimally viable product. This is awesome.
This commit is contained in:
Bradlee Speice 2014-11-27 23:36:35 -05:00
parent 799d8e2637
commit 320159f1bd

View File

@ -12,6 +12,11 @@ import org.crosswire.jsword.versification.BibleBook
import org.bspeice.minimalbible.activity.viewer.BookAdapter.ChapterInfo import org.bspeice.minimalbible.activity.viewer.BookAdapter.ChapterInfo
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import org.bspeice.minimalbible.service.lookup.VerseLookup 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 * Adapter used for displaying a book
@ -25,8 +30,16 @@ class BookAdapter(val b: Book, val lookup: VerseLookup)
val bookList = versification.getBooks() val bookList = versification.getBooks()
val chapterCount = bookList.map { versification.getLastChapter(it) - 1 }.sum() 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, 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 * 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(...) * } yield ChapterInfo(...)
* *
* Also note that getLastVerse() returns the number of verses in a chapter, * 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 // TODO: Lazy compute values needed for this list
val chapterList: List<ChapterInfo> = bookList.flatMap { val chapterList: List<ChapterInfo> = bookList.flatMap {
val currentBook = it val currentBook = it
(1..versification.getLastChapter(currentBook)).map { (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) 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, * 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) class PassageView(val v: TextView, val b: Book, val lookup: VerseLookup)
: RecyclerView.ViewHolder(v) { : RecyclerView.ViewHolder(v) {
fun getVerseText(verseRange: Progression<Int>) = // Span to be applied to an individual verse - doesn't know about the sizes
verseRange.map { lookup.getText(b.getVersification().decodeOrdinal(it)) } // 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<String>) = 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<String, List<SpanHolder>> {
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<SpanHolder> = 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<Int>, 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) { fun bind(info: ChapterInfo) {
v.setText(reduceText(getVerseText(info.vStart..info.vEnd))) v setText getAllVerses(info.vStart..info.vEnd, info)
} }
} }