package io.github.javiewer.view; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.speech.RecognizerIntent; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewAnimationUtils; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.EditText; import android.widget.Filter; import android.widget.Filterable; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import com.miguelcatalan.materialsearchview.SearchAdapter; import com.miguelcatalan.materialsearchview.utils.AnimationUtil; import java.lang.reflect.Field; import java.util.List; import io.github.javiewer.R; /** * @author Miguel Catalan Bañuls */ public class SimpleSearchView extends FrameLayout implements Filter.FilterListener { public static final int REQUEST_VOICE = 9999; private MenuItem mMenuItem; private boolean mIsSearchOpen = false; private int mAnimationDuration; private boolean mClearingFocus; //Views private View mSearchLayout; private View mTintView; private ListView mSuggestionsListView; private EditText mSearchSrcTextView; private ImageButton mBackBtn; private ImageButton mVoiceBtn; private ImageButton mEmptyBtn; private RelativeLayout mSearchTopBar; private CharSequence mOldQueryText; private CharSequence mUserQuery; private OnQueryTextListener mOnQueryChangeListener; private SearchViewListener mSearchViewListener; private ListAdapter mAdapter; private SavedState mSavedState; private boolean submit = false; private boolean ellipsize = false; private boolean allowVoiceSearch; private Drawable suggestionIcon; private Context mContext; public SimpleSearchView(Context context) { this(context, null); } public SimpleSearchView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SimpleSearchView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs); mContext = context; initiateView(); initStyle(attrs, defStyleAttr); } private void initStyle(AttributeSet attrs, int defStyleAttr) { TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.MaterialSearchView, defStyleAttr, 0); if (a != null) { if (a.hasValue(R.styleable.MaterialSearchView_searchBackground)) { setBackground(a.getDrawable(R.styleable.MaterialSearchView_searchBackground)); } if (a.hasValue(R.styleable.MaterialSearchView_android_textColor)) { setTextColor(a.getColor(R.styleable.MaterialSearchView_android_textColor, 0)); } if (a.hasValue(R.styleable.MaterialSearchView_android_textColorHint)) { setHintTextColor(a.getColor(R.styleable.MaterialSearchView_android_textColorHint, 0)); } if (a.hasValue(R.styleable.MaterialSearchView_android_hint)) { setHint(a.getString(R.styleable.MaterialSearchView_android_hint)); } if (a.hasValue(R.styleable.MaterialSearchView_searchVoiceIcon)) { setVoiceIcon(a.getDrawable(R.styleable.MaterialSearchView_searchVoiceIcon)); } if (a.hasValue(R.styleable.MaterialSearchView_searchCloseIcon)) { setCloseIcon(a.getDrawable(R.styleable.MaterialSearchView_searchCloseIcon)); } if (a.hasValue(R.styleable.MaterialSearchView_searchBackIcon)) { setBackIcon(a.getDrawable(R.styleable.MaterialSearchView_searchBackIcon)); } if (a.hasValue(R.styleable.MaterialSearchView_searchSuggestionBackground)) { setSuggestionBackground(a.getDrawable(R.styleable.MaterialSearchView_searchSuggestionBackground)); } if (a.hasValue(R.styleable.MaterialSearchView_searchSuggestionIcon)) { setSuggestionIcon(a.getDrawable(R.styleable.MaterialSearchView_searchSuggestionIcon)); } a.recycle(); } } private void initiateView() { LayoutInflater.from(mContext).inflate(R.layout.search_view, this, true); mSearchLayout = findViewById(R.id.search_layout); mSearchTopBar = (RelativeLayout) mSearchLayout.findViewById(R.id.search_top_bar); mSuggestionsListView = (ListView) mSearchLayout.findViewById(R.id.suggestion_list); mSearchSrcTextView = (EditText) mSearchLayout.findViewById(R.id.searchTextView); mBackBtn = (ImageButton) mSearchLayout.findViewById(R.id.action_up_btn); mVoiceBtn = (ImageButton) mSearchLayout.findViewById(R.id.action_voice_btn); mEmptyBtn = (ImageButton) mSearchLayout.findViewById(R.id.action_empty_btn); mTintView = mSearchLayout.findViewById(R.id.transparent_view); mSearchSrcTextView.setOnClickListener(mOnClickListener); mBackBtn.setOnClickListener(mOnClickListener); mVoiceBtn.setOnClickListener(mOnClickListener); mEmptyBtn.setOnClickListener(mOnClickListener); mTintView.setOnClickListener(mOnClickListener); allowVoiceSearch = false; showVoice(true); initSearchView(); mSuggestionsListView.setVisibility(GONE); setAnimationDuration(AnimationUtil.ANIMATION_DURATION_MEDIUM); } private void initSearchView() { mSearchSrcTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { onSubmitQuery(); return true; } }); mSearchSrcTextView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { mUserQuery = s; startFilter(s); SimpleSearchView.this.onTextChanged(s); } @Override public void afterTextChanged(Editable s) { } }); mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { showKeyboard(mSearchSrcTextView); showSuggestions(); } } }); } private void startFilter(CharSequence s) { if (mAdapter != null && mAdapter instanceof Filterable) { ((Filterable) mAdapter).getFilter().filter(s, SimpleSearchView.this); } } private final OnClickListener mOnClickListener = new OnClickListener() { public void onClick(View v) { if (v == mBackBtn) { closeSearch(); } else if (v == mVoiceBtn) { onVoiceClicked(); } else if (v == mEmptyBtn) { mSearchSrcTextView.setText(null); } else if (v == mSearchSrcTextView) { showSuggestions(); } else if (v == mTintView) { closeSearch(); } } }; private void onVoiceClicked() { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); //intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak an item name or number"); // user hint intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); // setting recognition model, optimized for short phrases – search queries intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1); // quantity of results we want to receive if (mContext instanceof Activity) { ((Activity) mContext).startActivityForResult(intent, REQUEST_VOICE); } } private void onTextChanged(CharSequence newText) { CharSequence text = mSearchSrcTextView.getText(); mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); if (hasText) { mEmptyBtn.setVisibility(VISIBLE); showVoice(false); } else { mEmptyBtn.setVisibility(GONE); showVoice(true); } if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) { mOnQueryChangeListener.onQueryTextChange(newText.toString()); } mOldQueryText = newText.toString(); } private void onSubmitQuery() { CharSequence query = mSearchSrcTextView.getText(); if (query != null && TextUtils.getTrimmedLength(query) > 0) { if (mOnQueryChangeListener == null || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) { closeSearch(); mSearchSrcTextView.setText(null); } } } private boolean isVoiceAvailable() { if (isInEditMode()) { return true; } PackageManager pm = getContext().getPackageManager(); List<ResolveInfo> activities = pm.queryIntentActivities( new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); return activities.size() == 0; } public void hideKeyboard(View view) { InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } public void showKeyboard(View view) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1 && view.hasFocus()) { view.clearFocus(); } view.requestFocus(); InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(view, 0); } //Public Attributes @Override public void setBackground(Drawable background) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mSearchTopBar.setBackground(background); } else { mSearchTopBar.setBackgroundDrawable(background); } } @Override public void setBackgroundColor(int color) { mSearchTopBar.setBackgroundColor(color); } public void setTextColor(int color) { mSearchSrcTextView.setTextColor(color); } public void setHintTextColor(int color) { mSearchSrcTextView.setHintTextColor(color); } public void setHint(CharSequence hint) { mSearchSrcTextView.setHint(hint); } public void setVoiceIcon(Drawable drawable) { mVoiceBtn.setImageDrawable(drawable); } public void setCloseIcon(Drawable drawable) { mEmptyBtn.setImageDrawable(drawable); } public void setBackIcon(Drawable drawable) { mBackBtn.setImageDrawable(drawable); } public void setSuggestionIcon(Drawable drawable) { suggestionIcon = drawable; } public void setSuggestionBackground(Drawable background) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mSuggestionsListView.setBackground(background); } else { mSuggestionsListView.setBackgroundDrawable(background); } } public void setCursorDrawable(int drawable) { try { // https://github.com/android/platform_frameworks_base/blob/kitkat-release/core/java/android/widget/TextView.java#L562-564 Field f = TextView.class.getDeclaredField("mCursorDrawableRes"); f.setAccessible(true); f.set(mSearchSrcTextView, drawable); } catch (Exception ignored) { Log.e("MaterialSearchView", ignored.toString()); } } public void setVoiceSearch(boolean voiceSearch) { allowVoiceSearch = voiceSearch; } //Public Methods /** * Call this method to show suggestions list. This shows up when adapter is set. Call {@link #setAdapter(ListAdapter)} before calling this. */ public void showSuggestions() { if (mAdapter != null && mAdapter.getCount() > 0 && mSuggestionsListView.getVisibility() == GONE) { mSuggestionsListView.setVisibility(VISIBLE); } } /** * Submit the query as soon as the user clicks the item. * * @param submit submit state */ public void setSubmitOnClick(boolean submit) { this.submit = submit; } /** * Set Suggest List OnItemClickListener * * @param listener */ public void setOnItemClickListener(AdapterView.OnItemClickListener listener) { mSuggestionsListView.setOnItemClickListener(listener); } /** * Set Adapter for suggestions list. Should implement Filterable. * * @param adapter */ public void setAdapter(ListAdapter adapter) { mAdapter = adapter; mSuggestionsListView.setAdapter(adapter); startFilter(mSearchSrcTextView.getText()); } /** * Set Adapter for suggestions list with the given suggestion array * * @param suggestions array of suggestions */ public void setSuggestions(String[] suggestions) { if (suggestions != null && suggestions.length > 0) { mTintView.setVisibility(VISIBLE); final SearchAdapter adapter = new SearchAdapter(mContext, suggestions, suggestionIcon, ellipsize); setAdapter(adapter); setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { setQuery((String) adapter.getItem(position), submit); } }); } else { mTintView.setVisibility(GONE); } } /** * Dismiss the suggestions list. */ public void dismissSuggestions() { if (mSuggestionsListView.getVisibility() == VISIBLE) { mSuggestionsListView.setVisibility(GONE); } } /** * Calling this will set the query to search text box. if submit is true, it'll submit the query. * * @param query * @param submit */ public void setQuery(CharSequence query, boolean submit) { mSearchSrcTextView.setText(query); if (query != null) { mSearchSrcTextView.setSelection(mSearchSrcTextView.length()); mUserQuery = query; } if (submit && !TextUtils.isEmpty(query)) { onSubmitQuery(); } } /** * if show is true, this will enable voice search. If voice is not available on the device, this method call has not effect. * * @param show */ public void showVoice(boolean show) { if (show && isVoiceAvailable() && allowVoiceSearch) { mVoiceBtn.setVisibility(VISIBLE); } else { mVoiceBtn.setVisibility(GONE); } } /** * Call this method and pass the menu item so this class can handle click events for the Menu Item. * * @param menuItem */ public void setMenuItem(MenuItem menuItem) { this.mMenuItem = menuItem; mMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { showSearch(); return true; } }); } /** * Return true if search is open * * @return */ public boolean isSearchOpen() { return mIsSearchOpen; } /** * Sets animation duration. ONLY FOR PRE-LOLLIPOP!! * * @param duration duration of the animation */ public void setAnimationDuration(int duration) { mAnimationDuration = duration; } /** * Open Search View. This will animate the showing of the view. */ public void showSearch() { showSearch(true); } /** * Open Search View. If animate is true, Animate the showing of the view. * * @param animate true for animate */ public void showSearch(boolean animate) { if (isSearchOpen()) { return; } //Request Focus mSearchSrcTextView.setText(null); mSearchSrcTextView.requestFocus(); if (animate) { setVisibleWithAnimation(); } else { mSearchLayout.setVisibility(VISIBLE); if (mSearchViewListener != null) { mSearchViewListener.onSearchViewShown(); } } mIsSearchOpen = true; } private void setVisibleWithAnimation() { AnimationUtil.AnimationListener animationListener = new AnimationUtil.AnimationListener() { @Override public boolean onAnimationStart(View view) { return false; } @Override public boolean onAnimationEnd(View view) { if (mSearchViewListener != null) { mSearchViewListener.onSearchViewShown(); } return false; } @Override public boolean onAnimationCancel(View view) { return false; } }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mSearchLayout.setVisibility(View.VISIBLE); AnimationUtil.reveal(mSearchTopBar, animationListener); } else { AnimationUtil.fadeInView(mSearchLayout, mAnimationDuration, animationListener); } } /** * Close search view. */ public void closeSearch() { if (!isSearchOpen()) { return; } AnimationUtil.AnimationListener animationListener = new AnimationUtil.AnimationListener() { @Override public boolean onAnimationStart(View view) { return false; } @Override public boolean onAnimationEnd(View view) { mSearchSrcTextView.setText(null); dismissSuggestions(); clearFocus(); mSearchLayout.setVisibility(GONE); if (mSearchViewListener != null) { mSearchViewListener.onSearchViewClosed(); } mIsSearchOpen = false; return false; } @Override public boolean onAnimationCancel(View view) { return false; } }; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mSearchLayout.setVisibility(View.VISIBLE); hide(mSearchTopBar, animationListener); } else { mSearchLayout.setVisibility(VISIBLE); AnimationUtil.fadeOutView(mSearchLayout, AnimationUtil.ANIMATION_DURATION_MEDIUM, animationListener); } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static void hide(final View view, final AnimationUtil.AnimationListener listener) { int cx = view.getWidth() - (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 24, view.getResources().getDisplayMetrics()); int cy = view.getHeight() / 2; int finalRadius = Math.max(view.getWidth(), view.getHeight()); Animator anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, finalRadius, 0); view.setVisibility(View.VISIBLE); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { listener.onAnimationStart(view); } @Override public void onAnimationEnd(Animator animation) { listener.onAnimationEnd(view); } @Override public void onAnimationCancel(Animator animation) { listener.onAnimationCancel(view); } @Override public void onAnimationRepeat(Animator animation) { } }); anim.start(); } /** * Set this listener to listen to Query Change events. * * @param listener */ public void setOnQueryTextListener(OnQueryTextListener listener) { mOnQueryChangeListener = listener; } /** * Set this listener to listen to Search View open and close events * * @param listener */ public void setOnSearchViewListener(SearchViewListener listener) { mSearchViewListener = listener; } /** * Ellipsize suggestions longer than one line. * * @param ellipsize */ public void setEllipsize(boolean ellipsize) { this.ellipsize = ellipsize; } @Override public void onFilterComplete(int count) { if (count > 0) { showSuggestions(); } else { dismissSuggestions(); } } @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { // Don't accept focus if in the middle of clearing focus if (mClearingFocus) return false; // Check if SearchView is focusable. if (!isFocusable()) return false; return mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect); } @Override public void clearFocus() { mClearingFocus = true; hideKeyboard(this); super.clearFocus(); mSearchSrcTextView.clearFocus(); mClearingFocus = false; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); mSavedState = new SavedState(superState); mSavedState.query = mUserQuery != null ? mUserQuery.toString() : null; mSavedState.isSearchOpen = this.mIsSearchOpen; return mSavedState; } @Override public void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } mSavedState = (SavedState) state; if (mSavedState.isSearchOpen) { showSearch(false); setQuery(mSavedState.query, false); } super.onRestoreInstanceState(mSavedState.getSuperState()); } static class SavedState extends BaseSavedState { String query; boolean isSearchOpen; SavedState(Parcelable superState) { super(superState); } private SavedState(Parcel in) { super(in); this.query = in.readString(); this.isSearchOpen = in.readInt() == 1; } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeString(query); out.writeInt(isSearchOpen ? 1 : 0); } //required field that makes Parcelables from a Parcel public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } public interface OnQueryTextListener { /** * Called when the user submits the query. This could be due to a key press on the * keyboard or due to pressing a submit button. * The listener can override the standard behavior by returning true * to indicate that it has handled the submit request. Otherwise return false to * let the SearchView handle the submission by launching any associated intent. * * @param query the query text that is to be submitted * @return true if the query has been handled by the listener, false to let the * SearchView perform the default action. */ boolean onQueryTextSubmit(String query); /** * Called when the query text is changed by the user. * * @param newText the new content of the query text field. * @return false if the SearchView should perform the default action of showing any * suggestions if available, true if the action was handled by the listener. */ boolean onQueryTextChange(String newText); } public interface SearchViewListener { void onSearchViewShown(); void onSearchViewClosed(); } }