【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解

来源:互联网 发布:日本心神战斗机 知乎 编辑:程序博客网 时间:2024/04/30 12:12

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992

在前一篇文章中,我们学习了如何进行逆向工程和TcpDump进行抓包,获取我们的数据接口,那么有了数据之后,我们就可以开始代码编写工作了。
本项目在前几天获得了daimajia大神的推荐,star数已经达到115,多谢大家的支持,欢迎提建议和意见。
项目地址:https://github.com/ZhaoKaiQiang/JianDan

  • 项目进度
    • 已完成的功能
    • 优化的功能
    • 想完成但没有完成的功能
    • 效果图
  • 项目整体架构介绍
    • 使用的开源框架
    • 项目整体介绍
  • 项目中遇到的问题及解决方案
    • 加载任意高度的图片
    • 评论的楼中楼多楼隐藏效果实现

目前项目已完成以下功能,本文章将会总结在编码过程中遇到的挑战和解决方案。

项目进度

已完成的功能

  • 查看段子
  • 查看无聊图(静态图和GIF动态图)
  • 查看妹子图(程序员必备)
  • 对段子、无聊图、妹子图进行投票
  • 段子的复制与分享
  • 无聊图、妹子图的保存与分享
  • 查看吐槽与回复
  • 图片详情页的动画效果
  • 添加新鲜事列表页
  • 添加新鲜事详情页

优化的功能

  • 添加加载等待动画
  • 添加加载失败提示
  • 添加段子列表界面,点击标题栏快速返回顶端
  • 添加评论楼层过多隐藏
  • 添加网络状态检测
  • 优化无聊图列表显示,非WIFI状态下,显示GIF缩略图,点击后下载
  • 加载模式全自动智能切换,显著提高加载速度,节省大量流量
  • 修改图片详情页为完全沉浸效果
  • 图片详情页添加投票结果的颜色标示
  • 添加图片列表滚动检测,滚动状态暂停加载,进一步提高加载速度,减少卡顿
  • 添加图片加载图片
  • 添加当前栏目标志,避免重复切换
  • 修改新鲜事列表页效果为CardView

想完成但没有完成的功能

  • 列表加载动画(虽然试过好多次,但是都不能实现首次加载,CardView进入时的动画效果,如果你能知道我如何实现,我将非常感激)
  • 本地缓存(后期将添加)

效果图



项目整体架构介绍

使用的开源框架

  • Volley
  • Fresco
  • Universal Image Load
  • butter knife
  • EventBus
  • material-dialogs
  • gson

项目整体介绍

从上面的效果图也可以看出来,我们使用的是Material Design风格,但是并不纯正,为了兼容4.x版本,我们使用Theme.AppCampat兼容主题、RecycleView和CardView来完成,从整体视觉效果来看比较统一和美观。同时为了整体的效果,使用开源项目material-dialogs来实现Material Design效果的对话框,这个在点击回复,完善个人信息的功能点上有所体现。

除了界面,网络请求框架我选择的是Volley,原因是Volley对小数据量、请求频繁的网络操作进行了优化,对于这个项目比较合适,而且作为Google的推荐项目,现在已经完善的比较成熟了,经过了很多项目的实战验证,所以比较放心。而且扩展性非常强,可以定制我们自己的请求解析需求,这一点相信看过我项目的朋友,应该有所感受,在com.socks.jiandan.net包下的请求类都经过了我的定制,使用方便。而且很重要的一点是,Volley在2.3之后是基于HttpURLConnection的封装实现,默认支持gzip压缩,在4.0之后的版本,还支持结果缓存,所以在性能和数据传输量上,相比HttpClient有很大的提高。

在本项目中一个很重要的功能就是加载图片,所以在图片加载框架上需要特别注意。最初我选择的图片加载框架是Fresco,因为之前翻译过关于Fresco的特性的文章,感觉非常的强大,所以想试一试。但是在后面使用的时候,还是遇到了很多的问题,让我不得不暂时放弃Fresco,改用UIL。原因如下:

  • 推出时间太短,虽然功能强大,但是还没经过考验,还不很成熟。Fresco的更新频率很快,我开始用的时候还是0.1.0版本,后来在加载图片的时候遇到问题,在这个版本上,Fresco没有对有304缓存的图片进行处理,所以在加载这类图片的时候会出现失败,我给Fresco项目提交issue之后,他们回复我,Fresco已经升级,在0.2.0完成了问题修复。所以我觉得,Fresco还需要一段时间的考验和完善,才能被用到生产环境中,现在我不很推荐大家在项目中使用
  • 不支持wrap_content。放弃Fresco的一个很重要的原因就是因为它不支持wrap_content,Fresco只支持match和固定长宽,在这个项目中需要展示大量宽度match,高度不定的图片,因为Fresco显示图片的控件也是自己定制的,所以自定义控件这条路也比较难走,在没有找到更好的解决方案的情况下,我决定暂时放弃Fresco,改用UIL。在本项目中,只有在评论列表页的头像是使用的Fresco,其他地方都是使用UIL和自定义控件实现,具体实现方案我会在下面讲到。

在IOC框架的选择上,使用butter knife,之前一直使用AFinal,但是AFinal属于运行期绑定,会影响性能,butter knife属于编译期绑定,不会影响。使用butter knife使用非常方便,就拿来一用。在本项目中,我感觉其实并不是很需要IOC,仅作一个尝试而已,不必深究。

在完成网络状态切换的功能上,需要在MainActivity注册一个网络状态监听器,当网络状态发生改变的时候,通知当前显示的Fragment切换图片的加载模式,或者是提示网络状态变化情况。在这种需求下,使用接口是可以完成的,每个Fragment都实现MainActivity的一个接口,当网络状态发生变化的时候,MainActivity调用Fragment的接口方法即可。但是这样不仅很麻烦,而且会增加耦合性,为此,我使用EventBus完成了这个功能,实现很简单,大家看源码就可以,耦合度为0。

这个项目中的所有数据接口基本都是Json格式,所以选择一个好的解析框架是很重要的。我之前写过三篇文章介绍了Json的不同解析方法,虽然Jackson的解析速度快,但是gson确实用起来很熟悉,而且我们要解析的数据量并不大,性能上的差异微乎其微,所以我选择了我比较熟悉的gson。在解析的一些地方还用到了一些JSON,这个大家可以自由选择。

  • Json
  • Gson
  • Jackson

项目中遇到的问题及解决方案

加载任意高度的图片

我们在前面介绍Fresco的时候提到过,之所以放弃它,很大的一个原因就是因为这个功能它不支持,我们先来看看我们要实现功能的详细分析。

  • 图片宽度要和ImageView的相同
  • 在上一个条件满足的情况下,完整的显示这个图片,高度自适应

也就是下面的效果

我的解决思路是这样的,宽度和ImageView相同,那么设置为match_parent即可,高度则是wrap_content,但是这样显示之后,图片可以完整显示,但是不能符合我们宽度填充,高度自适应的要求。那么我们可能就要设置ScaleType了,但是在试过了所有类型之后,也都满足不了我们的要求,要不就是只能显示一部分,要不就是宽度不能填充,或者是不能居中显示。为此,我们可以试一试自定义控件。

我们可以设置ScaleType为centerCrop,还记得centerCrop是什么意思么?以图片几何中心为基准,放缩短边至填充满。这样做的话,第一个填充效果就可以实现了,剩下的就是要高度自适应了。

我第一次在做这个功能的时候,走入了一个误区。

第一个思路就是,重写ImageView的setBitmap和setDrawable方法,在设置之后,获取bitmap,然后计算ImageView的宽度和bitmap的比例,以此比例计算bitmap的高度,然后生成新的Bitmap对象,设置给ImageView,设置之后,调用requestLayout(),重新布局,完成高度的改变。首先,使用这个方案是完全能解决问题的,计算完之后,重新布局,可以使得高度自适应,但是,你发现问题了吗?我在计算高度之后,又重新生成了Bitmap对象,而这一步是使用下面的方法完成的

 Matrix matrix = new Matrix(); matrix.postScale(1.5f,1.5f); //长和宽放大缩小的比例 Bitmap resizeBmp  = Bitmap.createBitmap(bitmap,0,0,Width,height,matrix,true);

在这个操作里面,使用到了矩阵,而矩阵计算会占用大量cpu时间,因此,当我这么完成之后,慢慢滑动列表是没有问题的,但是当我疯狂的快速滑动的时候,就会出现非常明显的卡顿。

那么怎么解决这个问题呢?其实我后来看代码,完全没必要再生成新的Bitmap,只计算合适的高度就可以完成我们的需求,因此,修改之后的代码如下

/** * 自定义控件,用于显示宽度和ImageView相同,高度自适应的图片显示模式. * 除此之外,还添加了最大高度限制,若图片长度大于等于屏幕长度,则高度显示为屏幕的1/3 * Created by zhaokaiqiang on 15/4/20. */public class ShowMaxImageView extends ImageView {    private float mHeight = 0;    public ShowMaxImageView(Context context) {        super(context);    }    public ShowMaxImageView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public ShowMaxImageView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    public void setImageBitmap(Bitmap bm) {        if (bm != null) {            getHeight(bm);        }        super.setImageBitmap(bm);        requestLayout();        invalidate();    }    @Override    public void setImageDrawable(Drawable drawable) {        if (drawable != null) {            getHeight(drawableToBitamp(drawable));        }        super.setImageDrawable(drawable);        requestLayout();        invalidate();    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        if (mHeight != 0) {            int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);            int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);            int resultHeight = (int) Math.max(mHeight, sizeHeight);            if (resultHeight >= ScreenSizeUtil.getScreenHeight((Activity) getContext())) {                resultHeight = ScreenSizeUtil.getScreenHeight((Activity) getContext()) / 3;            }            setMeasuredDimension(sizeWidth, resultHeight);        } else {            super.onMeasure(widthMeasureSpec, heightMeasureSpec);        }    }    private void getHeight(Bitmap bm) {        float bitmapWidth = bm.getWidth();        float bitmapHeight = bm.getHeight();        if (bitmapWidth > 0 && bitmapHeight > 0) {            float scaleWidth = getWidth() / bitmapWidth;            if (scaleWidth != 0) {                mHeight = bitmapHeight * scaleWidth;            }        }    }    private Bitmap drawableToBitamp(Drawable drawable) {        if (drawable != null) {            BitmapDrawable bd = (BitmapDrawable) drawable;            return bd.getBitmap();        } else {            return null;        }    }}

使用上面的代码之后,我们就完成了图片的完整显示,而且没有任何的性能问题,至此,问题解决。

评论的“楼中楼”、“多楼隐藏”效果实现

在讲解具体实现之前,我们需要先了解一下评论列表的数据结构。

我们以这个测试接口为例:http://jandan.duoshuo.com/api/threads/listPosts.json?thread_key=comment-2750904

因为数据太多,我就不在这里粘贴了,大家自己打开看就可以。

煎蛋使用的是多说的评论接口,所以获取接口都是从多说获取。

从上往下的标签意义如下:

  • hotPosts 热门评论
  • thread 当前被评论主体的信息,包括thread_id、thread_key、url、comments等重要数据
  • cursor 总数和评论页码
  • parentPosts 所有的具体评论数据
  • response 参与回复的所有主体的id
  • options 可选属性,暂时无用

我们需要重点关注的是hotPosts、parentPosts。

在了解我们要显示的数据结构之后,我们就要思考如何去实现我们的评论列表的效果。

第一个问题是,如何添加“热门评论”、“最新评论”的分割标志,并对评论进行分类。
这一步我实在自定义Request里面完成的,为了完成这个跟功能,我们需要一个评论的实体类,下面是重要字段

//评论内容标签    public static final String TAG_HOT = "hot";    public static final String TAG_NORMAL = "normal";    //评论布局类型    public static final int TYPE_HOT = 0;    public static final int TYPE_NEW = 1;    public static final int TYPE_NORMAL = 2;    private String avatar_url;    private String created_at;    private String name;    private String message;    //评论发送者id    private String post_id;    //这条评论所回复的评论id    private String parent_id;    //这条评论上的所有评论id    private String[] parents;    //所属楼层    private int floorNum;    //用于标示是否是热门评论    private String tag;    //用于区别布局类型:热门评论、最新评论、普通评论    private int type;

我们需要内容标签和布局类型,热门评论需要单独筛选出来显示,其他评论按照时间排序,算作最新评论,下面是自定义的Request的实现

/** * Created by zhaokaiqiang on 15/4/10. */public class Request4CommentList extends Request<ArrayList<Commentator>> {    private Response.Listener<ArrayList<Commentator>> listener;    private LoadFinishCallBack callBack;    public Request4CommentList(String url, Response            .Listener<ArrayList<Commentator>> listener,                               Response.ErrorListener errorListener,LoadFinishCallBack callBack) {        super(Method.GET, url, errorListener);        this.listener = listener;        this.callBack = callBack;    }    @Override    protected Response<ArrayList<Commentator>> parseNetworkResponse(NetworkResponse response) {        try {            //获取到所有的数据            String jsonStr = new String(response.data, HttpHeaderParser.parseCharset(response.headers));            //解析出所有的thread_id,并去掉非法字符,便与解析            JSONObject resultJson = new JSONObject(jsonStr);            String allThreadId = resultJson.getString("response").replace("[", "").replace                    ("]", "").replace("\"", "");            String[] threadIds = allThreadId.split("\\,");            callBack.loadFinish(resultJson.optJSONObject("thread").optString("thread_id"));            if (TextUtils.isEmpty(threadIds[0])) {                return Response.success(new ArrayList<Commentator>(), HttpHeaderParser                        .parseCacheHeaders(response));            } else {                //然后根据thread_id再去获得对应的评论和作者信息                JSONObject parentPostsJson = resultJson.getJSONObject("parentPosts");                //找出热门评论                String hotPosts = resultJson.getString("hotPosts").replace("[", "").replace                        ("]", "").replace("\"", "");                String[] allHotPosts = hotPosts.split("\\,");                ArrayList<Commentator> commentators = new ArrayList<>();                List<String> allHotPostsArray = Arrays.asList(allHotPosts);                for (String threadId : threadIds) {                    Commentator commentator = new Commentator();                    JSONObject threadObject = parentPostsJson.getJSONObject(threadId);                    //解析评论,打上TAG                    if (allHotPostsArray.contains(threadId)) {                        commentator.setTag(Commentator.TAG_HOT);                    } else {                        commentator.setTag(Commentator.TAG_NORMAL);                    }                    commentator.setPost_id(threadObject.optString("post_id"));                    commentator.setParent_id(threadObject.optString("parent_id"));                    String parentsString = threadObject.optString("parents").replace("[", "").replace                            ("]", "").replace("\"", "");                    String[] parents = parentsString.split("\\,");                    commentator.setParents(parents);                    //如果第一个数据为空,则只有一层                    if (TextUtil.isNull(parents[0])) {                        commentator.setFloorNum(1);                    } else {                        commentator.setFloorNum(parents.length + 1);                    }                    commentator.setMessage(threadObject.optString("message"));                    commentator.setCreated_at(threadObject.optString("created_at"));                    JSONObject authorObject = threadObject.optJSONObject("author");                    commentator.setName(authorObject.optString("name"));                    commentator.setAvatar_url(authorObject.optString("avatar_url"));                    commentator.setType(Commentator.TYPE_NORMAL);                    commentators.add(commentator);                }                return Response.success(commentators, HttpHeaderParser.parseCacheHeaders(response));            }        } catch (Exception e) {            e.printStackTrace();            return Response.error(new ParseError(e));        }    }    @Override    protected void deliverResponse(ArrayList<Commentator> response) {        listener.onResponse(response);    }}

我们在parseNetworkResponse方法里面完成了所有的数据解析,并且将热门评论打上tag区分开来,同时根据parents字段对应的数组长度,判断出当前楼层,至此,我们的数据就准备好了。

那么,解析完数据之后,应该怎么做呢?

我们来看一下请求完之后的回调做了什么。

在Adapter中,我封装了加载数据的方法loadData()。

public void loadData() {            executeRequest(new Request4CommentList(Commentator.getUrlCommentList(thread_key), new Response                    .Listener<ArrayList<Commentator>>() {                @Override                public void onResponse(ArrayList<Commentator> response) {                    google_progress.setVisibility(View.GONE);                    tv_error.setVisibility(View.GONE);                    if (response.size() == 0) {                        tv_no_thing.setVisibility(View.VISIBLE);                    } else {                        commentators.clear();                        ArrayList<Commentator> hotCommentator = new ArrayList<>();                        ArrayList<Commentator> normalComment = new ArrayList<>();                        //添加热门评论                        for (Commentator commentator : response) {                            if (commentator.getTag().equals(Commentator.TAG_HOT)) {                                hotCommentator.add(commentator);                            } else {                                normalComment.add(commentator);                            }                        }                        //添加热门评论标签                        if (hotCommentator.size() != 0) {                            Collections.sort(hotCommentator);                            Commentator hotCommentFlag = new Commentator();                            hotCommentFlag.setType(Commentator.TYPE_HOT);                            hotCommentator.add(0, hotCommentFlag);                            commentators.addAll(hotCommentator);                        }                        //添加最新评论及标签                        if (normalComment.size() != 0) {                            Commentator newCommentFlag = new Commentator();                            newCommentFlag.setType(Commentator.TYPE_NEW);                            commentators.add(newCommentFlag);                            Collections.sort(normalComment);                            commentators.addAll(normalComment);                        }                        mAdapter.notifyDataSetChanged();                    }                    mSwipeRefreshLayout.setRefreshing(false);                }            }, new Response.ErrorListener() {                @Override                public void onErrorResponse(VolleyError error) {                    mSwipeRefreshLayout.setRefreshing(false);                    google_progress.setVisibility(View.GONE);                    tv_error.setVisibility(View.VISIBLE);                    tv_no_thing.setVisibility(View.GONE);                }            }, new LoadFinishCallBack() {                @Override                public void loadFinish(Object obj) {                    thread_id = (String) obj;                }            }));        }    }

在这段代码里面,我们先清空了commentators,这个集合里面将防止我们处理好的数据。我创建了hotCommentator和normalComment两个集合,分别用来存放热门评论和一般评论。整个评论列表是通过RecycleView来做的,我们都知道ListView支持多种布局类型,RecycleView也一样,我们可以根据hotCommentator和normalComment这两个集合的长度来决定是否添加热门评论和最新评论是否显示,如果显示的话,添加一个设置好Type的Commentator对象即可。由于需要使用Collections.sort()进行排序,所以我们的实体类需要实现Comparable接口,然后根据发布时间排序

@Override    public int compareTo(Object another) {        String anotherTimeString = ((Commentator) another).getCreated_at().replace("T", " ");        anotherTimeString = anotherTimeString.substring(0, anotherTimeString.indexOf("+"));        String thisTimeString = getCreated_at().replace("T", " ");        thisTimeString = thisTimeString.substring(0, thisTimeString.indexOf("+"));        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+08"));        try {            Date anotherDate = simpleDateFormat.parse(anotherTimeString);            Date thisDate = simpleDateFormat.parse(thisTimeString);            return -thisDate.compareTo(anotherDate);        } catch (ParseException e) {            e.printStackTrace();            return 0;        }    }

那么怎么实现多种布局呢?
首先,需要实现getItemViewType方法,如下

@Overridepublic int getItemViewType(int position) {    return commentators.get(position).getType();}

设置好ViewType之后,我们在onCreateViewHolder里面就可以根据viewType生成ViewHolder了

        @Override        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {            switch (viewType) {                case Commentator.TYPE_HOT:                case Commentator.TYPE_NEW:                    return new ViewHolder(getLayoutInflater().inflate(R.layout                                    .item_comment_flag, parent,                            false));                case Commentator.TYPE_NORMAL:                    return new ViewHolder(getLayoutInflater().inflate(R.layout.item_comment, parent,                            false));                default:                    return null;            }        }

需要注意的是,我们在创建我们自己的ViewHolder的时候,需要把所有布局里面用到的View都绑定好,如下

private static class ViewHolder extends RecyclerView.ViewHolder {        private TextView tv_name;        private TextView tv_content;        private TextView tv_time;        private LinearLayout ll_vote;        private SimpleDraweeView img_header;        private FloorView floors_parent;        private TextView tv_flag;        public ViewHolder(View itemView) {            super(itemView);            tv_name = (TextView) itemView.findViewById(R.id.tv_name);            tv_content = (TextView) itemView.findViewById(R.id.tv_content);            tv_time = (TextView) itemView.findViewById(R.id.tv_time);            ll_vote = (LinearLayout) itemView.findViewById(R.id.ll_vote);            img_header = (SimpleDraweeView) itemView.findViewById(R.id.img_header);            floors_parent = (FloorView) itemView.findViewById(R.id.floors_parent);            tv_flag = (TextView) itemView.findViewById(R.id.tv_flag);            setIsRecyclable(false);        }    }

其实tv_flag在正常布局里面没有这个TextView,只存在于评论的分割布局里面,但是我们同样需要在这里find出来,否则没法使用。

至于这个setIsRecyclable(false)则是设置当前的ViewHolder不能够复用,因为在这里复用会导致布局混乱,不复用肯定会效率低一些,但是我还没找到其他好的解决方案。

这些工作做完之后,我们就需要在onBindViewHolder里面进行数据绑定了。

@Override        public void onBindViewHolder(ViewHolder holder, int position) {            final Commentator commentator = commentators.get(position);            switch (commentator.getType()) {                case Commentator.TYPE_HOT:                    holder.tv_flag.setText("热门评论");                    break;                case Commentator.TYPE_NEW:                    holder.tv_flag.setText("最新评论");                    break;                case Commentator.TYPE_NORMAL:                    holder.tv_name.setText(commentator.getName());                    holder.tv_content.setText(commentator.getMessage());                    ...                    //有楼层,盖楼                    if (commentator.getFloorNum() > 1) {                        SubComments cmts = new SubComments(addFloors(commentator));                        holder.floors_parent.setComments(cmts);                        holder.floors_parent.setFactory(new SubFloorFactory());                        holder.floors_parent.setBoundDrawer(getResources().getDrawable(                                R.drawable.bg_comment));                        holder.floors_parent.init();                    } else {                        holder.floors_parent.setVisibility(View.GONE);                    }                    ...                    break;            }        }

为了更清晰,我在中间省去了很多代码,前两个case就是填充我们的评论类型分割布局,第三个case则是真正的评论数据的填充,在这里我们就实现了“楼中楼”和“多楼隐藏”效果。

在开始正式介绍之前,我简单的介绍下实现的思路。

首先,我们完成这个效果,需要自定义一个Linearlayout,当只有一层楼时,我们隐藏它,如果有盖楼效果,我们需要把所有的楼层放到这个LinearLayout里面,评论内容放在TextView里面,楼层的外框需要我们单独画出。

下面是评论布局的xml文件

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"                xmlns:fresco="http://schemas.android.com/apk/res-auto"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:paddingTop="16.0dip">    <LinearLayout        android:id="@+id/left"        android:layout_width="56.0dip"        android:layout_height="wrap_content"        android:layout_marginLeft="16.0dip"        android:orientation="vertical"        >        <com.facebook.drawee.view.SimpleDraweeView            android:id="@+id/img_header"            android:layout_width="40dp"            android:layout_height="40dp"            fresco:roundedCornerRadius="5dp"            fresco:placeholderImage="@drawable/ic_loading_small"            />    </LinearLayout>    <RelativeLayout        android:id="@+id/right"        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:layout_toEndOf="@id/left"        android:layout_toRightOf="@id/left"        android:paddingEnd="16.0dip"        android:paddingLeft="0.0dip"        android:paddingRight="16.0dip"        android:paddingStart="0.0dip">        <View            android:id="@+id/left_placeholder"            android:layout_width="16.0dip"            android:layout_height="1.0dip"            android:visibility="visible"/>        <TextView            android:id="@+id/tv_name"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_toRightOf="@id/left_placeholder"            android:ellipsize="end"            android:singleLine="true"            android:text="AAAAAAAAAA"            android:maxLength="10"            android:textColor="@color/primary_text_default_material_light"            android:textSize="15.0sp"            android:textStyle="bold"/>        <TextView            android:id="@+id/tv_time"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_alignBaseline="@id/tv_name"            android:layout_marginLeft="8.0dip"            android:layout_toRightOf="@id/tv_name"            android:text="2 mins ago"            android:textColor="@color/secondary_text_default_material_light"            android:textSize="13.0sp"            android:visibility="visible"/>        <com.socks.jiandan.view.floorview.FloorView            android:id="@+id/floors_parent"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:layout_toRightOf="@id/left_placeholder"            android:layout_marginTop="8dp"            android:layout_below="@id/tv_name"            android:background="@drawable/bg_floor"            />        <TextView            android:id="@+id/tv_content"            android:layout_width="fill_parent"            android:layout_height="wrap_content"            android:layout_below="@id/floors_parent"            android:layout_marginTop="8dp"            android:layout_toRightOf="@id/left_placeholder"            android:lineSpacingExtra="4dp"            android:text="aaa"            android:textColor="@color/primary_text_default_material_light"            android:textSize="14sp"/>        <LinearLayout            android:id="@+id/ll_vote"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_alignBottom="@id/tv_name"            android:layout_alignParentRight="true"            android:gravity="right"            android:orientation="horizontal"            android:visibility="visible">            <LinearLayout                android:id="@+id/support"                android:layout_width="wrap_content"                android:layout_height="fill_parent"                android:orientation="horizontal">                <TextView                    android:id="@+id/like_descr"                    android:layout_width="wrap_content"                    android:layout_height="fill_parent"                    android:gravity="center"                    android:text="OO "                    android:textColor="@color/secondary_text_default_material_light"                    android:textSize="13.0sp"/>                <TextView                    android:id="@+id/like"                    android:layout_width="wrap_content"                    android:layout_height="fill_parent"                    android:gravity="center"                    android:textColor="@color/secondary_text_default_material_light"                    android:textSize="13.0sp"                    android:textStyle="normal"/>            </LinearLayout>            <LinearLayout                android:id="@+id/unsupport"                android:layout_width="wrap_content"                android:layout_height="fill_parent"                android:orientation="horizontal">                <TextView                    android:id="@+id/unlike_descr"                    android:layout_width="wrap_content"                    android:layout_height="fill_parent"                    android:gravity="center"                    android:text="   XX "                    android:textColor="@color/secondary_text_default_material_light"                    android:textSize="13.0sp"/>                <TextView                    android:id="@+id/unlike"                    android:layout_width="wrap_content"                    android:layout_height="fill_parent"                    android:gravity="center"                    android:textColor="@color/secondary_text_default_material_light"                    android:textSize="13.0sp"                    android:textStyle="normal"/>            </LinearLayout>        </LinearLayout>    </RelativeLayout>    <View        android:id="@+id/divider"        android:layout_width="fill_parent"        android:layout_height="1.0px"        android:layout_below="@id/right"        android:layout_marginTop="16.0dip"        android:layout_toEndOf="@id/left"        android:layout_toRightOf="@id/left"        android:background="#ffd9d9d9"/></RelativeLayout>

tv_content是评论内容,floors_parent则是我们自定义的控件,我们重点看下这个是如何实现的。

完成整个盖楼功能,需要三个类,如下

  • FloorView 自定义LinearLayout,完成楼层的内容填充、分割线绘制和布局添加
  • SubComments 对评论的再次封装,每一个评论都对应一个SubComments对象,它里面封装个这个评论的所有楼层内容
  • SubFloorFactory Floor工厂,用于根据不同类型产生正常评论View和隐藏楼层View,同时在产生过程中,完成了数据和View的适配

介绍完这三个类,我们看下用法

                        SubComments cmts = new SubComments(addFloors(commentator));                        holder.floors_parent.setComments(cmts);                        holder.floors_parent.setFactory(new SubFloorFactory());                        holder.floors_parent.setBoundDrawer(getResources().getDrawable(                                R.drawable.bg_comment));                        holder.floors_parent.init();

上面的代码完整的展示了调用的流程。

首先生成一个SubComments数据封装对象,这里调用了一个addFloors方法,代码如下

private List<Commentator> addFloors(Commentator commentator) {            //只有一层            if (commentator.getFloorNum() == 1) {                return null;            }            List<String> parentIds = Arrays.asList(commentator.getParents());            List<Commentator> commentators = new ArrayList<>();            for (Commentator comm : this.commentators) {                if (parentIds.contains(comm.getPost_id())) {                    commentators.add(comm);                }            }            Collections.reverse(commentators);            return commentators;        }

在addFloors里面其实我们就完成了一件事,那就是把当前commentator对象的所有父级对象都找出来,然后添加进集合后按时间排序,这样我们就能获取到一条评论的所有信息啦~

之后,我们又setComments、setFactory、setBoundDrawer,全部设置齐活,调用init()就出来了~

那么init到底做了些什么?下面是FloorView源码

/** * @author JohnnyShieh * @ClassName: FloorView * @Description: * @date Jan 25, 2014 2:09:36 PM */@TargetApi(Build.VERSION_CODES.HONEYCOMB)public class FloorView extends LinearLayout {    private int density;    private Drawable drawer;    private SubComments datas;    private SubFloorFactory factory;    public FloorView(Context context) {        super(context);        init(context);    }    public FloorView(Context context, AttributeSet attrs) {        super(context, attrs);        init(context);    }    public FloorView(Context context, AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);        init(context);    }    public void setBoundDrawer(Drawable drawable) {        drawer = drawable;    }    public void setComments(SubComments cmts) {        datas = cmts;    }    public void setFactory(SubFloorFactory fac) {        factory = fac;    }    public int getFloorNum() {        return getChildCount();    }    private void init(Context context) {        this.setOrientation(LinearLayout.VERTICAL);        density = (int) (3.0F * context.getResources().getDisplayMetrics().density);    }    public void init() {        if (null == datas.iterator())            return;        if (datas.getFloorNum() < 7) {            for (Iterator<Commentator> iterator = datas.iterator(); iterator                    .hasNext(); ) {                View view = factory.buildSubFloor(iterator.next(), this);                addView(view);            }        } else {            View view;            view = factory.buildSubFloor(datas.get(0), this);            addView(view);            view = factory.buildSubFloor(datas.get(1), this);            addView(view);            view = factory.buildSubHideFloor(datas.get(2), this);            view.setOnClickListener(new OnClickListener() {                @Override                public void onClick(View v) {                    TextView hide_text = (TextView) v                            .findViewById(R.id.hide_text);                    hide_text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0,                            0);                    v.findViewById(R.id.hide_pb).setVisibility(View.VISIBLE);                    removeAllViews();                    for (Iterator<Commentator> iterator = datas.iterator(); iterator                            .hasNext(); ) {                        View view = factory.buildSubFloor(iterator.next(),                                FloorView.this);                        addView(view);                    }                    reLayoutChildren();                }            });            addView(view);            view = factory.buildSubFloor(datas.get(datas.size() - 1), this);            addView(view);        }        reLayoutChildren();    }    public void reLayoutChildren() {        int count = getChildCount();        for (int i = 0; i < count; i++) {            View view = getChildAt(i);            LayoutParams layout = new LayoutParams(LayoutParams.MATCH_PARENT,                    LayoutParams.WRAP_CONTENT);            layout.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;            int margin = Math.min((count - i - 1), 4) * density;            layout.leftMargin = margin;            layout.rightMargin = margin;            if (i == count - 1) {                layout.topMargin = 0;            } else {                layout.topMargin = Math.min((count - i), 4) * density;            }            view.setLayoutParams(layout);        }    }    @Override    protected void dispatchDraw(Canvas canvas) {        int i = getChildCount();        if (null != drawer && i > 0) {            for (int j = i - 1; j >= 0; j--) {                View view = getChildAt(j);                drawer.setBounds(view.getLeft(), view.getLeft(),                        view.getRight(), view.getBottom());                drawer.draw(canvas);            }        }        super.dispatchDraw(canvas);    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        if (this.getChildCount() <= 0) {            setMeasuredDimension(0, 0);            return;        }        super.onMeasure(widthMeasureSpec, heightMeasureSpec);    }}

可以看到,在init()首先根据评论数量判断,是否隐藏,不隐藏,就调用SubFloorFactory的buildSubFloor创建一个楼层,要是隐藏呢?就创建一楼、二楼,然后创建一个隐藏楼层,然后创建最后一个楼层。创建完之后调用reLayoutChildren()。

public void reLayoutChildren() {        int count = getChildCount();        for (int i = 0; i < count; i++) {            View view = getChildAt(i);            LayoutParams layout = new LayoutParams(LayoutParams.MATCH_PARENT,                    LayoutParams.WRAP_CONTENT);            layout.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;            int margin = Math.min((count - i - 1), 4) * density;            layout.leftMargin = margin;            layout.rightMargin = margin;            if (i == count - 1) {                layout.topMargin = 0;            } else {                layout.topMargin = Math.min((count - i), 4) * density;            }            view.setLayoutParams(layout);        }    }

在这里面,根据不同的楼层,设置不同的margin,从而显示出一层挨着一层的效果。

那么每一层的间隔线呢?是在dispatchDraw()里面实现的

@Override    protected void dispatchDraw(Canvas canvas) {        int i = getChildCount();        if (null != drawer && i > 0) {            for (int j = i - 1; j >= 0; j--) {                View view = getChildAt(j);                drawer.setBounds(view.getLeft(), view.getLeft(),                        view.getRight(), view.getBottom());                drawer.draw(canvas);            }        }        super.dispatchDraw(canvas);    }

通过重写dispatchDraw(),在画childView之前,先把边框绘制出来,这样就实现了边框效果。注意绘制顺序,super.dispatchDraw(canvas)需要在最后调用,否则会覆盖。

如果存在隐藏楼层,怎么点击全部显示出来呢?

view.setOnClickListener(new OnClickListener() {                @Override                public void onClick(View v) {                    TextView hide_text = (TextView) v                            .findViewById(R.id.hide_text);                    hide_text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0,                            0);                    v.findViewById(R.id.hide_pb).setVisibility(View.VISIBLE);                    removeAllViews();                    for (Iterator<Commentator> iterator = datas.iterator(); iterator                            .hasNext(); ) {                        View view = factory.buildSubFloor(iterator.next(),                                FloorView.this);                        addView(view);                    }                    reLayoutChildren();                }            });

在点击之后,首先removeAllViews(),然后创建了新的view,使用addView添加进去,最后reLayoutChildren()就可以了。

至此,“盖楼”效果就完全实现了。

因为文章太长了,所以剩下的内容只能放到下一篇了,写的好累呀,休息下~

别忘记去项目star一下哦

项目地址:https://github.com/ZhaoKaiQiang/JianDan

14 1
原创粉丝点击