Autocomplete textbox highlighting the typed character in the suggestion list
Asked Answered
D

4

7

I have been working on AutoCompleteTextView. I was able to get the suggestion and all in the drop down list as we type.

So far

My question is: Can we highlight the typed character in the suggestion drop down list?

Disembarrass answered 27/2, 2013 at 18:4 Comment(2)
Please don't prefix the question title with Android:, the tag at the bottom is enough. It's possible to do what you want through a custom adapter and modifying the text when filtering it. But this would be unreliable.Illuse
Can you tell me how to do that. Will see if it solve my problem. Right now I'm using custom adapter for dropdown suggestion list.Disembarrass
M
20

I have achieved the functionality. The solution is as follows:

AutoCompleteAdapter.java

public class AutoCompleteAdapter extends ArrayAdapter<String> implements
        Filterable {

    private ArrayList<String> fullList;
    private ArrayList<String> mOriginalValues;
    private ArrayFilter mFilter;
    LayoutInflater inflater;
    String text = "";

    public AutoCompleteAdapter(Context context, int resource,
            int textViewResourceId, List<String> objects) {

        super(context, resource, textViewResourceId, objects);
        fullList = (ArrayList<String>) objects;
        mOriginalValues = new ArrayList<String>(fullList);
        inflater = LayoutInflater.from(context);

    }

    @Override
    public int getCount() {
        return fullList.size();
    }

    @Override
    public String getItem(int position) {
        return fullList.get(position);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = convertView;
        // tvViewResourceId = (TextView) view.findViewById(android.R.id.text1);
        String item = getItem(position);
        Log.d("item", "" + item);
        if (convertView == null) {
            convertView = view = inflater.inflate(
                    android.R.layout.simple_dropdown_item_1line, null);
        }
        // Lookup view for data population
        TextView myTv = (TextView) convertView.findViewById(android.R.id.text1);
        myTv.setText(highlight(text, item));
        return view;
    }

    @Override
    public Filter getFilter() {
        if (mFilter == null) {
            mFilter = new ArrayFilter();
        }
        return mFilter;
    }

    private class ArrayFilter extends Filter {
        private Object lock;

        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            FilterResults results = new FilterResults();
            if (prefix != null) {
                text = prefix.toString();
            }
            if (mOriginalValues == null) {
                synchronized (lock) {
                    mOriginalValues = new ArrayList<String>(fullList);
                }
            }

            if (prefix == null || prefix.length() == 0) {
                synchronized (lock) {
                    ArrayList<String> list = new ArrayList<String>(
                            mOriginalValues);
                    results.values = list;
                    results.count = list.size();
                }
            } else {
                final String prefixString = prefix.toString().toLowerCase();
                ArrayList<String> values = mOriginalValues;
                int count = values.size();

                ArrayList<String> newValues = new ArrayList<String>(count);

                for (int i = 0; i < count; i++) {
                    String item = values.get(i);
                    if (item.toLowerCase().contains(prefixString)) {
                        newValues.add(item);
                    }

                }

                results.values = newValues;
                results.count = newValues.size();
            }

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint,
                FilterResults results) {

            if (results.values != null) {
                fullList = (ArrayList<String>) results.values;
            } else {
                fullList = new ArrayList<String>();
            }
            if (results.count > 0) {
                notifyDataSetChanged();
            } else {
                notifyDataSetInvalidated();
            }
        }

    }

    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(Locale.ENGLISH);

        int start = normalizedText.indexOf(search.toLowerCase(Locale.ENGLISH));
        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 ForegroundColorSpan(Color.BLUE),
                        spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

                start = normalizedText.indexOf(search, spanEnd);
            }

            return highlighted;
        }
    }
}

MainActivity.java

public class MainActivity extends Activity {

    String[] languages = { "C", "C++", "Java", "C#", "PHP", "JavaScript",
            "jQuery", "AJAX", "JSON" };

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        List<String> wordList = new ArrayList<String>(); 
        Collections.addAll(wordList, languages); 
        AutoCompleteAdapter adapter = new AutoCompleteAdapter(this,
                android.R.layout.simple_dropdown_item_1line,
                android.R.id.text1,wordList);
        AutoCompleteTextView acTextView = (AutoCompleteTextView) findViewById(R.id.languages);
        acTextView.setThreshold(1);
        acTextView.setAdapter(adapter);
    }
}

Working like charm!

Enjoy!

Mydriasis answered 31/10, 2015 at 9:45 Comment(6)
I used this adapter but it wont popup the hints. what could be the problem?Didactic
to answer my question, i had to overwrite the clear() and addll(collection) methods that I use, to set the hints dynamically. thanks!Didactic
After getCount() add the lines below java @Override public void clear() { super.clear(); fullList.clear(); mOriginalValues.clear(); } @Override public void addAll(@NonNull Collection<? extends String> collection) { super.addAll(collection); fullList.addAll(collection); mOriginalValues.addAll(fullList); } Didactic
If you don't want custom filter logic, then implementing only a custom adapter with getView and hightlight is enough. You can get the current text from the AutocompleteTextView. Also this highlight implementation will hight all occurence, if you only want the first then just leave out the while loop.Gaudery
Thanks, it works. But contains references to android.R.layout.simple_dropdown_item_1line and android.R.id.text1 inside AutoCompleteAdapter, you should define yours to customize text colors (see stackoverflow.com/questions/12876840/…). Also it doesn't support list drop down on click (see stackoverflow.com/questions/15544943/…). See the solution at https://mcmap.net/q/1396903/-autocomplete-textbox-highlighting-the-typed-character-in-the-suggestion-list.Bondman
@JoM, what these 2 methods change in a logic? I don't see a difference. It filters the same as before. It doesn't show a full list when click.Bondman
G
3

I reckon that should be possible, provided you know the index/indices of the character(s) the user typed last. You can then use a SpannableStringBuilder and set a ForegroundColorSpan and BackgroundColorSpan to give the character(s) the appearance of a highlight.

The idea looks somewhat like this:

// start & end of the highlight
int start = ...;
int end = ...;
SpannableStringBuilder builder = new SpannableStringBuilder(suggestionText);
// set foreground color (text color) - optional, you may not want to change the text color too
builder.setSpan(new ForegroundColorSpan(Color.RED), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 
// set background color
builder.setSpan(new BackgroundColorSpan(Color.YELLOW), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// set result to AutoCompleteTextView
autocompleteTextview.setText(builder);

Note that the 'highlight' will remain as long as you don't type another character. You may want to remove the highlight when e.g. the user changes the cursor position in the AutoCompleteTextView, but I'll leave that up to you.

Gazelle answered 27/2, 2013 at 18:21 Comment(3)
That's great... whoever downvoted this might as well explain why.Gazelle
Although I have not down voted above answer, but if you copy from someone you need to reference it. androiddev.orkitra.com/?p=26284Bessette
@MurtazaHussan: I most definitely did not copy paste the content below from the website you're linking. As a matter of fact, it's the other way around. It looks like my answer is simply indexed there... Note: NOD32 actually prevents me from navigating there because of 'dangerous content'.Gazelle
O
1

I know it's to late for answering this question , But as I personally battled to find the answer , finally I wrote it myself (with the help of the answer from @MH. ofcourse), so here it is :

First , You have to create a Custom ArrayAdapter :

public class AdapterAustocomplete extends ArrayAdapter<String> {

private static final String TAG = "AdapterAustocomplete";
String q = "";

public AdapterAustocomplete(Context context, int resource, List objects) {
    super(context, resource, objects);
}


@Override
public View getView(int position, View convertView, ViewGroup parent) {


    String item = getItem(position);
    // Check if an existing view is being reused, otherwise inflate the view
    if (convertView == null) {
        convertView = 
   // I'll use a custom view for each Item , this way I can customize it also!
  G.inflater.from(getContext()).inflate(R.layout.textview_autocomplete, parent, false);

    }
    // Lookup view for data population
    TextView myTv = (TextView) convertView.findViewById(R.id.txt_autocomplete);

    int start = item.indexOf(q);
    int end = q.length()+start;
    SpannableStringBuilder builder = new SpannableStringBuilder(item);

    builder.setSpan(new ForegroundColorSpan(Color.RED), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);


    myTv.setText(builder);
    return convertView;
}

public void setQ(String q) {
    this.q = q;
}
}

And in the Code that you want to set the adapter for AutoCompleteTextView ;

   AutoCompleteTextView myAutoComplete = findViewById(its_id);
   AdapterAustocomplete adapter_autoComplete = new AdapterAustocomplete(getActivity(), 0, items); // items is an arrayList of Strings
  adapter_autoComplete.setQ(q);
  myAutoComplete.setAdapter(adapter_autoComplete);
Obe answered 11/5, 2015 at 1:9 Comment(1)
Too less code, I don't think we should use without full code.Bondman
B
1

Thanks to vadher jitendra I wrote the same and fixed some bugs.

  1. Changed a dropdown layout to own.

  2. Added showing a full list when clicking inside AutoCompleteTextView.

  3. Fixed a bug of freezing the list when showing (added a check for empty string in highlight).

row_dropdown.xml (item layout):

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/text1"
    style="?android:attr/dropDownItemStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"
    android:ellipsize="marquee"
    android:paddingTop="8dp"
    android:paddingBottom="8dp"
    android:singleLine="true"
    android:textColor="#333333"
    android:textSize="15sp"
    tools:text="text"
    tools:textAppearance="?android:attr/textAppearanceLargePopupMenu" />

To filter a list when typing we should implement ArrayAdapter. It depends on items (T class). You can later use AutoCompleteAdapter<String> or any data class you like.

AutoCompleteAdapter:

import android.content.Context;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;

import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class AutoCompleteAdapter<T> extends ArrayAdapter<T> implements Filterable {

    private Context context;
    @LayoutRes
    private int layoutRes;
    @IdRes
    private int textViewResId;
    private ArrayList<T> fullList;
    private ArrayList<T> originalValues;
    private ArrayFilter filter;
    private LayoutInflater inflater;
    private String query = "";

    public AutoCompleteAdapter(@NonNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects) {
        super(context, resource, textViewResourceId, objects);
        this.context = context;
        layoutRes = resource;
        textViewResId = textViewResourceId;
        fullList = (ArrayList<T>) objects;
        originalValues = new ArrayList<>(fullList);
        inflater = LayoutInflater.from(context);
    }

    @Override
    public int getCount() {
        return fullList.size();
    }

    @Override
    public T getItem(int position) {
        return fullList.get(position);
    }

    /**
     * You can use either
     * vadher jitendra method (getView)
     * or get the method from ArrayAdapter.java.
     */
//    @NotNull
//    @Override
//    public View getView(int position, View convertView, ViewGroup parent) {
//        View view = convertView;
//        T item = getItem(position);
//        Log.d("item", "" + item);
//        if (convertView == null) {
//            convertView = view = inflater.inflate(layoutRes, null);
//        }
//        // Lookup view for data population
//        TextView myTv = convertView.findViewById(textViewResId);
//        myTv.setText(highlight(query, item));
//        return view;
//    }
    @Override
    public @NonNull
    View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        return createViewFromResource(inflater, position, convertView, parent, layoutRes);
    }

    private @NonNull
    View createViewFromResource(@NonNull LayoutInflater inflater, int position,
                                @Nullable View convertView, @NonNull ViewGroup parent, int resource) {
        final View view;
        final TextView text;

        if (convertView == null) {
            view = inflater.inflate(resource, parent, false);
        } else {
            view = convertView;
        }

        try {
            if (textViewResId == 0) {
                //  If no custom field is assigned, assume the whole resource is a TextView
                text = (TextView) view;
            } else {
                //  Otherwise, find the TextView field within the layout
                text = view.findViewById(textViewResId);

                if (text == null) {
                    throw new RuntimeException("Failed to find view with ID "
                            + context.getResources().getResourceName(textViewResId)
                            + " in item layout");
                }
            }
        } catch (ClassCastException e) {
            Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
            throw new IllegalStateException(
                    "ArrayAdapter requires the resource ID to be a TextView", e);
        }

        final T item = getItem(position);
        text.setText(highlight(query, item.toString()));
//        if (item instanceof CharSequence) {
//            text.setText(highlight(query, (CharSequence) item));
//        } else {
//            text.setText(item.toString());
//        }

        return view;
    }

    @Override
    public @NonNull
    Filter getFilter() {
        if (filter == null) {
            filter = new ArrayFilter();
        }
        return filter;
    }

    private class ArrayFilter extends Filter {
        private final Object lock = new Object();

        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            FilterResults results = new FilterResults();
            if (prefix == null) {
                query = "";
            } else {
                query = prefix.toString();
            }
            if (originalValues == null) {
                synchronized (lock) {
                    originalValues = new ArrayList<>(fullList);
                }
            }

            if (prefix == null || prefix.length() == 0) {
                synchronized (lock) {
                    ArrayList<T> list = new ArrayList<>(originalValues);
                    results.values = list;
                    results.count = list.size();
                }
            } else {
                final String prefixString = prefix.toString().toLowerCase();
                ArrayList<T> values = originalValues;
                int count = values.size();

                ArrayList<T> newValues = new ArrayList<>(count);

                for (int i = 0; i < count; i++) {
                    T item = values.get(i);
                    if (item.toString().toLowerCase().contains(prefixString)) {
                        newValues.add(item);
                    }

                }

                results.values = newValues;
                results.count = newValues.size();
            }

            return results;
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            if (results.values != null) {
                fullList = (ArrayList<T>) results.values;
            } else {
                fullList = new ArrayList<>();
            }
            if (results.count > 0) {
                notifyDataSetChanged();
            } else {
                notifyDataSetInvalidated();
            }
        }

    }

    private static CharSequence highlight(@NonNull String search, @NonNull CharSequence originalText) {
        if (search.isEmpty())
            return 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(Locale.ENGLISH);

        int start = normalizedText.indexOf(search.toLowerCase(Locale.ENGLISH));
        if (start < 0) {
            // not found, nothing to do
            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 StyleSpan(Typeface.BOLD), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

                start = normalizedText.indexOf(search, spanEnd);
            }

            return highlighted;
        }
    }
}

In order to show dropdown list when clicked inside AutoCompleteTextView we need to override setOnTouchListener as described in https://mcmap.net/q/297249/-show-all-items-in-autocompletetextview-without-writing-text. Lint also prints warnings, so we have to write a custom view:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;

import androidx.appcompat.widget.AppCompatAutoCompleteTextView;

/*
Avoids a warning "Custom view `AutoCompleteTextView` has setOnTouchListener called on it but does not override performClick".
 */
public class AutoCompleteTV extends AppCompatAutoCompleteTextView {

    public AutoCompleteTV(Context context) {
        super(context);
    }

    public AutoCompleteTV(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AutoCompleteTV(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            performClick();
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean performClick() {
        super.performClick();
        return true;
    }
}

Then use it in activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.google.android.material.textfield.TextInputLayout
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.example.autocompletetextview1.AutoCompleteTV
            android:id="@+id/languages"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp"
            android:completionThreshold="1"
            android:hint="language"
            android:imeOptions="actionNext"
            android:maxLines="1"
            android:paddingLeft="10dp"
            android:paddingTop="15dp"
            android:paddingRight="10dp"
            android:paddingBottom="15dp"
            android:singleLine="true"
            android:textColor="#333333"
            android:textColorHint="#808080"
            android:textSize="12sp" />

    </com.google.android.material.textfield.TextInputLayout>

</LinearLayout>

I use TextInputLayout here for better decoration, in this case we have to add Material Design Components:

in build.gradle:

implementation 'com.google.android.material:material:1.3.0-alpha01'

and in styles.xml:

<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
...

MainActivity:

import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.AutoCompleteTextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    String[] items = {"C", "C++", "Java", "C#", "PHP", "JavaScript", "jQuery", "AJAX", "JSON"};

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<DataClass> wordList = new ArrayList<>();
        for (int i = 0; i < items.length; i++) {
            DataClass data = new DataClass(i, items[i]);
            wordList.add(data);
        }
        AutoCompleteAdapter<DataClass> adapter = new AutoCompleteAdapter<>(this,
                R.layout.row_dropdown, R.id.text1, wordList);
        //adapter.setDropDownViewResource(R.layout.row_dropdown);
        AutoCompleteTV acTextView = findViewById(R.id.languages);
        acTextView.setThreshold(1);
        acTextView.setAdapter(adapter);
        acTextView.setText("Java");
        acTextView.setOnTouchListener((v, event) -> {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        ((AutoCompleteTextView) v).showDropDown();
                        v.requestFocus();
                        v.performClick(); // Added to avoid warning "onTouch lambda should call View#performClick when a click is detected".
                    }
                    return false;
                }
        );
    }
}

enter image description here enter image description here enter image description here

Bondman answered 29/5, 2020 at 17:5 Comment(2)
Your answer works great. Just a comment, I didn't need to override setOnTouchListener in order to show the dropdown list, setOnItemClickListener works fine.Binate
@Xam, thank you for the comment! Agree with you. Later I advanced that code and forgot it in another old project. :)Bondman

© 2022 - 2024 — McMap. All rights reserved.