相信普通的用户在使用手机的时候经常遇到一个输入框没输入的时候会有灰色的提示,可一旦输入些许字符后,用户很快忘记了这个输入框要输入什么,但是退回去却又要重新输入,对于用户的这个要求,谷歌看在眼里,在2015推出了TextInputLayout来满足这个需求。那么对于怎么使用这个控件,我将用模仿网易邮箱大师的登录界面来一一告诉大家怎么畅快的玩起来。
下面来看看,我们将要实现的界面。
1.TextInputLayout在布局里面的那些事
首先导入:
implementation 'com.android.support:design:29.0.0'
对于TextInputLayout并不能单独的使用,必须配合TextInputEditText使用起来。下面我们来看看登录界面的布局XML代码:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/lyj_layout" android:orientation="vertical"> <com.google.android.material.textfield.TextInputLayout android:id="@+id/username_til" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/username_edit" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/login_username_edittext_string"/> </com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout android:id="@+id/password_til" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/password_edit" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="textPassword" android:hint="@string/login_password_edittext_string"/> </com.google.android.material.textfield.TextInputlayout> <Button android:id="@+id/okbut" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="@string/login_okbut_string"/> </LinearLayout>
上面只是简单的两个输入框EditText,与一个Button,唯一不同是EditText被TextInputLayout所包裹。这样我们的邮箱登录初始化界面就完成了。现在运行界面,我们就可以得到有动画效果的提示信息界面,如下:
2.实现网易邮箱提示信息
邮箱提示可以用android.support.v7.widget.ListPopupWindow来实现。它与ListView用法相同,也与PopupMenu,PopupWindow用法一样(此一样是说的其中的特性,不是完全)。
现在我们来初始化ListPopupWindow:
不过先还是写一下用到的所有成员变量:
private TextInputLayout usernameTil;//用户名输入框包裹的TextInputLayout private TextInputLayout passwordTil;//密码输入框包裹的TextInputLayout private TextInputEditText usernameEdit;//用户名输入框 private Button okBut; private ListPopupWindow listPopupWindow;//输入框弹出菜单 private List<String> strLists = new ArrayList<>();//adatper参数 private List<String> strListsFlag = new ArrayList<>();//记录所有邮箱提示,当匹配后提示邮箱减少后可以直接从该list中重启获取添加到strLists; private ArrayAdapter<String> adapter;//listpopupwindow适配器 private int numStartFlag = 0;//输入框的变化返回的永远是整个字符,要想字符加入提示框不重复,必须获取输入框@字符的索引 private int mLastNumFlag=0;//记录输入用户名后的前一次字符数,也就是输入是增加了还是减少了。 private boolean isHaveFlag=true;//记录删除用户名输入框的一个字符后,其还有匹配原来的邮箱吗?没有为false则需增加提示框邮箱。
后面的代码除方法外全部在onCreate()中:
this.listPopupWindow = new ListPopupWindow(this);//初始化 this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, strLists); this.listPopupWindow.setAdapter(adapter); this.listPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); this.listPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); this.listPopupWindow.setAnchorView(usernameTil);//设置弹出菜单相对谁的位置 this.listPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { usernameEdit.setText(strLists.get(position));//当点击提示弹出菜单的时候,设置输入框与弹出菜单选项一致 listPopupWindow.dismiss();//当然点击菜单后,关闭菜单 } });
适配器自不必说,setAnchor设置相对谁的位置。当然是我们的TextInputLayout,那么将会显示在其下面。当你输入一段输入到输入框的时候,点击ListPopupWindow中的选项,代码将会将选项写入到输入框,且这个时候也要关闭ListPopupWindow提示信息。
但是TextInputLayout虽然说可以直接从其获取他包裹的EditText的文本信息,可是其没有负责监听输入框变化的回调函数,故还是要获取EditText的控件进行操作。实现其监听如下:
this.usernameEdit.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { listPopupWindow.show();//当开始输入的时候弹出提示listPopupWindow } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { listPopupWindowChanged(s); } @Override public void afterTextChanged(Editable s) { } });
我们将提示算法分离出来写入listPopupWindowChanged(s)中,代码如下:
private void listPopupWindowChanged(CharSequence s) { //这就是大名鼎鼎的算法,我花了2个小时写的,只能说我算法不行啊。既然一个小小的匹配算法花了我2个小时 //当输入框已经输入@符号后 if (s.toString().indexOf('@') != -1) { //其实这里还可以判断一下@是不是文本框最后一个字符,判断后可以节省一个for循环时间。 //比如当我输入"liyuanjinglyj@1"到输入框后,这个时候,凡是没有包含这个的提示listpopupwindow子项都会被删除 for (int i = 0; i < strLists.size(); i++) { if (!strLists.get(i).trim().contains(s.toString().trim())) { strLists.remove(i); i--;//那么删除后strLists长度肯定减一,不然,不是匹配掉,就是数组越界。 } } //当我在进行删去文本框字符的时候调用里面的方法, // 因为我输入的时候,要匹配我输入的邮箱,已经删除了以前的listpopupwindow子项, //这个时候我删除字符,那么以前因为匹配而删除的并且有匹配能的应该添加到listpopupwindow中 if (s.toString().trim().length()<mLastNumFlag){ for (int i = 0; i < strListsFlag.size(); i++) { if (strListsFlag.get(i).contains(s.toString().trim().substring(s.toString().indexOf('@')))) { //当listpopupwindow有选项时 if(strLists.size()>0){ for (int j=0;j<strLists.size();j++){ if(strLists.get(j).contains(strListsFlag.get(i).substring(0))){ isHaveFlag=true; break; } isHaveFlag=false;//如果里面没有该项,标记为false } if(!isHaveFlag){//这里判断添加 strLists.add(s.toString().trim().substring(0,s.toString().indexOf('@'))+strListsFlag.get(i)); isHaveFlag=true; } }else{//当listpopupwindow没有选项时 strLists.add(s.toString().trim().substring(0,s.toString().indexOf('@'))+strListsFlag.get(i)); } } } } //当其有@了的时候,我listpopupwindow提示依然只需要前面的字符串所以要截取@前面的字符串即可 s = s.toString().substring(0, s.toString().indexOf('@')); } for (int i = 0; i < strLists.size(); i++) { strLists.set(i, s + strLists.get(i).substring(numStartFlag));//获取文本框字符加上邮箱后缀,得到提示。 } numStartFlag = s.length();//保存邮箱格式的开始索引,为了让输入框不重复输入字符 mLastNumFlag=usernameEdit.getText().toString().trim().length();//保存输入框文本长度 adapter.notifyDataSetChanged();//更新弹出的listpopupwindow }
对于这个算法,注释我写的已经很详细了。细节看注释。
不过这里还有一个问题,有必要说明一下,我的字符串数组是写在资源文件res/value/array.xml中的,但字符串数组string-array中<item>中并不能直接写入@符号,否则报错,可是加入转义符后虽然不报错,但是其连转义符一起写入了字符串,还是需要遍历,我百度了许久也没有看到解决方案,所以我干脆在代码遍历加入@。
这里暂且设置一个疑问,如果谁知道如何解决可以告诉我,回复在文章下面。
这里我的处理方式如下:
this.strLists = Arrays.asList(getResources().getStringArray(R.array.email_string_array)); this.strLists = new ArrayList<>(this.strLists);//上面解释了,如下不这样,那么匹配删除就是抛出上面的异常 //为什么还要循环添加@,而不写入资源文件 //是因为@在资源文件里面报错。具体怎么解决,我没有百度到,好像从来没人这么用。 for (int i = 0; i < this.strLists.size(); i++) { this.strLists.set(i, "@" + this.strLists.get(i)); } this.strListsFlag = new ArrayList<>(this.strLists);//复制给保存弹出listpopupwindow,免得删除提示后无法恢复
为什么要strLists=new ArrayList<>(this.strLists)原因如下:
Arrays.asList() 返回java.util.Arrays$ArrayList,而不是ArrayList。Arrays$ArrayList和ArrayList都是继承AbstractList,remove,add等method在AbstractList中是默认throw UnsupportedOperationException而且不作任何操作,ArrayList 覆盖这些method来对list进行操作,但是Arrays$ArrayList没有覆盖 remove(),add()等,所以使用就会throw UnsupportedOperationException。