一起撸个朋友圈吧(step5) 控件篇【控件组装&评论控件】

334 阅读9分钟

项目地址:https://github.com/razerdp/FriendCircle

上篇链接:http://www.jianshu.com/p/a2cdf81359fc

下篇链接:http://www.jianshu.com/p/ff9788581fb0

如您所见,在公司项目app提测后,在下终于闲下来继续去撸app了。花了一天时间,抱着服务器大哥的大腿狂问,终于初步弄出了一个服务器出来。

之前欠下的评论控件也得以展示了。

ps:在下非常欢迎PR,如果您有更好的想法,可以PR到dev分支哦-V-


预览图如下:(内容页还没开始,所以目前只有共有控件的拼装)

预览


嗯。。。因为部署在本机,所以网速比较快←_←,而且目前在下只弄了4条数据,同时内容区(可变部分)还没开干,所以画面看起来怪怪的。

先不管那么多

本篇主要讲解评论控件的实现:

评论控件采取的依然是继承TextView,做法跟点赞列表控件差不多,但在ListView里面,我们的评论区实现方案大概有两种:

  • ListView的item嵌套ListView?(不可取)
  • ListView的item嵌套LinearLayout,动态添加我们的自定义控件。 很明显,我们采取方案二。

首先实现我们的控件,因为评论控件比较简单,所以我们就不需要attrs了。

/**
 * Created by 大灯泡 on 2016/2/23.
 * 评论控件
 */
public class CommentWidget extends TextView {
    private static final String TAG = "CommentWidget";
    //用户名颜色
    private int textColor = 0xff517fae;
    private static final int textSize = 14;

    private int key;
    private SpannableStringBuilderAllVer mSpannableStringBuilderAllVer;

...构造器

 public void setCommentText(CommentInfo info) {
        if (info == null) return;
        boolean hasContent = false;
        //根据hashCode判断内容是否一致
        if (key == 0) {
            key = info.hashCode();
        }
        else {
            hasContent = (key == info.hashCode());
        }
        if (!hasContent) {
            key = info.hashCode();
            setText("");
            setTag(info);
            createCommentStringBuilder(info);
        }
        else {
            try {
                setText(mSpannableStringBuilderAllVer);
            } catch (NullPointerException e) {
                e.printStackTrace();
                Log.e(TAG, "虽然在下觉得不可能会有这个情况,但还是捕捉下吧,万一被打脸呢。。。");
            }
        }
    }

    private void createCommentStringBuilder(@NonNull CommentInfo info) {
        String content = ": " + info.content + "\0";
        if (mSpannableStringBuilderAllVer == null) {
            mSpannableStringBuilderAllVer = new SpannableStringBuilderAllVer();
            boolean isApply = (info.userB == null);
            // 用户B为空,证明是一条原创评论
            if (info.userA != null && isApply) {
                CommentClick userA = new CommentClick.Builder(getContext(), info.userA).setTextSize(textSize).build();
                mSpannableStringBuilderAllVer.append(info.userA.nick, userA, 0);
                mSpannableStringBuilderAllVer.append(content);
            }
            else if (info.userA != null && !isApply) {
                //用户A,B不空,证明是回复评论
                CommentClick userA = new CommentClick.Builder(getContext(), info.userA).setTextSize(textSize).build();
                mSpannableStringBuilderAllVer.append(info.userA.nick, userA, 0);
                mSpannableStringBuilderAllVer.append("回复");
                CommentClick userB = new CommentClick.Builder(getContext(), info.userB).setTextSize(textSize).build();
                mSpannableStringBuilderAllVer.append(info.userB.nick, userB, 0);
                mSpannableStringBuilderAllVer.append(content);
            }
        }
        setText(mSpannableStringBuilderAllVer);
    }

    public CommentInfo getData() throws ClassCastException {
        return (CommentInfo) getTag();
    }

代码不多,而且在下也写得比较清晰(命名二逼明了,咱们不故作深沉),我们主要观察createCommentStringBuilder()方法,这个方法我们主要判断userB,也就是被回复的用户是否为空,如果是空,则证明这是一条原创评论,也就是针对朋友的评论,不空则是回复别人。

然后CommentClick跟我们的点赞控件一样的ClickableSpan实现,这个没啥好说的。

另外需要注意的是记得在回复的内容后加上'\0',详情见点赞列表那篇。

评论控件大概就这样,但本篇文章重头戏在于控件的组装

还记得我们的一起撸个朋友圈吧(step3) - ListAdapter篇吗,我们的BaseItemDelegate留下了公共部分的初始化,这次我们就顺便的弄回。

我们公共的部分有如下几个(公共部分即无论哪种类型的朋友圈,都会存在的控件):

  • 头像/昵称/用户心情文字,组成item_header
  • 发布时间/评论按钮/点赞列表/评论区,组成item_bottom

这几个是无论如何都会存在的,所以我们的布局文件可以单独抽出来复用(xml就不贴了,又长又无聊):

item_header

item_bottom

值得注意的是,item_bootm里面有个地方嵌套布局比较多,原因如下: 我们的点赞&评论控件处于同一个LinearLayout里面,因为这两者之间存在着一条分割线,所以采用LinearLayout,其次,我们的评论列表则是单独使用LinearLayout动态添加控件的,所以这里嵌套了两层,这无法避免。。。(当然,也可以将点赞和评论控件封装在同一个LinearLayout里面,但个人觉得没必要)

布局弄好后,我们完善我们的baseitem代码

public abstract class BaseItemDelegate
        implements BaseItemView<MomentsInfo>, View.OnClickListener, View.OnLongClickListener {
    protected Activity context;
    //顶部
    protected SuperImageView avatar;
    protected TextView nick;
    protected ClickShowMoreLayout textField;
    //底部
    protected TextView createTime;
    protected ImageView commentImage;
    protected FrameLayout commentButton;
    protected LinearLayout commentAndPraiseLayout;
    protected PraiseWidget praiseWidget;
    protected View line;
    protected LinearLayout commentLayout;

    //中间内容层
    protected RelativeLayout contentLayout;

常量大概就是这些,内容层里面也许是gridview(图片),也许是viewGroup(网页分享),这个是不可变甚至可能没有的,所以我们这里仅负责调整间距,不负责其可视性,其余的控件都是共有的。

关于SuperImageView,这个东东是继承ImageView封装了Glide的方法,关于这个在我的毕业论文备忘录中的Day4 - ImageView封装Glide方法有记载,这里就不详述了

在我们的item里面,所有view的操作都是在onBindData进行的,我们父类进行初始化共有控件主要以下几个方法:

 @Override
    public void onBindData(int position, @NonNull View v, @NonNull MomentsInfo data, final int dynamicType) {
        mInfo = data;
        //初始化共用部分
        bindView(v);
        bindShareData(data);
        bindData(position, v, data, dynamicType);
    }
  • bindView(v),这里进行共有控件的findViewById,此处不展示
  • bindShareData(data),这里进行共有控件的数据展示
  • bindData(position, v, data, dynamicType),这个是抽象方法,交由子类实现,确保子类执行到这里的时候父类的共有控件初始完成。

本篇我们关注bindShareData(data)方法。

这个方法内容如下:

/** 共有数据绑定 */
    private void bindShareData(MomentsInfo data) {
        avatar.loadImageDefault(data.userInfo.avatar);
        nick.setText(data.userInfo.nick);
        textField.setText(data.textField);

        if (TextUtils.isEmpty(data.textField) && contentLayout != null) {
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) contentLayout.getLayoutParams();
            params.topMargin = -UIHelper.dipToPx(context, 8);
            contentLayout.setLayoutParams(params);
        }
        else {
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) contentLayout.getLayoutParams();
            params.topMargin = 0;
            contentLayout.setLayoutParams(params);
        }
        createTime.setText(TimeUtil.getTimeString(data.dynamicInfo.createTime));
        setCommentPraiseLayoutVisibility(data);
        //点赞
        praiseWidget.setDatas(data.praiseList);
        //评论
        addCommentWidget(data.commentList);
    }

我们重点关注addCommentWidget,在前面说过,我们的评论列表使用LinearLayout进行addView。

但这会导致一个问题:由于我们是一个listview,而我们的baseitem本质上是一个viewholder,这也就意味着假如我们划出屏幕,再滑回来,就会出现在原有的view基础上又重复add了一次。

也许有人说,那我们每次removeAllViews后再add不就可以了么,这的确可行,但假如量一大,比如连续10条朋友圈都包含着20~50条评论,也就意味着滑出去再滑回来就需要new 50个commentwidget,这造成的就是视觉上的卡顿,体验十分不好。

而我的解决方法目前想到两个:

  • 维持一个池,从池里拿出可用的进行复用(期望,暂未实现)
  • 动态添加/减去差额,多出remove,少了则new(目前采用,实际上这个方法跟上面的池结合最为妥善)

我目前采用方法2,具体操作如下:

  1. 获取当前评论区控件数量,记为childCount

  2. chidCount与bean的评论数(n)比较

  3. childCount>n,则remove掉childCount-n个view(期望维护一个池,将remove掉的放到复用池)

  4. childCount<n,则new出n-childCount个view

  5. childCount=n,则进行步骤3

  6. 所有view进行数据绑定(数据更新)

这样做的好处就是减少了new对象的操作,起码滑起来顺畅好多。

具体代码如下:

 private void addCommentWidget(List<CommentInfo> commentList) {
        if (commentList == null || commentList.size() == 0) return;
        /**
         * 优化方案:
         * 因为是在listview里面,那么复用肯定有,意味着滑动的时候必须要removeView或者addView
         * 但为了性能提高,不可以直接removeAllViews
         * 于是采取以下方案:
         *    根据现有的view进行remove/add差额
         *    然后统一设置
         * */
        final int childCount = commentLayout.getChildCount();
        if (childCount < commentList.size()) {
            //当前的view少于list的长度,则补充相差的view
            int subCount = commentList.size() - childCount;
            for (int i = 0; i < subCount; i++) {
                CommentWidget commentWidget = new CommentWidget(context);
                LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
                params.topMargin=1;
                params.bottomMargin=1;
                commentWidget.setLayoutParams(params);
                commentWidget.setLineSpacing(4,1);
                commentWidget.setOnClickListener(this);
                commentWidget.setOnLongClickListener(this);
                commentLayout.addView(commentWidget);
            }
        }
        else if (childCount > commentList.size()) {
            //当前的view的数目比list的长度大,则减去对应的view
            commentLayout.removeViews(commentList.size(), childCount - commentList.size());
        }
        //绑定数据
        for (int n = 0; n < commentList.size(); n++) {
            CommentWidget commentWidget = (CommentWidget) commentLayout.getChildAt(n);
            if (commentWidget != null) commentWidget.setCommentText(commentList.get(n));
        }
    }

最后是评论控件和点赞列表控件的分割线判定与layout可视性判定:

  /** 是否有点赞或者评论 */
    private void setCommentPraiseLayoutVisibility(MomentsInfo data) {
        if ((data.commentList == null || data.commentList.size() == 0) &&
                (data.praiseList == null || data.praiseList.size() == 0)) {
            //全空,取消显示
            commentAndPraiseLayout.setVisibility(View.GONE);
        }
        else {
            //某项不空,则展示layout
            commentAndPraiseLayout.setVisibility(View.VISIBLE);
            //点赞或者评论某个为空,分割线不展示
            if (data.commentList == null || data.commentList.size() == 0 ||
                    data.praiseList == null || data.praiseList.size() == 0) {
                line.setVisibility(View.GONE);
            }
            else {
                line.setVisibility(View.VISIBLE);
            }
            //点赞为空,取消点赞控件的可见性
            if (data.praiseList == null || data.praiseList.size() == 0) {
                praiseWidget.setVisibility(View.GONE);
            }
            else {
                praiseWidget.setVisibility(View.VISIBLE);
            }
            //评论
            if (data.commentList == null || data.commentList.size() == 0) {
                commentLayout.setVisibility(View.GONE);
            }
            else {
                commentLayout.setVisibility(View.VISIBLE);
            }
        }
    }

其余的相关代码,如bean实体,volley初始化等请看源码,这里就不写出来了。

下一篇将会进行内容页的定制以及默默地精神分裂构造朋友圈虚拟数据。

【附:】本篇JSON数据:

JSON

{
data: {
hostInfo: {
hostid: 1001,
hostAvatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
hostNick: "羽翼君",
hostWallPic: "http://www.pp3.cn/uploads/allimg/111118/10562Cb5-13.jpg"
},
moments: [
{
userInfo: {
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
},
dynamicInfo: {
dynamicId: 10001,
createUserId: 1001,
dynamicType: 10,
praiseState: 1,
createTime: 1456296202,
candelete: 1
},
textField: "这是第一条朋友圈哦",
praiseList: [
{
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
},
{
nick: "涵菱",
avatar: "http://img7.3wmm.cc/pic/c/f/d/cfd2c2291ba75df42efedfe4bc62ee39.jpg",
userId: 1004
},
{
nick: "短发美比我在这i",
avatar: "http://img0w.pconline.com.cn/pconline/1310/29/3719457_13667094527.jpg",
userId: 1044
},
{
nick: "丑化小丑不丑。",
avatar: "http://img1.touxiang.cn/uploads/20141128/28-021805_451.jpg",
userId: 1054
}
],
commentList: [
{
userA: {
nick: " 振然",
avatar: "http://cdn.duitang.com/uploads/item/201408/30/20140830175648_js4hP.png",
userId: 1014
},
commentId: 1,
content: "新年好",
candelete: 1,
createTime: 1454397315
},
{
userA: {
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
},
commentId: 1,
content: "hello~",
candelete: 1,
createTime: 1454483655
},
{
userA: {
nick: "诗雁",
avatar: "http://img4.duitang.com/uploads/item/201601/11/20160111175420_ZmTzU.jpeg",
userId: 1006
},
userB: {
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
},
commentId: 1,
content: "哇,好巧-V-",
candelete: 1,
createTime: 1454483715
}
]
},
{
userInfo: {
nick: "傲露",
avatar: "http://img1.hao661.com/uploads/allimg/c141030/141463I01W940-5IH0.jpg",
userId: 1010
},
dynamicInfo: {
dynamicId: 10003,
createUserId: 1010,
dynamicType: 11,
praiseState: 1,
createTime: 1454743095,
candelete: 0
},
textField: "咳咳。。。。测试一下",
praiseList: [
{
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
},
{
nick: "凌之",
avatar: "http://img5.imgtn.bdimg.com/it/u=3341777813,2293496692&fm=11&gp=0.jpg",
userId: 1003
},
{
nick: "白雪",
avatar: "http://img5.duitang.com/uploads/item/201406/26/20140626190424_TCXuP.jpeg",
userId: 1009
},
{
nick: "柔胤",
avatar: "http://img5.imgtn.bdimg.com/it/u=660454163,590477124&fm=11&gp=0.jpg",
userId: 1011
},
{
nick: "琪家",
avatar: "http://img5.duitang.com/uploads/item/201502/01/20150201174019_A5LYU.png",
userId: 1015
},
{
nick: "暮色伊人。",
avatar: "http://b.hiphotos.baidu.com/zhidao/wh%3D600%2C800/sign=6a5d1183d358ccbf1be9bd3c29e89006/9213b07eca806538d5541c2295dda144ad348241.jpg",
userId: 1041
},
{
nick: "短发美比我在这i",
avatar: "http://img0w.pconline.com.cn/pconline/1310/29/3719457_13667094527.jpg",
userId: 1044
},
{
nick: "~花舞う街で~",
avatar: "http://c.hiphotos.baidu.com/zhidao/wh%3D450%2C600/sign=fa3f854c8618367aaddc77d91b43a7e2/bba1cd11728b4710f37cb5a9c3cec3fdfc032307.jpg",
userId: 1045
},
{
nick: "妖视觉〃",
avatar: "http://img1.touxiang.cn/uploads/20141128/28-021817_497.jpg",
userId: 1063
},
{
nick: "墨烟三色倾人城。",
avatar: "http://img1.touxiang.cn/uploads/20140815/15-072749_540.jpg",
userId: 1079
},
{
nick: "别嘲笑胖女孩!",
avatar: "http://img1.touxiang.cn/uploads/20140812/12-072839_61.jpg",
userId: 1087
},
{
nick: "默 ’_哀、",
avatar: "http://img1.touxiang.cn/uploads/20140812/12-072932_837.jpg",
userId: 1094
}
],
commentList: [
{
userA: {
nick: "透过骨z1里的傲 つ",
avatar: "http://img1.touxiang.cn/uploads/20141128/28-021810_437.jpg",
userId: 1058
},
commentId: 4,
content: "这是啥",
candelete: 0,
createTime: 1454746695
},
{
userA: {
nick: "傲露",
avatar: "http://img1.hao661.com/uploads/allimg/c141030/141463I01W940-5IH0.jpg",
userId: 1010
},
userB: {
nick: "透过骨z1里的傲 つ",
avatar: "http://img1.touxiang.cn/uploads/20141128/28-021810_437.jpg",
userId: 1058
},
commentId: 4,
content: "have a test",
candelete: 0,
createTime: 1454746698
},
{
userA: {
nick: "透过骨z1里的傲 つ",
avatar: "http://img1.touxiang.cn/uploads/20141128/28-021810_437.jpg",
userId: 1058
},
userB: {
nick: "傲露",
avatar: "http://img1.hao661.com/uploads/allimg/c141030/141463I01W940-5IH0.jpg",
userId: 1010
},
commentId: 4,
content: "噢~so ga",
candelete: 0,
createTime: 1454750298
}
],
content: {
imgurl: [ ],
dynamicid: 0
}
},
{
userInfo: {
nick: "~花舞う街で~",
avatar: "http://c.hiphotos.baidu.com/zhidao/wh%3D450%2C600/sign=fa3f854c8618367aaddc77d91b43a7e2/bba1cd11728b4710f37cb5a9c3cec3fdfc032307.jpg",
userId: 1045
},
dynamicInfo: {
dynamicId: 10002,
createUserId: 1045,
dynamicType: 11,
createTime: 1454656515,
candelete: 0
},
praiseList: [ ],
commentList: [
{
userA: {
nick: "皓博",
avatar: "http://t2.du114.com/uploads/160105/18-16010511202M47.jpg",
userId: 1012
},
commentId: 3,
content: "~",
candelete: 0,
createTime: 1454742975
}
],
content: {
imgurl: [ ],
dynamicid: 0
}
},
{
userInfo: {
nick: "白雪",
avatar: "http://img5.duitang.com/uploads/item/201406/26/20140626190424_TCXuP.jpeg",
userId: 1009
},
dynamicInfo: {
dynamicId: 10004,
createUserId: 1009,
dynamicType: 11,
praiseState: 1,
createTime: 1454483715,
candelete: 0
},
textField: "我发发图,我不说话。",
praiseList: [
{
nick: "羽翼君",
avatar: "http://upload.jianshu.io/users/upload_avatars/684042/bd1b2f796e3a.jpg",
userId: 1001
}
],
commentList: [
{
userA: {
nick: "~花舞う街で~",
avatar: "http://c.hiphotos.baidu.com/zhidao/wh%3D450%2C600/sign=fa3f854c8618367aaddc77d91b43a7e2/bba1cd11728b4710f37cb5a9c3cec3fdfc032307.jpg",
userId: 1045
},
commentId: 2,
content: "路过评论。。。",
candelete: 0,
createTime: 1454742855
}
],
content: {
imgurl: [
"http://img5.duitang.com/uploads/item/201206/06/20120606175201_WZ2F3.thumb.700_0.jpeg",
"http://img5.duitang.com/uploads/item/201206/06/20120606175201_WZ2F3.thumb.700_0.jpeg",
"http://img5.duitang.com/uploads/item/201206/06/20120606175201_WZ2F3.thumb.700_0.jpeg"
],
dynamicid: 10004
}
}
]
},
stateCode: 200,
requestTime: 1456418501691,
start: 4,
loadMore: 0
}