mariotomo
mariotomo

Reputation: 9748

How can I implement a custom autocomplete dictionary for non standard text input?

I'm re-writing part of a larger GTK/Python program to Android. In one of the edit texts, my users will be typing a botanic taxon name, things that include words like Pachypharynx or Erythroxylaceae.

Such words are part of a "Botanic lexicon", and people who speak different languages pronounce them differently, and make different typing mistakes when entering them. Some have quasi-homonyms, like the /Hieronima/Hyeronima/Hieronyma/ group, where the first two are classified as Euphorbiaceae, the latter as Phyllanthaceae. Some are valid names but a better choice is available (eg: Coccus is considered a synonym of Cocos)

So where's the programming problem? Well, I want to help the user!

I have considered and discarded the following:

I would like to

and I consider this question worth being asked (I don't know why you guys/ladies downvote without explanation) and I think that the answer I found and built is going to help others.

Upvotes: 0

Views: 496

Answers (1)

mariotomo
mariotomo

Reputation: 9748

This answer builds on others, but most of all on Carl Anderson's. It is based on AutoCompleteTextView option, and I am going to look one by one at each element of the solution.

First of all, when creating the view, let's grab the ACTV and associate it with a custom adapter:

public View onCreateView([...]) {
 .
 .
    TaxonomyDatabase db = new TaxonomyDatabase(getContext());
    List<Epithet> allEpithets = db.getAllGenera();
    EpithetAdapter hints = new EpithetAdapter(getContext(),
            android.R.layout.select_dialog_item, allEpithets);
    AutoCompleteTextView widget = rootView.findViewById(R.id.etCollectSpecies);
    widget.setThreshold(2); // start hinting from second character
    widget.setAdapter(hints);

The TaxonomyDatabase.getAllGenera grabs all genera from the database, with their accepted equivalent, and the family they belong to.

List<Epithet> getAllGenera() {
    ArrayList<Epithet> r = new ArrayList<>();
    SQLiteDatabase db = getReadableDatabase();
    Cursor cr = db.rawQuery(
            "select o.epithet, a.epithet, o.phonetic, a.family_name " +
                    "from taxon o "+"" +
                    "left join taxon a on o.accepted_id = a.id " +
                    "where o.rank = 5 " +
                    "order by o.epithet",
            new String[]{});
     .
     .

Now the EpithetAdapter constructor accepts this list, and first adds it to its internal candidates list, but then again copies each Epithet again in the list: the first instance is for literal match, the second instance is to allow for phonetic match. The logic of this is hidden in Epithet copy constructor, that sets the isExact field to false.

EpithetAdapter(Context context, int textViewResourceId, List<Epithet> epithets) {
    super(context, textViewResourceId, epithets);
    // copy all the epithets twice
    mEpithets = new ArrayList<>(epithets.size() * 2);
    // first to be used as exact matches (String×4 constructor)
    mEpithets.addAll(epithets);
    // then again to be used as phonetic matches (copy constructor)
    for(Epithet e : epithets) {
        mEpithets.add(new Epithet(e));
    }
}

The Epithet class has a toString method, that takes care of how the Epithet shows in the ACTV drop down list:

public String toString() {
    if (accepted != null) {
        return epithet + " → " + accepted;
    } else {
        return epithet + " (" + family + ")";
    }
}

the Filter in the EpithetAdapter does the rest of the task: when user clicks on a suggestion, ConvertResultToString is invoked:

    public String convertResultToString(Object resultValue) {
        return ((Epithet)resultValue).epithet;
    }

Filtering itself, that is the performFiltering method, is just the logical consequence of all said up to now:

    protected FilterResults performFiltering(CharSequence constraint) {
        FilterResults results = new FilterResults();
        if (constraint != null) {
            String exact = constraint.toString();
            String phonetic = TaxonomyDatabase.phonetic(exact);
            exact = exact.substring(0, 1).toUpperCase() + exact.substring(1).toLowerCase();
            ArrayList<Epithet> suggestions = new ArrayList<>();
            for (Epithet epithet : mEpithets) {
                if (epithet.isExact) {
                    if (epithet.epithet.startsWith(exact)) {
                        suggestions.add(epithet);
                    }
                } else {
                    if (epithet.phonetic.startsWith(phonetic) &&
                            !epithet.epithet.startsWith(exact)) {
                        suggestions.add(epithet);
                    }
                }
            }
            results.values = suggestions;
            results.count = suggestions.size();
        }
        return results;
    }
  • last minor detail, disable text suggestions (android:inputType="textNoSuggestions" in the layout) in order not to dirty user dictionary.

when user types coccus, the software suggests:

enter image description here

when user types »galanth«, literal matches come first:

enter image description here

when user types »calanth«, again literal matches come first:

enter image description here

What you see in the drop down list is Epithet.toString. Clicking on a hint only copies Epithet.epithet.

Upvotes: 1

Related Questions