默认
打赏 发表评论 3
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
Android端社交应用中的评论和回复功能实战分享[图文+源码]
阅读(113063) | 评论(3 收藏5 淘帖2
微信扫一扫关注!

本文作者水月沐風,感谢无私分享。


1、内容概述


在Android的日常开发中,尤其像IM这类社交应用中,评论与回复功能是我们经常遇到的需求之一(比如微信的朋友圈、手机QQ的QQ空间等),其中评论与回复列表的展示一般在功能模块中占比较大。对于需求改动和迭代较频繁的公司来说,如何快速开发一个二级界面来适应我们的功能需求无疑优先级更高一些。

首先我们来看看其他社交类app的评论与回复列表如何展示的:
Android端社交应用中的评论和回复功能实战分享[图文+源码]_1.png Android端社交应用中的评论和回复功能实战分享[图文+源码]_2.png Android端社交应用中的评论和回复功能实战分享[图文+源码]_111.jpg

第1张图是我们设计给我找的,他说要按照这个风格来,尽量将评论和回复内容在一个页面展示。好吧,没办法,毕竟我们做前端的,UI要看设计脸色,数据要看后台脸色😂。

第2张图Twitter不用说了,全球知名社交平台,上亿用户量,他们的评论回复都只展示一级数据(评论数据),其他更多内容(回复内容),是需要页面跳转去查看,知乎也类似。

第3张图微信的朋友圈大家最熟悉了,评论回复也同样都只展示一级数据(评论数据),其他更多内容(回复内容),是需要页面跳转去查看,这个主流的社区APP(包括IM)都差不多。

看到设计图,我们脑海肯定第一时间联想一下解决方案:
用recyclerview?listview?不对,分析一下它的层级发现,评论是一个列表,里面的回复又是一个列表,难道用recyclerview或者listview的嵌套?

抱着不确定的态度,立马去网上查一下,果不其然,搜到的实现方式大多都是用嵌套实现的,来公司之前,其中一个项目里的评论回复功能就是用的嵌套listview,虽然处理了滑动冲突问题,但效果不佳,而且时常卡顿,所以,这里我肯定要换个思路。

网上还有说用自定义view实现的,但我发现大多没有处理view的复用,而且开发成本大,暂时不予考虑。那怎么办?无意中看到expandable这个关键词,我突然想到谷歌很早之前出过一个扩展列表的控件 - ExpandableListView,但听说比较老,存在一些问题。算了,试试再说,顺便熟悉一下以前基础控件的用法。

先来看一下最终的效果图吧:
Android端社交应用中的评论和回复功能实战分享[图文+源码]_3.gif

这只是一个简单的效果图,你可以在此基础上来完善它。好了,废话不多说,下面让我们来看看效果具体如何实现的吧。

大家应该不难看出来,页面整体采用了CoordinatorLayout来实现详情页的顶部视差效。同时,这里我采用ExpandableListView来实现多级列表,然后再解决它们的嵌套滑动问题。OK,我们先从ExpandableListView开始动手。

2、相关例子


用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]
高仿Android版手机QQ可拖拽未读数小气泡源码 [附件下载]
Android聊天界面源码:实现了聊天气泡、表情图标(可翻页) [附件下载]
高仿Android版手机QQ首页侧滑菜单源码 [附件下载]
分享java AMR音频文件合并源码,全网最全
Android版高仿微信聊天界面源码 [附件下载]
高仿手机QQ的Android版锁屏聊天消息提醒功能 [附件下载]
高仿iOS版手机QQ录音及振幅动画完整实现 [源码下载]

3、ExpandableListView


官方对于ExpandableListView给出这样的解释:

A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can individually be expanded to show its children. The items come from the ExpandableListAdapter associated with this view.


简单来说,ExpandableListView是一个用于垂直方向滚动的二级列表视图,ExpandableListView与listview不同之处在于,它可以实现二级分组,并通过ExpandableListAdapter来绑定数据和视图。下面我们来一起实现上图的效果。

3.1布局中定义


首先,我们需要在xml的布局文件中声明ExpandableListView:
<ExpandableListView
    android:id="@+id/detail_page_lv_comment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@null"
    android:layout_marginBottom="64dp"
    android:listSelector="@android:color/transparent"
    android:scrollbars="none"/>

这里需要说明两个问题:
  • 1)ExpandableListView默认为它的item加上了点击效果,由于item里面还包含了childItem,所以,点击后,整个item里面的内容都会有点击效果。我们可以取消其点击特效,避免其影响用户体验,只需要设置如上代码中的listSelector即可;
  • 2)ExpandableListView具有默认的分割线,可以通过divider属性将其隐藏。


3.2设置Adapter


正如使用listView那样,我们需要为ExpandableListView设置一个适配器Adapter,为其绑定数据和视图。

ExpandableListView的adapter需要继承自ExpandableListAdapter,具体代码如下:
public class CommentExpandAdapter extends BaseExpandableListAdapter {
    private static final String TAG = "CommentExpandAdapter";
    private List<CommentDetailBean> commentBeanList;
    private Context context;
    public CommentExpandAdapter(Context context, List<CommentDetailBean> commentBeanList)               {
        this.context = context;
        this.commentBeanList = commentBeanList;
    }
    @Override
    public int getGroupCount() {
        return commentBeanList.size();
    }
    @Override
    public int getChildrenCount(int i) {
        if(commentBeanList.get(i).getReplyList() == null){
            return 0;
        }else {
            return commentBeanList.get(i).getReplyList().size()>0 ? commentBeanList.get(i).getReplyList().size():0;
        }
    }
    @Override
    public Object getGroup(int i) {
        return commentBeanList.get(i);
    }
    @Override
    public Object getChild(int i, int i1) {
        return commentBeanList.get(i).getReplyList().get(i1);
    }
    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }
    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return getCombinedChildId(groupPosition, childPosition);
    }
    @Override
    public boolean hasStableIds() {
        return true;
    }
    boolean isLike = false;
    @Override
    public View getGroupView(final int groupPosition, boolean isExpand, View convertView, ViewGroup viewGroup) {
        final GroupHolder groupHolder;
        if(convertView == null){
            convertView = LayoutInflater.from(context).inflate(R.layout.comment_item_layout, viewGroup, false);
            groupHolder = new GroupHolder(convertView);
            convertView.setTag(groupHolder);
        }else {
            groupHolder = (GroupHolder) convertView.getTag();
        }
        Glide.with(context).load(R.drawable.user_other)
                .diskCacheStrategy(DiskCacheStrategy.RESULT)
                .error(R.mipmap.ic_launcher)
                .centerCrop()
                .into(groupHolder.logo);
        groupHolder.tv_name.setText(commentBeanList.get(groupPosition).getNickName());
        groupHolder.tv_time.setText(commentBeanList.get(groupPosition).getCreateDate());
        groupHolder.tv_content.setText(commentBeanList.get(groupPosition).getContent());
        groupHolder.iv_like.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(isLike){
                    isLike = false;
                    groupHolder.iv_like.setColorFilter(Color.parseColor("#aaaaaa"));
                }else {
                    isLike = true;
                    groupHolder.iv_like.setColorFilter(Color.parseColor("#FF5C5C"));
                }
            }
        });
        return convertView;
    }
    @Override
    public View getChildView(final int groupPosition, int childPosition, boolean b, View convertView, ViewGroup viewGroup) {
        final ChildHolder childHolder;
        if(convertView == null){
            convertView = LayoutInflater.from(context).inflate(R.layout.comment_reply_item_layout,viewGroup, false);
            childHolder = new ChildHolder(convertView);
            convertView.setTag(childHolder);
        }
        else {
            childHolder = (ChildHolder) convertView.getTag();
        }
        String replyUser = commentBeanList.get(groupPosition).getReplyList().get(childPosition).getNickName();
        if(!TextUtils.isEmpty(replyUser)){
            childHolder.tv_name.setText(replyUser + ":");
        }
        childHolder.tv_content.setText(commentBeanList.get(groupPosition).getReplyList().get(childPosition).getContent());
        return convertView;
    }
    @Override
    public boolean isChildSelectable(int i, int i1) {
        return true;
    }
    private class GroupHolder{
        private CircleImageView logo;
        private TextView tv_name, tv_content, tv_time;
        private ImageView iv_like;
        public GroupHolder(View view) {
            logo =  view.findViewById(R.id.comment_item_logo);
            tv_content = view.findViewById(R.id.comment_item_content);
            tv_name = view.findViewById(R.id.comment_item_userName);
            tv_time = view.findViewById(R.id.comment_item_time);
            iv_like = view.findViewById(R.id.comment_item_like);
        }
    }
    private class ChildHolder{
        private TextView tv_name, tv_content;
        public ChildHolder(View view) {
            tv_name = (TextView) view.findViewById(R.id.reply_item_user);
            tv_content = (TextView) view.findViewById(R.id.reply_item_content);
        }
    }
}

一般情况下,我们自定义自己的ExpandableListAdapter后,需要实现以下几个方法:

  • 构造方法,这个应该无需多说了,一般用来初始化数据等操作。
  • getGroupCount,返回group分组的数量,在当前需求中指代评论的数量。
  • getChildrenCount,返回所在group中child的数量,这里指代当前评论对应的回复数目。
  • getGroup,返回group的实际数据,这里指的是当前评论数据。
  • getChild,返回group中某个child的实际数据,这里指的是当前评论的某个回复数据。
  • getGroupId,返回分组的id,一般将当前group的位置传给它。
  • getChildId,返回分组中某个child的id,一般也将child当前位置传给它,不过为了避免重复,可以使用getCombinedChildId(groupPosition, childPosition);来获取id并返回。
  • hasStableIds,表示分组和子选项是否持有稳定的id,这里返回true即可。
  • isChildSelectable,表示分组中的child是否可以选中,这里返回true。
  • getGroupView,即返回group的视图,一般在这里进行一些数据和视图绑定的工作,一般为了复用和高效,可以自定义ViewHolder,用法与listview一样,这里就不多说了。
  • getChildView,返回分组中child子项的视图,比较容易理解,第一个参数是当前group所在的位置,第二个参数是当前child所在位置。

这里的数据是我自己做的模拟数据,不过应该算是较为通用的格式了,大体格式如下:
Android端社交应用中的评论和回复功能实战分享[图文+源码]_4.png
插一嘴:欢迎使用 http://www.wanandroid.com/tools/mockapi 模拟数据。

一般情况下,我们后台会通过接口返回给我们一部分数据,如果想要查看更多评论,需要跳转到“更多页面”去查看,这里为了方便,我们只考虑加载部分数据。

3.3Activity中使用


接下来,我们就需要在activity中显示评论和回复的二级列表了:
private ExpandableListView expandableListView;
private CommentExpandAdapter adapter;
private CommentBean commentBean;
private List<CommentDetailBean> commentsList;
...
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    private void initView() {
        expandableListView = findViewById(R.id.detail_page_lv_comment);
        initExpandableListView(commentsList);
    }
    /**
     * 初始化评论和回复列表
     */
    private void initExpandableListView(final List<CommentDetailBean> commentList){
        expandableListView.setGroupIndicator(null);
        //默认展开所有回复
        adapter = new CommentExpandAdapter(this, commentList);
        expandableListView.setAdapter(adapter);
        for(int i = 0; i<commentList.size(); i++){
            expandableListView.expandGroup(i);
        }
        expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
            @Override
            public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) {
                boolean isExpanded = expandableListView.isGroupExpanded(groupPosition);
                Log.e(TAG, "onGroupClick: 当前的评论id>>>"+commentList.get(groupPosition).getId());
//                if(isExpanded){
//                    expandableListView.collapseGroup(groupPosition);
//                }else {
//                    expandableListView.expandGroup(groupPosition, true);
//                }
                return true;
            }
        });
        expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
            @Override
            public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long l) {
                Toast.makeText(MainActivity.this,"点击了回复",Toast.LENGTH_SHORT).show();
                return false;
            }
        });
        expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
            @Override
            public void onGroupExpand(int groupPosition) {
                //toast("展开第"+groupPosition+"个分组");
            }
        });
    }
    /**
     * func:生成测试数据
     * @return 评论数据
     */
    private List<CommentDetailBean> generateTestData(){
        Gson gson = new Gson();
        commentBean = gson.fromJson(testJson, CommentBean.class);
        List<CommentDetailBean> commentList = commentBean.getData().getList();
        return commentList;
    }

就以上代码作一下简单说明:

  • 【1】ExpandableListView在默认情况下会为我们自带分组的icon(▶️),当前需求下,我们根本不需要展示,可以通过expandableListView.setGroupIndicator(null)来隐藏;
  • 【2】一般情况下,我们可能需要默认展开所有的分组,我就可以通过循环来调用expandableListView.expandGroup(i);方法;
  • 【3】ExpandableListView为我们提供了group和child的点击事件,分别通过setOnGroupClickListener和setOnChildClickListener来设置。值得注意的是,group的点击事件里如果我们返回的是false,那么我们点击group就会自动展开,但我这里碰到一个问题,当我返回false时,第一条评论数据会多出一条。通过百度查找方法,虽然很多类似问题,但终究没有解决,最后我返回了ture,并通过以下代码手动展开和收缩就可以了:
    if(isExpanded){
        expandableListView.collapseGroup(groupPosition);
    }else {
        expandableListView.expandGroup(groupPosition, true);
    }
  • 【4】此外,我们还可以通过setOnGroupExpandListener和setOnGroupCollapseListener来监听ExpandableListView的分组展开和收缩的状态。

4、评论和回复功能


为了模拟整个评论和回复功能,我们还需要手动插入收据并刷新数据列表。这里我就简单做一下模拟,请忽略一些UI上的细节。

4.1插入评论数据


插入评论数据比较简单,只需要在list中插入一条数据并刷新即可:
String commentContent = commentText.getText().toString().trim();
if(!TextUtils.isEmpty(commentContent)){
    //commentOnWork(commentContent);
    dialog.dismiss();
    CommentDetailBean detailBean = new CommentDetailBean("小明", commentContent,"刚刚");
    adapter.addTheCommentData(detailBean);
    Toast.makeText(MainActivity.this,"评论成功",Toast.LENGTH_SHORT).show();
}else {
    Toast.makeText(MainActivity.this,"评论内容不能为空",Toast.LENGTH_SHORT).show();
}

adapter中的addTheCommentData方法如下:
public void addTheCommentData(CommentDetailBean commentDetailBean){
    if(commentDetailBean!=null){
        commentBeanList.add(commentDetailBean);
        notifyDataSetChanged();
    }else {
        throw new IllegalArgumentException("评论数据为空!");
    }
}

代码比较容易理解,就不多做说明了。

4.2插入回复数据


首先,我们需要实现点击某一条评论,然后@ta,那么我们需要在group的点击事件里弹起回复框:
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
        @Override
        public boolean onGroupClick(
            ExpandableListView expandableListView, View view, int groupPosition, long l) {
            showReplyDialog(groupPosition);
            return true;
        }
    });
......
/**
 * func:弹出回复框
 */
private void showReplyDialog(final int position){
    dialog = new BottomSheetDialog(this);
    View commentView = LayoutInflater.from(this).inflate(R.layout.comment_dialog_layout,null);
    final EditText commentText = (EditText) commentView.findViewById(R.id.dialog_comment_et);
    final Button bt_comment = (Button) commentView.findViewById(R.id.dialog_comment_bt);
    commentText.setHint("回复 " + commentsList.get(position).getNickName() + " 的评论:");
    dialog.setContentView(commentView);
    bt_comment.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            String replyContent = commentText.getText().toString().trim();
            if(!TextUtils.isEmpty(replyContent)){
                dialog.dismiss();
                ReplyDetailBean detailBean = new ReplyDetailBean("小红",replyContent);
                adapter.addTheReplyData(detailBean, position);
                Toast.makeText(MainActivity.this,"回复成功",Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(MainActivity.this,"回复内容不能为空",Toast.LENGTH_SHORT).show();
            }
        }
    });
    commentText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        }
        @Override
        public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
            if(!TextUtils.isEmpty(charSequence) && charSequence.length()>2){
                bt_comment.setBackgroundColor(Color.parseColor("#FFB568"));
            }else {
                bt_comment.setBackgroundColor(Color.parseColor("#D8D8D8"));
            }
        }
        @Override
        public void afterTextChanged(Editable editable) {
        }
    });
    dialog.show();
}

插入回复的数据与上面插入评论类似,这里贴一下adapter中的代码:
/**
 * by moos on 2018/04/20
 * func:回复成功后插入一条数据
 * @param replyDetailBean 新的回复数据
 */
public void addTheReplyData(ReplyDetailBean replyDetailBean, int groupPosition){
    if(replyDetailBean!=null){
        Log.e(TAG, "addTheReplyData: >>>>该刷新回复列表了:"+replyDetailBean.toString() );
        if(commentBeanList.get(groupPosition).getReplyList() != null ){
            commentBeanList.get(groupPosition).getReplyList().add(replyDetailBean);
        }else {
            List<ReplyDetailBean> replyList = new ArrayList<>();
            replyList.add(replyDetailBean);
            commentBeanList.get(groupPosition).setReplyList(replyList);
        }
        notifyDataSetChanged();
    }else {
        throw new IllegalArgumentException("回复数据为空!");
    }
}

需要注意一点:由于不一定所有的评论都有回复数据,所以在插入数据前我们要判断ReplyList是否为空,如果不为空,直接获取当前评论的回复列表,并插入数据;如果为空,需要new一个ReplyList,插入数据后还要为评论set一下ReplyList。

5、解决CoordinatorLayout与ExpandableListView嵌套问题


如果你不需要使用CoordinatorLayout或者NestedScrollView,可以跳过本小节

一般情况下,我们产品为了更好的用户体验,还需要我们加上类似的顶部视差效果或者下拉刷新等,这就要我们处理一些常见的嵌套滑动问题了。

由于CoordinatorLayout实现NestedScrollingParent接口,RecycleView实现了NestedScrollingChild接口,所以就可以在NestedScrollingChildHelper的帮助下实现嵌套滑动。

那么我们也可以通过自定义的ExpandableListView实现NestedScrollingChild接口来达到同样的效果:
/**
 * Desc: 自定义ExpandableListView,解决与CoordinatorLayout滑动冲突问题
 */
public class CommentExpandableListView extends ExpandableListView implements NestedScrollingChild{
    private NestedScrollingChildHelper mScrollingChildHelper;
    public CommentExpandableListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScrollingChildHelper = new NestedScrollingChildHelper(this);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setNestedScrollingEnabled(true);
        }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }
    @Override
    public boolean isNestedScrollingEnabled() {
        return mScrollingChildHelper.isNestedScrollingEnabled();
    }
    @Override
    public boolean startNestedScroll(int axes) {
        return mScrollingChildHelper.startNestedScroll(axes);
    }
    @Override
    public void stopNestedScroll() {
        mScrollingChildHelper.stopNestedScroll();
    }
    @Override
    public boolean hasNestedScrollingParent() {
        return mScrollingChildHelper.hasNestedScrollingParent();
    }
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }
}

代码就不解释了,毕竟这不是本篇文章等重点,大家可以去网上查阅NestedScrollView相关文章或者源码去对照理解。

NestedScroll这块还可以学习:
Android NestedScrolling机制完全解析:带你玩转嵌套滑动
一点见解:Android嵌套滑动和NestedScrollView

6、源码附件下载


CommentWithReplyView-master-master(52im.net).zip (350.51 KB , 下载次数: 20 , 售价: 1 金币)

(原文链接:https://www.jianshu.com/p/eda8d09c9d7a,有改动)

附录:全站精品资源下载


[1] 精品源码下载:
Java NIO基础视频教程、MINA视频教程、Netty快速入门视频 [有源码]
轻量级即时通讯框架MobileIMSDK的iOS源码(开源版)[附件下载]
开源IM工程“蘑菇街TeamTalk”2015年5月前未删减版完整代码 [附件下载]
微信本地数据库破解版(含iOS、Android),仅供学习研究 [附件下载]
NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]
NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通信实战 [附件下载]
NIO框架入门(二):服务端基于MINA2的UDP双向通信Demo演示 [附件下载]
NIO框架入门(一):服务端基于Netty4的UDP双向通信Demo演示 [附件下载]
用于IM中图片压缩的Android工具类源码,效果可媲美微信 [附件下载]
高仿Android版手机QQ可拖拽未读数小气泡源码 [附件下载]
一个WebSocket实时聊天室Demo:基于node.js+socket.io [附件下载]
Android聊天界面源码:实现了聊天气泡、表情图标(可翻页) [附件下载]
高仿Android版手机QQ首页侧滑菜单源码 [附件下载]
开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]
分享java AMR音频文件合并源码,全网最全
微信团队原创Android资源混淆工具:AndResGuard [有源码]
一个基于MQTT通信协议的完整Android推送Demo [附件下载]
Android版高仿微信聊天界面源码 [附件下载]
高仿手机QQ的Android版锁屏聊天消息提醒功能 [附件下载]
高仿iOS版手机QQ录音及振幅动画完整实现 [源码下载]
Android端社交应用中的评论和回复功能实战分享[图文+源码]
仿微信的IM聊天时间显示格式(含iOS/Android/Web实现)[图文+源码]

[2] 精品文档和工具下载:
计算机网络通讯协议关系图(中文珍藏版)[附件下载]
史上最全即时通讯软件简史(精编大图版)[附件下载]
重磅发布:《阿里巴巴Android开发手册(规约)》[附件下载]
阿里技术结晶:《阿里巴巴Java开发手册(规约)-终极版》[附件下载]
基于RTMP协议的流媒体技术的原理与应用(技术论文)[附件下载]
独家发布《TCP/IP详解 卷1:协议》CHM版 [附件下载]
良心分享:WebRTC 零基础开发者教程(中文)[附件下载]
MQTT协议手册(中文翻译版)[附件下载]
经典书籍《UNIX网络编程》最全下载(卷1+卷2、中文版+英文版)[附件下载]
音视频开发理论入门书籍之《视频技术手册(第5版)》[附件下载]
国际电联H.264视频编码标准官方技术手册(中文版)[附件下载]
Apache MINA2.0 开发指南(中文版)[附件下载]
网络通讯数据抓包和分析工具 Wireshark 使用教程(中文) [附件下载]
最新收集NAT穿越(p2p打洞)免费STUN服务器列表 [附件下载]
高性能网络编程经典:《The C10K problem(英文)》[附件下载]
即时通讯系统的原理、技术和应用(技术论文)[附件下载]
技术论文:微信对网络影响的技术试验及分析[附件下载]
华为内部3G网络资料: WCDMA系统原理培训手册[附件下载]
网络测试:Android版多路ping命令工具EnterprisePing[附件下载]
Android反编译利器APKDB:没有美工的日子里继续坚强的撸
一款用于P2P开发的NAT类型检测工具 [附件下载]
两款增强型Ping工具:持续统计、图形化展式网络状况 [附件下载]

[3] 精选视频、演讲PPT下载:
开源实时音视频工程WebRTC的架构详解与实践总结(PPT+视频)[附件下载]
QQ空间百亿级流量的社交广告系统架构实践(视频+PPT)[附件下载]
海量实时消息的视频直播系统架构演进之路(视频+PPT)[附件下载]
YY直播在移动弱网环境下的深度优化实践分享(视频+PPT)[附件下载]
QQ空间移动端10亿级视频播放技术优化揭秘(视频+PPT)[附件下载]
RTC实时互联网2017年度大会精选演讲PPT [附件下载]
微信分享开源IM网络层组件库Mars的技术实现(视频+PPT)[附件下载]
微服务理念在微信海量用户后台架构中的实践(视频+PPT)[附件下载]
移动端IM开发和构建中的技术难点实践分享(视频+PPT)[附件下载]
网易云信的高品质即时通讯技术实践之路(视频+PPT)[附件下载]
腾讯音视频实验室:直面音视频质量评估之痛(视频+PPT)[附件下载]
腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT[附件下载]
微信朋友圈海量技术之道PPT[附件下载]
手机淘宝消息推送系统的架构与实践(音频+PPT)[附件下载]
如何进行实时音视频的质量评估与监控(视频+PPT)[附件下载]
Go语言构建高并发消息推送系统实践PPT(来自360公司)[附件下载]
网易IM云千万级并发消息处理能力的架构设计与实践PPT [附件下载]
手机QQ的海量用户移动化实践分享(视频+PPT)[附件下载]
钉钉——基于IM技术的新一代企业OA平台的技术挑战(视频+PPT)[附件下载]
微信技术总监谈架构:微信之道——大道至简(PPT讲稿)[附件下载]
Netty的架构剖析及应用案例介绍(视频+PPT)[附件下载]
声网架构师谈实时音视频云的实现难点(视频采访)
滴滴打车架构演变及应用实践(PPT讲稿)[附件下载]
微信海量用户背后的后台系统存储架构(视频+PPT)[附件下载]
在线音视频直播室服务端架构最佳实践(视频+PPT)[附件下载]
从0到1:万人在线的实时音视频直播技术实践分享(视频+PPT)[附件下载]
微信移动端应对弱网络情况的探索和实践PPT[附件下载]
Android版微信从300KB到30MB的技术演进(PPT讲稿)[附件下载]

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:开源实时音视频工程WebRTC的架构详解与实践总结(PPT+视频)[附件下载]下一篇:美图海量用户的IM架构零基础演进之路(PPT) [附件下载]

本帖已收录至以下技术专辑

推荐方案
评论 3
顶,学习一下
顶,学习一下
刚入门,来学习学习
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部