即時(shí)通訊—教你一步一步接入融云IM

前言

就目前市面的項(xiàng)目而已旨枯,即時(shí)通訊功能算是很普遍的需求深受boos喜愛(ài),boos最喜歡讓你擼出一個(gè)微信聊天來(lái)。當(dāng)然大多說(shuō)公司不會(huì)自己去搭建一個(gè)即時(shí)通訊技術(shù)棧癞志。更多的是集成第三方功能啥么,站在巨人的肩膀上快速開(kāi)發(fā)出boos要求的社交功能登舞。讀完本文你可以在3個(gè)小時(shí)內(nèi)作出一個(gè)心儀的IM功能(這個(gè)當(dāng)然是對(duì)于有一定開(kāi)發(fā)能力的讀者而言,初級(jí)碼仔先搬個(gè)小板凳慢慢學(xué)哦)...
當(dāng)然,接入融云的即時(shí)通訊功能最好是先看一遍官方文檔 https://www.rongcloud.cn/docs/ (這么做可以整體了解集成過(guò)程悬荣,做到心里有數(shù))菠秒。融云本身有為我們開(kāi)發(fā)者設(shè)計(jì)了界面組件庫(kù),可以極大地減少我們的開(kāi)發(fā)時(shí)間氯迂,在這里我比較建議創(chuàng)業(yè)型公司的同學(xué)最好先使用他們提供的頁(yè)面組件践叠,后面研究的比較透徹后可以自己去自定義UI.本文主要是對(duì)于自己項(xiàng)目中使用的功能做一個(gè)總結(jié),筆者只是小廠(chǎng)碼農(nóng)嚼蚀,也沒(méi)有去自己寫(xiě)UI禁灼,用的是改造官方的,所以大神們可以忽略本文轿曙。
我先屢一下大綱

圖片.png

1.注冊(cè)賬號(hào)弄捕,配置應(yīng)用信息

首先,在融云官網(wǎng)注冊(cè)https://www.rongcloud.cn/ 然后創(chuàng)建應(yīng)用导帝,獲取appkey(這個(gè)后面的sdk接入會(huì)用到)察藐。這些細(xì)節(jié)我就跳過(guò)了。

2.集成融云SDK

1.首先舟扎,在融云下載SDK(下載地址:https://www.rongcloud.cn/downloads)我這里只下載了IMLib分飞,IMKit前者是通訊能力庫(kù)是必須要的,后者是界面組件庫(kù)睹限,我們可以在它的基礎(chǔ)上快速開(kāi)發(fā)譬猫。如果你想自己寫(xiě)UI可以不要它。如果還想接入音視頻和紅包等功能還可以引入CallLib羡疗,RedPacket等庫(kù)染服,具體怎么選擇就看你的項(xiàng)目需求了。我這里只使用了IMLib和IMKit下文也是基于這兩者的叨恨。

2.根據(jù)官方指導(dǎo)采用導(dǎo)入Module的方式柳刮,AS中File -> New -> Import Module然后導(dǎo)入IMLib,IMKit。然后在settings.gradle文件中

include ':IMKit',':IMLib'

IMKit里面已經(jīng)引入IMLib所以我們只需要把IMKit進(jìn)行引入秉颗,在你使用的主項(xiàng)目的build.gradle中

implementation project(':IMKit')

3.在IMLib Module 的 AndroidManifest.xml 文件痢毒,把 meta-data RONG_CLOUD_APP_KEY 的值修改為您自己的 AppKey

<meta-data
    android:name="RONG_CLOUD_APP_KEY"
    android:value="您的應(yīng)用 AppKey" />

在你的應(yīng)用的主項(xiàng)目 App Module 的 AndroidManifest.xml 文件中,添加 FileProvider 相關(guān)配置蚕甥,修改 android:authorities 為您的應(yīng)用的 “ApplicationId”.FileProvider哪替。

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/rc_file_path" />
        </provider>

在Application里面初始化由于融云SDK有使用多進(jìn)程所以我建議這樣寫(xiě)

if (getApplicationInfo().packageName.equals(getCurProcessName(getApplicationContext()))) {
            RongIM.init(this);//init Im
            RongIM.setConnectionStatusListener(new RongIMClient.ConnectionStatusListener() {
                @Override
                public void onChanged(ConnectionStatus status) {
                    LogUtil.i("MyApplication----->Rongyun onChanged: ---" + status);
                    if (status == ConnectionStatus.TOKEN_INCORRECT) {
                        if (!TextUtils.isEmpty(imToken)) {
                            RongIM.connect(imToken, IMListener.getInstance().getConnectCallback());
                        } else {
                            LogUtil.i("token is empty, can not reconnect");
                        }
                    }
                }
            });
        }

3.獲取token,連接服務(wù)器

token需要后端去融云獲取菇怀,我們測(cè)試可以直接拷貝過(guò)去凭舶,實(shí)際使用中不建議這么做,我這里引入融云的原話(huà):
為了方便您在集成和測(cè)試過(guò)程中使用爱沟,我們還提供了 API 調(diào)試工具帅霜,在您不能部署服務(wù)器程序時(shí),可以直接通過(guò)傳入 userId 和 name 來(lái)獲得 Token呼伸。請(qǐng)?jiān)L問(wèn)融云開(kāi)發(fā)者平臺(tái)身冀,打開(kāi)您想測(cè)試的應(yīng)用,在左側(cè)菜單中選擇“API 調(diào)試”即可蜂大。
有了token之后可以連接服務(wù)器

private void connect(String token) {

    if (getApplicationInfo().packageName.equals(App.getCurProcessName(getApplicationContext()))) {

        RongIM.connect(token, new RongIMClient.ConnectCallback() {

            /**
             * Token 錯(cuò)誤闽铐。可以從下面兩點(diǎn)檢查 1.  Token 是否過(guò)期奶浦,如果過(guò)期您需要向 App Server 重新請(qǐng)求一個(gè)新的 Token
             *                  2.  token 對(duì)應(yīng)的 appKey 和工程里設(shè)置的 appKey 是否一致
             */
            @Override
            public void onTokenIncorrect() {

            }

            /**
             * 連接融云成功
             * @param userid 當(dāng)前 token 對(duì)應(yīng)的用戶(hù) id
             */
            @Override
            public void onSuccess(String userid) {
                Log.d("LoginActivity", "--onSuccess" + userid);
                startActivity(new Intent(LoginActivity.this, MainActivity.class));
                finish();
            }

            /**
             * 連接融云失敗
             * @param errorCode 錯(cuò)誤碼兄墅,可到官網(wǎng) 查看錯(cuò)誤碼對(duì)應(yīng)的注釋
             */
            @Override
            public void onError(RongIMClient.ErrorCode errorCode) {

            }
        });
    }
}

處理回調(diào),廣播接收器

我這里是把融云的回調(diào)統(tǒng)一放在類(lèi)里面進(jìn)行處理

public class IMListener implements RongIM.ConversationBehaviorListener, RongIM.ConversationListBehaviorListener, RongIMClient.ConnectionStatusListener, RongIM.UserInfoProvider, RongIM.GroupInfoProvider, RongIMClient.OnReceiveMessageListener, RongIM.IGroupMembersProvider {

    public static final String TAG = "IMListener";

    private Context mContext;
    private static IMListener mInstance;


    public static IMListener getInstance() {
        return mInstance;
    }

    private static ArrayList<Activity> mActivities;

    public IMListener(Context mContext) {
        this.mContext = mContext;
        initListener();
        mActivities = new ArrayList<>();
    }

    public static void init(Context context) {
        if (mInstance == null) {
            synchronized (IMListener.class) {
                if (mInstance == null) {
                    mInstance = new IMListener(context);
                }
            }
        }
    }

    private void initListener() {
        RongIM.setConversationBehaviorListener(this);//設(shè)置會(huì)話(huà)界面操作的監(jiān)聽(tīng)器澳叉。
        RongIM.setConversationListBehaviorListener(this);
        RongIM.setConnectionStatusListener(this);
        RongIM.setUserInfoProvider(this, true);
        RongIM.setGroupInfoProvider(this, true);
//        RongIM.setLocationProvider(this);//設(shè)置地理位置提供者,不用位置的同學(xué)可以注掉此行代碼
        RongIM.setOnReceiveMessageListener(this);//重點(diǎn)收到消息的回調(diào)
        setInputProvider();
        setReadReceiptConversationType();
        RongIM.getInstance().enableNewComingMessageIcon(true);
        RongIM.getInstance().enableUnreadMessageIcon(true);
        RongIM.getInstance().setGroupMembersProvider(this);
        setMessageItemLongClickAction(mContext);
    }

    private static void setMessageItemLongClickAction(Context context) {
        MessageItemLongClickAction action = new MessageItemLongClickAction.Builder()
                .titleResId(R.string.rc_dialog_item_message_delete)
                .actionListener(new MessageItemLongClickAction.MessageItemLongClickListener() {
                    @Override
                    public boolean onMessageItemLongClick(Context context, UIMessage message) {
                        Message[] messages = new Message[1];
                        messages[0] = message.getMessage();
                        RongIM.getInstance().deleteMessages(new int[]{message.getMessageId()}, null);
                        return false;
                    }
                }).build();
        RongMessageItemLongClickActionManager.getInstance().addMessageItemLongClickAction(action, 1);
    }

    private void setReadReceiptConversationType() {
        Conversation.ConversationType[] types = new Conversation.ConversationType[]{
                Conversation.ConversationType.PRIVATE,
                Conversation.ConversationType.GROUP,
                Conversation.ConversationType.DISCUSSION
        };
        RongIM.getInstance().setReadReceiptConversationTypeList(types);
    }

    /**
     * 設(shè)置輸入面板
     */
    private void setInputProvider() {

        // 沒(méi)有特殊需求就忽略
//        List<IExtensionModule> moduleList = RongExtensionManager.getInstance().getExtensionModules();
//        IExtensionModule defaultModule = null;
//        if (moduleList != null) {
//            for (IExtensionModule module : moduleList) {
//                if (module instanceof DefaultExtensionModule) {
//                    defaultModule = module;
//                    break;
//                }
//            }
//            if (defaultModule != null) {
//                RongExtensionManager.getInstance().unregisterExtensionModule(defaultModule);
//                RongExtensionManager.getInstance().registerExtensionModule(new SealExtensionModule(mContext));
//            }
//        }
    }

    public RongIMClient.ConnectCallback getConnectCallback() {
        RongIMClient.ConnectCallback connectCallback = new RongIMClient.ConnectCallback() {
            @Override
            public void onTokenIncorrect() {
                //token 錯(cuò)誤
                LogUtil.i("ImService----->onTokenIncorrect: 融云token錯(cuò)誤");
                ImService.start(mContext, AppConstant.UPDATE_TOKEN);
            }

            @Override
            public void onSuccess(String userid) {
                LogUtil.i("IMListener----->onSuccess: " + userid);
            }

            @Override
            public void onError(final RongIMClient.ErrorCode e) {
                LogUtil.i("IMListener----->onError: " + e);
            }
        };
        return connectCallback;
    }


    /******************   融云回調(diào)   ******************/
    /*---------------   消息交互   --------------*/
    @Override
    public boolean onUserPortraitClick(Context context, Conversation.ConversationType conversationType, UserInfo userInfo) {
        //點(diǎn)擊頭像 這里群組默認(rèn)都是專(zhuān)家
        if (conversationType == Conversation.ConversationType.CUSTOMER_SERVICE || conversationType == Conversation.ConversationType.PUBLIC_SERVICE || conversationType == Conversation.ConversationType.APP_PUBLIC_SERVICE) {
            return false;
        }
        //開(kāi)發(fā)測(cè)試時(shí),發(fā)送系統(tǒng)消息的userInfo只有id不為空
        if (userInfo != null && userInfo.getName() != null && userInfo.getPortraitUri() != null) {
            String mTargetId = userInfo.getUserId();
            if (mTargetId.length() >= 6) {
                try {
                    String id = mTargetId.substring(5, mTargetId.length());
                    AcardInfoActivity.show(context, id, AcardInfoActivity.ACARD_FRIENDS);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                LogUtil.i("ChatReportActivity----->initBundleData: targetId is not right——>"+mTargetId);
            }

        }
        return true;
    }

    @Override
    public boolean onUserPortraitLongClick(Context context, Conversation.ConversationType conversationType, UserInfo userInfo) {
        return false;
    }

    @Override
    public boolean onMessageClick(Context context, View view, Message message) {
        return false;
    }

    @Override
    public boolean onMessageLinkClick(Context context, String s) {
        return false;
    }

    @Override
    public boolean onMessageLongClick(Context context, View view, Message message) {
        return false;
    }
    /*---------------   消息交互   --------------*/

    /*---------------   會(huì)話(huà)的交互   --------------*/
    @Override
    public boolean onConversationPortraitClick(Context context, Conversation.ConversationType conversationType, String s) {

        return false;
    }

    @Override
    public boolean onConversationPortraitLongClick(Context context, Conversation.ConversationType conversationType, String s) {
        return false;
    }

    @Override
    public boolean onConversationLongClick(Context context, View view, UIConversation uiConversation) {
        return false;
    }

    @Override
    public boolean onConversationClick(Context context, View view, UIConversation uiConversation) {
        return false;
    }

    /*---------------   會(huì)話(huà)的交互   --------------*/
    @Override
    public void onChanged(ConnectionStatus connectionStatus) {
        Log.d(TAG, "ConnectionStatus onChanged = " + connectionStatus.getMessage());
        if (connectionStatus.equals(ConnectionStatus.KICKED_OFFLINE_BY_OTHER_CLIENT)) {
            GlobalDialogActivity.start(mContext);
        } else if (connectionStatus == ConnectionStatus.TOKEN_INCORRECT) {
//            SharedPreferences sp = mContext.getSharedPreferences("config", Context.MODE_PRIVATE);
//            final String cacheToken = sp.getString("loginToken", "");
//            if (!TextUtils.isEmpty(cacheToken)) {
//                RongIM.connect(cacheToken, getConnectCallback());
//            } else {
//                Log.e("seal", "token is empty, can not reconnect");
//            }
        }
    }

    @Override
    public UserInfo getUserInfo(String userId) {
        IMUserInfoManager.getInstance().getUserInfo(userId);
        return null;
    }


    @Override
    public Group getGroupInfo(String groupId) {
       String url = AppConstant.URL_STUDIO_TEAM_INFO + "?teamRcId=" + groupId;
        HttpUtil.fastGet(url, this, new GsonCallBack<StudioTeamInfoBean>() {
            @Override
            public void onSuccess(StudioTeamInfoBean bean) {
                StudioTeamInfoBean.DataBean data = bean.getData();
                Group groupInfo = new Group(data.getTeamRcId(), data.getName(), Uri.parse(AppConstant.BASE_URL + data.getPortrait()));
                RongIM.getInstance().refreshGroupInfoCache(groupInfo);
            }

            @Override
            public void onError(Exception e, int erroCode) {
                Log.d(TAG, "getGroupInfo==" + e.getMessage());
            }
        });
        return null;
    }

    @Override
    public boolean onReceived(Message message, int i) {
        LogUtil.i("融云消息=====" + message.getContent());
        MessageContent messageContent = message.getContent();
        if (messageContent instanceof DeleteFriendsMessage) {
            DeleteFriendsMessage deleteFriendsMessage = (DeleteFriendsMessage) messageContent;
            LogUtil.i("IMListener----->onReceived: " + message.toString());
            LogUtil.i("IMListener----->onReceived: " + deleteFriendsMessage.getContent());
            RongIM.getInstance().clearMessages(Conversation.ConversationType.PRIVATE, deleteFriendsMessage.getContent(), new RongIMClient.ResultCallback<Boolean>() {
                @Override
                public void onSuccess(Boolean aBoolean) {
                    RongIM.getInstance().removeConversation(Conversation.ConversationType.PRIVATE, deleteFriendsMessage.getContent(), null);
                }

                @Override
                public void onError(RongIMClient.ErrorCode e) {

                }
            });
        }
        else if (messageContent instanceof ContactNotificationMessage) {
            ContactNotificationMessage contactNotificationMessage = (ContactNotificationMessage) messageContent;
            LogUtil.e("contactNotificationMessage=====" + contactNotificationMessage.getMessage() + contactNotificationMessage.getExtra());
            if (contactNotificationMessage.getOperation().equals("Request")) {
                //對(duì)方發(fā)來(lái)好友邀請(qǐng)
            } else if (contactNotificationMessage.getOperation().equals("AcceptResponse")) {
                //對(duì)方同意我的好友請(qǐng)求
            }
        } else if (messageContent instanceof GroupNotificationMessage) {
            GroupNotificationMessage groupNotificationMessage = (GroupNotificationMessage) messageContent;
            LogUtil.e("groupNotificationMessage----" + "TargetId==\n" + message.getTargetId() + "data===\n" + groupNotificationMessage.getData() +
                    "getOperation==\n" + groupNotificationMessage.getOperation());
            String groupID = message.getTargetId();
            MyGroupNotificationMessageData data = null;
            try {
                String currentID = RongIM.getInstance().getCurrentUserId();
                try {
                    data = jsonToBean(groupNotificationMessage.getData());
                    data.setTargetGroupId(groupID);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (groupNotificationMessage.getOperation().equals("Create")) {
                    //創(chuàng)建群組
//                    SealUserInfoManager.getInstance().getGroups(groupID);
//                    SealUserInfoManager.getInstance().getGroupMember(groupID);
                    EventBus.getDefault().post(new ImEvent(ImMessageCode.CREATE,groupID));
                } else if (groupNotificationMessage.getOperation().equals("Dismiss")) {
                    //解散群組
                    handleGroupDismiss(groupID);
                } else if (groupNotificationMessage.getOperation().equals("Kicked")) {
                    //群組踢人
                    EventBus.getDefault().post(new ImEvent(ImMessageCode.KICKED,groupID));
                } else if (groupNotificationMessage.getOperation().equals("Add")) {
                    //群組添加人員
                    EventBus.getDefault().post(new ImEvent(ImMessageCode.ADD,groupID));
                } else if (groupNotificationMessage.getOperation().equals("Quit")) {
                    //退出群組
                    EventBus.getDefault().post(new ImEvent(ImMessageCode.QUIT,groupID));
                } else if (groupNotificationMessage.getOperation().equals("Rename")) {
                    //群組重命名
                    EventBus.getDefault().post(new ImEvent<String>(data.getTargetGroupName(),ImMessageCode.RENAME,groupID));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        } else if (messageContent instanceof ImageMessage) {
            //ImageMessage imageMessage = (ImageMessage) messageContent;
        }
        return false;
    }


    @Override
    public void getGroupMembers(String groupId, final RongIM.IGroupMemberCallback callback) {
//獲取群成員   數(shù)據(jù)庫(kù)+網(wǎng)絡(luò)
        String url = AppConstant.URL_STUDIO_TEAM_GET_GROUP_LIST;
        HttpUtil.fastGet(url + "?teamRcId=" + groupId + "&pageSize=200", this, new GsonCallBack<StudioGroupMemberBean>() {
            @Override
            public void onSuccess(StudioGroupMemberBean bean) {
                StudioGroupMemberBean.DataBean data = bean.getData();
                if (data != null) {
                    List<StudioGroupMemberBean.DataBean.ListBean> list = data.getList();
                    if (CheckUtil.isNotNull(list)) {
                        List<UserInfo> userInfos = new ArrayList<>();
                        for (StudioGroupMemberBean.DataBean.ListBean listBean : list) {
                            UserInfo userInfo = new UserInfo(listBean.getId(), listBean.getName(), Uri.parse(AppConstant.BASE_URL + listBean.getPortrait()));
                            userInfos.add(userInfo);
                        }
                        callback.onGetGroupMembersResult(userInfos);

                    }
                }
            }

            @Override
            public void onError(Exception e, int erroCode) {
                callback.onGetGroupMembersResult(null);
            }
        });
    }

}

我這里只處理了單聊和群聊兩種情況隙咸,具體其它的應(yīng)用情景筆者在這里不再給出,讀者可以自己看融云文檔根據(jù)具體使用情景進(jìn)行功能補(bǔ)充
自定義廣播接收器

public class IMNotificationReceiver extends PushMessageReceiver {

    @Override
    public boolean onNotificationMessageArrived(Context context, PushType pushType, PushNotificationMessage message) {
        return false; // 返回 false, 會(huì)彈出融云 SDK 默認(rèn)通知; 返回 true, 融云 SDK 不會(huì)彈通知, 通知需要由您自定義成洗。
    }

    @Override
    public boolean onNotificationMessageClicked(Context context, PushType pushType, PushNotificationMessage message) {
        return false;// 返回 false, 會(huì)走融云 SDK 默認(rèn)處理邏輯, 即點(diǎn)擊該通知會(huì)打開(kāi)會(huì)話(huà)列表或會(huì)話(huà)界面; 返回 true, 則由您自定義處理邏輯五督。
    }

    @Override
    public void onThirdPartyPushState(PushType pushType, String action, long resultCode) {
        super.onThirdPartyPushState(pushType, action, resultCode);
    }
}

4.在頁(yè)面組件庫(kù)的基礎(chǔ)上自定義UI

1.會(huì)話(huà)列表自定義

可以根據(jù)IMKit里面的ConversationListFragment修改成自己的MyConversationListFragment,其實(shí)我只是把里面的適配器重寫(xiě)了MyConversationListAdapter所以這里重點(diǎn)介紹Adapter

public class MyConversationListAdapter extends BaseAdapter<UIConversation> {
    private static final String TAG = "MyConversationListAdapter";
    LayoutInflater mInflater;
    Context mContext;
    private MyConversationListAdapter.OnPortraitItemClick mOnPortraitItemClick;

    public long getItemId(int position) {
        UIConversation conversation = (UIConversation) this.getItem(position);
        return conversation == null ? 0L : (long) conversation.hashCode();
    }

    public MyConversationListAdapter(Context context) {
        this.mContext = context;
        this.mInflater = LayoutInflater.from(this.mContext);
    }

    public int findGatheredItem(Conversation.ConversationType type) {
        int index = this.getCount();
        int position = -1;
        while (index-- > 0) {
            UIConversation uiConversation = (UIConversation) this.getItem(index);
            if (uiConversation.getConversationType().equals(type)) {
                position = index;
                break;
            }
        }
        return position;
    }

    public int findPosition(Conversation.ConversationType type, String targetId) {
        int index = this.getCount();
        int position = -1;
        while (index-- > 0) {
            if (((UIConversation) this.getItem(index)).getConversationType().equals(type) && ((UIConversation) this.getItem(index)).getConversationTargetId().equals(targetId)) {
                position = index;
                break;
            }
        }
        return position;
    }

    protected View newView(Context context, int position, ViewGroup group) {
        View result = this.mInflater.inflate(R.layout.my_rc_item_conversation, (ViewGroup) null);
        MyConversationListAdapter.ViewHolder holder = new MyConversationListAdapter.ViewHolder();
        holder.layout = this.findViewById(result, R.id.rc_item_conversation);
        holder.leftImageView = (AsyncImageView) this.findViewById(result, R.id.rc_left);
        holder.contentView = (ProviderContainerView) this.findViewById(result, R.id.rc_content);
        result.setTag(holder);
        return result;
    }

    protected void bindView(View v, int position, final UIConversation data) {
        MyConversationListAdapter.ViewHolder holder = (MyConversationListAdapter.ViewHolder) v.getTag();
        if (data != null) {
            IContainerItemProvider provider = RongContext.getInstance().getConversationTemplate(data.getConversationType().getName());
            if (provider == null) {
                LogUtil.i("MyConversationListAdapter----->bindView: provider is null");
            } else {
                View view = holder.contentView.inflate(provider);
                provider.bindView(view, position, data);//內(nèi)容view
                if (data.isTop()) {
                    holder.layout.setBackgroundDrawable(this.mContext.getResources().getDrawable(R.drawable.rc_item_top_list_selector));
                } else {
                    holder.layout.setBackgroundDrawable(this.mContext.getResources().getDrawable(R.drawable.rc_item_list_selector));
                }

                ConversationProviderTag tag = RongContext.getInstance().getConversationProviderTag(data.getConversationType().getName());
                int defaultId;
                if (data.getConversationType().equals(Conversation.ConversationType.GROUP)) {
                    defaultId = R.drawable.portrait_circle_holder;
                } else if (data.getConversationType().equals(Conversation.ConversationType.DISCUSSION)) {
                    defaultId = R.drawable.portrait_circle_holder;
                } else {
                    defaultId = R.drawable.portrait_circle_holder;
                }

                if (tag.portraitPosition() == 1) {
                    if (data.getConversationGatherState()) {
                        holder.leftImageView.setAvatar((String) null, defaultId);
                    } else if (data.getIconUrl() != null) {
                        holder.leftImageView.setAvatar(data.getIconUrl().toString(), defaultId);
                    } else {
                        holder.leftImageView.setAvatar((String) null, defaultId);
                    }
//                    data.getUnReadMessageCount()

                }

                MessageContent content = data.getMessageContent();
                if (content != null && content.isDestruct()) {
                    RongIMClient.getInstance().getMessage(data.getLatestMessageId(), new RongIMClient.ResultCallback<Message>() {
                        public void onSuccess(Message message) {
                            if (message == null) {
                                EventBus.getDefault().post(new Event.MessageDeleteEvent(new int[]{data.getLatestMessageId()}));
                            } else if (message.getReadTime() > 0L) {
                                long readTime = message.getReadTime();
                                long serverTime = System.currentTimeMillis() - RongIMClient.getInstance().getDeltaTime();
                                long delay = message.getContent().getDestructTime() - (serverTime - readTime) / 1000L;
                                if (delay > 0L) {
                                    RongIM.getInstance().createDestructionTask(message, (DestructionTaskManager.OnOverTimeChangeListener) null, ConversationListFragment.TAG);
                                } else {
                                    EventBus.getDefault().post(new Event.DestructionEvent(message));
                                }
                            }

                        }

                        public void onError(RongIMClient.ErrorCode e) {
                        }
                    });
                }

            }
        }
    }
    public void setOnPortraitItemClick(MyConversationListAdapter.OnPortraitItemClick onPortraitItemClick) {
        this.mOnPortraitItemClick = onPortraitItemClick;
    }

    public interface OnPortraitItemClick {
        void onPortraitItemClick(View var1, UIConversation var2);
        boolean onPortraitItemLongClick(View var1, UIConversation var2);
    }

    protected class ViewHolder {
        public View layout;
        public AsyncImageView leftImageView;
        public ProviderContainerView contentView;
        protected ViewHolder() {
        }
    }
}

主要就是修改item的布局然后作相應(yīng)處理瓶殃,內(nèi)容組件繼續(xù)使用融云提供的充包,融云頁(yè)提供了內(nèi)容組件的自定義方式:自定義會(huì)話(huà)模板MyPrivateConversationProvider

@ConversationProviderTag(
        conversationType = "private",
        portraitPosition = 1
)
public class MyPrivateConversationProvider implements ConversationProvider<UIConversation> {
    private static final String TAG = "MyPrivateConversationProvider";

    public MyPrivateConversationProvider() {
    }

    public View newView(Context context, ViewGroup viewGroup) {
        View result = LayoutInflater.from(context).inflate(R.layout.my_rc_item_base_conversation, (ViewGroup) null);
        MyPrivateConversationProvider.ViewHolder holder = new MyPrivateConversationProvider.ViewHolder();
        holder.title = (TextView) result.findViewById(R.id.rc_conversation_title);
        holder.time = (TextView) result.findViewById(R.id.rc_conversation_time);
        holder.content = (TextView) result.findViewById(R.id.rc_conversation_content);
        holder.notificationBlockImage = (ImageView) result.findViewById(R.id.rc_conversation_msg_block);
        holder.readStatus = (ImageView) result.findViewById(R.id.rc_conversation_status);
        holder.unread = (NotifycationView) result.findViewById(R.id.unread_message);
        result.setTag(holder);
        return result;
    }

    private void handleMentionedContent(final MyPrivateConversationProvider.ViewHolder holder, final View view, final UIConversation data) {
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        final String preStr = view.getContext().getString(R.string.rc_message_content_mentioned);
        if (holder.content.getWidth() > 60) {
            CharSequence cutStr = TextUtils.ellipsize(preStr + " " + data.getConversationContent(), holder.content.getPaint(), (float) (holder.content.getWidth() - 60), TextUtils.TruncateAt.END);
            SpannableString string = new SpannableString(cutStr);
            string.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_mentioned_color)), 0, preStr.length(), 33);
            builder.append(string);
            AndroidEmoji.ensure(builder);
            holder.content.setText(builder, TextView.BufferType.SPANNABLE);
        } else {
            holder.content.post(new Runnable() {
                public void run() {
                    if (holder.content.getWidth() > 60) {
                        CharSequence cutStr = TextUtils.ellipsize(preStr + " " + data.getConversationContent(), holder.content.getPaint(), (float) (holder.content.getWidth() - 40), TextUtils.TruncateAt.END);
                        SpannableString strx = new SpannableString(cutStr);
                        strx.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_mentioned_color)), 0, preStr.length(), 33);
                        builder.append(strx);
                    } else {
                        SpannableString str = new SpannableString(preStr + " " + data.getConversationContent());
                        str.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_mentioned_color)), 0, preStr.length(), 33);
                        builder.append(str);
                    }

                    AndroidEmoji.ensure(builder);
                    holder.content.setText(builder, TextView.BufferType.SPANNABLE);
                }
            });
        }

    }

    private void handleDraftContent(final MyPrivateConversationProvider.ViewHolder holder, final View view, final UIConversation data) {
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        final String preStr = view.getContext().getString(R.string.rc_message_content_draft);
        if (holder.content.getWidth() > 60) {
            CharSequence cutStr = TextUtils.ellipsize(preStr + " " + data.getDraft(), holder.content.getPaint(), (float) (holder.content.getWidth() - 60), TextUtils.TruncateAt.END);
            SpannableString string = new SpannableString(cutStr);
            string.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_draft_color)), 0, preStr.length(), 33);
            builder.append(string);
            AndroidEmoji.ensure(builder);
            holder.content.setText(builder, TextView.BufferType.SPANNABLE);
        } else {
            holder.content.post(new Runnable() {
                public void run() {
                    if (holder.content.getWidth() > 60) {
                        CharSequence cutStr = TextUtils.ellipsize(preStr + " " + data.getDraft(), holder.content.getPaint(), (float) (holder.content.getWidth() - 60), TextUtils.TruncateAt.END);
                        SpannableString strx = new SpannableString(cutStr);
                        strx.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_draft_color)), 0, preStr.length(), 33);
                        builder.append(strx);
                    } else {
                        SpannableString str = new SpannableString(preStr + " " + data.getDraft());
                        str.setSpan(new ForegroundColorSpan(view.getContext().getResources().getColor(R.color.rc_draft_color)), 0, preStr.length(), 33);
                        builder.append(str);
                    }

                    AndroidEmoji.ensure(builder);
                    holder.content.setText(builder, TextView.BufferType.SPANNABLE);
                }
            });
        }

    }

    private void handleCommonContent(final MyPrivateConversationProvider.ViewHolder holder, UIConversation data) {
        if (holder.content.getWidth() > 60 && data.getConversationContent() != null) {
            CharSequence cutStr = TextUtils.ellipsize(data.getConversationContent(), holder.content.getPaint(), (float) (holder.content.getWidth() - 60), TextUtils.TruncateAt.END);
            holder.content.setText(cutStr, TextView.BufferType.SPANNABLE);
        } else {
            final CharSequence cutStr = data.getConversationContent();
            holder.content.post(new Runnable() {
                public void run() {
                    if (holder.content.getWidth() > 60 && cutStr != null) {
                        CharSequence str = TextUtils.ellipsize(cutStr, holder.content.getPaint(), (float) (holder.content.getWidth() - 60), TextUtils.TruncateAt.END);
                        holder.content.setText(str, TextView.BufferType.SPANNABLE);
                    } else {
                        holder.content.setText(cutStr);
                    }

                }
            });
        }

    }

    public void bindView(View view, int position, UIConversation data) {
        MyPrivateConversationProvider.ViewHolder holder = (MyPrivateConversationProvider.ViewHolder) view.getTag();
        ProviderTag tag = null;
        if (data == null) {
            holder.title.setText((CharSequence) null);
            holder.time.setText((CharSequence) null);
            holder.content.setText((CharSequence) null);
            holder.unread.setVisibility(View.GONE);
        } else {
            holder.title.setText(data.getUIConversationTitle());
            String time = RongDateUtils.getConversationListFormatDate(data.getUIConversationTime(), view.getContext());
            holder.time.setText(time);
            if (data.getUnReadMessageCount()>0) {
                holder.unread.setVisibility(View.VISIBLE);
                holder.unread.setNotifyCount(data.getUnReadMessageCount());
            }else {
                holder.unread.setVisibility(View.GONE);
            }

            if (TextUtils.isEmpty(data.getDraft()) && !data.getMentionedFlag()) {
                boolean readRec = false;

                try {
                    readRec = view.getResources().getBoolean(R.bool.rc_read_receipt);
                } catch (Resources.NotFoundException var10) {
                    RLog.e("MyPrivateConversationProvider", "rc_read_receipt not configure in rc_config.xml");
                    var10.printStackTrace();
                }

                if (readRec) {
                    if (data.getSentStatus() == Message.SentStatus.READ && data.getConversationSenderId().equals(RongIM.getInstance().getCurrentUserId()) && !(data.getMessageContent() instanceof RecallNotificationMessage)) {
                        holder.readStatus.setVisibility(View.VISIBLE);
                    } else {
                        holder.readStatus.setVisibility(View.GONE);
                    }
                }

                this.handleCommonContent(holder, data);
            } else {
                if (data.getMentionedFlag()) {
                    this.handleMentionedContent(holder, view, data);
                } else {
                    this.handleDraftContent(holder, view, data);
                }

                holder.readStatus.setVisibility(View.GONE);
            }

            if (RongContext.getInstance() != null && data.getMessageContent() != null) {
                tag = RongContext.getInstance().getMessageProviderTag(data.getMessageContent().getClass());
            }

            if (data.getSentStatus() != null && (data.getSentStatus() == Message.SentStatus.FAILED || data.getSentStatus() == Message.SentStatus.SENDING) && tag != null && tag.showWarning() && data.getConversationSenderId() != null && data.getConversationSenderId().equals(RongIM.getInstance().getCurrentUserId())) {
                Bitmap bitmap = BitmapFactory.decodeResource(view.getResources(), R.drawable.rc_conversation_list_msg_send_failure);
                int width = bitmap.getWidth();
                Drawable drawable = null;
                if (data.getSentStatus() == Message.SentStatus.FAILED && TextUtils.isEmpty(data.getDraft())) {
                    drawable = view.getContext().getResources().getDrawable(R.drawable.rc_conversation_list_msg_send_failure);
                } else if (data.getSentStatus() == Message.SentStatus.SENDING && TextUtils.isEmpty(data.getDraft())) {
                    drawable = view.getContext().getResources().getDrawable(R.drawable.rc_conversation_list_msg_sending);
                }

                if (drawable != null) {
                    drawable.setBounds(0, 0, width, width);
                    holder.content.setCompoundDrawablePadding(10);
                    holder.content.setCompoundDrawables(drawable, (Drawable) null, (Drawable) null, (Drawable) null);
                }
            } else {
                holder.content.setCompoundDrawables((Drawable) null, (Drawable) null, (Drawable) null, (Drawable) null);
            }

            Conversation.ConversationNotificationStatus status = data.getNotificationStatus();
            if (status != null && status.equals(Conversation.ConversationNotificationStatus.DO_NOT_DISTURB)) {
                holder.notificationBlockImage.setVisibility(View.VISIBLE);
            } else {
                holder.notificationBlockImage.setVisibility(View.GONE);
            }
        }

    }

    public Spannable getSummary(UIConversation data) {
        return null;
    }

    public String getTitle(String userId) {
        UserInfo userInfo = RongUserInfoManager.getInstance().getUserInfo(userId);
        return userInfo == null ? userId : userInfo.getName();
    }

    public Uri getPortraitUri(String userId) {
        UserInfo userInfo = RongUserInfoManager.getInstance().getUserInfo(userId);
        return userInfo == null ? null : userInfo.getPortraitUri();
    }

    protected class ViewHolder {
        public TextView title;
        public TextView time;
        public TextView content;
        public ImageView notificationBlockImage;
        public ImageView readStatus;
        public NotifycationView unread;

        protected ViewHolder() {
        }
    }
}

然后在Application里面注冊(cè)會(huì)話(huà)模板

//注冊(cè)會(huì)話(huà)模板
 RongIM.getInstance().registerConversationTemplate(new MyPrivateConversationProvider());

2.消息自定義

融云提供了消息的自定義和會(huì)話(huà)一樣需要自定義消息模板

@MessageTag(
        value = "AH:SysMsg",
        flag = 3
)
@DestructionTag
public class AntihivSystemMessage extends MessageContent {

    public static final Creator<AntihivSystemMessage> CREATOR = new Creator<AntihivSystemMessage>() {
        public AntihivSystemMessage createFromParcel(Parcel source) {
            return new AntihivSystemMessage(source);
        }

        public AntihivSystemMessage[] newArray(int size) {
            return new AntihivSystemMessage[size];
        }
    };

    private String content;
    protected String extra;

    public AntihivSystemMessage(Parcel in) {
        this.setExtra(ParcelUtils.readFromParcel(in));
        this.setContent(ParcelUtils.readFromParcel(in));
        this.setUserInfo((UserInfo) ParcelUtils.readFromParcel(in, UserInfo.class));
        this.setMentionedInfo((MentionedInfo) ParcelUtils.readFromParcel(in, MentionedInfo.class));
        this.setDestruct(ParcelUtils.readIntFromParcel(in) == 1);
        this.setDestructTime(ParcelUtils.readLongFromParcel(in));
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setExtra(String extra) {
        this.extra = extra;
    }

    public String getContent() {
        return content;
    }

    public String getExtra() {
        return extra;
    }

    @Override
    public byte[] encode() {
        JSONObject jsonObj = new JSONObject();

        try {
            jsonObj.put("content", getEmotion(getContent()));
            if (!TextUtils.isEmpty(getExtra())) {
                jsonObj.put("extra", getExtra());
            }

            if (getJSONUserInfo() != null) {
                jsonObj.putOpt("user", getJSONUserInfo());
            }

            if (getJsonMentionInfo() != null) {
                jsonObj.putOpt("mentionedInfo", getJsonMentionInfo());
            }

            jsonObj.put("isBurnAfterRead", isDestruct());
            jsonObj.put("burnDuration", getDestructTime());
        } catch (JSONException var4) {
            RLog.e("TextMessage", "JSONException " + var4.getMessage());
        }

        try {
            return jsonObj.toString().getBytes("UTF-8");
        } catch (UnsupportedEncodingException var3) {
            RLog.e("TextMessage", "UnsupportedEncodingException ", var3);
            return null;
        }
    }

    public AntihivSystemMessage(byte[] data) {
        String jsonStr = null;

        try {
            if (data != null && data.length >= 40960) {
                RLog.e("TextMessage", "TextMessage length is larger than 40KB, length :" + data.length);
            }

            jsonStr = new String(data, "UTF-8");
        } catch (UnsupportedEncodingException var5) {
            RLog.e("TextMessage", "UnsupportedEncodingException ", var5);
        }

        try {
            JSONObject jsonObj = new JSONObject(jsonStr);
            if (jsonObj.has("content")) {
                this.setContent(jsonObj.optString("content"));
            }

            if (jsonObj.has("extra")) {
                this.setExtra(jsonObj.optString("extra"));
            }

            if (jsonObj.has("user")) {
                this.setUserInfo(this.parseJsonToUserInfo(jsonObj.getJSONObject("user")));
            }

            if (jsonObj.has("mentionedInfo")) {
                this.setMentionedInfo(this.parseJsonToMentionInfo(jsonObj.getJSONObject("mentionedInfo")));
            }

            if (jsonObj.has("isBurnAfterRead")) {
                this.setDestruct(jsonObj.getBoolean("isBurnAfterRead"));
            }

            if (jsonObj.has("burnDuration")) {
                this.setDestructTime(jsonObj.getLong("burnDuration"));
            }
        } catch (JSONException var4) {
            RLog.e("TextMessage", "JSONException " + var4.getMessage());
        }

    }

    private String getEmotion(String content) {
        Pattern pattern = Pattern.compile("\\[/u([0-9A-Fa-f]+)\\]");
        Matcher matcher = pattern.matcher(content);
        StringBuffer sb = new StringBuffer();

        while (matcher.find()) {
            int inthex = Integer.parseInt(matcher.group(1), 16);
            matcher.appendReplacement(sb, String.valueOf(Character.toChars(inthex)));
        }

        matcher.appendTail(sb);
        return sb.toString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        ParcelUtils.writeToParcel(dest, this.getExtra());
        ParcelUtils.writeToParcel(dest, this.content);
        ParcelUtils.writeToParcel(dest, this.getUserInfo());
        ParcelUtils.writeToParcel(dest, this.getMentionedInfo());
        ParcelUtils.writeToParcel(dest, this.isDestruct() ? 1 : 0);
        ParcelUtils.writeToParcel(dest, this.getDestructTime());
    }

    public List<String> getSearchableWord() {
        List<String> words = new ArrayList();
        words.add(this.content);
        return words;
    }

    @Override
    public String toString() {
        return "AntihivSystemMessage{" +
                "content='" + content + '\'' +
                ", extra='" + extra + '\'' +
                '}';
    }
}

同樣需要在Application進(jìn)行注冊(cè)
這里我給出我寫(xiě)初始化代碼

private void initIm() {
        final String imToken = SharePreferenceUtil.getString(this, AppConstant.IM_TOKEN);
        if (getApplicationInfo().packageName.equals(getCurProcessName(getApplicationContext()))) {
            RongIM.init(this);//init Im
            IMListener.init(this);
            IMUserInfoManager.getInstance().openDB();
            //注冊(cè)該會(huì)話(huà)模板
            RongIM.getInstance().registerConversationTemplate(new MyPrivateConversationProvider());
            //注冊(cè)自定義消息
            RongIM.registerMessageType(AntihivSystemMessage.class);
            RongIM.registerMessageType(DeleteFriendsMessage.class);
            //注冊(cè)消息模板
            RongIM.getInstance().registerMessageTemplate(new AntihivSystemMessageProvider());
            RongIM.setConnectionStatusListener(new RongIMClient.ConnectionStatusListener() {
                @Override
                public void onChanged(ConnectionStatus status) {
                    LogUtil.i("Application----->Rongyun onChanged: ---" + status);
                    if (status == ConnectionStatus.TOKEN_INCORRECT) {
                        if (!TextUtils.isEmpty(imToken)) {
                            RongIM.connect(imToken, IMListener.getInstance().getConnectCallback());
                        } else {
                            LogUtil.i("token is empty, can not reconnect");
                        }
                    }
                }
            });
        }

    }

其它

其它需要主頁(yè)的地方就是如果想要修改一些配置比如@功能,回執(zhí)等可以修改rc_config.xml這個(gè)文件遥椿。
使用了混淆的項(xiàng)目需要加上融云的混淆:

# ------融云混淆
-keepattributes Exceptions,InnerClasses

-keepattributes Signature

# RongCloud SDK
-keep class io.rong.** {*;}
-keep class cn.rongcloud.** {*;}
-keep class * implements io.rong.imlib.model.MessageContent {*;}
-dontwarn io.rong.push.**
-dontnote com.xiaomi.**
-dontnote com.google.android.gms.gcm.**
-dontnote io.rong.**

# VoIP
-keep class io.agora.rtc.** {*;}

# Location
-keep class com.amap.api.**{*;}
-keep class com.amap.api.services.**{*;}

# 紅包
-keep class com.google.gson.** { *; }
-keep class com.uuhelper.Application.** {*;}
-keep class net.sourceforge.zbar.** { *; }
-keep class com.google.android.gms.** { *; }
-keep class com.alipay.** {*;}
-keep class com.jrmf360.rylib.** {*;}

-ignorewarnings
#融云EventBus需要以onEvent開(kāi)頭
-keepclassmembers class ** {
 public void onEvent*(**);
}
# ------融云混淆  end

總結(jié)

做到以上幾點(diǎn)基本上可以實(shí)現(xiàn)你所需要的即時(shí)通訊能力和頁(yè)面效果了基矮,如果對(duì)UI要求更高就需要多花心思在看融云官方api和自定義UI上面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末冠场,一起剝皮案震驚了整個(gè)濱河市家浇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碴裙,老刑警劉巖钢悲,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件点额,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡莺琳,警方通過(guò)查閱死者的電腦和手機(jī)还棱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芦昔,“玉大人诱贿,你說(shuō)我怎么就攤上這事娃肿」径校” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵料扰,是天一觀的道長(zhǎng)凭豪。 經(jīng)常有香客問(wèn)我,道長(zhǎng)晒杈,這世上最難降的妖魔是什么嫂伞? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮拯钻,結(jié)果婚禮上帖努,老公的妹妹穿的比我還像新娘。我一直安慰自己粪般,他們只是感情好拼余,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著亩歹,像睡著了一般匙监。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上小作,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天亭姥,我揣著相機(jī)與錄音,去河邊找鬼顾稀。 笑死达罗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的静秆。 我是一名探鬼主播粮揉,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诡宗!你這毒婦竟也來(lái)了滔蝉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤塔沃,失蹤者是張志新(化名)和其女友劉穎蝠引,沒(méi)想到半個(gè)月后阳谍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡螃概,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年矫夯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吊洼。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡训貌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出冒窍,到底是詐尸還是另有隱情递沪,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布综液,位于F島的核電站款慨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏谬莹。R本人自食惡果不足惜檩奠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望附帽。 院中可真熱鬧埠戳,春花似錦、人聲如沸蕉扮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)慢显。三九已至爪模,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荚藻,已是汗流浹背屋灌。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留应狱,地道東北人共郭。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疾呻,于是被迫代替她去往敵國(guó)和親除嘹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354