harveyslash
harveyslash

Reputation: 6024

AutoCompleteTextView Show particular Item Always

I am using an AutoCompleteTextView somewhat like this:

public class MainActivity extends Activity {

   private AutoCompleteTextView actv;
   private MultiAutoCompleteTextView mactv;

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


   String[] countries = getResources().
   getStringArray(R.array.list_of_countries);
   ArrayAdapter adapter = new ArrayAdapter
   (this,android.R.layout.simple_list_item_1,countries);


   actv = (AutoCompleteTextView) findViewById(R.id.autoCompleteTextView1);
   mactv = (MultiAutoCompleteTextView) findViewById
   (R.id.multiAutoCompleteTextView1);

   actv.setAdapter(adapter);
   mactv.setAdapter(adapter);

   mactv.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());


   }

   @Override
   public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate the menu; this adds items to the action bar if it is present.
      getMenuInflater().inflate(R.menu.main, menu);
      return true;
   }

}

This gets most of the job done. But, in my case, I need autocomplete to show something like 'custom...' at the bottom of the returned dropdown suggestions.

So, if there are autocomplete suggestions, they will show , followed by the 'custom...' suggestions.And if there arent any suggestions, it will still show ONE suggestion 'custom...' .I also require a click listener for the 'custom...'.

Upvotes: 3

Views: 3290

Answers (2)

shafeeq
shafeeq

Reputation: 1589

Thanks @sled, Your answer is pretty nice. I have converted this to kotlin with some modification.

class CustomAutoCompleteAdapter<T>(
    var context: Context,
    private var resource: Int,
    var objects: ArrayList<T>,
    private var footerText: String
): BaseAdapter(), Filterable {
    private var footerClickListener: OnFooterClickListener? = null
    private var filter =  ArrayFilter()

    interface OnFooterClickListener {
        fun onFooterClick()
    }

    fun setOnFooterClickListener(listener: OnFooterClickListener) {
        footerClickListener = listener
    }

    override fun getCount() = objects.size + 1

    override fun getItem(position: Int) = if(position <= objects.size - 1) objects[position].toString() else footerText

    override fun getItemId(position: Int) = position.toLong()

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        return createViewFromSource(position, convertView, parent, resource)
    }

    private fun createViewFromSource(
        position: Int,
        convertView: View?,
        parent: ViewGroup?,
        resource: Int
    ): View {
        val text: TextView
        val view = convertView ?: LayoutInflater.from(parent?.context).inflate(resource, parent, false)
        try {
            text = view as TextView
        } catch (ex: ClassCastException) {
            throw IllegalStateException("Layout xml is not text field", ex)
        }

        text.text = getItem(position)
        if(position == count - 1) {
            view.setOnClickListener {
                footerClickListener?.onFooterClick()
            }
        } else {
            view.setOnClickListener(null)
            view.isClickable = false
        }

        return view
    }

    override fun getFilter(): Filter {
        return filter
    }

    override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View {
        return createViewFromSource(position, convertView, parent, resource)
    }

    inner class ArrayFilter: Filter() {
        private var originalValues = ArrayList<T>()

        override fun performFiltering(constraint: CharSequence?): FilterResults {
            val filterResults = FilterResults()
            if(originalValues.isEmpty())
                originalValues.addAll(objects)

            if(constraint == null || constraint.isEmpty()) {
                val outcomes = ArrayList<T>(originalValues)
                filterResults.values = outcomes
                filterResults.count = outcomes.size + 1
            } else {
                val prefixStr = constraint.toString().toLowerCase(Locale.ENGLISH)
                val values = ArrayList<T>(originalValues)

                val newValues = ArrayList<T>()
                for(value in values) {
                    val valueText = value.toString().toLowerCase(Locale.ENGLISH)
                    if(valueText.contains(prefixStr)) {
                        newValues.add(value)
                    } else {
                        val words = valueText.split(" ", "(", ")", ",", "-")
                        for(word in words) {
                            if(word.startsWith(prefixStr)) {
                                newValues.add(value)
                                break
                            }
                        }
                    }
                }

                filterResults.values = newValues
                filterResults.count = newValues.size + 1
            }
            return filterResults
        }

        override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
            @Suppress("UNCHECKED_CAST")
            objects = results?.values as ArrayList<T>
            notifyDataSetChanged()
        }
    }
}

Upvotes: 0

sled
sled

Reputation: 14635

Interesting question, I gave it a shot and implemented a simple custom adapter based on the ArrayAdapter source code.

For brevity I omitted most of the unused code and comments, so if you're unsure - have a look at the source code of the ArrayAdapter I linked above, it is well commented.

The principle of operation is quite simple, the getCount() of the adapter adds one to the actual number of elements. Also the getItem(int position) will check if the last, "virtual" item is requested and will return your "Custom..." string then.

The createViewFromResource(...) method also checks whether it is going to show the last, "virtual" item, if yes it will bind an onClick listener.

The overwritten Filter also adds one to the result count, in order to make the AutoCompleteView believe there is a match so it keeps the dropdown list open.

MainActivity.java

public class MainActivity extends ActionBarActivity {

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

        String[] countries  = new String[]{
                "Switzerland", "Mexico", "Poland", "United States of Murica"};

        // the footer item's text
        String   footerText = "Custom Footer....";

        // our custom adapter with the custom footer text as last parameter
        CustomAutoCompleteAdapter adapter = new CustomAutoCompleteAdapter(
                this, android.R.layout.simple_list_item_1, countries, footerText);

        // bind to our custom click listener interface
        adapter.setOnFooterClickListener(new CustomAutoCompleteAdapter.OnFooterClickListener() {
            @Override
            public void onFooterClicked() {
                // your custom item has been clicked, make some toast
                Toast toast = Toast.makeText(getApplicationContext(), "Yummy Toast!", Toast.LENGTH_LONG);
                toast.setGravity(Gravity.CENTER, 0, 0);
                toast.show();
            }
        });

        // find auto complete text view
        AutoCompleteTextView actv = (AutoCompleteTextView) findViewById(R.id.autoCompleteTextView1);
        actv.setThreshold(0);
        actv.setAdapter(adapter);
    }
}

CustomAutoCompleteAdapter.java

public class CustomAutoCompleteAdapter extends BaseAdapter implements Filterable {

    public interface OnFooterClickListener {
        public void onFooterClicked();
    }

    private List<String> mObjects;
    private final Object mLock = new Object();

    private int mResource;
    private int mDropDownResource;

    private ArrayList<String> mOriginalValues;
    private ArrayFilter mFilter;

    private LayoutInflater mInflater;

    // the last item, i.e the footer
    private String mFooterText;

    // our listener
    private OnFooterClickListener mListener;

    public CustomAutoCompleteAdapter(Context context, int resource, String[] objects, String footerText) {
        mInflater   = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mResource   = mDropDownResource = resource;
        mObjects    = Arrays.asList(objects);
        mFooterText = footerText;
    }


    /**
     * Set listener for clicks on the footer item
     */
    public void setOnFooterClickListener(OnFooterClickListener listener) {
        mListener = listener;
    }

    @Override
    public int getCount() {
        return mObjects.size()+1;
    }

    @Override
    public String getItem(int position) {
        if(position == (getCount()-1)) {
            // last item is always the footer text
            return mFooterText;
        }

        // return real text
        return mObjects.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return createViewFromResource(position, convertView, parent, mResource);
    }

    private View createViewFromResource(int position, View convertView, ViewGroup parent,
                                        int resource) {
        View view;
        TextView text;

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

        try {
            //  If no custom field is assigned, assume the whole resource is a TextView
            text = (TextView) view;
        } catch (ClassCastException e) {
            Log.e("CustomAutoCompleteAdapter", "Layout XML file is not a text field");
            throw new IllegalStateException("Layout XML file is not a text field", e);
        }

        text.setText(getItem(position));

        if(position == (getCount()-1)) {
            // it's the last item, bind click listener
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(mListener != null) {
                        mListener.onFooterClicked();
                    }
                }
            });
        } else {
            // it's a real item, set click listener to null and reset to original state
            view.setOnClickListener(null);
            view.setClickable(false);
        }

        return view;
    }

    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        return createViewFromResource(position, convertView, parent, mDropDownResource);
    }

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

    /**
     * <p>An array filter constrains the content of the array adapter with
     * a prefix. Each item that does not start with the supplied prefix
     * is removed from the list.</p>
     */
    private class ArrayFilter extends Filter {
        @Override
        protected FilterResults performFiltering(CharSequence prefix) {
            FilterResults results = new FilterResults();

            if (mOriginalValues == null) {
                synchronized (mLock) {
                    mOriginalValues = new ArrayList<String>(mObjects);
                }
            }

            if (prefix == null || prefix.length() == 0) {
                ArrayList<String> list;
                synchronized (mLock) {
                    list = new ArrayList<String>(mOriginalValues);
                }
                results.values = list;

                // add +1 since we have a footer item which is always visible
                results.count = list.size()+1;
            } else {
                String prefixString = prefix.toString().toLowerCase();

                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;
                // add one since we always show the footer
                results.count = newValues.size()+1;
            }

            return results;
        }

        @Override
        protected void publishResults(CharSequence constraint, FilterResults results) {
            //noinspection unchecked
            mObjects = (List<String>) results.values;
            notifyDataSetChanged();
        }
    }
}

layout/activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"   
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp"
    tools:context=".MainActivity">

    <AutoCompleteTextView
        android:id="@+id/autoCompleteTextView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

enter image description here

Upvotes: 7

Related Questions