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.Point; import android.graphics.drawable.Drawable; import android.os.Build; import android.speech.RecognizerIntent; 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.Display; 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.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import com.balysv.materialmenu.MaterialMenuDrawable.IconState; import com.balysv.materialmenu.MaterialMenuView; import java.util.ArrayList; import java.util.List; import io.codetail.animation.ReverseInterpolator; import io.codetail.animation.SupportAnimator; import io.codetail.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 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 isVoiceRecognitionIntentSupported; private VoiceRecognitionListener voiceRecognitionListener; private Activity mContainerActivity; private Fragment mContainerFragment; private android.support.v4.app.Fragment mContainerSupportFragment; /** * 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(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.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>(); results.setAdapter(new SearchAdapter(context, resultList)); 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 = "Logo"; micStateChanged(); mic.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (voiceRecognitionListener != null) { voiceRecognitionListener.onClick(); } else { micClick(); } } }); } public boolean isSearchOpened() { return searchOpen; } private static boolean isIntentAvailable(Context context, Intent intent) { PackageManager mgr = context.getPackageManager(); List<ResolveInfo> list = mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); return list.size() > 0; } /*** * 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. Can be called regardless of result list length * @param activity Activity */ public void hideCircularly(Activity activity){ Display display = activity.getWindowManager().getDefaultDisplay(); Point size = new Point(); final FrameLayout layout = (FrameLayout) activity.getWindow().getDecorView() .findViewById(android.R.id.content); RelativeLayout root = (RelativeLayout) findViewById(R.id.search_root); display.getSize(size); Resources r = getResources(); float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96, r.getDisplayMetrics()); int cx = layout.getLeft() + layout.getRight(); int cy = layout.getTop(); int finalRadius = (int) Math.max(layout.getWidth()*1.5, px); SupportAnimator animator = ViewAnimationUtils.createCircularReveal( root, cx, cy, 0, finalRadius); animator.setInterpolator(new ReverseInterpolator()); animator.setDuration(250); 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() { } }); } /*** * Toggle the searchbox's open/closed state manually */ public void toggleSearch() { if (searchOpen) { if (TextUtils.isEmpty(getSearchText())) { setLogoTextInt(logoText); } closeSearch(); } else { openSearch(true); } searchOpen = !searchOpen; } /*** * 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(); } /*** * 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 matches Matches */ public void populateEditText(ArrayList<String> matches) { toggleSearch(); String text = ""; for (int x = 0; x < matches.size(); x++) { text = text + matches.get(x) + " "; } text = text.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++) { if (searchables.get(x).title.toLowerCase().startsWith( getSearchText().toLowerCase()) && count < 5) { addResult(searchables.get(x)); 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 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); } /*** * Get the searchbox's current text * @return Text */ public String getSearchText() { return search.getText().toString(); } /*** * Set the searchbox's current text manually * @param text Text */ public void setSearchString(String text) { search.setText(text); } /*** * Add a result * @param result SearchResult */ private void addResult(SearchResult result) { if (resultList != null && resultList.size() < 6) { resultList.add(result); ((SearchAdapter) results.getAdapter()).notifyDataSetChanged(); } } /*** * Clear all the results */ public void clearResults() { if (resultList != null) { resultList.clear(); ((SearchAdapter) results.getAdapter()).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); } /*** * 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 cx = layout.getLeft() + layout.getRight(); int cy = layout.getTop(); int finalRadius = (int) Math.max(layout.getWidth(), px); SupportAnimator animator = ViewAnimationUtils.createCircularReveal( root, cx, cy, 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) { if(!searchWithoutSuggestions && getNumberOfResults() == 0)return; setSearchString(result.title); if (!TextUtils.isEmpty(getSearchText())) { setLogoTextInt(result.title); if (listener != null) listener.onSearch(result.title); } else { setLogoTextInt(logoText); } toggleSearch(); } private void openSearch(Boolean openKeyboard) { this.materialMenu.animateState(IconState.ARROW); this.logo.setVisibility(View.GONE); this.drawerLogo.setVisibility(View.GONE); this.search.setVisibility(View.VISIBLE); search.requestFocus(); this.results.setVisibility(View.VISIBLE); animate = true; results.setAdapter(new SearchAdapter(context, resultList)); search.addTextChangedListener(new TextWatcher() { @Override public void afterTextChanged(Editable s) { if (s.length() > 0) { micStateChanged(false); mic.setImageDrawable(context.getResources().getDrawable( R.drawable.ic_clear)); updateResults(); } else { micStateChanged(true); mic.setImageDrawable(context.getResources().getDrawable( R.drawable.ic_action_mic)); if(initialResults != null){ setInitialResults(); }else{ updateResults(); } } if (listener != null) listener.onSearchTermChanged(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } }); results.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { SearchResult result = resultList.get(arg2); search(result); } }); 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() { this.materialMenu.animateState(IconState.BURGER); this.logo.setVisibility(View.VISIBLE); this.drawerLogo.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); } private void setLogoTextInt(String text) { logo.setText(text); } private void search(String text) { SearchResult option = new SearchResult(text, null); search(option); } class SearchAdapter extends ArrayAdapter<SearchResult> { public SearchAdapter(Context context, ArrayList<SearchResult> options) { super(context, 0, options); } 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 (animate) { Animation anim = AnimationUtils.loadAnimation(context, R.anim.anim_down); anim.setDuration(400); convertView.startAnimation(anim); if (count == this.getCount()) { animate = 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); if (option.icon != null) { icon.setImageDrawable(option.icon); } else { icon.setImageResource(option.drawableResId); } ImageView up = (ImageView) convertView.findViewById(R.id.up); up.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { setSearchString(title.getText().toString()); search.setSelection(search.getText().length()); } }); return convertView; } } 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(); /** * Called when a search happens, with a result * @param result */ public void onSearch(String 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(); } }