Skip to content

Instantly share code, notes, and snippets.

@alashow
Last active March 15, 2020 15:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alashow/692261fbbdaac24f5853bd9dc0e74958 to your computer and use it in GitHub Desktop.
Save alashow/692261fbbdaac24f5853bd9dc0e74958 to your computer and use it in GitHub Desktop.
import android.graphics.Typeface
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.CharacterStyle
import android.text.style.StyleSpan
import android.widget.TextView
import java.text.Normalizer
import java.util.Locale
import java.util.regex.Pattern
/**
* From https://cyrilmottier.com/2017/03/06/highlighting-search-terms/
*/
class QueryHighlighter(
var highlightStyle: CharacterStyle = StyleSpan(Typeface.BOLD),
var queryNormalizer: QueryNormalizer = QueryNormalizer.FOR_SEARCH,
var mode: Mode = Mode.CHARACTERS
) {
enum class Mode { CHARACTERS, WORDS }
class QueryNormalizer(var normalizer: (source: CharSequence) -> CharSequence = { s -> s }) {
operator fun invoke(source: CharSequence) = normalizer(source)
companion object {
val NONE: QueryNormalizer = QueryNormalizer()
val CASE: QueryNormalizer = QueryNormalizer { source ->
if (TextUtils.isEmpty(source)) {
source
} else source.toString().toUpperCase(Locale.ROOT)
}
private val PATTERN_DIACRITICS = Pattern.compile("\\p{InCombiningDiacriticalMarks}+")
private val PATTERN_NON_LETTER_DIGIT_TO_SPACES = Pattern.compile("[^\\p{L}\\p{Nd}]")
val FOR_SEARCH: QueryNormalizer = QueryNormalizer { searchTerm ->
var result = Normalizer.normalize(searchTerm, Normalizer.Form.NFD)
result = PATTERN_DIACRITICS.matcher(result).replaceAll("")
result = PATTERN_NON_LETTER_DIGIT_TO_SPACES.matcher(result).replaceAll(" ")
result.toLowerCase(Locale.ROOT)
}
}
}
fun apply(text: CharSequence, wordPrefix: CharSequence): CharSequence {
val normalizedText = queryNormalizer(text)
val normalizedWordPrefix = queryNormalizer(wordPrefix)
val index = indexOfQuery(normalizedText, normalizedWordPrefix)
return if (index != -1) {
SpannableString(text).apply {
setSpan(highlightStyle, index, index + normalizedWordPrefix.length, 0)
}
} else text
}
fun apply(view: TextView, text: CharSequence, query: CharSequence) {
view.text = apply(text, query)
}
private fun indexOfQuery(text: CharSequence?, query: CharSequence?): Int {
if (query == null || text == null) {
return -1
}
val textLength = text.length
val queryLength = query.length
if (queryLength == 0 || textLength < queryLength) {
return -1
}
for (i in 0..textLength - queryLength) {
// Only match word prefixes
if (mode == Mode.WORDS && i > 0 && text[i - 1] != ' ') {
continue
}
var j = 0
while (j < queryLength) {
if (text[i + j] != query[j]) {
break
}
j++
}
if (j == queryLength) {
return i
}
}
return -1
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment