Cheok Yan Cheng
Cheok Yan Cheng

Reputation: 42768

How to preserve AutoCompleteTextView's DropDown state when gets back from launched Activity

Currently, when I

I would like to preserve AutoCompleteTextView's drop-down state which includes

I'm not exactly sure the reason why AutoCompleteTextView's dropdown will be hidden when I back from launched Activity. Hence, I had tried 2 things

  1. Change windowSoftInputMode of launched Activity from stateAlwaysHidden to stateUnchanged.
  2. In onActivityResult, when the launched Activity is closed, perform mSearchSrcTextView.showDropDown(); explicitly.

However, I am still facing the issue. The previous scroll position of AutoCompleteTextView's dropdown is not preserved. It is reset back to top of the list.

Here's the screen-shot to better illustrate the problem I am facing.


enter image description here

(Current AutoCompleteTextView's dropdown is scrolled to the end. I click on the last item and launch a new Activity)


enter image description here

(New Activity is launched. Now, I click on the BACK soft key twice, to close the keyboard and then close the Activity)


enter image description here

(Due to the explicit call of mSearchSrcTextView.showDropDown(); in onActivityResult, the drop down is shown again. However, its previous scrolled position is not being preserved. Start of list is being shown instead of end of list)

I was wondering, is there any way to preserved the AutoCompleteTextView's DropDown state, when closing a previous launched Activity?

Upvotes: 3

Views: 1555

Answers (3)

Ben P.
Ben P.

Reputation: 54234

It sounds like you've already figured out how to show the drop-down on demand (via showDropDown()), so I'll only address how to restore the scroll position of the dropdown.

You can access the first visible position of the dropdown like this:

autocomplete.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        int firstVisiblePosition = parent.getFirstVisiblePosition();
        // save this value somehow
    }
});

Save the value of this int however you'd like (in memory, via onSaveInstanceState(), pass it through to the started activity so that it can pass it back via onActivityResult(), etc). Then, wherever you re-show the dropdown, do this:

autocomplete.showDropDown();
autocomplete.setListSelection(firstVisiblePosition);

The shortcoming of this technique is that it makes the item at firstVisiblePosition completely visible, so if it was halfway scrolled out of view, the list position won't be restored perfectly. Unfortunately, I don't believe there's any way to save/restore this partial-view offset.

Upvotes: 1

Twometer
Twometer

Reputation: 1721

After an hour of coding, much trying and a lot of googling around, I've put together a solution that does just what you want. It uses reflection to access the ListView within the Dropdown menu and to access the dropdown state when you leave the activity.

The code for this is kinda long, so I'll walk you through all the parts. Firstly, I have some variables we will need:

boolean wasDropdownOpen;
int oldDropdownY;
Handler handler;

The handler will be neccessary for later, as we have to do a little trick in the onResume() method. Initialize it as usual in your onCreate() method:

handler = new Handler(getMainLooper());

Now, let's get to the tricky part.

You need to call the following method before you start any activity. It can't be done in onPause() since the Dropdown menu is already closed when this method is called. In my test code I've overridden the startActivity() and startActivityForResult() method, and called it there, but you can do this however you like.

private void processBeforeStart() {
    ListPopupWindow window = getWindow(textView);
    if(window == null) return;
    wasDropdownOpen = window.isShowing();

    ListView lv = getListView(window);
    if(lv == null) return;

    View view = lv.getChildAt(0);
    oldDropdownY = -view.getTop() + lv.getFirstVisiblePosition() * view.getHeight();
}

This will save your dropdown ListView's state for later. Now, we will load it. This is the onResume() method we will need for this:

@Override
protected void onResume() {
    super.onResume();
    if (wasDropdownOpen)
        textView.showDropDown();

    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            ListView lv = getListView(getWindow(textView));
            if (lv != null)
                scrollToY(lv, oldDropdownY);
        }
    }, 150);
}

First of all, let me explain this method. We saved the state if the dropdown was open, so we reopen the menu if it was. Simple. The next part is the scrolling. We need to do this in a Handler because the UI is not yet fully loaded when onResume() is called and therefore the ListView is still inaccessible.

The scrollToY() method you see there is a modified version of the code from this post, as Android's ListView does not have an inbuilt method to set the scroll position as precisely as we want it here.

The implementation of this method is as follows:

private void scrollToY(ListView lv, int position) {
    int itemHeight = lv.getChildAt(0).getHeight();
    int item = (int) Math.floor(position / itemHeight);
    int scroll = (item * itemHeight) - position;
    lv.setSelectionFromTop(item, scroll);// Important
}

Now, you've probably seen the getWindow() and getListView() methods I've used above. These are the reflection methods, which we have to use because Android does not expose a public API to access the ListView within the ListPopupWindow of the AutoCompleteTextView. Additionally, the DropDownListView, a subclass of ListView that is actually used within this object, is not visible to the oudside as well, so we have to use Reflection once again.

Here is the implementation of my two helper methods:

private ListView getListView(ListPopupWindow window) {
    for (Field field : window.getClass().getDeclaredFields()) {
        if (field.getType().getName().equals("android.widget.DropDownListView")) {
            field.setAccessible(true);
            try {
                return (ListView) field.get(window);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}

private ListPopupWindow getWindow(AutoCompleteTextView tv) {
    Class realClass = tv.getClass().getName().contains("support") ? tv.getClass().getSuperclass() : tv.getClass();
    for (Field field : realClass.getDeclaredFields()) {
        if (field.getType().getName().equals(ListPopupWindow.class.getName())) {
            field.setAccessible(true);
            try {
                return (ListPopupWindow) field.get(tv);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}

I've tested this on Android O (API level 26) and it works just as you described you want it to work.

I hope that the effort I put into this answer gets me a chance on the Bounty ;-)

Upvotes: 1

Shuwn Yuan Tee
Shuwn Yuan Tee

Reputation: 5748

For AutoCompleteTextView, it has a method called dismissDropDown(). I believe when back from newly launched activity, this function is being triggered. So we workaround this problem by extending AutoCompleteTextView & override it's dismissDropDown().

We add a boolean flag temporaryIgnoreDismissDropDown, to indicate whether to temporarily ignore dismissDropDown.

public class MyAutoCompleteTextView extends AutoCompleteTextView {
    private boolean temporaryIgnoreDismissDropDown = false;

    .....

    @Override
    public void dismissDropDown() {
        if (this.temporaryIgnoreDismissDropDown) {
            this.temporaryIgnoreDismissDropDown = false;
            return;
        }

        super.dismissDropDown();
    }

    public void setTemporaryIgnoreDismissDropDown(boolean flag) {
        this.temporaryIgnoreDismissDropDown = flag;
    }
}

Before launching new Activity, we set dismissDropDown to true. After coming back from launched activity, dismissDropDown is called. The override method checks if temporaryIgnoreDismissDropDown is true, just set it to false & do nothing. So the real dismissDropDown is skipped.

// myAutoCompleteTextView is instance of MyAutoCompleteTextView
myAutoCompleteTextView.setTemporaryIgnoreDismissDropDown(true);

// launch new Activity
startActivity(....);

Hope this help, good luck!

Upvotes: 4

Related Questions