package com.cyrilmottier.android.queryhighlight.text.format; 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 com.cyrilmottier.android.queryhighlight.text.Normalizer; import java.util.Locale; import java.util.Objects; public final class QueryHighlighter { public enum Mode { CHARACTERS, WORDS } public static abstract class QueryNormalizer { public static final QueryNormalizer FOR_SEARCH = new QueryNormalizer() { @Override public CharSequence normalize(CharSequence source) { return Normalizer.forSearch(source); } }; public static final QueryNormalizer CASE = new QueryNormalizer() { @Override public CharSequence normalize(CharSequence source) { if (TextUtils.isEmpty(source)) { return source; } return source.toString().toUpperCase(Locale.ROOT); } }; public static final QueryNormalizer NONE = new QueryNormalizer() { @Override public CharSequence normalize(CharSequence source) { return source; } }; public abstract CharSequence normalize(CharSequence source); } private CharacterStyle mHighlightStyle = new StyleSpan(Typeface.BOLD); private QueryNormalizer mQueryNormalizer = QueryNormalizer.NONE; private Mode mMode = Mode.WORDS; public QueryHighlighter setHighlightStyle(CharacterStyle highlightStyle) { mHighlightStyle = Objects.requireNonNull(highlightStyle, "highlightStyle cannot be null"); return this; } public QueryHighlighter setQueryNormalizer(QueryNormalizer queryNormalizer) { mQueryNormalizer = Objects.requireNonNull(queryNormalizer, "queryNormalizer cannot be null"); return this; } public QueryHighlighter setMode(Mode mode) { mMode = Objects.requireNonNull(mode, "mode cannot be null"); return this; } public CharSequence apply(CharSequence text, CharSequence wordPrefix) { final CharSequence normalizedText = mQueryNormalizer.normalize(text); final CharSequence normalizedWordPrefix = mQueryNormalizer.normalize(wordPrefix); final int index = indexOfQuery(normalizedText, normalizedWordPrefix); if (index != -1) { final SpannableString result = new SpannableString(text); result.setSpan(mHighlightStyle, index, index + normalizedWordPrefix.length(), 0); return result; } else { return text; } } public void setText(TextView view, CharSequence text, CharSequence query) { view.setText(apply(text, query)); } private int indexOfQuery(CharSequence text, CharSequence query) { if (query == null || text == null) { return -1; } final int textLength = text.length(); final int queryLength = query.length(); if (queryLength == 0 || textLength < queryLength) { return -1; } for (int i = 0; i <= textLength - queryLength; i++) { // Only match word prefixes if (mMode == Mode.WORDS && i > 0 && text.charAt(i - 1) != ' ') { continue; } int j; for (j = 0; j < queryLength; j++) { if (text.charAt(i + j) != query.charAt(j)) { break; } } if (j == queryLength) { return i; } } return -1; } }