An implementation of a floating search box with search suggestions, also called persistent search bar.
Use it as follows:
Step 1: Install it
- 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.