SearchView in ActionBar -- problems with the *Up* button
Asked Answered
W

3

7

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).

The *Up* button example at the right image

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?

Wen answered 9/8, 2013 at 14:14 Comment(5)
Interesting, what version of Android are you developing for? I wanted that effect but was unable to achieve it, my search DIDN't clear when I collapsed the searchview... :/Protean
The latest 4.3. I am just learning, trying to build demo app. Being compatible with older Android versions is the task planned for later time. Nice to have your confirmation as it seemed to me that it worked earlier when I did use 4.2. But I was not sure whether the code did not change too much from that time.Wen
Yes, I believe that it is actually a bug in earlier android. Have you looked at developer.android.com/reference/android/view/… for the onActionViewCollapsed() to handle the clearing of search results? (A bug in earlier android meant this method is useless sigh)Protean
Please, formulate is as the answer. I will let it unaccepted for a while, possibly update the question and offer the bounty to attract the attention.Wen
Regarding your latest update, the objective of Stateful SearchView is to prevent the onQueryListener not to be invoked with empty string only when it is called when the action view is expanded/collapsed. You can notice that I remove the listener you passed and readd it later again. Take your time to go over my code. Meanwhile I'll improve it and remove the clutter.Anesthesiology
A
7

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.

Anesthesiology answered 15/8, 2013 at 2:1 Comment(5)
Hi Nikola. Thanks for the code. I currently do not have the Android device to test. I am not that good to understand details of the StatefulSearchView, yet. Anyway, the onActionViewCollapsed did not contain the first setQuery("", false) in the previous version. This seems to be the trace...Wen
I can see it better now. So, the core is to override the onActionViewCollapsed and onActionViewExpanded methods. The super.onActionViewXxxx(); executes the default code and adds the derived one. The mSaveText is the boolean flag I have mentioned in the update. Possibly, I will probably choose to use the flag in the in the onQueryTextChange instead of changing the listener.Wen
Yep, that's long story short.Anesthesiology
@Wen I have edited the code. It works flawlessly. I tried scenario when the up item is pressed. Please try it and let me know.Anesthesiology
The code is valuable for me. Thanks. Anyway, I am going to study more of the related things if the problem cannot be solved the way that integrates better with the SearchView as is.Wen
G
1

I'm not sure I understand your problem but you can just detect when up is clicked like this:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            doSOmething();
            return true;
    }
    return super.onOptionsItemSelected(item);
}

If you intercept the up click, you can presumably do anything you want here. Returning true will consume the event and that should prevent any default action from taking place. This way you can do whatever you want the up button to do while at the same time consuming the up event to prevent clearing of your filters.

Geosphere answered 15/8, 2013 at 0:44 Comment(2)
I have updated the question. Please, have a look at it. If I understand your code correctly, this may be the way to know when the SearchView is to be expanded. Anyway, I have found the "trick" only as recommendation for Remaining Backward Compatibility (developer.android.com/training/search/backward-compat.html). Isn't there any better way inside the SearchView?Wen
Thanks, and my +1 for reminding me that the SearchView is still related to the menu in the case.Wen
C
1

I struggle with this a little until I found the solution.

Declare your menuItem like this, check the showAsAction attribute, type only ifRoom, if you set collapseActionView the widget will collapse and show the back button on the actionbar

<menu xmlns:android="http://schemas.android.com/apk/res/android" >
 <item
        android:id="@+id/search"
        android:actionViewClass="android.widget.SearchView"
        android:icon="@drawable/ic_2_action_search"
        android:showAsAction="ifRoom"
        android:title="@null"/>
</menu>

Set up your SearchView as usual, remember to Add setIconifiedByDefault this will make the icon to start up as an icon

SearchManager searchManager = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
searchView = (SearchView) menu.findItem(R.id.search).getActionView();
searchView.setIconifiedByDefault(true);
searchView.setOnQueryTextListener(new SearchViewOnQueryListener());
searchView.setOnCloseListener(new SearchViewOnCloseListener());
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));

On your QueryListener is where you handle the end of your search like so, here is where you use onActionViewCollapse() which collapse back the ViewSearch

@Override
public boolean onQueryTextSubmit(String query) {
            makeSearchRequest(SEARCH_TYPE_KEYWORD, query);
        searchView.setQuery("", false);
        searchView.clearFocus();
        searchView.onActionViewCollapsed();
        buttonClearSearchResults.setVisibility(View.VISIBLE);
        return false;
    }
Cyclone answered 25/4, 2014 at 18:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.