还是《第一行代码》中的例子,这次是广播一章最后的实例,利用广播实现强制下线。
1. 使用TextInputLayout
原书中在登录用的LoginActivity里使用的是EditText,但是Android Studio提供的LoginActivity模板中在文本框外嵌套了TextInputLayout。经过查阅资料发现,TextInputLayout能实现很多有意思的功能。先来看看官方文档怎么说(以下本人渣翻译):
(
TextInputLayout)是包裹一个EditText(或其子类)的布局,用于在用户进行输入操作,hint隐藏时在文本框上方显示一段悬浮文本。
它也支持通过setErrorEnabled(boolean)和setError(CharSequence)方法显示(输入)错误以及通过setCounterEnabled(boolean)显示字符计数器。切换密码可见性也是由其setPasswordVisibilityToggleEnabled(boolean)API及相关(XML)属性支持的。如果启用此功能,当你的EditText被设为显示密码时,会显示一个用于切换密码是否可见的按钮。
看起来确实非常实用,那就试试吧。
- 首先要在 - app/build.gradle中添加库依赖:- 1 
 2
 3
 4
 5- dependencies { 
 ...
 implementation 'com.android.support:design:27.1.1'
 ...
 }
- 然后在布局文件中添加控件: - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34- ... 
 <android.support.design.widget.TextInputLayout
 android:id="@+id/til_email"
 android:layout_width="match_parent"
 android:layout_height="wrap_content">
 <EditText
 android:id="@+id/email"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:hint="@string/prompt_email"
 android:inputType="textEmailAddress"
 android:maxLines="1"
 android:singleLine="true" />
 </android.support.design.widget.TextInputLayout>
 <android.support.design.widget.TextInputLayout
 android:id="@+id/til_pass"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 app:passwordToggleEnabled="true">
 <!--app:passwordToggleEnabled属性就是设置切换密码可见性的按钮-->
 <EditText
 android:id="@+id/password"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:hint="@string/prompt_password"
 android:imeActionId="6"
 android:imeActionLabel="@string/action_sign_in_short"
 android:imeOptions="actionUnspecified"
 android:inputType="textPassword"
 android:maxLines="1"
 android:singleLine="true" />
 </android.support.design.widget.TextInputLayout>
 ...
- 设置密码可见性的按钮已经设置完成,下面来写显示错误的逻辑。 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68- public class LoginActivity extends BaseActivity { 
 private EditText email;
 private EditText password;
 private Button login_btn;
 private TextInputLayout til_account;
 private TextInputLayout til_password;
 
 protected void onCreate(@Nullable Bundle savedInstanceState) {
 //此处省略初始化界面和设置控件实例的代码
 login_btn.setOnClickListener(new OnClickListener() {
 
 public void onClick(View v) {
 String emailText = email.getText().toString();
 String passText = password.getText().toString();
 if(validateAccount(emailText) & validatePassword(passText)
 //此处使用非短路与是因为有必要确保用户名和密码都得到检查,以便显示错误
 ){
 if(emailText.equals("xxx@yyy.com") && passText.equals("password")){
 Intent intent = new Intent(LoginActivity.this, MainActivity.class);
 intent.putExtra("username", emailText.substring(0, emailText.indexOf("@")));
 startActivity(intent);
 }else{
 Toast.makeText(LoginActivity.this, "用户名或密码错误!", Toast.LENGTH_SHORT).show();
 }
 }
 }
 });
 }
 /**
 * 显示错误提示,并获取焦点
 * @param textInputLayout 要设置错误的TextInputLayout布局
 * @param error 要显示的错误提示
 */
 private void showError(TextInputLayout textInputLayout, String error){
 textInputLayout.setError(error);//设置错误信息,注意此处只要error为非空则setErrorEnable()会自动设为true
 
 //确保可以设置焦点
 textInputLayout.getEditText().setFocusable(true);
 textInputLayout.getEditText().setFocusableInTouchMode(true);
 //(要求)获取焦点
 textInputLayout.getEditText().requestFocus();
 }
 /**
 * 验证用户名
 * @param account 要验证的用户名
 * @return boolean
 */
 private boolean validateAccount(String account){
 if(account == null || account.equals("")
 //更严谨地检验空字符串
 ){
 showError(til_account,"用户名不能为空");
 return false;
 }
 //检验正确别忘了取消错误显示
 til_account.setErrorEnabled(false);
 return true;
 }
 /**
 * 验证密码
 * @param password 要验证的密码
 * @return boolean
 */
 private boolean validatePassword(String password) {
 //与验证用户名相似,此处省略
 }
 }
- 此处的几个知识点: - View.setFocusable()与- setFousableInTouchMode()的区别:顾名思义,前者用于物理输入时,比如鼠标键盘和遥控器等,而后者用于触控模式。不过个人觉得有点奇怪,为什么要把二者分开呢?或者说为什么不让- setFocusable()也设置触控模式为真呢?
- 据官方文档,TextInputLayout与其下的EditText可能并非是直接的父子关系,而是隔有中间层,所以不能调用EditText.getParent()方法获取TextInputLayout。此处并未用到,只是一起总结一下。
- textInputLayout.setError(String)方法用于设置错误信息,注意此处只要字符串非空则- setErrorEnable()会自动设为- true。
- Java中检验空字符串的几个方法:- str == null || str.equals("")效率低
- str == null || str == ""比较地址,效率高,慎用
- str == null || str.length() <= 0效率高
- str == null || str.isEmpty()效率高,自Java SE 6.0支持
 
- TextView也有类似的- setError方法,不同的是它是在文本框右侧显示一张图片,并且弹出一个小弹窗显示设置的错误信息。
 
- 这时候登录界面的基本功能已经写好了,可以发现TextInputLayout提供的密码可见切换按钮在点击时还有动画,可以说是非常精致和贴心了。美中不足的是想要更新错误信息需要点击登录按钮才行,不是实时更新。更好的实现方式可以写成在焦点监听OnFocusChangeListener中更新,也可以写成在文本变化监听TextWatcher中更新。2.实现强制下线
- 接下来实现强制下线的逻辑。此处采用书中的框架,编写 - BaseActivity类来实现结束所有活动和弹出- AlertDialog。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49- public class BaseActivity extends AppCompatActivity { 
 protected static LocalBroadcastManager manager;
 private ForceOfflineReceiver receiver;
 
 protected void onCreate(@Nullable Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 ActivityCollector.addActivity(this);
 manager = LocalBroadcastManager.getInstance(this);
 }
 
 protected void onDestroy() {
 super.onDestroy();
 ActivityCollector.removeActivity(this);
 }
 
 protected void onResume() {
 super.onResume();
 receiver = new ForceOfflineReceiver();
 manager.registerReceiver(receiver, new IntentFilter("com.example.zyf.broadcasttest2.FORCE_OFFLINE"));
 }
 
 protected void onPause() {
 super.onPause();
 manager.unregisterReceiver(receiver);
 receiver = null;
 }
 class ForceOfflineReceiver extends BroadcastReceiver{
 
 public void onReceive(final Context context, Intent intent) {
 AlertDialog.Builder builder = new AlertDialog.Builder(BaseActivity.this, R.style.Theme_AppCompat_Light_Dialog_Alert);
 builder.setTitle("强制下线警告");
 builder.setCancelable(false);
 builder.setMessage("您已被强制下线,请重新登录。");
 builder.setPositiveButton("好的", new DialogInterface.OnClickListener() {
 
 public void onClick(DialogInterface dialog, int which) {
 ActivityCollector.finishAll();
 Intent intent = new Intent(context, LoginActivity.class);
 context.startActivity(intent);
 }
 });
 builder.show();
 }
 }
 }
- 显然一个应用里只需要一个 - LocalBroadcastManager(虽然经测试多个也完全没问题),并且只需要处于栈顶(因为要弹出- AlertDialog)的活动接收广播。所以我将- LocalBroadcastManager设为- static,并且按照书中的想法把注册与取消注册接收器的逻辑放在- BaseActivity的- onResume()和- onPause()方法中。
- 此处主要的知识点就是AlertDialog.Builder的使用方法了。- setCancalable(boolean):设置窗口是否可以取消(按返回键等),默认为可取消。
- setMessage(int/CharSequence):设置窗口显示的信息。
- setPositiveButton(int/CharSequence, DialogInterface.OnClickListener):设置显示给定文本和有给定监听器的“确定”按钮。
- setNegativeButton(int/CharSequence, DialogInterface.OnClickListener):设置显示给定文本和有给定监听器的“取消”按钮。
- setNeutralButton(int/CharSequence, DialogInterface.OnClickListener):设置显示给定文本和有给定监听器的“中性操作”按钮。
- show():创建并立即显示对话框。
- setTitle(int/CharSequence):设置窗口标题。
- setView(int/View):向窗口中添加自定义的View/布局。
 提示:也可以直接用- Activity做对话框,然后在- AndroidManifest.xml文件的- <activity>标签中将主题设置为- @android:style/Theme.Material.Dialog。- ==注意:==\ 
 上面的- AlertDialog.Builder(Context, int)中,- Context只能是- Activity,否则会报错:android.view.WindowManager$BadTokenException: Unable to add window – token null is not for an application。经过测试,直接使用- onReceive(Context, Intent)中的- Context属于- android.app.Application,不是- Activity类的。\
 这个方法的另一个问题是,如果不用override的- (Context, int)参数的方法可能报错:java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.
 
后记
- 在学习过程中我又尝试了一下焦点监听和文本变化监听,发现文本变化监听虽然有3个方法,但是不是很难实现。而焦点监听则并没有想象中那么简单,需要彻底理清每一种情况下焦点变化的过程,否则很容易出错。并且单靠文本变化监听或焦点监听也不足以实现跟常见登录界面相似的检查功能。在Android Studio默认的LoginActivity中有很多代码,不过因为不感兴趣和时间有限,进度等原因没有深入探究。