/* * The MIT License (MIT) * * Copyright (c) 2015 Piasy * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.github.piasy.handywidgets.clearableedittext; import com.jakewharton.rxbinding.widget.RxTextView; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.CheckResult; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.text.Editable; import android.text.InputFilter; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import rx.Observable; import rx.Subscriber; import rx.Subscription; /** * Created by Piasy{github.com/Piasy} on 15/9/7. * * A widget that has functions of {@link EditText} and have a clear button. With full ability for * customization. */ public final class ClearableEditText extends LinearLayout { private final boolean mIsEditTextAutoFocus; private final EditText mEditText; private final ImageButton mClearButton; private boolean mHasVisibilitySwitch = false; private final CheckBox mCheckBox; private OnTextChangedListener mOnTextChangedListener; private Subscription mOnTextChangedSubscription; private boolean mIsEditorActionsMonitored = false; private OnEditorActionDoneListener mOnEditorActionDoneListener; private Subscription mEditorActionsSubscription; public ClearableEditText(Context context) { this(context, null); } public ClearableEditText(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ClearableEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOrientation(HORIZONTAL); setGravity(Gravity.CENTER_VERTICAL); TypedArray a = context.getTheme() .obtainStyledAttributes(attrs, R.styleable.ClearableEditText, 0, 0); boolean hasIcon = a.getBoolean(R.styleable.ClearableEditText_hasIcon, false); int iconRes = a.getResourceId(R.styleable.ClearableEditText_iconRes, 0); int iconMarginLeft = a.getDimensionPixelSize(R.styleable.ClearableEditText_iconMarginLeft, dip2px(10, context)); int iconMarginRight = a.getDimensionPixelSize(R.styleable.ClearableEditText_iconMarginRight, dip2px(10, context)); int editTextColor = a.getColor(R.styleable.ClearableEditText_clearableEditTextColor, 0xFF000000); String editTextContent = a.getString(R.styleable.ClearableEditText_editTextContent); int editTextHintColor = a.getColor(R.styleable.ClearableEditText_clearableEditTextHintColor, 0xFF333333); String editTextHintContent = a.getString(R.styleable.ClearableEditText_editTextHintContent); float editTextSize = a.getDimension(R.styleable.ClearableEditText_editTextSize, 20); mIsEditTextAutoFocus = a.getBoolean(R.styleable.ClearableEditText_editTextAutoFocus, true); int inputType = getInputType(context, attrs); int editTextBg = a.getResourceId(R.styleable.ClearableEditText_clearableEditTextBackground, 0); int clearIconRes = a.getResourceId(R.styleable.ClearableEditText_clearIconRes, 0); int clearIconMarginLeft = a.getDimensionPixelSize(R.styleable.ClearableEditText_clearIconMarginLeft, dip2px(10, context)); int clearIconMarginRight = a.getDimensionPixelSize(R.styleable.ClearableEditText_clearIconMarginRight, dip2px(10, context)); mHasVisibilitySwitch = a.getBoolean(R.styleable.ClearableEditText_hasVisibilitySwitch, false); int visibilitySwitchMarginRight = a.getDimensionPixelSize(R.styleable.ClearableEditText_visibilitySwitchMarginRight, dip2px(10, context)); int visibilitySwitchWidth = a.getDimensionPixelSize(R.styleable.ClearableEditText_visibilitySwitchWidth, -2); int visibilitySwitchHeight = a.getDimensionPixelSize(R.styleable.ClearableEditText_visibilitySwitchHeight, -2); int visibilitySwitchBg = a.getResourceId(R.styleable.ClearableEditText_visibilitySwitchBg, 0); a.recycle(); if (clearIconRes == 0) { throw new IllegalArgumentException("Clear icon resource must be set!"); } if (hasIcon) { ImageView searchIcon = new ImageView(context); searchIcon.setImageResource(iconRes); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); params.leftMargin = iconMarginLeft; params.rightMargin = iconMarginRight; searchIcon.setLayoutParams(params); addView(searchIcon); } // to set `android:textCursorDrawable="@null"` mEditText = (EditText) LayoutInflater.from(context) .inflate(R.layout.ui_clearable_edit_text_edit_text, this, false); mEditText.setGravity(Gravity.CENTER_VERTICAL); mEditText.setPadding(0, 0, 0, 0); mEditText.setSingleLine(); mEditText.setTextColor(editTextColor); mEditText.setHintTextColor(editTextHintColor); mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, editTextSize); mEditText.setText(editTextContent); mEditText.setHint(editTextHintContent); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); params.width = 0; params.weight = 1; mEditText.setLayoutParams(params); mEditText.setBackgroundResource(editTextBg); mEditText.setCursorVisible(true); if (inputType != EditorInfo.TYPE_NULL) { mEditText.setInputType(inputType); } addView(mEditText); mClearButton = new ImageButton(context); mClearButton.setBackgroundResource(0); LayoutParams params2 = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); params2.leftMargin = clearIconMarginLeft; params2.rightMargin = clearIconMarginRight; mClearButton.setLayoutParams(params2); mClearButton.setImageResource(clearIconRes); addView(mClearButton); mClearButton.setVisibility(INVISIBLE); if (mHasVisibilitySwitch) { mCheckBox = new CheckBox(context); mCheckBox.setButtonDrawable(R.color.clearable_edit_text_transparent); mCheckBox.setBackgroundResource(visibilitySwitchBg); LayoutParams params3 = new LayoutParams(visibilitySwitchWidth, visibilitySwitchHeight); params3.rightMargin = visibilitySwitchMarginRight; mCheckBox.setLayoutParams(params3); addView(mCheckBox); } else { mCheckBox = null; } initListener(); } private int getInputType(@NonNull Context context, @NonNull AttributeSet attrs) { int[] systemAttrs = {android.R.attr.inputType}; TypedArray a = context.obtainStyledAttributes(attrs, systemAttrs); int inputType = a.getInt(0, EditorInfo.TYPE_NULL); a.recycle(); return inputType; } private void initListener() { if (mHasVisibilitySwitch && mCheckBox != null) { mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); } else { mEditText.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } mEditText.setSelection(mEditText.getText().length()); } }); } mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (v == mEditText && hasFocus && !TextUtils.isEmpty(mEditText.getText())) { mClearButton.setVisibility(VISIBLE); } else { mClearButton.setVisibility(INVISIBLE); } } }); mEditText.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) { if (!TextUtils.isEmpty(s)) { mClearButton.setVisibility(VISIBLE); } else { mClearButton.setVisibility(INVISIBLE); } } @Override public void afterTextChanged(Editable s) { } }); mClearButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (v == mClearButton) { mEditText.setText(""); } } }); } @Override protected void onFinishInflate() { super.onFinishInflate(); if (mIsEditTextAutoFocus) { mEditText.requestFocus(); } else { mEditText.clearFocus(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mOnTextChangedSubscription != null && !mOnTextChangedSubscription.isUnsubscribed()) { mOnTextChangedSubscription.unsubscribe(); } mOnTextChangedSubscription = null; mOnTextChangedListener = null; if (mEditorActionsSubscription != null && !mEditorActionsSubscription.isUnsubscribed()) { mEditorActionsSubscription.unsubscribe(); } mEditorActionsSubscription = null; mOnEditorActionDoneListener = null; mIsEditorActionsMonitored = false; } private int dip2px(int dipValue, @NonNull Context context) { final float reSize = context.getResources().getDisplayMetrics().density; return (int) (dipValue * reSize + 0.5); } /** * Create an observable of character sequences for text changes on {@code view}. * <p> * <em>Warning:</em> The created observable keeps a strong reference to {@code view}. * Unsubscribe * to free this reference. * <p> * <em>Note:</em> A value will be emitted immediately on subscribe. */ @CheckResult @NonNull public Observable<CharSequence> textChanges() { return RxTextView.textChanges(mEditText); } /** * Set the Ime option for the {@link EditText} within it. * * @param option the option to set to {@link #mEditText} */ public void setImeOptions(int option) { mEditText.setImeOptions(option); } /** * Get the Observable which emit the Editor Action on the EditText. To make this method works * properly, you should call {@link #setImeOptions(int)} before call this method. * * <p> * <em>Warning:</em> The created observable keeps a strong reference to {@code view}. * Unsubscribe to free this reference. * <p> * <p> * <em>Warning:</em> The editor action could be only notified by one listener, and if * a {@link OnEditorActionDoneListener} was set by {@link #setOnEditorActionDoneListener * (OnEditorActionDoneListener)} or an Observable is already get through {@link * #editorActions()}, an {@link IllegalStateException} will be thrown, to notify * this error as soon as possible. * <p> * * <em>Note:</em> the listener will no longer receive any event when the {@link * #onDetachedFromWindow()} is called. And it's free to set this listener/get Observable again. */ @CheckResult @NonNull public Observable<Integer> editorActions() { if (mIsEditorActionsMonitored) { throw new IllegalStateException("The editor action has already been monitored!"); } else { mIsEditorActionsMonitored = true; } return RxTextView.editorActions(mEditText); } /** * Set a listener to get notified when the text content change. * * <em>Note:</em> the listener will no longer receive any event when the {@link * #onDetachedFromWindow()} is called. * * @param onTextChangedListener the listener to get notified when the text content change. */ public void setOnTextChangedListener(final OnTextChangedListener onTextChangedListener) { mOnTextChangedListener = onTextChangedListener; mOnTextChangedSubscription = textChanges().subscribe(new Subscriber<CharSequence>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(CharSequence charSequence) { if (mOnEditorActionDoneListener != null) { mOnTextChangedListener.onTextChanged(charSequence); } } }); } /** * Set a listener to get notified when the {@link EditorInfo#IME_ACTION_DONE} happen. * * <p> * <em>Warning:</em> The editor action could be only notified by one listener, and if * a {@link OnEditorActionDoneListener} was set by {@link #setOnEditorActionDoneListener * (OnEditorActionDoneListener)} or an Observable is already get through {@link * #editorActions()}, an {@link IllegalStateException} will be thrown, to notify * this error as soon as possible. * <p> * * <em>Note:</em> the listener will no longer receive any event when the {@link * #onDetachedFromWindow()} is called. And it's free to set this listener/get Observable again. * * @param onEditorActionDoneListener the listener to get notified when the {@link * EditorInfo#IME_ACTION_DONE} happen. */ public void setOnEditorActionDoneListener( final OnEditorActionDoneListener onEditorActionDoneListener) { if (mIsEditorActionsMonitored) { throw new IllegalStateException("The editor action has already been monitored!"); } else { mOnEditorActionDoneListener = onEditorActionDoneListener; mEditText.setImeOptions(EditorInfo.IME_ACTION_DONE); mEditorActionsSubscription = editorActions().subscribe(new Subscriber<Integer>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(Integer code) { if (code == EditorInfo.IME_ACTION_DONE && mOnEditorActionDoneListener != null) { mOnEditorActionDoneListener.onEditorActionDone(); } } }); } } /** * Request focus for EditText. */ public void requestFocusOnEditText() { mEditText.requestFocus(); } /** * Show soft keyboard. */ public void showKeyboard() { mEditText.requestFocus(); ((InputMethodManager) getContext().getSystemService( Context.INPUT_METHOD_SERVICE)).showSoftInput(mEditText, InputMethodManager.SHOW_FORCED); } /** * Hide soft keyboard. */ public void hideKeyboard() { mEditText.clearFocus(); ((InputMethodManager) getContext().getSystemService( Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(mEditText.getWindowToken(), 0); } /** * Get current text. * * @return current text. */ public CharSequence getText() { return mEditText.getText(); } /** * Set current text. * * @param text text to set. */ public void setText(CharSequence text) { mEditText.setText(text); } /** * Set current text. * * @param text text resource id to set. */ public void setText(@StringRes int text) { mEditText.setText(text); } /** * Get current hint. * * @return current hint. */ public CharSequence getHint() { return mEditText.getHint(); } /** * Set current hint. * * @param hint hint to set. */ public void setHint(CharSequence hint) { mEditText.setHint(hint); } /** * Set current hint. * * @param hint hint resource id to set. */ public void setHint(@StringRes int hint) { mEditText.setHint(hint); } /** * Set filters to EditText. * * @param filters filters to set. */ public void setFilters(@NonNull InputFilter[] filters) { mEditText.setFilters(filters); } public void setTextColor(@ColorInt int color) { mEditText.setTextColor(color); } /** * set selection for edit text * @param position select position */ public void setSelection(int position) { mEditText.setSelection(position); } }