Android开发实战“基于环信的通信工具”——02.联系人模块的实现

1.登录页面的实现

在上一篇博客中我们主要完成了闪屏页面(SplashActivity)以及注册页面(RegistActivity)的逻辑实现,接下来就需要完善登录页面(LoginActivity)的实现

  1. 由于项目中采用了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);

            }
        });
    }
}
  1. 在presenter包下,新建LoginPresenter接口,代码如下:
package com.dianbin.fastec.im95.presenter;

public interface LoginPresenter {

    void login(String username,String pwd);
    
}
  1. 为了使用环信的登录方法,这里需要创建一个用于环信回调的类,在项目下新建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) {
            // 加载过程
        }
    }
    
  2. 在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);

            }
        });
    }
}
  1. 完善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模式的设计步骤:

  1. 创建presenter层的接口:presenter层的接口中声明的方法,只跟业务逻辑相关,不涉及页面的操作,例如联网获取数据
  2. 创建view层的接口:view层只去处理界面相关的逻辑,包括ui的加载跟用户的实现
  3. 创建presenter层的实现:presenter层具体的实现类,需要持有view层的引用
  4. 创建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.主页面的实现

前面实现了登录页面和注册页面,接下来我们开始实现主页面的编写

  1. 修改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>
  1. 修改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的实现

  1. 根据主页面,需要依次有消息、联系人、动态等三个页面,这里使用碎片来进行实现,这里在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);
    }
}
  1. 同样地,在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>
  1. 在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."联系人"模块中功能的实现

  1. 由于要展示联系人列表,所以需要提供一些相应的操作(初始化联系人,更新联系人,删除联系人),在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);
}
  1. 在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);
                        }
                    });
                }
            }
        });

    }
}
  1. 相对应的,在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);
}
  1. 与此同时,在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);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赈川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值