Reputation: 6024
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
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
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>
Upvotes: 7