FloatingSearchView

An implementation of a floating search box with search suggestions, also called persistent search bar.

Alt text
Alt text
Alt text
Alt text

Use it as follows:

Step 1: Install it

  1. In your dependencies, add
implementation 'xyz.quaver:floatingsearchview:1.2.0-rc2'

Step 2: Add to Layout

Add a FloatingSearchView to your view hierarchy, and make sure that it takes up the full width and height of the screen

Example:

<xyz.quaver.floatingsearchview.FloatingSearchView
    android:id="@+id/floating_search_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:searchBarMarginLeft="@dimen/search_view_inset"
    app:searchBarMarginTop="@dimen/search_view_inset"
    app:searchBarMarginRight="@dimen/search_view_inset"
    app:searchHint="Search..."
    app:suggestionsAnimDuration="250"
    app:showSearchKey="false"
    app:leftActionMode="showHamburger"
    app:menu="@menu/menu_main"
    app:elevation="4dp"
    app:close_search_on_keyboard_dismiss="true"/>

Step 3: Write Code

Listen to query changes and provide suggestion items that implement SearchSuggestion

binding.searchView.onQueryChangeListener = { oldQuery, newQuery -> {
    //get suggestions based on newQuery

    //pass them on to the search view
    binding.searchView.swapSuggestions(newSuggestions);
}

Java

mSearchView.setOnQueryChangeListener((oldQuery, newQuery) -> {
    //get suggestions based on newQuery

    //pass them on to the search view
    mSearchView.swapSuggestions(newSuggestions);

    return Unit.INSTANCE;
});

Left action mode:

The left action can be configured as follows:

Add

app:leftActionMode="[insert one of the options from table below]"

Listen to hamburger button clicks:

 binding.searchView.onMenuClickListener = object: FloatingSearchView.OnLeftMenuClickListener {
     ...
 }

To quickly connect your NavigationDrawer to the hamburger button:

binding.menuView.attachNavigationDrawerToMenuButton(mDrawerLayout);

Listen to home (back arrow) button clicks:

binding.menuView.setOnHomeActionClickListener(
      new FloatingSearchView.OnHomeActionClickListener() { ... });

Add a menu resource

app:menu="@menu/menu_main"

In the menu resource, set items' app:showAsAction="[insert one of the options described in the table below]"

| never | Puts the menu item in the overflow options popup |
| ifRoom | Shows an action icon for the menu if the following conditions are met: 1. The search is not focused. 2. There is enough room for it. |
| always | Shows an action icon for the menu if there is room, regardless of whether the search is focused or not. |

Listen for item selections

binding.searchView.onMenuItemClickListener = { item ->

}

Configure suggestion item:

First, implement SearchSuggestion

Optional:

Set a callback for when a given suggestion is bound to the suggestion list.

For the history icons to show, you would need to implement this. Refer to the sample app for an example implementation.

binding.searchView.onBindSuggestionCallback = { binding, item, itemPosition ->
  // suggestionView = binding.root, leftIcon = binding.leftIcon, textView = binding.body
  //here you can set some attributes for the suggestion's left icon and text. For example,
  //you can choose your favorite image-loading library for setting the left icon's image.
});

Styling:

Available styling:

   <style name="SearchView">
           <item name="backgroundColor"></item>
           <item name="viewSearchInputTextColor"></item>
           <item name="viewSuggestionItemTextColor"></item>
           <item name="hintTextColor"></item>
           <item name="dividerColor"></item>
           <item name="clearBtnColor"></item>
           <item name="leftActionColor"></item>
           <item name="menuItemIconColor"></item>
           <item name="suggestionRightIconColor"></item>
           <item name="actionMenuOverflowColor"></item>
   </style>

backgroundColor uses colorSurface value defined in Theme.
To change backgroundColor according to Light/Dark Theme, define colorSurface or use Theme.Material/Theme.MaterialComponents as a parent.
Otherwise, backgroundColor defaults to White.

Full Example

Find full example

/*
 *     tom5079/FloatingSearchView was ported from arimorty/FloatingSearchView
 *
 *     Copyright 2015 Ari C.
 *     Copyright 2020 tom5079
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 */

package xyz.quaver.floatingsearchview.sample.fragment;

import android.graphics.Color;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;

import com.google.android.material.appbar.AppBarLayout;

import java.util.List;

import kotlin.Unit;
import xyz.quaver.floatingsearchview.FloatingSearchView;
import xyz.quaver.floatingsearchview.sample.R;
import xyz.quaver.floatingsearchview.sample.data.ColorSuggestion;
import xyz.quaver.floatingsearchview.sample.data.ColorWrapper;
import xyz.quaver.floatingsearchview.sample.data.DataHelper;
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion;

public class ScrollingSearchExampleFragment extends BaseExampleFragment implements AppBarLayout.OnOffsetChangedListener {
    private final String TAG = "BlankFragment";

    public static final long FIND_SUGGESTION_SIMULATED_DELAY = 250;

    private FloatingSearchView mSearchView;
    private AppBarLayout mAppBar;

    private boolean mIsDarkSearchTheme = false;

    private String mLastQuery = "";

    public ScrollingSearchExampleFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_scrolling_search_example, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mSearchView = (FloatingSearchView) view.findViewById(R.id.floating_search_view);
        mAppBar = (AppBarLayout) view.findViewById(R.id.appbar);

        mAppBar.addOnOffsetChangedListener(this);

        setupDrawer();
        setupSearchBar();
    }

    private void setupSearchBar() {
        mSearchView.setOnQueryChangeListener((oldQuery, newQuery) -> {
            if (!oldQuery.equals("") && newQuery.equals("")) {
                mSearchView.clearSuggestions();
            } else {

                //this shows the top left circular progress
                //you can call it where ever you want, but
                //it makes sense to do it when loading something in
                //the background.
                mSearchView.showProgress();

                //simulates a query call to a data source
                //with a new query.
                DataHelper.findSuggestions(getActivity(), newQuery, 5,
                        FIND_SUGGESTION_SIMULATED_DELAY, new DataHelper.OnFindSuggestionsListener() {

                            @Override
                            public void onResults(List<ColorSuggestion> results) {

                                //this will swap the data and
                                //render the collapse/expand animations as necessary
                                mSearchView.swapSuggestions(results);

                                //let the users know that the background
                                //process has completed
                                mSearchView.hideProgress();
                            }
                        });
            }

            Log.d(TAG, "onSearchTextChanged()");

            return Unit.INSTANCE;
        });

        mSearchView.setOnSearchListener(new FloatingSearchView.OnSearchListener() {
            @Override
            public void onSuggestionClicked(final SearchSuggestion searchSuggestion) {

                ColorSuggestion colorSuggestion = (ColorSuggestion) searchSuggestion;
                DataHelper.findColors(getActivity(), colorSuggestion.getBody(),
                        new DataHelper.OnFindColorsListener() {

                            @Override
                            public void onResults(List<ColorWrapper> results) {
                                //show search results
                            }

                        });
                Log.d(TAG, "onSuggestionClicked()");

                mLastQuery = searchSuggestion.getBody();
            }

            @Override
            public void onSearchAction(String query) {
                mLastQuery = query;

                DataHelper.findColors(getActivity(), query,
                        new DataHelper.OnFindColorsListener() {

                            @Override
                            public void onResults(List<ColorWrapper> results) {
                                 //show search results
                            }

                        });
                Log.d(TAG, "onSearchAction()");
            }
        });

        mSearchView.setOnFocusChangeListener(new FloatingSearchView.OnFocusChangeListener() {
            @Override
            public void onFocus() {

                //show suggestions when search bar gains focus (typically history suggestions)
                mSearchView.swapSuggestions(DataHelper.getHistory(getActivity(), 3));

                Log.d(TAG, "onFocus()");
            }

            @Override
            public void onFocusCleared() {

                //set the title of the bar so that when focus is returned a new query begins
                mSearchView.setSearchBarTitle(mLastQuery);

                //you can also set setSearchText(...) to make keep the query there when not focused and when focus returns
                //mSearchView.setSearchText(searchSuggestion.getBody());

                Log.d(TAG, "onFocusCleared()");
            }
        });

        //handle menu clicks the same way as you would
        //in a regular activity
        mSearchView.setOnMenuItemClickListener(item -> {
            if (item.getItemId() == R.id.action_change_colors) {

                mIsDarkSearchTheme = true;

                /*TODO
                //demonstrate setting colors for items
                mSearchView.setBackgroundColor(Color.parseColor("#787878"));
                mSearchView.setViewTextColor(Color.parseColor("#e9e9e9"));
                mSearchView.setHintTextColor(Color.parseColor("#e9e9e9"));
                mSearchView.setActionMenuOverflowColor(Color.parseColor("#e9e9e9"));
                mSearchView.setMenuItemIconColor(Color.parseColor("#e9e9e9"));
                mSearchView.setLeftActionIconColor(Color.parseColor("#e9e9e9"));
                mSearchView.setClearBtnColor(Color.parseColor("#e9e9e9"));
                mSearchView.setDividerColor(Color.parseColor("#BEBEBE"));
                mSearchView.setLeftActionIconColor(Color.parseColor("#e9e9e9"));*/
            } else {

                //just print action
                Toast.makeText(getActivity().getApplicationContext(), item.getTitle(),
                        Toast.LENGTH_SHORT).show();
            }

            return Unit.INSTANCE;
        });

        //use this listener to listen to menu clicks when app:leftAction="showHome"
        mSearchView.setOnHomeActionClickListener(() -> {
            Log.d(TAG, "onHomeClicked()");
            return Unit.INSTANCE;
        });

        /*
         * Here you have access to the left icon and the text of a given suggestion
         * item after as it is bound to the suggestion list. You can utilize this
         * callback to change some properties of the left icon and the text. For example, you
         * can load the left icon images using your favorite image loading library, or change text color.
         *
         *
         * Important:
         * Keep in mind that the suggestion list is a RecyclerView, so views are reused for different
         * items in the list.
         */
        mSearchView.setOnBindSuggestionCallback((binding, item, position) -> {
            ColorSuggestion colorSuggestion = (ColorSuggestion) item;

            String textColor = mIsDarkSearchTheme ? "#ffffff" : "#000000";
            String textLight = mIsDarkSearchTheme ? "#bfbfbf" : "#787878";

            if (colorSuggestion.getIsHistory()) {
                binding.leftIcon.setImageDrawable(ResourcesCompat.getDrawable(getResources(),
                        R.drawable.history, null));

                /*TODO Util.setIconColor(leftIcon, Color.parseColor(textColor));*/
                binding.leftIcon.setAlpha(.36f);
            } else {
                binding.leftIcon.setAlpha(0.0f);
                binding.leftIcon.setImageDrawable(null);
            }

            binding.body.setTextColor(Color.parseColor(textColor));
            String text = colorSuggestion.getBody()
                    .replaceFirst(mSearchView.getQuery(),
                            "<font color=\"" + textLight + "\">" + mSearchView.getQuery() + "</font>");
            binding.body.setText(Html.fromHtml(text));

            return Unit.INSTANCE;
        });
    }

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        mSearchView.setTranslationY(verticalOffset);
    }

    @Override
    public boolean onActivityBackPress() {
        //if mSearchView.setSearchFocused(false) causes the focused search
        //to close, then we don't want to close the activity. if mSearchView.setSearchFocused(false)
        //returns false, we know that the search was already closed so the call didn't change the focus
        //state and it makes sense to call supper onBackPressed() and close the activity
        if (!mSearchView.setSearchFocused(false)) {
            return false;
        }
        return true;
    }

    private void setupDrawer() {
        attachSearchViewActivityDrawer(mSearchView);
    }
}

Find full example here.

Reference

Read more here.
Download code here.
Follow code author here.