Question

I have an AutoCompleteTextView I populate dynamically. I do this dynamically, because I have about 10000 proposals (streets) to show, so I split the list according to their first letter. Let's say somebody enters "a", I populate the Adapter with all streets beginning with "a". This works and is fast enough in emulator and it was fast enough on my old phone Android 2.1. Suddenly I recognized that the filling of the list is very slow. It takes about 10 seconds to populate. But I think it's not a problem of my code but a problem of my phone. Some time ago I upgraded to Android 2.3.7 with CyanogenMod 7.2.0-blade. I'm absolutely sure that I never had this problems before, because I would never had put some laggy implementation on production. I recognized something strange while I made a trace. All the performance is frittered away on a method called TextUtils.hasArabicCharacters(). See the cyan bars...

performance issue

I don't find anything about this method. TextUtils doesn't have hasArabicCharacters, so I guess it's some proprietary thing -> CyanogenMod? If I trace the same code on any emulator, no method called 'hasArabicCharacters' is ever invoked and the autocomplete behaviour is pretty fast. Tested under Android 2.1, 2.3.3 and 4.1.2 emulator.

This is the invocation chain (upwards):

TextUtils.hasArabicCharacter() -> TextUtils.reshapeArabic() -> Paint.measureText() -> Styled.drawDirectionalRun() -> Styled.measureText() -> BoringLayout.isBoring() -> TextView.onMeasure() -> View.measure() -> ListView.measureScrapChild() -> ListView.measureHeightOfChildren() -> AutoCompleteTextView.buildDropdown() -> AutoCompleteTextView.showDropDown() -> AutoCompleteTextView.updateDropDownForFilter() -> AutoCompleteTextView.access$1700 -> AutoCompleteTextView$PopulateDataSetObserver$1.run() -> Handler.handleCallback() -> Handler.dispatchMessage()

This is how I populate my Adapter. Maybe there is some workaround I could apply. Any ideas?

Activity:

        final StreetArrayAdapter adapter = new StreetArrayAdapter(this, R.layout.simple_dropdown_item_1line);

        autoCompleteTextView.setAdapter(adapter);
        autoCompleteTextView.setValidator(new Validator());
        autoCompleteTextView.setThreshold(0);
        autoCompleteTextView.setOnItemClickListener(new OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
                streetInfoField.setText("");
            }
        });

        autoCompleteTextView.addTextChangedListener(new StreetTextWatcher(adapter));

......

    class Validator implements AutoCompleteTextView.Validator {

        @Override
        public CharSequence fixText(CharSequence invalidText) {
            return "";
        }

        @Override
        public boolean isValid(CharSequence text) {
            Log.v(TAG, "Checking if valid: " + text);
            String[] streets = StreetNameFactory.getStreetsWithLetter(text.subSequence(0, 1).toString().toUpperCase(Locale.US));

            Arrays.sort(streets);
            if (Arrays.binarySearch(streets, text.toString()) >= 0) {
                return true;
            }

            return false;
        }
    }

StreetNameFactory.getStreetsWithLetter

    public static String[] getStreetsWithLetter(String letter)  {
        Log.i(StreetNameFactory.class.getSimpleName(), "letter:" + letter);
        if ("A".equals(letter))  {
            return StreetNames.STREETS_A;
        }
        if ("Ä".equals(letter))  {
            return StreetNames.STREETS_A;
        }
        if ("B".equals(letter))  {
            return StreetNames.STREETS_B;
        }
.....

StreetTextWatcher:

public class StreetTextWatcher implements TextWatcher {

    private final StreetArrayAdapter adapter;
    private boolean alreadyAdded = false;

    public StreetTextWatcher(StreetArrayAdapter adapter) {
        this.adapter = adapter;
    }

    @Override
    public void afterTextChanged(Editable s) {
        //not used
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        if (s.length() < 1)  {
            adapter.clear();

            alreadyAdded = false;
        }
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {           
        if (s.length() == 1) {
            populateAdapter(s);

            alreadyAdded = true;
        }
    }

    private synchronized void populateAdapter(CharSequence s) { 
        String charSequence = s.toString().toUpperCase(Locale.US);
        if (charSequence.startsWith("A") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_A);
        }

        if (charSequence.startsWith("Ä") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_A);
        }

        if (charSequence.startsWith("B") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_B);
        }

        if (charSequence.startsWith("C") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_C);
        }

        if (charSequence.startsWith("D") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_D);
        }

        if (charSequence.startsWith("E") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_E);
        }
        //more code....

        if (charSequence.startsWith("Z") && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_Z);
        }
        if (Pattern.matches("[1-9]", s.toString()) && !alreadyAdded)  {
            adapter.addAll(StreetNames.STREETS_NUMBERS);
        }
    }
}

StreetArrayAdapter:

public class StreetArrayAdapter extends ArrayAdapter<String> {

    private final String TAG = StreetArrayAdapter.class.getSimpleName();

    public StreetArrayAdapter(Context context, int textViewResourceId) {
        super(context, textViewResourceId);
    }

    public void addAll(String[] streets) {
        Log.i(TAG, "BEGIN LIST FILL at: " + new Date(System.currentTimeMillis()).toString());
        for (String street : streets) {
            add(street);
        }
        Log.i(TAG, "END LIST FILL at: " + new Date(System.currentTimeMillis()).toString());
    }
}

STREETS_A, STREETS_B, STREETS_C are just string arrays I assign.

[UPDATE]
I was able to find a nice workaround. I must not load the list when I enter the first letter. When I load the list after I typed at least 3 letters (StreetTextWatcher.onTextChange), I have no freeze and the dropdown is very fast again. Moreover the user probably doesn't even recognize the change.

Was it helpful?

Solution

I think it's a flaw of CyanogenMod 7.2.

Looking at the source code shows that in every (in your case) TextView.onMeasure() call, BoringLayout.isBoring() is called, which calls Styled.measureText(), which calls Styled.drawDirectionalRun(), which looks like a quite heavy method (it calls a lot of other methods too).

I think it can be hard to workaround this in your code. I was looking for some flag that maybe you can set to omit this heavy "arabic" methods, something like setIsArabic(false), but I haven't found anything. Maybe you could replace bytecode of for example Styled.measureText(), like suggested here, but it seems pretty time-consuming.

It looks like in newer version, for example CyanogenMod 10.1, the behaviour is different. So maybe just upgrade your CyanogenMod and the problem will dissapear.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top