I am developing an emoji keyboard for android but don't know how to add a animated emoji in currentInputConnection of InputMethodService.
Edittext content= findVie......
sb = new SpannableStringBuilder();
String dummyText = "-";
sb.append(dummyText);
try {
sb.setSpan(anim = new AnimatedImageSpan(new AnimatedGifDrawable(
getAssets().open("54.gif"),
new AnimatedGifDrawable.UpdateListener() {
#Override
public void update() {
content.requestLayout();
content.invalidate();
}
})), sb.length() - dummyText.length(), sb.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
content.setText(sb);
here content is an editText view but I don't have any edit text view. All I have is currentInputConnection return by InputMethodService.getCurrentInputConnection().
If I understand you correctly, you're trying to send a "animated image" (sth like a gif) to EditText right? As far as I know, that may be impossible. If you have read the documentation of InputConnection, you should know that it doesn't provide any API to do this.
Actually I'm working on an Android IME project in which we implement an static Emoji input keyboard. All we do is to simply send the encoded bytes of emoji through InputConnection then the smiling face appears in the edit box.
If you have used WeChat, you may notice that its client has implemented a custom emoji keyboard(including static and dynamic content). That's because they know exactly what they're doing.
I am also trying to do the same thing. then i got my answer that .
If you want to implement within your app and sent message to your own app.
Then you can map each image pattern to some value.
////////
public class ViewsUtils {
private static final Map<Pattern, Integer> emoticons = new HashMap<Pattern, Integer>();
static {
addPattern(emoticons, "\ud83d\udeb6", R.drawable.emot_d83ddeb6);
...
}
private static void addPattern(Map<Pattern, Integer> map, String smile,
int resource) {
map.put(Pattern.compile(Pattern.quote(smile)), resource);
}
public static boolean addSmiles(Context context, Spannable spannable) {
boolean hasChanges = false;
for (Entry<Pattern, Integer> entry : emoticons.entrySet()) {
Matcher matcher = entry.getKey().matcher(spannable);
while (matcher.find()) {
boolean set = true;
for (ImageSpan span : spannable.getSpans(matcher.start(),
matcher.end(), ImageSpan.class))
if (spannable.getSpanStart(span) >= matcher.start()
&& spannable.getSpanEnd(span) <= matcher.end())
spannable.removeSpan(span);
else {
set = false;
break;
}
if (set) {
hasChanges = true;
spannable.setSpan(new ImageSpan(context, entry.getValue()),
matcher.start(), matcher.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
return hasChanges;
}
public static void setText(TextView view, String text) {
if (null != view && null != text) {
Spanned spanned = Html.fromHtml(text);
SpannableString spannableString = SpannableString.valueOf(spanned);
addSmiles(view.getContext(), spannableString);
view.setText(spannableString);
}
}
public static void setText(View parent, int viewId, int resId) {
if (null != parent) {
String text = parent.getContext().getString(resId);
setText(parent, viewId, text);
}
}
public static void setText(View parent, int viewId, String text) {
if (null != parent) {
TextView view = (TextView) parent.findViewById(viewId);
if (null != view && null != text) {
Spanned spanned = Html.fromHtml(text);
SpannableString spannableString = SpannableString.valueOf(spanned);
addSmiles(view.getContext(), spannableString);
view.setText(spannableString);
}
}
}
public static void setText(View parent, int viewId, String text,
int visibility) {
if (null != parent) {
TextView view = (TextView) parent.findViewById(viewId);
if (null != view && null != text) {
Spanned spanned = Html.fromHtml(text);
SpannableString spannableString = SpannableString.valueOf(spanned);
addSmiles(view.getContext(), spannableString);
view.setText(spannableString);
view.setVisibility(visibility);
}
}
}
}
You just have to add map entries for the emote icons you want to handle in the static block. Using this class is pretty easy after that - just call some of the setText (or call addSmiles directly) methods. It would handle Html parsing too.
Related
I want to use edittext with strike tags. Of course I have already used italic, bold tags.
According to what I have found, android does not support strike tag basically.
https://stackoverflow.com/a/34931560/7068422
I saw this post, but I can't follow it because of an error.
import android.text.Html;
public class StrikeTagHandler {
public class HtmlTagHandler : Object, Html.ITagHandler {
}
}
The error is ITagHandler does not exist.
How should I use strike tag?
T T
You can use :
yourEditText.setPaintFlags(tv.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
I search your question and found solutions:
Based on Combining Spannable with String.format()
YOur code should be:
String html = "<b>bold test</b> <u>underline test</u> <i>italic test</i> <strike>strike test 1</strike> <del>strike test 2</del> <s>strike test 3</s>";
Spanned spanned = Html.fromHtml(html, null, new MyHtmlTagHandler());
tv.setText(spanned);
EditText edt = (EditText) findViewById(R.id.edt);
edt.setText(spanned);
and MyHtmlTagHandler
public class MyHtmlTagHandler implements Html.TagHandler {
public void handleTag(boolean opening, String tag, Editable output,
XMLReader xmlReader) {
if (tag.equalsIgnoreCase("strike") || tag.equals("s")) {
processStrike(opening, output);
}
}
private void processStrike(boolean opening, Editable output) {
int len = output.length();
if (opening) {
output.setSpan(new StrikethroughSpan(), len, len, Spannable.SPAN_MARK_MARK);
} else {
Object obj = getLast(output, StrikethroughSpan.class);
int where = output.getSpanStart(obj);
output.removeSpan(obj);
if (where != len) {
output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
private Object getLast(Editable text, Class kind) {
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
for (int i = objs.length; i > 0; i--) {
if (text.getSpanFlags(objs[i - 1]) == Spannable.SPAN_MARK_MARK) {
return objs[i - 1];
}
}
return null;
}
}
}
I have a textview with multiple clickable spans in it. I want to be able to test clicking these spans.
I tried setting up a custom ViewAction that would find the clickablespans in the TextView and then match their text with the desired text and then click on the xy coordinates of that text. However, it seems the spans added to the TextView aren't of type ClickableSpan and are instead the the fragment that added the span.
Therefore, I am not able to distinguish the link spans. Is there a better way to do this?
Adding the spans:
Util.addClickableSpan(spannableString, string, linkedString, new ClickableSpan() {
#Override
public void onClick(View textView) {}
});
tvAcceptTc.setText(spannableString);
tvAcceptTc.setMovementMethod(LinkMovementMethod.getInstance());
Utility method:
public static void addClickableSpan(SpannableString spannableString,
String text,
String subText,
ClickableSpan clickableSpan) {
int start = text.indexOf(subText);
int end = text.indexOf(subText) + subText.length();
int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
spannableString.setSpan(clickableSpan, start, end, flags);
}
Defining the ViewAction:
#Override
public void perform(UiController uiController, View view) {
uiController.loopMainThreadUntilIdle();
if (view instanceof TextView) {
TextView textView = (TextView) view;
Layout textViewLayout = textView.getLayout();
SpannableString fullSpannable = new SpannableString(textView.getText());
Object[] spans = fullSpannable.getSpans(0, fullSpannable.length(), Object.class);
ClickableSpan span = null;
for (Object object : spans) {
if (object instanceof BaseFragment) {
ClickableSpan foundSpan = (ClickableSpan)object;
int spanStart = fullSpannable.getSpanStart(foundSpan);
int spanEnd = fullSpannable.getSpanEnd(foundSpan);
if (fullSpannable.subSequence(spanStart, spanEnd).equals(aSubstring)) {
//Found the correct span!
span = foundSpan;
}
}
} ... go on to click the xy-coordinates
This is my solution. It's simpler because we don't need to find the coordinates. Once we have found the ClickableSpan, we just click on it:
public static ViewAction clickClickableSpan(final CharSequence textToClick) {
return new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return Matchers.instanceOf(TextView.class);
}
#Override
public String getDescription() {
return "clicking on a ClickableSpan";
}
#Override
public void perform(UiController uiController, View view) {
TextView textView = (TextView) view;
SpannableString spannableString = (SpannableString) textView.getText();
if (spannableString.length() == 0) {
// TextView is empty, nothing to do
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
ClickableSpan spanCandidate;
for (ClickableSpan span : spans) {
spanCandidate = span;
int start = spannableString.getSpanStart(spanCandidate);
int end = spannableString.getSpanEnd(spanCandidate);
CharSequence sequence = spannableString.subSequence(start, end);
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView);
return;
}
}
}
// textToClick not found in TextView
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
};
}
Now you can use our custom ViewAction just like that:
onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));
Here is the Kotlin version of accepted answer
fun clickClickableSpan(textToClick: CharSequence): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> {
return Matchers.instanceOf(TextView::class.java)
}
override fun getDescription(): String {
return "clicking on a ClickableSpan";
}
override fun perform(uiController: UiController, view: View) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
if (spannableString.isEmpty()) {
// TextView is empty, nothing to do
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
if (spans.isNotEmpty()) {
var spanCandidate: ClickableSpan
for (span: ClickableSpan in spans) {
spanCandidate = span
val start = spannableString.getSpanStart(spanCandidate)
val end = spannableString.getSpanEnd(spanCandidate)
val sequence = spannableString.subSequence(start, end)
if (textToClick.toString().equals(sequence.toString())) {
span.onClick(textView)
return;
}
}
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
}
The best option would be to subclass a ViewAction. Here is the way of doing it in Kotlin:
class SpannableTextClickAction(val text: String) : ViewAction {
override fun getDescription(): String = "SpannableText click action"
override fun getConstraints(): Matcher<View> =
isAssignableFrom(TextView::class.java)
override fun perform(uiController: UiController?, view: View?) {
val textView = view as TextView
val spannableString = textView.text as SpannableString
val spans = spannableString.getSpans(0, spannableString.count(), ClickableSpan::class.java)
val spanToLocate = spans.firstOrNull { span: ClickableSpan ->
val start = spannableString.getSpanStart(span)
val end = spannableString.getSpanEnd(span)
val spanText = spannableString.subSequence(start, end).toString()
spanText == text
}
if (spanToLocate != null) {
spanToLocate.onClick(textView)
return
}
// textToClick not found in TextView
throw NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build()
}
}
and use it as:
onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))
It worked with a minor change.
just recheck the "textToClick" and the variable "sequence" in:
CharSequence sequence = spannableString.subSequence(start, end);
are exactly same.
I have to use trim() like this:
textToClick.toString() == sequence.trim().toString()
because my textToClick value is "click here" and sequence value that I got " click here"
Note: The space before the "click".
I hope this is useful for someone.
This works for me:
/**
* Clicks the first ClickableSpan in the TextView
*/
public static ViewAction clickFirstClickableSpan() {
return new GeneralClickAction(
Tap.SINGLE,
new CoordinatesProvider() {
#Override
public float[] calculateCoordinates(View view) {
//https://leons.im/posts/how-to-get-coordinate-of-a-clickablespan-inside-a-textview/
TextView textView = (TextView) view;
Rect parentTextViewRect = new Rect();
SpannableString spannableString = (SpannableString) textView.getText();
Layout textViewLayout = textView.getLayout();
ClickableSpan spanToLocate = null;
if (spannableString.length() == 0) {
return new float[2];
}
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
spanToLocate = spans[0];
}
if (spanToLocate == null) {
// no specific view found
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
double startOffsetOfClickedText = spannableString.getSpanStart(spanToLocate);
double endOffsetOfClickedText = spannableString.getSpanEnd(spanToLocate);
double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) startOffsetOfClickedText);
double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) endOffsetOfClickedText);
// Get the rectangle of the clicked text
int currentLineStartOffset = textViewLayout.getLineForOffset((int) startOffsetOfClickedText);
int currentLineEndOffset = textViewLayout.getLineForOffset((int) endOffsetOfClickedText);
boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);
// Update the rectangle position to his real position on screen
int[] parentTextViewLocation = {0, 0};
textView.getLocationOnScreen(parentTextViewLocation);
double parentTextViewTopAndBottomOffset = (
parentTextViewLocation[1] -
textView.getScrollY() +
textView.getCompoundPaddingTop()
);
parentTextViewRect.top += parentTextViewTopAndBottomOffset;
parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
parentTextViewRect.left += (
parentTextViewLocation[0] +
startXCoordinatesOfClickedText +
textView.getCompoundPaddingLeft() -
textView.getScrollX()
);
parentTextViewRect.right = (int) (
parentTextViewRect.left +
endXCoordinatesOfClickedText -
startXCoordinatesOfClickedText
);
int screenX = (parentTextViewRect.left + parentTextViewRect.right) / 2;
int screenY = (parentTextViewRect.top + parentTextViewRect.bottom) / 2;
if (keywordIsInMultiLine) {
screenX = parentTextViewRect.left;
screenY = parentTextViewRect.top;
}
return new float[]{screenX, screenY};
}
},
Press.FINGER);
}
you may use Spannable instead of SpannableString compatible with SpannableStringBuilder.
sorry, i am a new man , have only 1 Reputation , can not add a comment.Even my English is very poor.....
i suggest to use:
Spannable spannableString = (Spannable) textView.getText();
instead of :
SpannableString spannableString = (SpannableString) textView.getText();
post all the code below:
public class CustomViewActions {
/**
* click specific spannableString
*/
public static ViewAction clickClickableSpan(final CharSequence textToClick) {
return clickClickableSpan(-1, textToClick);
}
/**
* click the first spannableString
*/
public static ViewAction clickClickableSpan() {
return clickClickableSpan(0, null);
}
/**
* click the nth spannableString
*/
public static ViewAction clickClickableSpan(final int index) {
return clickClickableSpan(index, null);
}
public static ViewAction clickClickableSpan(final int index,final CharSequence textToClick) {
return new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return instanceOf(TextView.class);
}
#Override
public String getDescription() {
return "clicking on a ClickableSpan";
}
#Override
public void perform(UiController uiController, View view) {
TextView textView = (TextView) view;
Spannable spannableString = (Spannable) textView.getText();
ClickableSpan spanToLocate = null;
if (spannableString.length() == 0) {
// TextView is empty, nothing to do
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
// Get the links inside the TextView and check if we find textToClick
ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
if (spans.length > 0) {
if(index >=spans.length){
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}else if (index >= 0) {
spanToLocate = spans[index];
spanToLocate.onClick(textView);
return;
}
for (int i = 0; i < spans.length; i++) {
int start = spannableString.getSpanStart(spans[i]);
int end = spannableString.getSpanEnd(spans[i]);
CharSequence sequence = spannableString.subSequence(start, end);
if (textToClick.toString().equals(sequence.toString())) {
spanToLocate = spans[i];
spanToLocate.onClick(textView);
return;
}
}
}
// textToClick not found in TextView
throw new NoMatchingViewException.Builder()
.includeViewHierarchy(true)
.withRootView(textView)
.build();
}
};
}
}
Espresso has a one-liner for this:
onView(withId(R.id.textView)).perform(openLinkWithText("..."))
I want twitter mentions to be in redcolor and hashtags is another color,if any tweet contains any weblinks.. links should be clickable and passed through intent to another Activity(WebView).
How can I achieve this ??
TransformFilter filter = new TransformFilter() {
public final String transformUrl(final Matcher match, String url) {
return match.group();
}
};
Pattern mentionPattern = Pattern.compile("#([A-Za-z0-9_-]+)");
String mentionScheme = "http://www.twitter.com/";
Linkify.addLinks(tvMessage, mentionPattern, mentionScheme, null, filter);
Pattern hashtagPattern = Pattern.compile("#([A-Za-z0-9_-]+)");
String hashtagScheme = "http://www.twitter.com/search/";
Linkify.addLinks(tvMessage, hashtagPattern, hashtagScheme, null, filter);
Pattern urlPattern = Patterns.WEB_URL;
Linkify.addLinks(tvMessage, urlPattern, null, null, filter);
// tvMessage.setLinkTextColor(Color.parseColor("#3467BB"));
Finally i have achieved hashtags weblinks clickable and made them very attractive by keeping different colors using android sdk SpannableString and it's ClickableSpan .
private void Linkfiy(String a, TextView textView) {
Pattern urlPattern = Patterns.WEB_URL;
Pattern mentionPattern = Pattern.compile("(#[A-Za-z0-9_-]+)");
Pattern hashtagPattern = Pattern.compile("#(\\w+|\\W+)");
Matcher o = hashtagPattern.matcher(a);
Matcher mention = mentionPattern.matcher(a);
Matcher weblink = urlPattern.matcher(a);
SpannableString spannableString = new SpannableString(a);
//#hashtags
while (o.find()) {
spannableString.setSpan(new NonUnderlinedClickableSpan(o.group(),
0), o.start(), o.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// --- #mention
while (mention.find()) {
spannableString.setSpan(
new NonUnderlinedClickableSpan(mention.group(), 1), mention.start(), mention.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//#weblink
while (weblink.find()) {
spannableString.setSpan(
new NonUnderlinedClickableSpan(weblink.group(), 2), weblink.start(), weblink.end(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(spannableString);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
NonUnderlinedClickableSpan class
public class NonUnderlinedClickableSpan extends ClickableSpan {
int type;// 0-hashtag , 1- mention, 2- url link
String text;// Keyword or url
String time;
public NonUnderlinedClickableSpan(String text, int type) {
this.text = text;
this.type = type;
this.time = time;
}
#Override
public void updateDrawState(TextPaint ds) {
//adding colors
if (type == 1)
ds.setColor(InstagramIndetail.this.getResources().getColor(
R.color.link_color_mention));
else if (type == 2)
ds.setColor(InstagramIndetail.this.getResources().getColor(
R.color.link_color_url));
else
ds.setColor(InstagramIndetail.this.getResources().getColor(
R.color.link_color_hashtag));
ds.setUnderlineText(false);
// ds.setTypeface(Typeface.DEFAULT_BOLD);
}
#Override
public void onClick(View v) {
Debug.e("click done", "ok " + text);
if (type == 0) {
//pass hashtags to activity using intents
} else if (type == 1) {
//do for mentions
} else {
// passing weblinks urls to webview activity
startWebViewActivity(text);
}
}
}
i am making a chatting application in which i am using emoticons functionality.My Emoticons functionality is working properly for single image ,but when i am taking multiple emotive images it is not converting in to particular image..,at a time only single image is converting, My problem is
i am unable to separate the spanned object in edit text field..,for single value it is working but for multiple value its is not working..
Example.i am taking 4 different images in edit text field, like this here
now i want to seprate its spanned object.,how can i do this
here is code
public void keyClickedIndex( final String index)
{
ImageGetter imageGetter = new ImageGetter()
{
public Drawable getDrawable(String source)
{
StringTokenizer st = new StringTokenizer(index, ".");
Drawable d = new BitmapDrawable(getResources(),emoticons[Integer.parseInt(st.nextToken()) - 1]);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
return d;
}
};
Spanned cs = Html.fromHtml("<img src ='"+ index +"'/>", imageGetter, null);
int cursorPosition = mSendText.getSelectionStart();
mSendText.getText().insert(cursorPosition, cs);
please help me..,Thanks in Advance.
you can use emoticon handler method
private static class EmoticonHandler implements TextWatcher {
private final EditText mEditor;
private final ArrayList<ImageSpan> mEmoticonsToRemove = new ArrayList<ImageSpan>();
//public String txt;
XMPPClient act;
public EmoticonHandler(EditText editor,XMPPClient act) {
// Attach the handler to listen for text changes.
mEditor = editor;
mEditor.addTextChangedListener(this);
this.act = act;
}
public void insert(String emoticon, int resource)
{
// Create the ImageSpan
Drawable drawable = mEditor.getResources().getDrawable(resource);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
ImageSpan span = new ImageSpan(drawable,emoticon,ImageSpan.ALIGN_BASELINE);
// Get the selected text.
int start = mEditor.getSelectionStart();
int end = mEditor.getSelectionEnd();
Editable message = mEditor.getEditableText();
// Insert the emoticon.
message.replace(start, end, emoticon);
message.setSpan(span, start, start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
#Override
public void beforeTextChanged(CharSequence text, int start, int count, int after) {
// Check if some text will be removed.
if (count > 0) {
int end = start + count;
Editable message = mEditor.getEditableText();
ImageSpan[] list = message.getSpans(start, end, ImageSpan.class);
boolean check = false;
for (ImageSpan span : list)
{
// Get only the emoticons that are inside of the changed
// region.
check = true;
int spanStart = message.getSpanStart(span);
int spanEnd = message.getSpanEnd(span);
//txt = text.toString();
act.emorTxt = text.toString();
if ((spanStart < end) && (spanEnd > start)) {
// Add to remove list
mEmoticonsToRemove.add(span);
}
}
if(!check)
{
act.emorTxt = text.toString();
}
}
}
#Override
public void afterTextChanged(Editable text) {
Editable message = mEditor.getEditableText();
// Commit the emoticons to be removed.
for (ImageSpan span : mEmoticonsToRemove)
{
int start = message.getSpanStart(span);
int end = message.getSpanEnd(span);
// Remove the span
message.removeSpan(span);
// Remove the remaining emoticon text.
if (start != end) {
message.delete(start, end);
}
}
mEmoticonsToRemove.clear();
}
#Override
public void onTextChanged(CharSequence text, int start, int before, int count) {
}
}
it will work perfectly....:)
I have a ListView with Strings. With the below code I can highlight search results, but the user must type the words to search case sensitive. How can I implement a none - case sensitive highlighting of search results for example like the native Android Contact search?
Here is my code for Highlighting. I extend the ArrayAdapter and implement customized filter to get the string to search. In the getView method I check if my String in ListView contains the prefixString and highlight it.
public class HighlightListAdapter extends ArrayAdapter {
ArrayList<String> objects;
final Object mLock =new Object();
private ArrayList<String> mOriginalValues;
private ArrayFilter filter;
private String prefixString;
public AuthorsListAdapter(Context context, int textViewResourceId, ArrayList<String> objects) {
super(context, textViewResourceId, objects);
this.objects = objects;
}
class ViewHolder{
TextView author;
}
public View getView(final int position, View convertView, ViewGroup parent){
// assign the view we are converting to a local variable
View v = convertView;
ViewHolder holder = null;
// first check to see if the view is null. if so, we have to inflate it.
// to inflate it basically means to render, or show, the view.
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (v == null) {
holder = new ViewHolder();
v = inflater.inflate(R.layout.author_list_item, null);
holder.author =(TextView) v.findViewById(R.id.author_list_item_text);
v.setTag(holder);
}else{
holder = (ViewHolder) v.getTag();
}
final String author = objects.get(position);
if (author != null) {
holder.author.setText(author);
if(prefixString !=null && prefixString.length()>1){
String s = author;
**if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}** // higlight
}
}
return v;
}
#Override
public int getCount() {
// TODO Auto-generated method stub
return objects.size();
}
#Override
public Filter getFilter() {
// TODO Auto-generated method stub
if(filter == null){
filter =new ArrayFilter();
}
return filter;
}
#Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return this.objects.get(position);
}
private class ArrayFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<String>(objects);
}
}
if (prefix == null || prefix.length() == 0) {
ArrayList<String> list;
synchronized (mLock) {
list = new ArrayList<String>(mOriginalValues);
}
results.values = list;
results.count = list.size();
} else {
**prefixString = prefix.toString();** // get string to search
ArrayList<String> values;
synchronized (mLock) {
values = new ArrayList<String>(mOriginalValues);
}
final int count = values.size();
final ArrayList<String> newValues = new ArrayList<String>();
for (int i = 0; i < count; i++) {
final String value = values.get(i);
final String valueText = value.toString().toLowerCase();
// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
newValues.add(value);
} else {
final String[] words = valueText.split(" ");
final int wordCount = words.length;
// Start at index 0, in case valueText starts with space(s)
for (int k = 0; k < wordCount; k++) {
if (words[k].startsWith(prefixString)) {
newValues.add(value);
break;
}
}
}
}
results.values = newValues;
results.count = newValues.size();
}
return results;
}
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
objects = (ArrayList<String>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
}
This what I use :
Every occurence is replaced (not only prefix)
Case and accent are ignored while searching but retained in the result.
It uses directly SpannableString, which you can use in setText(). I believe it's more efficient than using an intermediate html step.
.
public static CharSequence highlight(String search, String originalText) {
// ignore case and accents
// the same thing should have been done for the search text
String normalizedText = Normalizer.normalize(originalText, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase();
int start = normalizedText.indexOf(search);
if (start < 0) {
// not found, nothing to to
return originalText;
} else {
// highlight each appearance in the original text
// while searching in normalized text
Spannable highlighted = new SpannableString(originalText);
while (start >= 0) {
int spanStart = Math.min(start, originalText.length());
int spanEnd = Math.min(start + search.length(), originalText.length());
highlighted.setSpan(new BackgroundColorSpan(<background_color>), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = normalizedText.indexOf(search, spanEnd);
}
return highlighted;
}
}
The accepted answer is nice. But you can do it by a single line of code. What I've done in my case to avoid the case sensitive issue is:
Spannable sb = new SpannableString(originalText);
sb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), originalText.toLowerCase().indexOf(query.toLowerCase()),
originalText.toLowerCase().indexOf(query.toLowerCase()) + query.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setText(sb);
Hope it might help!
Note: Here 'query' is the part of the string that you want to highlight.
Simple & Advanced Search Highlighting Example [Case Insensitive Order]
1. Simple Search (Html):
public static void setSearchTextHighlightSimpleHtml(TextView textView, String fullText, String searchText) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<span style=\"background-color:#FCFF48;\"><b><big><font color='#a10901'>$1</font></big></b></span>");
textView.setText(Html.fromHtml(fullText, Html.FROM_HTML_MODE_LEGACY), TextView.BufferType.SPANNABLE);
} else {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<b><big><font color='red'>$1</font></big></b>");
textView.setText(Html.fromHtml(fullText), TextView.BufferType.SPANNABLE);
}
} catch (Exception e) {
textView.setText(fullText);
}
}
2. Simple Search (Spannable):
public static void setSearchTextHighlightSimpleSpannable(TextView textView, String fullText, String searchText) {
// highlight search text
if (null != searchText && !searchText.isEmpty()) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
int wordEnd = m.end();
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
3. Advanced Search (Spannable):
public static void setAdvancedSearch(TextView textView, String fullText, String searchText) {
if (searchText.length() == 0) {
textView.setText(fullText);
return;
}
final String searchBoundary = " \n()ред.,;?-+!";
char[] boundaries = searchBoundary.toCharArray();
// highlight search text
if (isNotEquals(searchText, boundaries)) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
while (wordStart >= 0 && isNotEquals(fullText.charAt(wordStart), boundaries)) {
--wordStart;
}
wordStart = wordStart + 1;
int wordEnd = m.end();
while (wordEnd < fullText.length() && isNotEquals(fullText.charAt(wordEnd), boundaries)) {
++wordEnd;
}
setWordSpan(wordSpan, wordStart, wordEnd);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
private static boolean isNotEquals(char charAt, char[] boundaries) {
return isNotEquals(String.valueOf(charAt), boundaries);
}
private static boolean isNotEquals(String searchText, char[] boundaries) {
for (char boundary : boundaries) {
boolean equals = searchText.equals(String.valueOf(boundary));
if (equals) return false;
}
return true;
}
private static void setWordSpan(SpannableStringBuilder wordSpan, int wordStart, int wordEnd) {
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
First, your code
if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}
is not good. You should use String.startsWith to check if the start of s equals to prefixString. Your actual code works, but it checks presence of prefixString in s, but doesn't care about its position.
For having case insensitive search, you can use String.toLowerCase or String.toUpperCase on both strings when checking presence of prefixString. Case will be ignored.
if(s.toLowerCase().startsWith(prefixString.toLowerCase())){
String rep = "<b><font color=#2825A6>" + prefixString + "</font></b>" + s.substring(prefixString.length());
holder.author.setText(Html.fromHtml(rep));
}