1.登录页面的实现
在上一篇博客中我们主要完成了闪屏页面(SplashActivity)以及注册页面(RegistActivity)的逻辑实现,接下来就需要完善登录页面(LoginActivity)的实现
- 由于项目中采用了MVP的设计模式,所以同样需要在view包下新建LoginView接口,代码如下:
package com.dianbin.fastec.im95.presenter.impl;
import com.dianbin.fastec.im95.callback.MyEmCallBack;
import com.dianbin.fastec.im95.presenter.LoginPresenter;
import com.dianbin.fastec.im95.view.LoginView;
import com.hyphenate.chat.EMClient;
public class LoginPresenterImpl implements LoginPresenter {
private LoginView loginView;
public LoginPresenterImpl(LoginView loginView) {
this.loginView = loginView;
}
@Override
public void login(String username, String pwd) {
EMClient.getInstance().login(username, pwd, new MyEmCallBack() {
// 环信的EmCallBack回调是在子线程执行的
@Override
public void success() {
// 使用自己编写的MyEmCallBack这个回调,确保success方法执行在主线程中
loginView.onGetLoginState(username,true,null);
}
@Override
public void error(int i, String s) {
loginView.onGetLoginState(username,false,s);
}
});
}
}
- 在presenter包下,新建LoginPresenter接口,代码如下:
package com.dianbin.fastec.im95.presenter;
public interface LoginPresenter {
void login(String username,String pwd);
}
-
为了使用环信的登录方法,这里需要创建一个用于环信回调的类,在项目下新建callback包,并且创建一个MyEmCallBack抽象类,代码如下:
package com.dianbin.fastec.im95.callback; import com.dianbin.fastec.im95.utils.ThreadUtils; import com.hyphenate.EMCallBack; public abstract class MyEmCallBack implements EMCallBack { /** * 在主线程中处理环信操作成功的回调方法 */ public abstract void success(); /** * 在主线程中处理环信操作失败的回调方法 */ public abstract void error(int i ,String s); @Override public void onSuccess() { // 成功 ThreadUtils.runOnMainThread(() -> success()); } @Override public void onError(final int i, final String s) { // 失败 ThreadUtils.runOnMainThread(() -> error(i,s)); } @Override public void onProgress(int i, String s) { // 加载过程 } }
-
在presenter/impl包下,新建LoginPresenterImpl实现类,代码如下:
package com.dianbin.fastec.im95.presenter.impl;
import com.dianbin.fastec.im95.callback.MyEmCallBack;
import com.dianbin.fastec.im95.presenter.LoginPresenter;
import com.dianbin.fastec.im95.view.LoginView;
import com.hyphenate.chat.EMClient;
public class LoginPresenterImpl implements LoginPresenter {
private LoginView loginView;
public LoginPresenterImpl(LoginView loginView) {
this.loginView = loginView;
}
@Override
public void login(String username, String pwd) {
EMClient.getInstance().login(username, pwd, new MyEmCallBack() {
// 环信的EmCallBack回调是在子线程执行的
@Override
public void success() {
// 使用自己编写的MyEmCallBack这个回调,确保success方法执行在主线程中
loginView.onGetLoginState(username,true,null);
}
@Override
public void error(int i, String s) {
loginView.onGetLoginState(username,false,s);
}
});
}
}
- 完善LoginActivity,实现LoginView,并实现相应业务逻辑,注意这里要动态申请权限,代码如下:
package com.dianbin.fastec.im95.view;
import android.Manifest;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.PermissionChecker;
import com.dianbin.fastec.im95.MainActivity;
import com.dianbin.fastec.im95.R;
import com.dianbin.fastec.im95.presenter.LoginPresenter;
import com.dianbin.fastec.im95.presenter.impl.LoginPresenterImpl;
import com.google.android.material.textfield.TextInputLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class LoginActivity extends BaseActivity implements LoginView{
@BindView(R.id.iv_avatar)
ImageView ivAvatar;
@BindView(R.id.et_username)
EditText etUsername;
@BindView(R.id.til_username)
TextInputLayout tilUsername;
@BindView(R.id.et_pwd)
EditText etPwd;
@BindView(R.id.til_pwd)
TextInputLayout tilPwd;
@BindView(R.id.btn_login)
Button btnLogin;
@BindView(R.id.tv_newuser)
TextView tvNewuser;
private LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
//获取缓存的用户名密码
if(getPwd()!=null&&getPwd().trim().length()>0)
etPwd.setText(getPwd());
if(getUsername()!=null&&getUsername().trim().length()>0)
etUsername.setText(getUsername());
presenter = new LoginPresenterImpl(this);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
// showToast("onNewIntent");
//获取缓存的用户名密码
if(getPwd()!=null&&getPwd().trim().length()>0)
etPwd.setText(getPwd());
if(getUsername()!=null&&getUsername().trim().length()>0)
etUsername.setText(getUsername());
}
@OnClick({R.id.btn_login, R.id.tv_newuser})
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_login:
login();
break;
case R.id.tv_newuser:
startActivity(RegistActivity.class,false);
break;
}
}
private void login() {
showProgressDialog("正在登录... ...");
String username = etUsername.getText().toString().trim();
String pwd = etPwd.getText().toString().trim();
//判断sdk版本 如果是6.0以后
if(Build.VERSION.SDK_INT<23){
presenter.login(username,pwd);
}else{
// 如果用到隐私的权限 6.0之后 需要手动检测是否有权限
if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)==PermissionChecker.PERMISSION_GRANTED){
//如果有权限的话就执行登录
presenter.login(username,pwd);
}else{
//如果没有权限 就动态申请
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0);
}
}
}
@Override
public void onGetLoginState(String username, boolean isLogin, String errorMsg) {
cancelProgressDialog();
if(isLogin)
//如果登录成功跳到主界面
startActivity(MainActivity.class,true);
else{
//如果登录失败弹吐司
showToast(errorMsg);
}
}
//动态申请权限之后 会走回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//requestCode 区分不同的权限申请操作 注意requestCode不能<0
//permissions 动态申请的权限数组
//grantResults int数组 用来封装每一个权限授权的结果 PermissionChecker.PERMISSION_GRANTED 授权了
// PermissionChecker.PERMISSION_DENIED; 拒绝了
if(grantResults[0]== PermissionChecker.PERMISSION_GRANTED){
//说明给权限了
login();
}else{
//说明没给权限
showToast("没授权sd卡权限 数据库保存会失败 影响使用");
}
}
}
这里再复习一下MVP模式的设计步骤:
- 创建presenter层的接口:presenter层的接口中声明的方法,只跟业务逻辑相关,不涉及页面的操作,例如联网获取数据
- 创建view层的接口:view层只去处理界面相关的逻辑,包括ui的加载跟用户的实现
- 创建presenter层的实现:presenter层具体的实现类,需要持有view层的引用
- 创建view层的实现:view层的实现,需要持有presenter层的引用,直接new出来就可以了,在创建presenter的引用时把当前对象传递给p
一个巨坑:如果你的Android版本很高(Android P),在进行Http网络连接时可能会出现网络连接失败的问题,即:
W/System.err: java.io.IOException: Cleartext HTTP traffic to **** not permitted
这个时候可以参考https://blog.csdn.net/gengkui9897/article/details/82863966博客来解决
2.修改Activity的启动模式
为了防止在注册中按下回退键会出现两个登录Activity,这里可以修改一下Activity的启动模式,修改manifest.xml,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dianbin.fastec.im95">
<!-- Required -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<activity android:name=".view.RegistActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<activity android:name=".view.LoginActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<activity android:name=".view.SplashActivity"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity><!-- 设置环信应用的AppKey -->
<meta-data
android:name="EASEMOB_APPKEY"
android:value="1116200331098371#im95" /> <!-- 声明SDK所需的service SDK核心功能 -->
<service
android:name="com.hyphenate.chat.EMChatService"
android:exported="true" />
<service
android:name="com.hyphenate.chat.EMJobService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" /> <!-- 声明SDK所需的receiver -->
<receiver android:name="com.hyphenate.chat.EMMonitorReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
<!-- 可选filter -->
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
<activity android:name=".MainActivity"
android:launchMode="singleTask">
</activity>
</application>
</manifest>
与此同时,由于将启动模式修改成了SingleTask,在注册页面注册好的账号和密码可能不会同步(通过SharedPreferences)到登录页面中。之前重写了onNewIntent()方法来继续获取前一个页面的数据,这里就不再赘述这个问题
3.主页面的实现
前面实现了登录页面和注册页面,接下来我们开始实现主页面的编写
- 修改activity_main.xml,添加相应组件,这里使用了两个自定义控件(Toolbar和BottomNavigationBar)代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
>
<android.support.v7.widget.Toolbar
android:id="@+id/tb_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/btn_normal"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
>
<TextView
android:layout_gravity="center"
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="消息"
android:textSize="20sp"
android:textColor="@android:color/white"/>
</android.support.v7.widget.Toolbar>
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</FrameLayout>
<com.ashokvarma.bottomnavigation.BottomNavigationBar
android:id="@+id/bottom_navigation_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
- 修改MainActivity,添加相应业务逻辑,主要需要注意菜单和底部导航栏的使用,代码如下
package com.dianbin.fastec.im95;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TextView;
import com.ashokvarma.bottomnavigation.BadgeItem;
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
import com.dianbin.fastec.im95.view.BaseActivity;
import com.hyphenate.chat.EMClient;
import com.hyphenate.chat.EMMessage;
import java.util.List;
public class MainActivity extends BaseActivity {
private String[] titles = {"消息","联系人","动态"};
private Toolbar toolbar;
private TextView tv_title;
private BottomNavigationBar bottomNavigationBar;
private BadgeItem badgeItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
toolbar = findViewById(R.id.tb_toolbar);
tv_title = (TextView) findViewById(R.id.tv_title);
bottomNavigationBar = (BottomNavigationBar) findViewById(R.id.bottom_navigation_bar);
initToolbar();
initBottomNavigationBar();
initFirstFragment();
}
/**
* 初始化第一个fragment
*/
private void initFirstFragment() {
List<Fragment> fragments = getSupportFragmentManager().getFragments();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if(fragments!=null && fragments.size()>0){
//说明有之前缓存的fragment 处理fragment重影的问题
for(int i = 0;i<fragments.size();i++){
transaction.remove(fragments.get(i));
}
transaction.commit();
}
BaseFragment fragment = FragmentFactory.getFragment(0);
transaction = getSupportFragmentManager().beginTransaction();
transaction.add(R.id.fl_container,fragment,"0").commit();
}
private void initBottomNavigationBar() {
BottomNavigationItem conversationItem = new BottomNavigationItem(R.mipmap.conversation_selected_2, "消息");
//BadgeItem 底部导航图标 右上角的圆圈文字
badgeItem = new BadgeItem();
updateBadgeItem();
//右上角的圆圈文字 显示出来
conversationItem.setBadgeItem(badgeItem);
// item.setActiveColor(getResources().getColor(R.color.btn_normal));
// item.setInActiveColor(getResources().getColor(R.color.inactive));
bottomNavigationBar.addItem(conversationItem);
BottomNavigationItem item = new BottomNavigationItem(R.mipmap.contact_selected_2, "联系人");
// item.setActiveColor(getResources().getColor(R.color.btn_normal));
// item.setInActiveColor(getResources().getColor(R.color.inactive));
bottomNavigationBar.addItem(item);
item = new BottomNavigationItem(R.mipmap.plugin_selected_2, "动态");
// item.setActiveColor(getResources().getColor(R.color.btn_normal));
// item.setInActiveColor(getResources().getColor(R.color.inactive));
bottomNavigationBar.addItem(item);
//设置选中时的颜色
bottomNavigationBar.setActiveColor(R.color.btn_normal);
// 设置没被选中时的颜色
bottomNavigationBar.setInActiveColor(R.color.inactive);
//可以修改第一次加载时被选中的tab的 位置
// bottomNavigationBar.setFirstSelectedPosition(1);
//初始化
bottomNavigationBar.initialise();
bottomNavigationBar.setTabSelectedListener(new BottomNavigationBar.OnTabSelectedListener(){
@Override
public void onTabSelected(int position) {
//被选中了
Log.e("Mainactivity","onTabSelected"+position);
//通过postion拿到fragment
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
BaseFragment fragment = FragmentFactory.getFragment(position);
if(fragment.isAdded()){
//如果这个fragment 已经被add进来 显示一下
transaction.show(fragment).commit();
}else{
// 如果这个fragment 没有被add进来 那么把fragmentadd到主界面中
transaction.add(R.id.fl_container,fragment,position+"").commit();
}
tv_title.setText(titles[position]);
}
@Override
public void onTabUnselected(int position) {
Log.e("Mainactivity","onTabUnselected"+position);
//没被选中
//通过position找到fragment隐藏起来
getSupportFragmentManager().beginTransaction().hide(FragmentFactory.getFragment(position)).commit();
}
@Override
public void onTabReselected(int position) {
}
});
}
public void updateBadgeItem() {
//获取所有的未读条目数量
int unreadMessageCount = EMClient.getInstance().chatManager().getUnreadMessageCount();
if(unreadMessageCount==0){
badgeItem.hide(true);
}else if(unreadMessageCount>99){
badgeItem.show(true);
badgeItem.setText(String.valueOf(99));
}else{
badgeItem.show(true);
badgeItem.setText(String.valueOf(unreadMessageCount));
}
}
private void initToolbar() {
//如果不设置title 默认会把appname显示在toolbar上
toolbar.setTitle("");
setSupportActionBar(toolbar);
//如果不设置 左边的返回图标就消失了
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
//创建一个菜单
//找到菜单布局填充器
MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.menu_main,menu);
MenuBuilder builder = (MenuBuilder) menu;
//设置图标可见
builder.setOptionalIconsVisible(true);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId){
case R.id.menu_item_addfriend:
startActivity(AddFriendActivity.class,false);
break;
case R.id.menu_item_scan:
showToast("扫一扫");
break;
case R.id.menu_item_about:
showToast("关于");
break;
case android.R.id.home:
finish();
break;
}
return super.onOptionsItemSelected(item);
}
@Subscribe(threadMode = ThreadMode.MAIN)
void onGetMessageEvent(List<EMMessage> messages){
//更新未读消息的图标
updateBadgeItem();
}
@Override
protected void onResume() {
super.onResume();
updateBadgeItem();
}
}
4.FragmentFactory的实现
- 根据主页面,需要依次有消息、联系人、动态等三个页面,这里使用碎片来进行实现,这里在utils包下使用了一个简单工厂类的FragmentFactory对Fragment进行使用,代码如下:
package com.dianbin.fastec.im95.utils;
import com.dianbin.fastec.im95.view.BaseFragment;
import com.dianbin.fastec.im95.view.ContactFragment;
import com.dianbin.fastec.im95.view.ConversationFragment;
import com.dianbin.fastec.im95.view.PlugInFragment;
/**
* Created by fullcircle on 2017/1/3.
*/
public class FragmentFactory {
private static ContactFragment contactFragment = null;
private static ConversationFragment conversationFragment = null;
private static PlugInFragment plugInFragment = null;
/**
* 根据底部导航的索引 获取对应的fragment的实例
* @param position
* @return
*/
public static BaseFragment getFragment(int position){
BaseFragment fragment = null;
switch (position){
case 0:
if(conversationFragment == null){
conversationFragment = new ConversationFragment();
}
fragment = conversationFragment;
break;
case 1:
if(contactFragment == null){
contactFragment = new ContactFragment();
}
fragment = contactFragment;
break;
case 2:
if(plugInFragment == null){
plugInFragment = new PlugInFragment();
}
fragment = plugInFragment;
break;
}
return fragment;
}
}
5."联系人"模块中自定义View的实现
在view包下新建一个ConversationFragment,作为联系人的Fragment,这里用到了一个自定义VIew(ContactLayout),实现效果是一个侧滑栏,在项目下新建widget包,作为存放自定义View的地方,然后新建ContactLayout,代码如下:
package com.itheima.imclient95.widget;
import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.itheima.imclient95.R;
/**
* Created by fullcircle on 2017/1/3.
*/
public class ContactLayout extends RelativeLayout {
private RecyclerView recyclerView;
private TextView tv_float;
private Slidebar slidebar;
private SwipeRefreshLayout swipeRefreshLayout;
public ContactLayout(Context context) {
this(context,null);
}
public ContactLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
View.inflate(getContext(), R.layout.contact_layout, this);
recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
tv_float = (TextView) findViewById(R.id.tv_float);
slidebar = (Slidebar) findViewById(R.id.slidebar);
swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefresh);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public ContactLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ContactLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context,attrs);
}
public void setAdapter(RecyclerView.Adapter adapter){
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
}
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener listener){
swipeRefreshLayout.setOnRefreshListener(listener);
}
public void setRefreshing(boolean isRefreshing){
swipeRefreshLayout.setRefreshing(isRefreshing);
}
}
- 同样地,在res目录下新建contact_layout.xml,作为自定义布局的布局文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/tv_float"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="M"
android:layout_centerInParent="true"
android:gravity="center"
android:textSize="25sp"
android:textColor="@android:color/white"
android:padding="30dp"
android:background="@drawable/ease_show_head_toast_bg"
android:visibility="gone"/>
<com.itheima.imclient95.widget.Slidebar
android:id="@+id/slidebar"
android:layout_alignParentRight="true"
android:layout_width="30dp"
android:layout_height="match_parent" />
</RelativeLayout>
- 在ContactLayout中,我们再添加一个自定义控件:Slidebar,作为侧滑栏控件,同样在widget中新建Slidebar,代码如下:
package com.itheima.imclient95.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.TextView;
import com.itheima.imclient95.R;
import com.itheima.imclient95.adapter.ContactAdapter;
import com.itheima.imclient95.utils.StringUtils;
import java.util.List;
/**
* Created by fullcircle on 2017/1/3.
*/
public class Slidebar extends View {
private String[] sections = {"搜","A","B","C","D","E","F","G","H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U","V","W","X","Y","Z"};
private Paint paint;
private int x;
private int viewHeight;
private RecyclerView recyclerView;
private TextView tv_float;
private ContactAdapter adapter;
public Slidebar(Context context) {
this(context,null);
}
public Slidebar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint = new Paint();
paint.setAntiAlias(true);
paint.setTextAlign(Paint.Align.CENTER);
paint.setColor(Color.GRAY);
paint.setTextSize(getResources().getDimension(R.dimen.slide_text_size));
}
public Slidebar(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public Slidebar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
x = w/2;
viewHeight = h;
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(tv_float == null){
//找到父容器
ViewGroup parent = (ViewGroup) getParent();
//在父容器中找到要操作的控件
recyclerView = (RecyclerView) parent.findViewById(R.id.recyclerview);
tv_float = (TextView) parent.findViewById(R.id.tv_float);
adapter = (ContactAdapter) recyclerView.getAdapter();
}
String startChar;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
setBackgroundResource(R.color.background_gray);
tv_float.setVisibility(View.VISIBLE);
startChar = sections[getIndex(event.getY())];
tv_float.setText(startChar);
scrollRecyclerView(startChar);
// recyclerView.smoothScrollToPosition();
break;
case MotionEvent.ACTION_MOVE:
startChar = sections[getIndex(event.getY())];
tv_float.setText(startChar);
scrollRecyclerView(startChar);
break;
case MotionEvent.ACTION_UP:
setBackgroundColor(Color.TRANSPARENT);
tv_float.setVisibility(View.GONE);
break;
}
return true;
}
private void scrollRecyclerView(String startChar) {
List<String> contacts = adapter.getContacts();
if(contacts!=null && contacts.size()>0){
for(int i = 0;i<contacts.size();i++){
if(StringUtils.getFirstChar(contacts.get(i)).equals(startChar)){
recyclerView.smoothScrollToPosition(i);
break;
}
}
}
}
int getIndex(float y){
int sectionHeight = viewHeight/sections.length;
int result = (int)y/sectionHeight;
return result<0?0:result>sections.length-1?sections.length-1:result;
}
@Override
protected void onDraw(Canvas canvas) {
for(int i = 0;i<sections.length;i++){
//第一个参数 要绘制的文字 第二个参数 第三个参数 绘制文字左下角的坐标
canvas.drawText(sections[i],x,viewHeight/sections.length *(i+1),paint);
}
}
}
6."联系人"模块中功能的实现
- 由于要展示联系人列表,所以需要提供一些相应的操作(初始化联系人,更新联系人,删除联系人),在presenter层写一个ContactPresenter接口,代码如下:
package com.itheima.imclient95.presenter;
/**
* Created by fullcircle on 2017/1/3.
*/
public interface ContactPresenter {
void initContact();
void updateContact();
void deleteContact(String username);
}
- 在presenter/impl包下写对应的ContactPresenterImpl,代码如下:
package com.itheima.imclient95.presenter.impl;
import com.hyphenate.chat.EMClient;
import com.hyphenate.exceptions.HyphenateException;
import com.itheima.imclient95.db.ContactOpenHelper;
import com.itheima.imclient95.db.DBUtils;
import com.itheima.imclient95.presenter.ContactPresenter;
import com.itheima.imclient95.utils.ThreadUtils;
import com.itheima.imclient95.view.ContactView;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by fullcircle on 2017/1/3.
*/
public class ContactPresenterImpl implements ContactPresenter {
private ContactView contactView;
public ContactPresenterImpl(ContactView contactView) {
this.contactView = contactView;
}
@Override
public void initContact() {
//①先到数据库中获取
List<String> contacts = DBUtils.initContact(EMClient.getInstance().getCurrentUser());
//ContactOpenHelper openHelper = new ContactOpenHelper()
contactView.onInitContact(contacts);
//②联网更新数据库
getContactsFromServer();
}
@Override
public void updateContact() {
getContactsFromServer();
}
@Override
public void deleteContact(final String username) {
ThreadUtils.runOnNonUIThread(new Runnable() {
@Override
public void run() {
try {
//deleteContact 这个方法没有开线程
EMClient.getInstance().contactManager().deleteContact(username);
//如果没有走异常 说明删除成功
ThreadUtils.runOnMainThread(new Runnable() {
@Override
public void run() {
//在主线程中 进行View层的回调
contactView.onDeleteContact(true,null);
}
});
} catch (final HyphenateException e) {
e.printStackTrace();
//走异常说明删除失败了
ThreadUtils.runOnMainThread(new Runnable() {
@Override
public void run() {
contactView.onDeleteContact(false,e.getMessage());
}
});
}
}
});
}
public void getContactsFromServer() {
ThreadUtils.runOnNonUIThread(new Runnable() {
@Override
public void run() {
try {
final List<String> contactList = EMClient.getInstance().contactManager().getAllContactsFromServer();
Collections.sort(contactList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
//获取数据后 保存到数据库
DBUtils.updateContactFromEMServer(EMClient.getInstance().getCurrentUser(),contactList);
ThreadUtils.runOnMainThread(new Runnable() {
@Override
public void run() {
contactView.onUpdateContact(contactList,true,null);
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnMainThread(new Runnable() {
@Override
public void run() {
contactView.onUpdateContact(null,false,null);
}
});
}
}
});
}
}
- 相对应的,在View层简历ContactView接口,代码如下:
package com.itheima.imclient95.view;
import java.util.List;
/**
* Created by fullcircle on 2017/1/3.
*/
public interface ContactView {
void onInitContact(List<String> contact);
void onUpdateContact(List<String> contact,boolean isUpdateSuccess,String errorMsg);
void onDeleteContact(boolean isDeleteSuccess,String errorMsg);
}
- 与此同时,在view包下创建ContentFragment,代码如下:
package com.itheima.imclient95.view;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.itheima.imclient95.R;
import com.itheima.imclient95.adapter.ContactAdapter;
import com.itheima.imclient95.event.ContactChangeEvent;
import com.itheima.imclient95.presenter.ContactPresenter;
import com.itheima.imclient95.presenter.impl.ContactPresenterImpl;
import com.itheima.imclient95.utils.ToastUtils;
import com.itheima.imclient95.widget.ContactLayout;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
/**
* Created by fullcircle on 2017/1/3.
*/
public class ContactFragment extends BaseFragment implements ContactView {
private ContactLayout contactLayout;
private ContactPresenter presenter;
private ContactAdapter adapter;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_contact,null);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
contactLayout = (ContactLayout) view.findViewById(R.id.contactlayout);
presenter = new ContactPresenterImpl(this);
presenter.initContact();
super.onViewCreated(view, savedInstanceState);
}
@Override
public void onInitContact(List<String> contact) {
//获取了初始化数据 需要通过recyclerview进行展示
//setAdapter
adapter = new ContactAdapter(contact);
contactLayout.setAdapter(adapter);
contactLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
presenter.updateContact();
}
});
//给adapter设置单击和长按事件的监听
adapter.setOnItemClickListener(new ContactAdapter.OnItemClickListener() {
@Override
public void onclick(View v,String username) {
//如果是单击 跳转到聊天界面
Intent intent = new Intent(getContext(),ChatActivity.class);
intent.putExtra("contact",username);
startActivity(intent);
}
@Override
public boolean onLongClick(View v, final String username) {
//如果长按弹出 Snackbar 通过Snackbar处理联系人的删除
Snackbar.make(contactLayout,"真的要删除"+username+"吗?",Snackbar.LENGTH_LONG)
.setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击确定删除联系人
presenter.deleteContact(username);
}
}).show();
return true;
}
});
}
@Override
public void onUpdateContact(List<String> contact, boolean isUpdateSuccess, String errorMsg) {
contactLayout.setRefreshing(false);
if(isUpdateSuccess){
adapter.setContacts(contact);
adapter.notifyDataSetChanged();
}else{
ToastUtils.showToast(getActivity(),"新联系人失败!");
}
}
@Override
public void onDeleteContact(boolean isDeleteSuccess, String errorMsg) {
if(isDeleteSuccess){
ToastUtils.showToast(getContext(),"删除成功");
}else{
ToastUtils.showToast(getContext(),"删除失败:"+errorMsg);
}
}
/**
* POSTING 发布消息的线程和处理消息的线程 是同一个线程
* Main 在主线程中处理消息
* ASYNC 处理消息的线程 和发布消息的线程不同一个线程中 如果发布消息在子线程中 会再开一个子线程来处理消息
* Backgound 在子线程中处理消息 如果发布消息是在子线程发布的就在这个线程中处理
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onGetContactChangeEvent(ContactChangeEvent event){
//收到这个消息 说明有联系人的变化 需要联网更新数据
presenter.updateContact();
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
}