前几天总结了欢迎页里面的知识点FanChat学习笔记(一)——MVP模式的应用,今天继续学习第二个Activity,登录页。
在开始温习MVP模式的代码之前,我们先学习一个关于软键盘的技巧:
1. android:imeOptions
2. TextView.OnEditorActionListener
第一个属性指的是软键盘的文本和图片的设定,github是这样介绍的:
注意配置EditText的imeOptions属性时,需要配合inputType才能起作用。
android:imeOptions="actionNext"//下一个
android:imeOptions="actionGo"//启动
android:imeOptions="actionDone"//完成
android:imeOptions="actionPrevious"//上一个
android:imeOptions="actionSearch"//搜索
android:imeOptions="actionSend"//发送
如果上面的说明还不够直观,那么看看下面的图片和说明:
actionUnspecified 未指定,对应常量EditorInfo.IME_ACTION_UNSPECIFIED.效果:
actionNone 没有动作,对应常量EditorInfo.IME_ACTION_NONE 效果:
actionGo 去往,对应常量EditorInfo.IME_ACTION_GO 效果:
actionSearch 搜索,对应常量EditorInfo.IME_ACTION_SEARCH 效果:
actionSend 发送,对应常量EditorInfo.IME_ACTION_SEND 效果:
actionNext 下一个,对应常量EditorInfo.IME_ACTION_NEXT 效果:
actionDone 完成,对应常量EditorInfo.IME_ACTION_DONE 效果:
当然,不止xml文件可以设置,java代码也可以设置,这里设置一个很少用的常量:
EditText edit = new EditText(this);
edit.setImeOptions(EditorInfo.IME_FLAG_FORCE_ASCII);
OK,我们已经把图片设置好了,那么如何由”EditText + Button” 形成一个 “输入+按键响应” 的事件呢?我们只要重写EditText的OnEditorActionListener事件即可,代码如下:
private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
// TODO 待编辑点击事件
return true;
}
return false;
}
};
mPassword.setOnEditorActionListener(mOnEditorActionListener);
OK,学习了上面的小知识后,我们继续学习登录页的Activity,该Activity的业务逻辑只有一个登陆,然后UI逻辑却有账号错误、密码错误、开始登录、登录成功、登录失败,我们先看看这些逻辑是怎么来通过MVP模式表达,书接上回,先看看业务逻辑:
package com.itheima.leon.qqdemo.presenter;
/**
* 创建者: Leon
* 创建时间: 2016/10/16 21:16
* 描述: TODO
*/
public interface LoginPresenter {
public static final String TAG = "LoginPresenter";
void login(String userName, String pwd);
}
接下来看看UI逻辑:
package com.itheima.leon.qqdemo.view;
/**
* 创建者: Leon
* 创建时间: 2016/10/16 21:13
* 描述: TODO
*/
public interface LoginView {
public static final String TAG = "LoginView";
void onUserNameError();
void onPasswordError();
void onStartLogin();
void onLoginSuccess();
void onLoginFailed();
}
OK,接下来还是将业务逻辑与UI逻辑进行整合,那么它们是如何判断什么时候来调用相关的UI逻辑呢?看代码:
package com.itheima.leon.qqdemo.presenter.impl;
import com.hyphenate.chat.EMClient;
import com.itheima.leon.qqdemo.adpater.EMCallBackAdapter;
import com.itheima.leon.qqdemo.presenter.LoginPresenter;
import com.itheima.leon.qqdemo.utils.StringUtils;
import com.itheima.leon.qqdemo.utils.ThreadUtils;
import com.itheima.leon.qqdemo.view.LoginView;
/**
* 创建者: Leon
* 创建时间: 2016/10/16 21:17
* 描述: TODO
*/
public class LoginPresenterImpl implements LoginPresenter {
public static final String TAG = "LoginPresenterImpl";
public LoginView mLoginView;
public LoginPresenterImpl(LoginView loginView) {
mLoginView = loginView;
}
@Override
public void login(String userName, String pwd) {
if (StringUtils.checkUserName(userName)) {
if (StringUtils.checkPassword(pwd)) {
mLoginView.onStartLogin();
startLogin(userName, pwd);
} else {
mLoginView.onPasswordError();
}
} else {
mLoginView.onUserNameError();
}
}
private void startLogin(String userName, String pwd) {
EMClient.getInstance().login(userName, pwd, mEMCallBack);
}
private EMCallBackAdapter mEMCallBack = new EMCallBackAdapter() {
@Override
public void onSuccess() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginSuccess();
}
});
}
@Override
public void onError(int i, String s) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginFailed();
}
});
}
};
}
这里,我们需要注意的是作者是如何检查用户名与密码的?还是那句话,细节有魔鬼啊!看代码:
package com.itheima.leon.qqdemo.utils;
/**
* 创建者: Leon
* 创建时间: 2016/10/16 21:18
* 描述: TODO
*/
public class StringUtils {
private static final String USER_NAME_REGEX = "^[a-zA-Z]\\w{2,19}$";
private static final String PASSWORD_REGEX = "^[0-9]{3,20}$";
public static boolean checkUserName(String userName) {
return userName.matches(USER_NAME_REGEX);
}
public static boolean checkPassword(String pwd) {
return pwd.matches(PASSWORD_REGEX);
}
}
对于正则表达式,github已经有很清晰的注释了!还是看代码:
private static final String USER_NAME_REGEX = "^[a-zA-Z]\\w{2,19}$";//用户名的正则表达式
private static final String PASSWORD_REGEX = "^[0-9]{3,20}$";//密码的正则表达式
1. ^ 匹配输入字符串的开始位置
2. [a-zA-Z] 字符范围。匹配指定范围内的任意字符。
3. \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
4. $ 匹配输入字符串的结束位置
接下来可以看看Activity里面的实现了:
package com.itheima.leon.qqdemo.ui.activity;
import android.Manifest;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.PermissionChecker;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.itheima.leon.qqdemo.R;
import com.itheima.leon.qqdemo.presenter.LoginPresenter;
import com.itheima.leon.qqdemo.presenter.impl.LoginPresenterImpl;
import com.itheima.leon.qqdemo.view.LoginView;
import butterknife.BindView;
import butterknife.OnClick;
/**
* 创建者: Leon
* 创建时间: 2016/10/16 19:31
* 描述: TODO
*/
public class LoginActivity extends BaseActivity implements LoginView{
public static final String TAG = "LoginActivity";
private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 0;
@BindView(R.id.user_name)
EditText mUserName;
@BindView(R.id.password)
EditText mPassword;
@BindView(R.id.login)
Button mLogin;
@BindView(R.id.new_user)
TextView mNewUser;
private LoginPresenter mLoginPresenter;
/**
* 绑定布局
* @return
*/
@Override
public int getLayoutRes() {
return R.layout.activity_login;
}
/**
* 初始化
*/
@Override
protected void init() {
super.init();
mLoginPresenter = new LoginPresenterImpl(this);
mPassword.setOnEditorActionListener(mOnEditorActionListener);
}
/**
* 实现点击事件
* @param view
*/
@OnClick({R.id.login, R.id.new_user})
public void onClick(View view) {
switch (view.getId()) {
case R.id.login:
startLogin();
break;
case R.id.new_user:
startActivity(RegisterActivity.class);
break;
}
}
/**
* 开始登录
*/
private void startLogin() {
if (hasWriteExternalStoragePermission()) {
login();
} else {
applyPermission();
}
}
/**
* 判断是否已经拥有写入SD卡的权限
* @return
*/
private boolean hasWriteExternalStoragePermission() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PermissionChecker.PERMISSION_GRANTED;
}
/**
* 请求权限
*/
private void applyPermission() {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_EXTERNAL_STORAGE);
}
private void login() {
hideKeyBoard();
String userName = mUserName.getText().toString().trim();
String password = mPassword.getText().toString().trim();
mLoginPresenter.login(userName, password);
}
@Override
public void onUserNameError() {
mUserName.setError(getString(R.string.user_name_error));
}
@Override
public void onPasswordError() {
mPassword.setError(getString(R.string.user_password_error));
}
@Override
public void onStartLogin() {
showProgress(getString(R.string.logining));
}
@Override
public void onLoginSuccess() {
hideProgress();
toast(getString(R.string.login_success));
startActivity(MainActivity.class);
}
@Override
public void onLoginFailed() {
hideProgress();
toast(getString(R.string.login_failed));
}
/**
* 申请权限回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_WRITE_EXTERNAL_STORAGE:
if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) {
login();
} else {
toast(getString(R.string.not_get_permission));
}
break;
}
}
private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
startLogin();
return true;
}
return false;
}
};
}
现在整篇文章看上去很简洁,也很清晰,所以第一个需要学习的就是注释框架。我以前接触到的注释框架会改变当前的Activity,使用的时候需要在当前的类名前增加一个“_”,但是这里已经不用了。该注释框架叫黄油刀,github地址:https://github.com/JakeWharton/butterknife,学习博客:Android中ButterKnife(黄油刀)的详细使用。由于这个我也没有使用,这里也不能使用更多的语言来介绍了。
在该Activity还有一个知识点需要提到的就是Android6.0动态权限管理,该权限的各个方法都写好了注释,相信应该很好理解。需要强调的是:
1.Android6.0动态权限管理兼容6.0以下的版本,所以不用判断版本;
2.如果文件清单没有添加个人权限(如下),则不会弹出选择是否允许的dialog,而是系统直接返回申请权限失败,测试权限:
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>-->
最后还有一个地方也值得我们学习这种编程思维,就是接口适配,先看看作者是怎么描述的?
EMCallBack是环信的一个请求回调接口,包括请求成功的回调onSuccess,请求失败的回调onError和请求进度回调onProgress, 但在实际使用过程中,通常只使用到请求成功和失败的回调,请求进度回调通常留在那里成了一个空方法,对于一个有 代码洁癖的搬砖师来说,这是很难受的。所以我们可以创建一个适配器类实现这个接口,使用时用适配器类来替换EMCallBack 接口,这样只需要覆写我们想覆写的方法就可以了(学习代码,这里不讨论登录的进度条实时显示进度的问题,主要是学习优点)。
//EMCallBack接口的适配器
public class EMCallBackAdapter implements EMCallBack{
@Override
public void onSuccess() {
}
@Override
public void onError(int i, String s) {
}
@Override
public void onProgress(int i, String s) {
}
}
//EMCallBack适配器的使用
private EMCallBackAdapter mEMCallBack = new EMCallBackAdapter() {
@Override
public void onSuccess() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginSuccess();
}
});
}
@Override
public void onError(int i, String s) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mLoginView.onLoginFailed();
}
});
}
};
OK,上面就是我从这篇Activity里面学习到的小知识的运用。老规矩,我们也得动脑袋自己进行再加工。这里我就不实现进度条实时显示进度了,我觉得第一个运行时权限可以进一步封装,如郭霖直播中所讲,封装代码如下:
/**
* 请求权限
*/
public void applyPermission(String[] permissions, onRequestPermissionsListener listener) {
this.listener = listener;
List<String> permissionList = new ArrayList<>();
for (String permission:permissions ) {
if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PermissionChecker.PERMISSION_GRANTED){
permissionList.add(permission);
}
}
if(permissionList.isEmpty()){
listener.onSuccess();
}else{
ActivityCompat.requestPermissions(this,permissionList.toArray(new String[permissionList.size()]), WRITE_EXTERNAL_STORAGE);
}
}
/**
* 申请权限回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
List<String> defeatedValue = new ArrayList<>();
switch (requestCode) {
case WRITE_EXTERNAL_STORAGE:
for (int i = 0; i < grantResults.length; i++) {
String value = permissions[i];
if(grantResults[i] != PermissionChecker.PERMISSION_GRANTED){
defeatedValue.add(value);
}
}
if(defeatedValue.isEmpty()){
listener.onSuccess();
}else{
listener.onDefeated(defeatedValue);
}
break;
}
}
private onRequestPermissionsListener listener;
public interface onRequestPermissionsListener{
void onSuccess();
void onDefeated(List<String> values);
}
调用方法:
/**
* 开始登录
*/
private void startLogin() {
applyPermission(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},new BaseActivity.onRequestPermissionsListener(){
@Override
public void onSuccess() {
login();
}
@Override
public void onDefeated(List<String> values) {
for (int i = 0; i < values.size(); i++) {
toast(values.get(i)+":权限获取失败");
}
}
});
}
OK,Android6.0动态权限管理已经封装完毕,那么接下来我觉得接口的适配可以修改为抽象类,这样的话new的时候自动就将方法重写了,可以更懒一点,弊端暂时还没有想到,欢迎大神拍板砖!
public abstract class EMCallBackAdapter implements EMCallBack {
public static final String TAG = "EMCallBackAdapter";
@Override
public void onProgress(int i, String s) {
}
}
调用方法还是一样,这里就不贴代码了!!!
今天就总结到这里,明天继续!
学习的项目地址:
github:https://github.com/uncleleonfan/FanChat
郭霖——Android 6.0运行时权限视频讲解