package org.odk.collect.android.widgets; import org.javarosa.core.model.SelectChoice; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.SelectOneData; import org.javarosa.core.model.data.helper.Selection; import org.javarosa.form.api.FormEntryPrompt; import android.content.Context; import android.graphics.Color; import android.view.Gravity; import android.view.inputmethod.InputMethodManager; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Filter; import android.widget.Filterable; import android.widget.Toast; import java.util.ArrayList; import java.util.Vector; /** * AutoCompleteWidget handles select-one fields using an autocomplete text box. The user types part * of the desired selection and suggestions appear in a list below. The full list of possible * answers is not displayed to the user. The goal is to be more compact; this question type is best * suited for select one questions with a large number of possible answers. If images, audio, or * video are specified in the select answers they are ignored. * * @author Jeff Beorse (jeff@beorse.net) */ public class AutoCompleteWidget extends QuestionWidget { AutoCompleteAdapter choices; AutoCompleteTextView autocomplete; Vector<SelectChoice> mItems; // Defines which filter to use to display autocomplete possibilities String filterType; // The various filter types String match_substring = "substring"; String match_prefix = "prefix"; String match_chars = "chars"; public AutoCompleteWidget(Context context, FormEntryPrompt prompt, String filterType) { super(context, prompt); mItems = prompt.getSelectChoices(); mPrompt = prompt; choices = new AutoCompleteAdapter(getContext(), android.R.layout.simple_list_item_1); autocomplete = new AutoCompleteTextView(getContext()); // Default to matching substring if (filterType != null) { this.filterType = filterType; } else { this.filterType = match_substring; } for (SelectChoice sc : mItems) { choices.add(prompt.getSelectChoiceText(sc)); } choices.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line); autocomplete.setAdapter(choices); autocomplete.setTextColor(Color.BLACK); setGravity(Gravity.LEFT); // Fill in answer String s = null; if (mPrompt.getAnswerValue() != null) { s = ((Selection) mPrompt.getAnswerValue().getValue()).getValue(); } for (int i = 0; i < mItems.size(); ++i) { String sMatch = mItems.get(i).getValue(); if (sMatch.equals(s)) { autocomplete.setText(mItems.get(i).getLabelInnerText()); } } addView(autocomplete); } /* * (non-Javadoc) * @see org.odk.collect.android.widgets.QuestionWidget#getAnswer() */ @Override public IAnswerData getAnswer() { String response = autocomplete.getText().toString(); for (SelectChoice sc : mItems) { if (response.equals(mPrompt.getSelectChoiceText(sc))) { return new SelectOneData(new Selection(sc)); } } // If the user has typed text into the autocomplete box that doesn't match any answer, warn // them that their // solution didn't count. if (!response.equals("")) { Toast.makeText(getContext(), "Warning: \"" + response + "\" does not match any answers. No answer recorded.", Toast.LENGTH_LONG).show(); } return null; } /* * (non-Javadoc) * @see org.odk.collect.android.widgets.QuestionWidget#clearAnswer() */ @Override public void clearAnswer() { autocomplete.setText(""); } /* * (non-Javadoc) * @see org.odk.collect.android.widgets.QuestionWidget#setFocus(android.content.Context) */ @Override public void setFocus(Context context) { // Hide the soft keyboard if it's showing. InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.hideSoftInputFromWindow(this.getWindowToken(), 0); } private class AutoCompleteAdapter extends ArrayAdapter<String> implements Filterable { private ItemsFilter mFilter; public ArrayList<String> mItems; public AutoCompleteAdapter(Context context, int textViewResourceId) { super(context, textViewResourceId); mItems = new ArrayList<String>(); } /* * (non-Javadoc) * @see android.widget.ArrayAdapter#add(java.lang.Object) */ @Override public void add(String toAdd) { super.add(toAdd); mItems.add(toAdd); } /* * (non-Javadoc) * @see android.widget.ArrayAdapter#getCount() */ @Override public int getCount() { return mItems.size(); } /* * (non-Javadoc) * @see android.widget.ArrayAdapter#getItem(int) */ @Override public String getItem(int position) { return mItems.get(position); } /* * (non-Javadoc) * @see android.widget.ArrayAdapter#getPosition(java.lang.Object) */ @Override public int getPosition(String item) { return mItems.indexOf(item); } public Filter getFilter() { if (mFilter == null) { mFilter = new ItemsFilter(mItems); } return mFilter; } /* * (non-Javadoc) * @see android.widget.ArrayAdapter#getItemId(int) */ @Override public long getItemId(int position) { return position; } private class ItemsFilter extends Filter { final ArrayList<String> mItemsArray; public ItemsFilter(ArrayList<String> list) { if (list == null) { mItemsArray = new ArrayList<String>(); } else { mItemsArray = new ArrayList<String>(list); } } /* * (non-Javadoc) * @see android.widget.Filter#performFiltering(java.lang.CharSequence) */ @Override protected FilterResults performFiltering(CharSequence prefix) { // Initiate our results object FilterResults results = new FilterResults(); // If the adapter array is empty, check the actual items array and use it if (mItems == null) { mItems = new ArrayList<String>(mItemsArray); } // No prefix is sent to filter by so we're going to send back the original array if (prefix == null || prefix.length() == 0) { results.values = mItemsArray; results.count = mItemsArray.size(); } else { // Compare lower case strings String prefixString = prefix.toString().toLowerCase(); // Local to here so we're not changing actual array final ArrayList<String> items = mItems; final int count = items.size(); final ArrayList<String> newItems = new ArrayList<String>(count); for (int i = 0; i < count; i++) { final String item = items.get(i); String item_compare = item.toLowerCase(); // Match the strings using the filter specified if (filterType.equals(match_substring) && (item_compare.startsWith(prefixString) || item_compare .contains(prefixString))) { newItems.add(item); } else if (filterType.equals(match_prefix) && item_compare.startsWith(prefixString)) { newItems.add(item); } else if (filterType.equals(match_chars)) { char[] toMatch = prefixString.toCharArray(); boolean matches = true; for (int j = 0; j < toMatch.length; j++) { int index = item_compare.indexOf(toMatch[j]); if (index > -1) { item_compare = item_compare.substring(0, index) + item_compare.substring(index + 1); } else { matches = false; break; } } if (matches) { newItems.add(item); } } else { // Default to substring if (item_compare.startsWith(prefixString) || item_compare.contains(prefixString)) { newItems.add(item); } } } // Set and return results.values = newItems; results.count = newItems.size(); } return results; } /* * (non-Javadoc) * @see android.widget.Filter#publishResults(java.lang.CharSequence, android.widget.Filter.FilterResults) */ @SuppressWarnings("unchecked") @Override protected void publishResults(CharSequence constraint, FilterResults results) { mItems = (ArrayList<String>) results.values; // Let the adapter know about the updated list if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } } /* * (non-Javadoc) * @see org.odk.collect.android.widgets.QuestionWidget#setOnLongClickListener(android.view.View.OnLongClickListener) */ @Override public void setOnLongClickListener(OnLongClickListener l) { autocomplete.setOnLongClickListener(l); } /* * (non-Javadoc) * @see org.odk.collect.android.widgets.QuestionWidget#cancelLongPress() */ @Override public void cancelLongPress() { super.cancelLongPress(); autocomplete.cancelLongPress(); } }