package com.quinny898.library.persistentsearch;
import android.animation.LayoutTransition;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.speech.RecognizerIntent;
import android.support.annotation.MenuRes;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.balysv.materialmenu.ps.MaterialMenuDrawable.IconState;
import com.balysv.materialmenu.ps.MaterialMenuView;
import java.util.ArrayList;
import java.util.List;
import io.codetailps.animation.ReverseInterpolator;
import io.codetailps.animation.SupportAnimator;
import io.codetailps.animation.ViewAnimationUtils;
public class SearchBox extends RelativeLayout {
public static final int VOICE_RECOGNITION_CODE = 1234;
private MaterialMenuView materialMenu;
private TextView logo;
private EditText search;
private Context context;
private ListView results;
private ArrayList<SearchResult> resultList;
private ArrayList<SearchResult> searchables;
private boolean searchOpen;
private boolean animate;
private View tint;
private boolean isMic;
private ImageView mic;
private ImageView overflow;
private PopupMenu popupMenu;
private ImageView drawerLogo;
private SearchListener listener;
private MenuListener menuListener;
private FrameLayout rootLayout;
private String logoText;
private ProgressBar pb;
private ArrayList<SearchResult> initialResults;
private boolean searchWithoutSuggestions = true;
private boolean animateDrawerLogo = true;
private boolean isVoiceRecognitionIntentSupported;
private VoiceRecognitionListener voiceRecognitionListener;
private Activity mContainerActivity;
private Fragment mContainerFragment;
private android.support.v4.app.Fragment mContainerSupportFragment;
private SearchFilter mSearchFilter;
private ArrayAdapter<? extends SearchResult> mAdapter;
/**
* Create a new searchbox
* @param context Context
*/
public SearchBox(Context context) {
this(context, null);
}
/**
* Create a searchbox with params
* @param context Context
* @param attrs Attributes
*/
public SearchBox(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Create a searchbox with params and a style
* @param context Context
* @param attrs Attributes
* @param defStyle Style
*/
public SearchBox(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.searchbox, this);
this.searchOpen = false;
this.isMic = true;
this.materialMenu = (MaterialMenuView) findViewById(R.id.material_menu_button);
this.logo = (TextView) findViewById(R.id.logo);
this.search = (EditText) findViewById(R.id.search);
this.results = (ListView) findViewById(R.id.results);
this.context = context;
this.pb = (ProgressBar) findViewById(R.id.pb);
this.mic = (ImageView) findViewById(R.id.mic);
this.overflow = (ImageView) findViewById(R.id.overflow);
this.drawerLogo = (ImageView) findViewById(R.id.drawer_logo);
materialMenu.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (searchOpen) {
toggleSearch();
} else {
if (menuListener != null)
menuListener.onMenuClick();
}
}
});
resultList = new ArrayList<SearchResult>();
setAdapter(new SearchAdapter(context, resultList, search));
animate = true;
isVoiceRecognitionIntentSupported = isIntentAvailable(context, new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH));
logo.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
toggleSearch();
}
});
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
RelativeLayout searchRoot = (RelativeLayout) findViewById(R.id.search_root);
LayoutTransition lt = new LayoutTransition();
lt.setDuration(100);
searchRoot.setLayoutTransition(lt);
}
searchables = new ArrayList<SearchResult>();
search.setOnEditorActionListener(new OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId,
KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
search(getSearchText());
return true;
}
return false;
}
});
search.setOnKeyListener(new OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (TextUtils.isEmpty(getSearchText())) {
toggleSearch();
} else {
search(getSearchText());
}
return true;
}
return false;
}
});
logoText = "";
micStateChanged();
mic.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (voiceRecognitionListener != null) {
voiceRecognitionListener.onClick();
} else {
micClick();
}
}
});
overflow.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
popupMenu.show();
}
});
search.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
if (s.length() > 0) {
micStateChanged(false);
mic.setImageDrawable(getContext().getResources().getDrawable(
R.drawable.ic_clear));
updateResults();
} else {
micStateChanged(true);
mic.setImageDrawable(getContext().getResources().getDrawable(
R.drawable.ic_action_mic));
if(initialResults != null){
setInitialResults();
}else{
updateResults();
}
}
if (listener != null)
listener.onSearchTermChanged(s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
}
});
// Default search Algorithm
mSearchFilter = new SearchFilter() {
@Override
public boolean onFilter(SearchResult searchResult, String searchTerm) {
return searchResult.title.toLowerCase()
.startsWith(searchTerm.toLowerCase());
}
};
}
private static boolean isIntentAvailable(Context context, Intent intent) {
PackageManager mgr = context.getPackageManager();
if (mgr != null) {
List<ResolveInfo> list = mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
return list.size() > 0;
}
return false;
}
/***
* Reveal the searchbox from a menu item. Specify the menu item id and pass the activity so the item can be found
* @param id View ID
* @param activity Activity
*/
public void revealFromMenuItem(int id, Activity activity) {
setVisibility(View.VISIBLE);
View menuButton = activity.findViewById(id);
if (menuButton != null) {
FrameLayout layout = (FrameLayout) activity.getWindow().getDecorView()
.findViewById(android.R.id.content);
if (layout.findViewWithTag("searchBox") == null) {
int[] location = new int[2];
menuButton.getLocationInWindow(location);
revealFrom((float) location[0], (float) location[1],
activity, this);
}
}
}
/***
* Hide the searchbox using the circle animation which centres upon the provided menu item. Can be called regardless of result list length
* @param id ID of menu item
* @param activity Activity
*/
public void hideCircularlyToMenuItem(int id, Activity activity){
View menuButton = activity.findViewById(id);
if (menuButton != null) {
FrameLayout layout = (FrameLayout) activity.getWindow().getDecorView()
.findViewById(android.R.id.content);
if (layout.findViewWithTag("searchBox") == null) {
int[] location = new int[2];
menuButton.getLocationInWindow(location);
hideCircularly(location[0] + menuButton.getWidth() * 2 / 3, location[1],
activity);
}
}
}
/***
* Hide the searchbox using the circle animation. Can be called regardless of result list length
* @param activity Activity
*/
public void hideCircularly(int x, int y, Activity activity){
final FrameLayout layout = (FrameLayout) activity.getWindow().getDecorView()
.findViewById(android.R.id.content);
RelativeLayout root = (RelativeLayout) findViewById(R.id.search_root);
Resources r = getResources();
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96,
r.getDisplayMetrics());
int finalRadius = (int) Math.max(layout.getWidth()*1.5, px);
SupportAnimator animator = ViewAnimationUtils.createCircularReveal(
root, x, y, 0, finalRadius);
animator.setInterpolator(new ReverseInterpolator());
animator.setDuration(500);
animator.start();
animator.addListener(new SupportAnimator.AnimatorListener() {
@Override
public void onAnimationStart() {
}
@Override
public void onAnimationEnd() {
setVisibility(View.GONE);
}
@Override
public void onAnimationCancel() {
}
@Override
public void onAnimationRepeat() {
}
});
}
/***
* Hide the searchbox using the circle animation. Can be called regardless of result list length
* @param activity Activity
*/
public void hideCircularly(Activity activity){
final FrameLayout layout = (FrameLayout) activity.getWindow().getDecorView()
.findViewById(android.R.id.content);
hideCircularly(layout.getLeft() + layout.getRight(), layout.getTop(), activity);
}
/***
* Toggle the searchbox's open/closed state manually
*/
public void toggleSearch() {
if (searchOpen) {
if (TextUtils.isEmpty(getSearchText())) {
setLogoTextInt(logoText);
}
closeSearch();
} else {
openSearch(true);
}
}
public boolean getSearchOpen(){
return getVisibility() == VISIBLE;
}
/***
* Hide the search results manually
*/
public void hideResults(){
this.search.setVisibility(View.GONE);
this.results.setVisibility(View.GONE);
}
/***
* Start the voice input activity manually
*/
public void startVoiceRecognition() {
if (isMicEnabled()) {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,
context.getString(R.string.speak_now));
if (mContainerActivity != null) {
mContainerActivity.startActivityForResult(intent, VOICE_RECOGNITION_CODE);
} else if (mContainerFragment != null) {
mContainerFragment.startActivityForResult(intent, VOICE_RECOGNITION_CODE);
} else if (mContainerSupportFragment != null) {
mContainerSupportFragment.startActivityForResult(intent, VOICE_RECOGNITION_CODE);
}
}
}
/***
* Enable voice recognition for Activity
* @param context Context
*/
public void enableVoiceRecognition(Activity context) {
mContainerActivity = context;
micStateChanged();
}
/***
* Enable voice recognition for Fragment
* @param context Fragment
*/
public void enableVoiceRecognition(Fragment context) {
mContainerFragment = context;
micStateChanged();
}
/***
* Enable voice recognition for Support Fragment
* @param context Fragment
*/
public void enableVoiceRecognition(android.support.v4.app.Fragment context) {
mContainerSupportFragment = context;
micStateChanged();
}
private boolean isMicEnabled() {
return isVoiceRecognitionIntentSupported && (mContainerActivity != null || mContainerSupportFragment != null || mContainerFragment != null);
}
private void micStateChanged() {
mic.setVisibility((!isMic || isMicEnabled()) ? VISIBLE : INVISIBLE);
}
private void micStateChanged(boolean isMic) {
this.isMic = isMic;
micStateChanged();
}
public void setOverflowMenu(@MenuRes int overflowMenuResId) {
overflow.setVisibility(VISIBLE);
popupMenu = new PopupMenu(context, overflow);
popupMenu.getMenuInflater().inflate(overflowMenuResId, popupMenu.getMenu());
}
public void setOverflowMenuItemClickListener(PopupMenu.OnMenuItemClickListener onMenuItemClickListener) {
popupMenu.setOnMenuItemClickListener(onMenuItemClickListener);
}
/***
* Set whether to show the progress bar spinner
* @param show Whether to show
*/
public void showLoading(boolean show){
if(show){
pb.setVisibility(View.VISIBLE);
mic.setVisibility(View.INVISIBLE);
}else{
pb.setVisibility(View.INVISIBLE);
mic.setVisibility(View.VISIBLE);
}
}
/***
* Mandatory method for the onClick event
*/
public void micClick() {
if (!isMic) {
setSearchString("");
} else {
startVoiceRecognition();
}
}
/***
* Populate the searchbox with words, in an arraylist. Used by the voice input
* @param match Matches
*/
public void populateEditText(String match) {
toggleSearch();
String text = match.trim();
setSearchString(text);
search(text);
}
/***
* Force an update of the results
*/
public void updateResults() {
resultList.clear();
int count = 0;
for (int x = 0; x < searchables.size(); x++) {
SearchResult searchable = searchables.get(x);
if(mSearchFilter.onFilter(searchable,getSearchText()) && count < 5) {
addResult(searchable);
count++;
}
}
if (resultList.size() == 0) {
results.setVisibility(View.GONE);
} else {
results.setVisibility(View.VISIBLE);
}
}
/***
*
* Set the results that are shown (up to 5) when the searchbox is opened with no text
* @param results Results
*/
public void setInitialResults(ArrayList<SearchResult> results){
this.initialResults = results;
}
/***
* Set whether the menu button should be shown. Particularly useful for apps that adapt to screen sizes
* @param visibility Whether to show
*/
public void setMenuVisibility(int visibility){
materialMenu.setVisibility(visibility);
}
/***
* Set the menu listener
* @param menuListener MenuListener
*/
public void setMenuListener(MenuListener menuListener) {
this.menuListener = menuListener;
}
/***
* Set the search listener
* @param listener SearchListener
*/
public void setSearchListener(SearchListener listener) {
this.listener = listener;
}
/***
* Set whether to search without suggestions being available (default is true). Disable if your app only works with provided options
* @param state Whether to show
*/
public void setSearchWithoutSuggestions(boolean state){
this.searchWithoutSuggestions = state;
}
/***
* Set the maximum length of the searchbox's edittext
* @param length Length
*/
public void setMaxLength(int length) {
search.setFilters(new InputFilter[]{new InputFilter.LengthFilter(
length)});
}
/***
* Set the text of the logo (default text when closed)
* @param text Text
*/
public void setLogoText(String text) {
this.logoText = text;
setLogoTextInt(text);
}
/***
* Set the text color of the logo
* @param color
*/
public void setLogoTextColor(int color){
logo.setTextColor(color);
}
/***
* Set the image drawable of the drawer icon logo (do not set if you have not hidden the menu icon)
* @param icon Icon
*/
public void setDrawerLogo(Drawable icon) {
drawerLogo.setImageDrawable(icon);
}
public void setDrawerLogo(Integer icon) {
setDrawerLogo(getResources().getDrawable(icon));
}
/***
* Set the SearchFilter used to filter out results based on the current search term
* @param filter SearchFilter
*/
public void setSearchFilter(SearchFilter filter) {
this.mSearchFilter = filter;
}
/***
* Sets the hint for the Search Field
* @param hint The hint for Search Field
*/
public void setHint(String hint) {
this.search.setHint(hint);
}
/***
* Get result list
* @return Results
*/
public ArrayList<SearchResult> getResults() {
return resultList;
}
/***
* Get the searchbox's current text
* @return Text
*/
public String getSearchText() {
return search.getText().toString();
}
/***
* Set the adapter for the search results
* @param adapter Adapter
*/
public void setAdapter(ArrayAdapter<? extends SearchResult> adapter) {
mAdapter = adapter;
results.setAdapter(adapter);
}
/***
* Set the searchbox's current text manually
* @param text Text
*/
public void setSearchString(String text) {
search.setText("");
search.append(text);
}
/***
* Add a result
* @param result SearchResult
*/
private void addResult(SearchResult result) {
if (resultList != null) {
resultList.add(result);
mAdapter.notifyDataSetChanged();
}
}
/***
* Clear all the results
*/
public void clearResults() {
if (resultList != null) {
resultList.clear();
mAdapter.notifyDataSetChanged();
}
listener.onSearchCleared();
}
/***
* Return the number of results that are currently shown
* @return Number of Results
*/
public int getNumberOfResults() {
if (resultList != null)return resultList.size();
return 0;
}
/***
* Set the searchable items from a list (replaces any current items)
*/
public void setSearchables(ArrayList<SearchResult> searchables){
this.searchables = searchables;
}
/***
* Add a searchable item
* @param searchable SearchResult
*/
public void addSearchable(SearchResult searchable) {
if (!searchables.contains(searchable))
searchables.add(searchable);
}
/***
* Add all searchable items
* @param searchable SearchResult
*/
public void addAllSearchables(ArrayList<? extends SearchResult> searchable) {
searchables.addAll(searchable);
}
/***
* Remove a searchable item
* @param searchable SearchResult
*/
public void removeSearchable(SearchResult searchable) {
if (searchables.contains(searchable))
searchables.remove(search);
}
/***
* Clear all searchable items
*/
public void clearSearchable() {
searchables.clear();
}
/***
* Get all searchable items
* @return ArrayList of SearchResults
*/
public ArrayList<SearchResult> getSearchables() {
return searchables;
}
private void revealFrom(float x, float y, Activity a, SearchBox s) {
FrameLayout layout = (FrameLayout) a.getWindow().getDecorView()
.findViewById(android.R.id.content);
RelativeLayout root = (RelativeLayout) s.findViewById(R.id.search_root);
Resources r = getResources();
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96,
r.getDisplayMetrics());
int finalRadius = (int) Math.max(layout.getWidth(), px);
SupportAnimator animator = ViewAnimationUtils.createCircularReveal(
root, (int)x, (int)y, 0, finalRadius);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(500);
animator.addListener(new SupportAnimator.AnimatorListener() {
@Override
public void onAnimationCancel() {
}
@Override
public void onAnimationEnd() {
toggleSearch();
}
@Override
public void onAnimationRepeat() {
}
@Override
public void onAnimationStart() {
}
});
animator.start();
}
private void search(SearchResult result, boolean resultClicked) {
if(!searchWithoutSuggestions && getNumberOfResults() == 0)return;
setSearchString(result.title);
if (!TextUtils.isEmpty(getSearchText())) {
setLogoTextInt(result.title);
if (listener != null) {
if (resultClicked)
listener.onResultClick(result);
else
listener.onSearch(result.title);
}
} else {
setLogoTextInt(logoText);
}
toggleSearch();
}
/***
* Set to false to retain the logo from setDrawerLogo() instead of animating to the arrow during searches.
* @param show Should the SearchBox animate the drawer logo
*/
public void setAnimateDrawerLogo(boolean show){
animateDrawerLogo = show;
}
private void openSearch(Boolean openKeyboard) {
if(animateDrawerLogo){
this.materialMenu.animateState(IconState.ARROW);
this.drawerLogo.setVisibility(View.GONE);
}
this.logo.setVisibility(View.GONE);
this.search.setVisibility(View.VISIBLE);
search.requestFocus();
this.results.setVisibility(View.VISIBLE);
animate = true;
setAdapter(new SearchAdapter(context, resultList, search));
searchOpen = true;
results.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
SearchResult result = resultList.get(arg2);
search(result, true);
}
});
if(initialResults != null){
setInitialResults();
}else{
updateResults();
}
if (listener != null)
listener.onSearchOpened();
if (getSearchText().length() > 0) {
micStateChanged(false);
mic.setImageDrawable(context.getResources().getDrawable(
R.drawable.ic_clear));
}
if (openKeyboard) {
InputMethodManager inputMethodManager = (InputMethodManager) context
.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInputFromWindow(
getApplicationWindowToken(),
InputMethodManager.SHOW_FORCED, 0);
}
}
private void setInitialResults(){
resultList.clear();
int count = 0;
for (int x = 0; x < initialResults.size(); x++) {
if (count < 5) {
addResult(initialResults.get(x));
count++;
}
}
if (resultList.size() == 0) {
results.setVisibility(View.GONE);
} else {
results.setVisibility(View.VISIBLE);
}
}
private void closeSearch() {
if(animateDrawerLogo){
this.materialMenu.animateState(IconState.BURGER);
this.drawerLogo.setVisibility(View.VISIBLE);
}
this.logo.setVisibility(View.VISIBLE);
this.search.setVisibility(View.GONE);
this.results.setVisibility(View.GONE);
if (tint != null && rootLayout != null) {
rootLayout.removeView(tint);
}
if (listener != null)
listener.onSearchClosed();
micStateChanged(true);
mic.setImageDrawable(context.getResources().getDrawable(
R.drawable.ic_action_mic));
InputMethodManager inputMethodManager = (InputMethodManager) context
.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(getApplicationWindowToken(),
0);
searchOpen = false;
}
private void setLogoTextInt(String text) {
logo.setText(text);
}
private void search(String text) {
SearchResult option = new SearchResult(text, null);
search(option, false);
}
public static class SearchAdapter extends ArrayAdapter<SearchResult> {
private boolean mAnimate;
private EditText mSearch;
public SearchAdapter(Context context, ArrayList<SearchResult> options, EditText search) {
super(context, 0, options);
mSearch = search;
}
int count = 0;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SearchResult option = getItem(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(
R.layout.search_option, parent, false);
if (mAnimate) {
Animation anim = AnimationUtils.loadAnimation(getContext(),
R.anim.anim_down);
anim.setDuration(400);
convertView.startAnimation(anim);
if (count == this.getCount()) {
mAnimate = false;
}
count++;
}
}
View border = convertView.findViewById(R.id.border);
if (position == 0) {
border.setVisibility(View.VISIBLE);
} else {
border.setVisibility(View.GONE);
}
final TextView title = (TextView) convertView
.findViewById(R.id.title);
title.setText(option.title);
ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
icon.setImageDrawable(option.icon);
ImageView up = (ImageView) convertView.findViewById(R.id.up);
up.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mSearch.setText(title.getText().toString());
mSearch.setSelection(mSearch.getText().length());
}
});
return convertView;
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.KEYCODE_BACK && getVisibility() == View.VISIBLE){
hideCircularly((Activity) getContext());
return true;
}
return super.dispatchKeyEvent(e);
}
public interface SearchListener {
/**
* Called when the searchbox is opened
*/
public void onSearchOpened();
/**
* Called when the clear button is pressed
*/
public void onSearchCleared();
/**
* Called when the searchbox is closed
*/
public void onSearchClosed();
/**
* Called when the searchbox's edittext changes
*/
public void onSearchTermChanged(String term);
/**
* Called when a search happens, with a result
* @param result
*/
public void onSearch(String result);
/**
* Called when a search result is clicked, with the result
* @param result
*/
public void onResultClick(SearchResult result);
}
public interface MenuListener {
/**
* Called when the menu button is pressed
*/
public void onMenuClick();
}
public interface VoiceRecognitionListener {
/**
* Called when the menu button is pressed
*/
public void onClick();
}
public interface SearchFilter {
/**
* Called against each Searchable to determine if it should be filtered out of the results
*/
public boolean onFilter(SearchResult searchResult ,String searchTerm);
}
}