网易云信-新增自定义消息(Android版)

96
醉生夢死
0.1 2017.12.25 15:52* 字数 1685

前言

公司业务需要,PC端,移动端都用到了第三方 网易云信 IM 来实现在线客服咨询。
在这当中难免遇到一些需求是网易云信没有提供,需要自行编码进行扩展的。写此篇文章的目的正是因业务需要,需要在网易云信的基础上进行消息类型的扩展。

此篇文章里的代码是基于 网易云信 NIM_Android_Demo_v4.5.1 版 进行修改的

如下图所示的消息类型

带图片和文字,并且可点击的消息类型,(注意收到的消息和发送的消息文本颜色不一样)

标题是Android版,可想而知,肯定还有其他如 iOS版,Web版等,不可能此类型的消息(我称它为图文消息)只支持android,而在iOS或Web端无法显示问题。以下附上其他版本扩展的链接


正文

  1. 使用 android studio 打开项目运行试试,确保项目运行没有问题。

  2. 运行没有问题后,修改以下几个文件配置,将demo修改为自己所用。


    修改com.netease.nim.appKey的value值为云信后台你自己的appKey
  3. 修改NimApplication.javaonCreate 函数中的以下几行参数,以下几行参数用于android平台一些机型消息推送问题。

@Override
public void onCreate() {
    super.onCreate();

    DemoCache.setContext(this);
    // 注册小米推送,参数:小米推送证书名称(需要在云信管理后台配置)、appID 、appKey,该逻辑放在 NIMClient init 之前
    NIMPushClient.registerMiPush(this, "证书名", "appID", "appKey");
    // 注册华为推送,参数:华为推送证书名称(需要在云信管理后台配置)
    NIMPushClient.registerHWPush(this, "证书名");
   
    // ...
}

登录网易云信后台,应用 -> [你的应用] -> 证书管理 ,在此添加你的证书后,然后将证书名,appID 、appKey 填写到上面代码中。


Android.png

以上三个步骤是通用的,将云信demo工程改为自己所用必须要做的修改。接下来就是添加代码,增加自定义图文链接消息功能了。

  1. 这个功能主要用于我们给网站用户发送促销或活动等使用,图文链接消息的发送功能不开放给用户。下图给出示例图,当用户点击咨询时,我们自动给他回复一条图文链接消息。
用户点击网站上的咨询时,自动发送一条图文链接消息

此处有些读者可能会想,这有什么难的,不就是个网页嘛。后台自动回复消息时发送一段如下的html代码,然后设置下样式排版一下就行了。

<a href="#" style="样式省略了">
   <div>头部标题</div>
   <img src="图片" />
   <div>底部描述</div>
</a>

这样一来就有个问题,消息显示面板支持发送html代码了。其他懂行行家要是发送如下的代码就惨了。

发送一段无限循环javascript脚本

应该没人会这么傻吧,自己坑害自己。当然我们使用js的 escapeunescape 在消息的收发的时候转化下就好了。
但是此时我们要是在app里看这个消息
因为消息是同步的,所以在其他端也能看到消息

看到的却是网页代码,要是在PC客户端看这条消息肯定也是这样。当然我们可以针对不平终端使用正则或者模版匹配然后使其显示成我们想要的样子,如此一来,又有个问题,消息推送内容的显示。如下图
因为发送的是一段普通文本类型消息,所以会显示消息内容

还有其它问题我就不一一列出了,你会发现自己在这条不归路上越陷越深,做各种兼容,哪天产品需求一变你就痛苦去吧。(我为什么花一个篇幅来说这个问题。1.这种解决问题的方式是在给自己挖坑。2.作为一个开发工程师第一时间应该想到的是扩展。3.我们目前就是这种做法,我看着是着实的难受。)

在demo的uikit模块下就有自定义消息的文档说明


可以参考demo中的此文档
  1. 依照文档扩展新增自定义消息
    在接口 CustomAttachmentType.java 中新增一个图文类型 LINK
public interface CustomAttachmentType {
    // 多端统一
    int Guess = 1;
    int SnapChat = 2;
    int Sticker = 3;
    int RTS = 4;
    // 新增图文链接消息
    int LINK = 5;
}

在包 com.netease.nim.demo.session.extension 目录下创建图文链接消息对象 LinkAttachment.java

// 此处忽略import包

public class LinkAttachment extends CustomAttachment {

    private final String KEY_TITLE = "title";
    private final String KEY_DESCRIBE = "describe";
    private final String KEY_IMAGE_URL = "image_url";
    private final String KEY_LINK_URL = "link_url";

    // 图文消息的标题,必须有
    private String title;
    // 图文消息的描述,可以为空
    private String describe;
    // 点击图文消息跳转的链接地址,必须有
    private String linkUrl;
    // 图文消息的图片
    private String imageUrl;

    public LinkAttachment() { super(CustomAttachmentType.LINK); }

    public String getTitle() { return title; }

    public String getDescribe() { return describe; }

    public String getLinkUrl() { return linkUrl; }

    public String getImageUrl() { return imageUrl; }

    public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }

    public void setTitle(String title) { this.title = title; }

    public void setDescribe(String describe) { this.describe = describe; }

    public void setLinkUrl(String linkUrl) { this.linkUrl = linkUrl; }

    // 解析数据内容
    @Override
    protected void parseData(JSONObject data) {
        title = data.getString(KEY_TITLE);
        describe = data.getString(KEY_DESCRIBE);
        linkUrl = data.getString(KEY_LINK_URL);
        imageUrl = data.getString(KEY_IMAGE_URL);
    }

    // 数据打包
    @Override
    protected JSONObject packData() {
        JSONObject data = new JSONObject();

        data.put(KEY_TITLE, getTitle());
        data.put(KEY_DESCRIBE, getDescribe());
        data.put(KEY_IMAGE_URL, getImageUrl());
        data.put(KEY_LINK_URL, getLinkUrl());

        return data;
    }

}

res/layout目录下创建图文消息的显示的布局文件 link_image.xml,用来显示我们自定义的消息如何展现。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
        android:layout_width="220dp"
        android:layout_height="wrap_content"
        android:background="@color/transparent"
        android:orientation="vertical"
        android:padding="10dp">

        <!-- 图文消息的标题 -->
        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:adjustViewBounds="true"
            android:gravity="center_vertical"
            android:maxLines="2"
            android:textSize="16sp"
            android:textColor="@color/color_blue_0888ff"
            android:textStyle="bold" />

        <!-- 图文消息中的图片,设置个默认图片 -->
        <ImageView
            android:id="@+id/link_image"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            android:layout_gravity="center_vertical"
            android:adjustViewBounds="true"
            android:src="@drawable/default_image"
            android:background="@drawable/image_border" />

        <!-- 图文消息中的描述 -->
        <TextView
            android:id="@+id/describe"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:adjustViewBounds="true"
            android:lineSpacingExtra="2dp"
            android:maxLines="3"
            android:paddingTop="5dp"
            android:textColor="@color/color_blue_3a9efb"
            android:textSize="14sp" />

    </LinearLayout>

</merge>

上面文件中用了@drawable/image_border,用来显示图片上下边框的,非必须。
demo/res/drawable/image_border.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape>
            <solid android:color="@color/split_line_grey_color_d9d9d9" />
        </shape>
    </item>

    <item
        android:bottom="1px"
        android:top="1px">
        <shape>
            <solid android:color="@color/white" />
        </shape>
    </item>

</layer-list>

接着创建图文消息的显示类 MsgViewHolderLink.java,用来处理我们自定义的消息根据业务如何展现。

public class MsgViewHolderLink extends MsgViewHolderBase {

    private LinkAttachment attachment;
    //  图片
    private ImageView imageView;
    // 标题
    private TextView titleView;
    // 描述
    private TextView describeView;

    public MsgViewHolderLink(BaseMultiItemFetchLoadAdapter adapter) {
        super(adapter);
    }

    @Override
    protected int getContentResId() {
        // 布局文件使用上面创建的文件
        return R.layout.link_image;
    }

    @Override
    protected void inflateContentView() {
        imageView = (ImageView) view.findViewById(R.id.link_image);
        titleView = (TextView) view.findViewById(R.id.title);
        describeView = (TextView) view.findViewById(R.id.describe);
    }

    // 此条消息点击时响应事件
    @Override
    protected void onItemClick() {
          // app内打开浏览器,将网页URL传入,此处有误先别着急,往下看文档或者先注释
          WebViewActivity.startActivity(context, attachment.getTitle(), attachment.getLinkUrl());
    }

    @Override
    protected void bindContentView() {
        attachment = (LinkAttachment) message.getAttachment();

        // MsgDirectionEnum.Out 表示发出去的消息, In 标示收到的消息
        // 设置发送的消息和收到的消息文本颜色
        if (message.getDirect() == MsgDirectionEnum.Out) {
            int color = DemoCache.getContext().getResources().getColor(R.color.white);
            titleView.setTextColor(color);
            describeView.setTextColor(color);
        } else if (message.getDirect() == MsgDirectionEnum.In) {
            int titleColor = DemoCache.getContext().getResources().getColor(R.color.color_blue_0888ff);
            int describeColor = DemoCache.getContext().getResources().getColor(R.color.color_blue_3a9efb);
            titleView.setTextColor(titleColor);
            describeView.setTextColor(describeColor);
        }

        titleView.setText(attachment.getTitle());

        // 判断是否传了图片,如果没有传图片,则不显示图片区域
        if (TextUtils.isEmpty(attachment.getImageUrl())) {
            imageView.setVisibility(View.GONE);
        } else {
            imageView.setVisibility(View.VISIBLE);
            // 图片加载器,异步加载网络图片,此处有误先别着急,往下看文档或者先注释掉此处
            ImageLoader.onLoadImage(attachment.getImageUrl(), new ImageLoader.LoadImageListener() {

                @Override
                public void onLoadImage(Bitmap bitmap, String bitmapPath) {
                    if (bitmap != null) {
                        imageView.setImageBitmap(bitmap);
                    }
                }
            });

        }

        // 判断是否传了描述,如果没有描述,则不显示描述区域
        if (TextUtils.isEmpty(attachment.getDescribe())) {
            describeView.setVisibility(View.GONE);
        } else {
            describeView.setText(attachment.getDescribe());
            describeView.setVisibility(View.VISIBLE);
        }
    }

}

接下再创建上面用到的WebViewActivity.java和布局文件webview_activity.xml,用于显示点击消息时app内跳转页面显示网页。

demo/res/layout/webview_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_background">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:elevation="0dp">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:titleTextAppearance="@style/Toolbar.TitleText"></android.support.v7.widget.Toolbar>
    </android.support.design.widget.AppBarLayout>

    <ProgressBar
        android:id="@+id/web_loading_progress"
        android:layout_width="60dp"
        android:layout_height="match_parent"
        android:layout_below="@id/app_bar_layout"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:elevation="1dp"
        android:max="100"
        android:min="0"
        android:visibility="gone" />

    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/app_bar_layout"></WebView>

</RelativeLayout>

com.netease.nim.demo.session.activity.WebViewActivity.java

// 此处忽略import包

//app内嵌网页支持
public class WebViewActivity extends UI {

    private final static String EXTRA_TITLE = "EXTRA_TITLE";
    private final static String EXTRA_LINK_URL = "EXTRA_LINK_URL";

   // 显示网页
    private WebView webView;
    // 加载进度状态
    private ProgressBar progress;

    public static void startActivity(Context context, String title, String url) {
        Intent intent = new Intent();
        intent.setClass(context, WebViewActivity.class);
        intent.putExtra(EXTRA_LINK_URL, url);
        intent.putExtra(EXTRA_TITLE, title);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle saveInstanceState) {
        super.onCreate(saveInstanceState);
        setContentView(R.layout.webview_activity);

        ToolBarOptions options = new NimToolBarOptions();
        options.titleString = getIntent().getStringExtra(EXTRA_TITLE);
        options.logoId = 0;

        setToolBar(R.id.toolbar, options);

        findViews();
        initViewData();
    }

    private void findViews() {
        webView = findViewById(R.id.webView);
        progress = findViewById(R.id.web_loading_progress);
    }

    private void initViewData() {
        // 加载传入过来的链接地址
        webView.loadUrl(getIntent().getStringExtra(EXTRA_LINK_URL));

        // 重写WebViewClient的shouldOverrideUrlLoading方法,实现在webview内加载网页
        WebViewClient webViewClient = new WebViewClient() {
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                progress.setVisibility(View.VISIBLE);
                progress.setProgress(0);
                super.onPageStarted(view, url, favicon);
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                progress.setVisibility(View.GONE);
                super.onPageFinished(view, url);
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }

        };

        WebChromeClient chromeClient = new WebChromeClient() {

            // 页面加载进度改变时候的回调
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                progress.setProgress(ScreenUtil.screenWidth * (newProgress / 100));
                super.onProgressChanged(view, newProgress);
            }

        };

        webView.setWebViewClient(webViewClient);
        webView.setWebChromeClient(chromeClient);
        // 获取 WebSettings 对象
        WebSettings settings = webView.getSettings();
        // 设置支持js
        settings.setJavaScriptEnabled(true);
        settings.setDefaultTextEncodingName("UTF-8");
        // 设置缓存使用方式为优先加载本地缓存
        settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        //设置开启数据库存储API权限
        settings.setDatabaseEnabled(true);
        // 开启DOM存储API功能
        settings.setDomStorageEnabled(true);
        // 设置是否开启定位功能
        settings.setGeolocationEnabled(true);
        // 构造缓存路径
        String cacheDirPath = getFilesDir().getAbsolutePath() + "/webcache/";
        // 设置保存地理信息数据路径
        settings.setGeolocationDatabasePath(cacheDirPath);
        // 设置数据库缓存路径
        settings.setAppCacheEnabled(true);
        settings.setAppCachePath(cacheDirPath);
        // 设置是否使用viewport
        // false:加载页面的宽度总是使用webview
        // true:由页面的viewport标签决定
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);
        settings.setAllowContentAccess(true);
        // 设置是否支持文件访问
        settings.setAllowFileAccess(true);
        // 设置缩放
        settings.setSupportZoom(true);
        // 设置是否开启内置缩放机制
        settings.setBuiltInZoomControls(true);
        // 设置开启内置缩放机制后是否显示缩放控件
        settings.setDisplayZoomControls(true);
        // 设置是否支持保存表单数据
        settings.setSaveFormData(true);
        // 设置webView的底层布局算法
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);

    }

    @Override
    public void onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack();
        } else {
            super.onBackPressed();
        }
    }

}

完成上面后还需在AndroidManifest.xml中添加如下节点

<!-- 网页WebView -->
<activity android:name=".session.activity.WebViewActivity" android:screenOrientation="portrait" />

再创建图片加载工具类ImageLoader.java,用于显示图片
com.netease.nim.demo.common.imageView.ImageLoader

public class ImageLoader {

    public static void onLoadImage(final String imageUrl, final LoadImageListener loadImageListener) {
        final Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                loadImageListener.onLoadImage((Bitmap) msg.obj, null);
            }
        };

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    URL url = new URL(imageUrl);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    InputStream is = conn.getInputStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(is);

                    is.close();

                    Message msg = new Message();
                    msg.obj = bitmap;
                    handler.sendMessage(msg);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            protected void finalize() throws Throwable {
                super.finalize();
            }
        }).start();
    }

    public interface LoadImageListener {
        public void onLoadImage(Bitmap bitmap, String bitmapPath);
    }

}
  1. 注册使用自定义的消息类型


    我们希望能显示出比较友好的内容

SessionListFragment.java中的getDigestOfAttachment函数中添加以下代码

@Override
public String getDigestOfAttachment(RecentContact recentContact, MsgAttachment attachment) {
      // 设置自定义消息的摘要消息,展示在最近联系人列表的消息缩略栏上
      // 当然,你也可以自定义一些内建消息的缩略语,例如图片,语音,音视频会话等,自定义的缩略语会被优先使用。
      if (attachment instanceof GuessAttachment) {
           GuessAttachment guess = (GuessAttachment) attachment;
           return guess.getValue().getDesc();
      } else if (attachment instanceof RTSAttachment) {
           return "[白板]";
      } else if (attachment instanceof StickerAttachment) {
           return "[贴图]";
      } else if (attachment instanceof SnapChatAttachment) {
           return "[阅后即焚]";
      } else if (attachment instanceof LinkAttachment) {
           // 添加显示自定义的内容
           return "[图文链接]";
      }
     return null;
}

另外我们自定义的消息应该禁止用户长按转发给其他人的功能,编辑SessionHelper.java,修改registerMsgForwardFilter函数,还需注册我们自定义的消息类型解析显示,修改 registerViewHolders函数,添加自定义的注册类。

private static void registerMsgForwardFilter() {
    NimUIKit.setMsgForwardFilter(new MsgForwardFilter() {
        @Override
        public boolean shouldIgnore(IMMessage message) {
            if (message.getDirect() == MsgDirectionEnum.In
                    && (message.getAttachStatus() == AttachStatusEnum.transferring
                    || message.getAttachStatus() == AttachStatusEnum.fail)) {
                // 接收到的消息,附件没有下载成功,不允许转发
                return true;
            } else if (message.getMsgType() == MsgTypeEnum.custom && message.getAttachment() != null
                    && (message.getAttachment() instanceof SnapChatAttachment
                    || message.getAttachment() instanceof RTSAttachment
                    // 添加此处判断
                    || message.getAttachment() instanceof LinkAttachment)) {
                // 白板消息和阅后即焚消息,红包消息,图文链接消息 不允许转发
                return true;
            } else if (message.getMsgType() == MsgTypeEnum.robot && message.getAttachment() != null && ((RobotAttachment) message.getAttachment()).isRobotSend()) {
                return true; // 如果是机器人发送的消息 不支持转发
            }
            return false;
        }
    });
}

//...

private static void registerViewHolders() {
    NimUIKit.registerMsgItemViewHolder(FileAttachment.class, MsgViewHolderFile.class);
    NimUIKit.registerMsgItemViewHolder(AVChatAttachment.class, MsgViewHolderAVChat.class);
    NimUIKit.registerMsgItemViewHolder(GuessAttachment.class, MsgViewHolderGuess.class);
    NimUIKit.registerMsgItemViewHolder(CustomAttachment.class, MsgViewHolderDefCustom.class);
    NimUIKit.registerMsgItemViewHolder(StickerAttachment.class, MsgViewHolderSticker.class);
    NimUIKit.registerMsgItemViewHolder(SnapChatAttachment.class, MsgViewHolderSnapChat.class);
    NimUIKit.registerMsgItemViewHolder(RTSAttachment.class, MsgViewHolderRTS.class);
    // 注册图文类型消息
    NimUIKit.registerMsgItemViewHolder(LinkAttachment.class, MsgViewHolderLink.class);
    NimUIKit.registerTipMsgViewHolder(MsgViewHolderTip.class);
}

到此处,已经完成了所有操作了。但不知道是否有效,因此我们希望能够通过一种方式方便自己调试使用。

添加测试按钮,方便调试

如图所示,添加一键发送图文消息按钮,正式上线后,将此按钮去掉即可

com.netease.nim.demo.session.action创建LinkAction.java

public class LinkAction extends BaseAction {


    public LinkAction() {
        // R.string.input_panel_textimg 需要在 strings.xml 中添加
        // <string name="input_panel_textimg">图文消息</string>
        super(R.drawable.message_plus_guess_selector, R.string.input_panel_textimg);
    }

    @Override
    public void onClick() {
        LinkAttachment attachment = new LinkAttachment();
        attachment.setTitle("暖冬季欢乐送");
        attachment.setDescribe("家具满1000元减100元再返100元现金券!点击查看详情!");
        attachment.setLinkUrl("点击消息后跳转的地址");
        attachment.setImageUrl("显示的图片url");

        // 以下 "图文链接:" + attachment.getTitle() 用来显示app消息推送时,图片显示的内容。
        IMMessage message = MessageBuilder.createCustomMessage(getAccount(), getSessionType(), "图文链接:" + attachment.getTitle(), attachment);
        sendMessage(message);
    }

}

在 SessionHelper.java 中添加如下代码,将上面创建的按钮功能注册到消息面板中

// 定制化单聊界面。如果使用默认界面,返回null即可
private static SessionCustomization getP2pCustomization() {
    //...
    actions.add(new RTSAction());
    actions.add(new SnapChatAction());
    actions.add(new GuessAction());
    actions.add(new FileAction());
    // 添加图文链接消息测试按钮
    actions.add(new LinkAction());
    //...
}

添加完成后,运行代码在单聊窗口中,点击右下角的 + 会发现多了个 图文消息按钮,点击即可发送图文消息内容,还可以尝试注释掉 LinkAction.java 中的 attachment.setDescribe("...");attachment.setImageUrl("...");然后再测试,发送之后和点击图文消息链接后如下。

发送只有标题的消息,发送只有标题和图片的消息
点击消息在app内跳转网页

尾篇

到此,云信android端的扩展自定义消息已经完成。当然,这只是android的显示正常了,其他如web,iOS,pc等客户端收到此类的消息,显示有问题,也是需要扩展调整的。此篇文章其他端的文章我会陆续更新,如果有需要的同学可以关注下。

以下附上其他版本扩展的链接

Android
Web note ad 1