SearchView in ActionBar -- problems with the *Up* button

android, android-listview

Solution

I have written a `StatefulSearchView` which retains the text:

import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.SearchView;
import android.widget.SearchView.OnQueryTextListener;
import android.widget.TextView;

public class StatefulSearchView extends SearchView implements android.view.View.OnLayoutChangeListener, OnQueryTextListener,android.widget.SearchView.OnCloseListener{

    private boolean mSaveText=true;
    private OnQueryTextListener mQueryListener;
    private String mQuery;
    private OnCloseListener mCloseListener;
    private boolean fromIconify = true;

    public StatefulSearchView(Context context, AttributeSet attrs) {
        super(context, attrs);
        addOnLayoutChangeListener(this);
        super.setOnCloseListener(this);
    }

    public StatefulSearchView(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
        addOnLayoutChangeListener(this);
        super.setOnCloseListener(this);
    }

    public void setSaveSearchTextState(boolean save){
        this.mSaveText = save;
        this.setSaveEnabled(mSaveText);

    }


    public void setOnStatefulQueryTextListener(OnQueryTextListener listener) {
        mQueryListener = listener; 
        super.setOnQueryTextListener(this);
    }

    @Override
    public void onLayoutChange(View v, int left, int top, int right,
            int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        if(super.isIconfiedByDefault() || !super.isIconified() && !TextUtils.isEmpty(mQuery) && mSaveText){      
              setSavedText(mQuery);              
        }
         Log.i("onLayoutChanged()",""+mQuery);

    }


    @Override
    public void setIconified(boolean iconify) {
        mQuery = getQuery().toString();
        Log.i("setIconified()",""+mQuery);
        super.setOnCloseListener(null);
        super.setIconified(iconify);
        super.setIconified(iconify);
        super.setOnCloseListener(this);
        fromIconify = true;
    }


    @Override
    public void setOnCloseListener(OnCloseListener listener) {
        mCloseListener = listener;
        super.setOnCloseListener(this);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable state =  super.onSaveInstanceState();
        return new SearchQueryState(state, mQuery, mSaveText);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SearchQueryState sqs = (SearchQueryState)state;
        super.onRestoreInstanceState(sqs.getSuperState());
        mQuery = sqs.getSavedQuery();
        mSaveText = sqs.getSaveText();
    }

    @Override
    public boolean onQueryTextChange(String arg0) {
        mQuery = arg0;
        return mQueryListener.onQueryTextChange(mQuery);
    }

    @Override
    public boolean onQueryTextSubmit(String arg0) {
        // TODO Auto-generated method stub
        return mQueryListener.onQueryTextSubmit(arg0);
    }

    private TextView getTextView(){
        int searchTextViewId = getContext().getResources().getIdentifier("android:id/search_src_text", null, null);
        return (TextView) this.findViewById(searchTextViewId);
    }

    private void setSavedText(String s){
       super.setOnQueryTextListener(null);
       Log.i("setSavedText()",""+s);
       TextView t = getTextView();
       t.setText(s);
       if(!TextUtils.isEmpty(s))
           ((EditText)t).setSelection(s.length());
       super.setOnQueryTextListener(mQueryListener);
    }
    private class SearchQueryState extends BaseSavedState{

        private boolean mSaveText;
        private String mQueryText;
        public SearchQueryState(Parcel arg0) {
            super(arg0);
            this.mQueryText = arg0.readString();
            this.mSaveText = arg0.readInt() == 1;
        }

        public SearchQueryState(Parcelable superState, String queryText, boolean saveText) {
            super(superState);
            this.mQueryText = queryText;
            this.mSaveText = saveText;
        }

        public boolean getSaveText(){
            return this.mSaveText;
        }


        public String getSavedQuery(){
            return mQueryText;
        }
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            // TODO Auto-generated method stub
            super.writeToParcel(dest, flags);
            dest.writeString(mQueryText);
            dest.writeInt(mSaveText? 1: 0);
        }


    }

    @Override
    public boolean onClose() {
        Log.i("onClose()", "Is from setIconified(): "+fromIconify);
        if(!fromIconify){
            mQuery = null;
            fromIconify = false;
        }
        return mCloseListener == null ? false : mCloseListener.onClose();
    }


}

In demonstration activity:

public class MainActivity extends Activity{

    private StatefulSearchView mSearchView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getActionBar().setHomeButtonEnabled(true);
    }

    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
      if(item.getItemId()==android.R.id.home) {
          mSearchView.setIconified(true);
          return true;
      }
        return super.onMenuItemSelected(featureId, item);
    }

    @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);

            MenuItem item = menu.findItem(R.id.action_search);

         mSearchView =(StatefulSearchView)item.getActionView();
         mSearchView.setSaveSearchTextState(true);
         mSearchView.setOnStatefulQueryTextListener(new OnQueryTextListener(){

            @Override
            public boolean onQueryTextChange(String newText) {
                // TODO Auto-generated method stub
                return false;
            }

            @Override
            public boolean onQueryTextSubmit(String query) {
                // TODO Auto-generated method stub
                return false;
            }});
        return true;
    }

In menu xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/action_search"
        android:orderInCategory="100"
        android:showAsAction="always"
        android:actionViewClass="com.nikola.despotoski.saveablesearchview.StatefulSearchView"
        android:title="@string/action_settings"/>

</menu>

In the source of the `SearchView`, it clearly says that they change the text to `""`:

@Override
    public void onActionViewCollapsed() {
        setQuery("", false);
        clearFocus();
        updateViewsVisibility(true);
        mQueryTextView.setImeOptions(mCollapsedImeOptions);
        mExpandedInActionView = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onActionViewExpanded() {
        if (mExpandedInActionView) return;

        mExpandedInActionView = true;
        mCollapsedImeOptions = mQueryTextView.getImeOptions();
        mQueryTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
        mQueryTextView.setText("");
        setIconified(false);
    }

Let me know if you have issues.

Problem

I am using the the `SearchView` in the `ActionBar` of the `ListView`. The magnifying glass can be touched, the `SearchView` shows its edit box, and the user can enter the text for filtering the content of the list. It almost works. However, when the user presses the Up button, the `SearchView` collapses back to the icon, the text inside the widget is cleared, and the filtering is reset. The effect (in my case) is that the list can be filtered only when the `SearchView` is not iconified. The wanted behaviour is to keep the filter text also after the `SearchView` was collapsed. Attention: The behaviour probably changed in Android 4.3. With 4.2.2 it worked as wanted. See the observations below. Details: To be more specific, the menu contains the following item: ``` <item android:id="@+id/menu_search_customers" android:title="@string/menu_search_text" android:icon="@android:drawable/ic_menu_search" android:showAsAction="ifRoom|collapseActionView" android:actionViewClass="android.widget.SearchView" /> ``` Notice the icon and the `android:showAsAction`. I belive the Up button appears by default when the `SearchView` is expanded (by Up I mean the `<` plus the icon -- see the right image with the blue book from the official Navigation with Back and Up). It seems that the default handler implementation just collapses the expanded `SearchView` (returns back to the icon state). When debugging, I have found that the `onQueryTextChange()` is fired with the empty text when the Up is used. (I believe this was not the case with Android 4.2.2, because it worked as wanted before the OS update.) This is the reason why the filtering of the list items is also reset -- see my `onQueryTextChange()` below. I want the `SearchView` collapsed, and the filter text displayed as subtitle in the action bar. So far, my code related to the `SearchView` looks like this: ``` @Override public boolean onCreateOptionsMenu(Menu menu) { // MenuInflater adds the magnifying glass icon for the SearchView // to the ActionBar as the always visible menu item. MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.customers_menu, menu); // Get the related SearchView widget. SearchView sv = (SearchView) menu.findItem(R.id.menu_search_customers) .getActionView(); // Get the changes immediately. sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { // I am not sure whether the onQueryTextSubmit() is important // for the purpose. @Override public boolean onQueryTextSubmit(String query) { getActionBar().setSubtitle(mCurFilter); return true; } @Override public boolean onQueryTextChange(String newText) { // The newText is stored into a member variable that // is used when the new CursorLoader is created. mCurFilter = newText; getActionBar().setSubtitle(mCurFilter); getLoaderManager().restartLoader(0, null, CustomersOverviewActivity.this); return true; } }); return true; } ``` The restarted loader calls the `onCreateLoader`. Notice the `mCurFilter` is used for building the SQL query: ``` @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { String[] projection = { CustomerTable._ID, CustomerTable.CODE, CustomerTable.NAME, CustomerTable.STREET, CustomerTable.TOWN }; String selection = null; // init String[] selectionArgs = null; // init if ( ! mCurFilter.isEmpty()) { selection = CustomerTable.NAME + " like ?"; selectionArgs = new String[]{ "%" + mCurFilter +"%" }; } CursorLoader cursorLoader = new CursorLoader(this, DemoContentProvider.CUSTOMERS_CONTENT_URI, projection, selection, selectionArgs, orderInfo); return cursorLoader; } ``` I would like to detect the situation when the Up is pressed before the `onQueryTextChange()` is called. This way (say) I could set a flag and block the `mCurFilter` assignment by the emptied `SearchView` content. Also, when the search icon is expanded again, I would like to initialize the text in the expanded `SearchView` from the `mCurFilter` before it is shown (i.e. the expanded view is preset with the filter text). How that can be done? Update: The earlier implementation of the `SearchView` had... ``` @Override public void onActionViewCollapsed() { clearFocus(); updateViewsVisibility(true); mQueryTextView.setImeOptions(mCollapsedImeOptions); mExpandedInActionView = false; } ``` Now, it contains... ``` @Override public void onActionViewCollapsed() { setQuery("", false); clearFocus(); updateViewsVisibility(true); mQueryTextView.setImeOptions(mCollapsedImeOptions); mExpandedInActionView = false; } ``` Do you know what could be the reason for setting the query to the empty string? Should I override the new implementation by the old code? Or is there a better way?

Original source