package com.photo.photogallery.other; import android.app.Activity; import android.content.Context; 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.support.constraint.ConstraintLayout; import android.text.TextUtils; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.TextView; import com.jakewharton.rxbinding.widget.RxTextView; import com.jakewharton.rxbinding.widget.TextViewTextChangeEvent; import com.photo.photogallery.R; import java.lang.reflect.Field; import java.util.concurrent.TimeUnit; import rx.Observer; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Func1; import timber.log.Timber; public class CustomSearchView extends FrameLayout { private boolean mIsSearchOpen = false; private int mAnimationDuration; private boolean mClearingFocus; private long searchDebounce; //Views private View mSearchLayout; private View mTintView; private EditText mSearchSrcTextView; private ImageButton mBackBtn; private ImageButton mEmptyBtn; private ConstraintLayout mSearchTopBar; private CharSequence mOldQueryText; private CharSequence mUserQuery; private OnQueryTextListener mOnQueryChangeListener; private SearchViewListener mSearchViewListener; private SavedState mSavedState; private boolean submit = false; private boolean ellipsize = false; private Context mContext; private Subscription subscription; public CustomSearchView(Context context) { this(context, null); } public CustomSearchView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomSearchView(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.CustomSearchView, defStyleAttr, 0); if (a != null) { if (a.hasValue(R.styleable.CustomSearchView_searchBackground)) { setBackground(a.getDrawable(R.styleable.CustomSearchView_searchBackground)); } if (a.hasValue(R.styleable.CustomSearchView_android_textColor)) { setTextColor(a.getColor(R.styleable.CustomSearchView_android_textColor, 0)); } if (a.hasValue(R.styleable.CustomSearchView_android_textColorHint)) { setHintTextColor(a.getColor(R.styleable.CustomSearchView_android_textColorHint, 0)); } if (a.hasValue(R.styleable.CustomSearchView_android_hint)) { setHint(a.getString(R.styleable.CustomSearchView_android_hint)); } if (a.hasValue(R.styleable.CustomSearchView_searchCloseIcon)) { setCloseIcon(a.getDrawable(R.styleable.CustomSearchView_searchCloseIcon)); } if (a.hasValue(R.styleable.CustomSearchView_searchBackIcon)) { setBackIcon(a.getDrawable(R.styleable.CustomSearchView_searchBackIcon)); } a.recycle(); } } private void initiateView() { LayoutInflater.from(mContext).inflate(R.layout.search_view, this, true); mSearchLayout = findViewById(R.id.search_layout); mSearchTopBar = (ConstraintLayout) mSearchLayout.findViewById(R.id.search_top_bar); mSearchSrcTextView = (EditText) mSearchLayout.findViewById(R.id.searchTextView); mBackBtn = (ImageButton) mSearchLayout.findViewById(R.id.action_up_btn); mEmptyBtn = (ImageButton) mSearchLayout.findViewById(R.id.action_empty_btn); mTintView = mSearchLayout.findViewById(R.id.transparent_view); mBackBtn.setOnClickListener(mOnClickListener); mEmptyBtn.setOnClickListener(mOnClickListener); mTintView.setOnClickListener(mOnClickListener); initSearchView(); 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; } }); subscription = RxTextView.textChangeEvents(mSearchSrcTextView)// .debounce(750, TimeUnit.MILLISECONDS)// default Scheduler is Computation .filter(new Func1<TextViewTextChangeEvent, Boolean>() { @Override public Boolean call(TextViewTextChangeEvent changes) { String str = mSearchSrcTextView.getText().toString(); return !(str == null || str.length() == 0); } }) .observeOn(AndroidSchedulers.mainThread())// .subscribe(getSearchObserver()); mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { showKeyboard(mSearchSrcTextView); } } }); } private Observer<TextViewTextChangeEvent> getSearchObserver() { return new Observer<TextViewTextChangeEvent>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(TextViewTextChangeEvent onTextChangeEvent) { String query = onTextChangeEvent.text().toString(); mUserQuery = query; CustomSearchView.this.onTextChanged(query); } }; } private final OnClickListener mOnClickListener = new OnClickListener() { public void onClick(View v) { if (v == mBackBtn) { //closeSearch(); if (mSearchViewListener != null) { mSearchViewListener.onBackButtonPressed(); } } else if (v == mEmptyBtn) { mSearchSrcTextView.setText(null); } else if (v == mTintView) { closeSearch(); } } }; private void onTextChanged(CharSequence newText) { CharSequence text = mSearchSrcTextView.getText(); mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); if (hasText) { mEmptyBtn.setVisibility(VISIBLE); } else { mEmptyBtn.setVisibility(GONE); } 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); } } } public void hideKeyboard(Activity activity) { InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); } public void hideKeyboard(View view) { InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } public void showKeyboard(Activity activity) { InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 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 public void setSearchDebounce(long timeout) { searchDebounce = timeout; } public Subscription getSubscription() { return subscription; } @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 setCloseIcon(Drawable drawable) { mEmptyBtn.setImageDrawable(drawable); } public void setBackIcon(Drawable drawable) { mBackBtn.setImageDrawable(drawable); } 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) { Timber.e("set cursor drawable error: %s", ignored.toString()); } } //Public Methods /** * Submit the query as soon as the user clicks the item. * * @param submit submit state */ public void setSubmitOnClick(boolean submit) { this.submit = submit; } /** * 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(); } } /** * 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; } mSearchSrcTextView.setText(null); clearFocus(); //mSearchLayout.setVisibility(GONE); if (mSearchViewListener != null) { mSearchViewListener.onSearchViewClosed(); } mIsSearchOpen = false; } /** * 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 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(); void onBackButtonPressed(); } }