重学Android-EditText的进阶操作

EditText的进阶使用

EditText 是我们常用的输入框控件,平常我们只是使用它输入文本,这里记录一些它不太常见的操作和一些解决方案。

一、焦点的自动获取

如果一个页面内定义了EditText,那么有可能我们进入此页面的时候会自动弹起软键盘,(分机型,有的会弹,有的不弹)。如果我们需要弹软键盘,我们制定给 EditText 设置

    android:focusable="true"
    android:focusableInTouchMode="true"

但是如果我们不想这个页面进去就弹出软键盘,我们可以给根布局或者 EditText 的父布局设置 focusable 。

二、光标和背景的控制

默认的 EditText 是带下划线和粗光标的,我们可以对它们进行简单的修改

android:background="@null" //去掉了下划线

android:textCursorDrawable="@null"  //去掉光标的颜色

自定义光标的颜色和宽度:

<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <size android:width="2dp" />

    <solid android:color="#BDC7D8" />

</shape>

使用自定义光标

android:textCursorDrawable="@drawable/edittext_cursor"

三、限制小数点位数

我们可以通过监听 EditText 的文本变化的方式来改变文本值,我们还能通过 DigitsKeyListener 的方式监听文本的改变。

3.1 TextWatcher的方式

我们可以通过监听 EditText 的文本变化,比如我们只想要小数点后面2位数,我们就监听文本变化,点后面的2位数,如果多了就把他删除掉。

public class MoneyTextWatcher implements TextWatcher {
    private EditText editText;
    private int digits = 2;

    public MoneyTextWatcher(EditText et) {
        editText = et;
    }
    public MoneyTextWatcher setDigits(int d) {
        digits = d;
        return this;
    }


    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        //删除“.”后面超过2位后的数据
        if (s.toString().contains(".")) {
            if (s.length() - 1 - s.toString().indexOf(".") > digits) {
                s = s.toString().subSequence(0,
                        s.toString().indexOf(".") + digits+1);
                editText.setText(s);
                editText.setSelection(s.length()); //光标移到最后
            }
        }
        //如果"."在起始位置,则起始位置自动补0
        if (s.toString().trim().substring(0).equals(".")) {
            s = "0" + s;
            editText.setText(s);
            editText.setSelection(2);
        }

        //如果起始位置为0,且第二位跟的不是".",则无法后续输入
        if (s.toString().startsWith("0")
                && s.toString().trim().length() > 1) {
            if (!s.toString().substring(1, 2).equals(".")) {
                editText.setText(s.subSequence(0, 1));
                editText.setSelection(1);
                return;
            }
        }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
}

使用:

//默认两位小数
mEditText.addTextChangedListener(new MoneyTextWatcher(mEditText1));

//手动设置其他位数,例如3
mEditText.addTextChangedListener(new MoneyTextWatcher(mEditText1).setDigits(3);
3.2 DigitsKeyListener的方式
public class ETMoneyValueFilter extends DigitsKeyListener {

    public ETMoneyValueFilter(int d) {
        super(false, true);
        digits = d;
    }

    private int digits = 2;  //默认显示二位数的小数点

    public ETMoneyValueFilter setDigits(int d) {
        digits = d;
        return this;
    }

    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        CharSequence out = super.filter(source, start, end, dest, dstart, dend);

        if (out != null) {
            source = out;
            start = 0;
            end = out.length();
        }

        int len = end - start;

        if (len == 0) {
            return source;
        }

        //以点开始的时候,自动在前面添加0
        if (source.toString().equals(".") && dstart == 0) {
            return "0.";
        }
        //如果起始位置为0,且第二位跟的不是".",则无法后续输入
        if (!source.toString().equals(".") && dest.toString().equals("0")) {
            return "";
        }

        int dlen = dest.length();

        for (int i = 0; i < dstart; i++) {
            if (dest.charAt(i) == '.') {
                return (dlen - (i + 1) + len > digits) ?
                        "" :
                        new SpannableStringBuilder(source, start, end);
            }
        }

        for (int i = start; i < end; ++i) {
            if (source.charAt(i) == '.') {
                if ((dlen - dend) + (end - (i + 1)) > digits)
                    return "";
                else
                    break;  
            }
        }

        return new SpannableStringBuilder(source, start, end);
    }
}

其实是和 TextWatcher 类似的方式,那么使用的时候我们这样使用:

//默认两位小数
mEditText.setFilters(new InputFilter[]{new MoneyValueFilter()});

//手动设置其他位数,例如3
mEditText.setFilters(new InputFilter[]{new MoneyValueFilter().setDigits(3)});

在Kotlin代码中是这样使用:

et_input.filters = arrayOf(ETMoneyValueFilter().setDigits(3))

这样就可以实现小数点后面二位数的控制,还顺便加入了.的判断,自动加0的操作。

四、EditText的Search操作

当此 EditText 软键盘弹起的时候,右下角的确定变为搜索,我们需要给 EditText 设置一个属性:

 android:imeOptions="actionSearch"

然后给软键盘设置一个监听

       //点击软键盘搜索按钮
        etSearch.setOnKeyListener(new View.OnKeyListener() {

            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {

                if (keyCode == KeyEvent.KEYCODE_ENTER) {
                    // 先隐藏键盘
                    ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE))
                            .hideSoftInputFromWindow(TransactionHistorySearchActivity.this.getCurrentFocus()
                                    .getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);

                    if (isSearch){
                        isSearch = false;
                        if (!TextUtils.isEmpty(etSearch.getText().toString()))
                            searchHistory(etSearch.getText().toString());
                    }

                }
                return false;
            }
        });

这里使用一个flag来判断,是因为部分机型会回调2次。所以为了统一效果,我们使用拦截判断只调用一次。

当然Search的逻辑如果你使用 Kotlin + DataBinding 来实现,那么就更简单了。

        //执行搜索
        fun doSearch() {
            KeyboardUtils.hideSoftInput(mActivity)
            scrollTopRefresh()
        }

        //搜索的删除
        fun searchDel() {
            mViewModel.mKeywordLiveData.value = ""
            doSearch()
        }
           <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="32dp"
                android:layout_marginLeft="@dimen/d_15dp"
                android:background="@drawable/shape_search_gray_bg_corners20"
                android:gravity="center_vertical"
                android:orientation="horizontal">

                <ImageView
                    android:layout_width="@dimen/d_16dp"
                    android:layout_height="@dimen/d_16dp"
                    android:layout_marginLeft="@dimen/d_12dp"
                    android:src="@drawable/search_icon"
                    binding:clicks="@{click.doSearch}" />

                <EditText
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="@dimen/d_12dp"
                    android:layout_weight="1"
                    android:background="@color/transparent"
                    android:hint="大家都在搜"
                    android:imeOptions="actionSearch"
                    android:singleLine="true"
                    android:text="@={viewModel.mKeywordLiveData}"
                    android:textColor="@color/black"
                    android:textColorHint="@color/gray_99"
                    android:textSize="@dimen/d_14sp"
                    binding:onKeyEnter="@{click.doSearch}"
                    binding:typefaceMedium="@{true}" />

                <ImageView
                    android:layout_width="@dimen/d_16dp"
                    android:layout_height="@dimen/d_16dp"
                    android:layout_marginRight="@dimen/d_10dp"
                    android:src="@drawable/search_delete"
                    android:visibility="gone"
                    binding:clicks="@{click.searchDel}"
                    binding:isVisibleGone="@{!TextUtils.isEmpty(viewModel.MKeywordLiveData)}" />

            </LinearLayout>

主要的 Binding Adapter 方法为 onKeyEnter ,它实现了软键盘的搜索。

下面是自定义BindingAdapter的方法:

var _viewClickFlag = false
var _clickRunnable = Runnable { _viewClickFlag = false }

/**
 * Edit的确认按键事件
 */
@BindingAdapter("onKeyEnter")
fun EditText.onKeyEnter(action: () -> Unit) {
    setOnKeyListener { _, keyCode, _ ->
        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            KeyboardUtils.closeSoftKeyboard(this)

            if (!_viewClickFlag) {
                _viewClickFlag = true
                action()
            }
            removeCallbacks(_clickRunnable)
            postDelayed(_clickRunnable, 1000)
        }
        return@setOnKeyListener false
    }
}

和上面Java的实现方式类似,同样的做了防抖的操作。为了部分机型连续调用多次的问题。

效果:

五、焦点与软键盘的自由控制

上面说到的焦点,不自动弹出软键盘,如果我想自由的控制焦点与软键盘怎么办?

一个例子来说明,比如我们的需求,点击 EditText 的时候弹出弹框提示用户注意事项,当点击确定或者取消之后再继续输入。分解步骤如下:

  1. 我们点击EditText不能弹出软键盘
  2. 监听焦点获取之后弹出弹框
  3. 弹框完成之后我们需要手动的给EditText焦点
  4. 获取焦点之后需要设置光标与软键盘

代码逻辑如下:

      mBankAccountEt.setShowSoftInputOnFocus(false);
        mBankAccountEt.setOnFocusChangeListener((v, hasFocus) -> {
            if (hasFocus && !isShowedBankAccountNotice) {
                showBankAccountNoticePopup();
            }
        });


     private void showBankAccountNoticePopup() {
        BasePopupView mPopupView = new XPopup.Builder(mActivity)
                .moveUpToKeyboard(false)
                .hasShadowBg(true)
                .asCustom(new BankNameNoticePopup(mActivity, () -> {
                    isShowedBankAccountNotice = true;
                    mBankAccountEt.setShowSoftInputOnFocus(true);

                    //需要把焦点设置回EditText
                    mBankAccountEt.setFocusable(true);
                    mBankAccountEt.setFocusableInTouchMode(true);
                    mBankAccountEt.requestFocus();
                    mBankAccountEt.setSelection(mBankAccountEt.getText().toString().length());

                    KeyboardUtils.showKeyboard(mBankAccountEt);
                }));

        if (mPopupView != null && !mPopupView.isShow()) {
            mPopupView.show();
        }
    }

弹框就不给大家展示了,非常简单的弹窗,定义使用的弹窗库,逻辑都在完成的回调中。

KeyboardUtils工具类,控制EditText的软键盘展示与隐藏

	/*
	* 显示键盘
	* */
	public static void showKeyboard(View view) {
		InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
		if (imm != null) {
			view.requestFocus();
			imm.showSoftInput(view, 0);
		}
	}

	/*
	* 隐藏键盘
	* */
	public static void hideKeyboard(View view){
		InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
		if (imm != null) {
			imm.hideSoftInputFromWindow(view.getWindowToken(),0);
		}
	}

效果:

六、RV + EditText复用的问题

不知道大家有没有在RV中使用过 EditText ,Item中如果有 EditText 那么在滚出屏幕之后 再拉回来可能刚才输入的文本就消失了,或者换成不是刚才输入的文本了,是因为缓存复用,可能复用了别的Item上面的 EditText 控件。

有几种解决方法如下:

方法一: 强制的停用Recyclerview的复用

helper.setIsRecyclable(false);

但是RV就无法缓存与回收了,如果你的Item数量就是固定的并且不多,那么使用这个方法是最好的。

方法二: 通过监听焦点来添加或移除Edittext的TextChangedListener

@Override
protected void convert(BaseViewHolder helper, EdittextInRecyclerViewOfBean item) {
    EditText editText = helper.getView(R.id.et);
    editText.setText(item.getNum() + "");

    TextWatcher textWatcher = 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) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            //这里处理数据
            if (TextUtils.isEmpty(s.toString())) {
                item.setNum(0);
            } else {
                item.setNum(Integer.parseInt(s.toString()));
            }
        }
    };
    
    editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
        @Override
        public void onFocusChange(View v, boolean hasFocus) {
            if (hasFocus){
                editText.addTextChangedListener(textWatcher);
            }else {
                editText.removeTextChangedListener(textWatcher);
            }
        }
    });
}

方法三: 通过view的setTag()方法解决

@Override
protected void convert(BaseViewHolder helper, EdittextInRecyclerViewOfBean item) {
    TextWatcher textWatcher = 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) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            if (TextUtils.isEmpty(s.toString())) {
                item.setNum(0);
            } else {
                item.setNum(Integer.parseInt(s.toString()));
            }
        }
    };

    EditText editText = helper.getView(R.id.et);
    //为了避免TextWatcher在调用settext()时被调用,提前将它移除
    if (editText.getTag() instanceof TextWatcher) {
        editText.removeTextChangedListener((TextWatcher) editText.getTag());
    }
    editText.setText(item.getNum() + "");
    //重新添加上TextWatcher监听
    editText.addTextChangedListener(textWatcher);
    //将TextWatcher绑定到EditText
    editText.setTag(textWatcher);
}

方法四: 为每个EditText的绑定位置

public class EditTextInRecyclerViewAdapter extends RecyclerView.Adapter {
    private List<EdittextInRecyclerViewOfBean> mList = new ArrayList<>();

    public void setData(List<EdittextInRecyclerViewOfBean> list) {
        this.mList = list;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_edittext, parent, false);
        return new ViewHolder(v, new ITextWatcher());
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        ViewHolder viewHolder = (ViewHolder) holder;
        viewHolder.mITextWatcher.bindPosition(position);
        viewHolder.mEditText.setText(mList.get(position).getNum()+"");
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        EditText mEditText;
        ITextWatcher mITextWatcher;

        private ViewHolder(View v, ITextWatcher watcher) {
            super(v);
            this.mEditText = v.findViewById(R.id.et);
            this.mITextWatcher = watcher;
            this.mEditText.addTextChangedListener(watcher);
        }
    }

    class ITextWatcher implements TextWatcher {
        private int position;

        private void bindPosition(int position) {
            this.position = position;
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
        }

        @Override
        public void afterTextChanged(Editable s) {
            if (TextUtils.isEmpty(s.toString())) {
                mList.get(position).setNum(0);
            } else {
                mList.get(position).setNum(Integer.parseInt(s.toString()));
            }
        }
    }
}

方法五: 构造方法中添加TextChanged

class PicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    var ivPic: ImageView = itemView.findViewById(R.id.ivPic)
    var etScore: EditText = itemView.findViewById(R.id.etScore)
    var tvTitle: TextView = itemView.findViewById(R.id.tvTitle)
    var myTextWatcher: MyTextWatcher = MyTextWatcher()

    init {
        etScore.addTextChangedListener(myTextWatcher)
    }

    fun updateView(picItem: PicItem) {
        myTextWatcher.picItem = picItem
        ivPic.setImageResource(picItem.picResId)
        tvTitle.text = picItem.title
        if (picItem.score == null) {
            etScore.hint = "请输入分数"
            etScore.setText("")
        } else {
            etScore.setText(picItem.score)
        }
    }
}

class MyTextWatcher: TextWatcher {

    lateinit var picItem:PicItem

    override fun afterTextChanged(s: Editable?) {
        picItem?.apply {
            score=s?.toString()
        }
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    }
}

方法六: 让产品改需求,不要使用EditText,或者我们干脆使用TextView,然后点击Item弹出输入框的弹框方式来实现。

后记

总结了很多 EditText 不常见的一些问题与解决方案,不知道大家有没有遇到别的问题,或者一些 EditText 的骚操作,都可以评论区留言补充哦!

关于 EditText 的分享就到这里了,后面如果有补充我会继续更新!

完结.

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7115621556700577828