码迷,mamicode.com
首页 > 移动开发 > 详细

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

时间:2015-04-27 15:13:47      阅读:278      评论:0      收藏:0      [点我收藏+]

标签:煎蛋   高仿   赵凯强   

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

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

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

项目进度

已完成的功能

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

优化的功能

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

想完成但没有完成的功能

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

效果图

技术分享
技术分享
技术分享

项目整体架构介绍

使用的开源框架

项目整体介绍

从上面的效果图也可以看出来,我们使用的是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,这个大家可以自由选择。

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

加载任意高度的图片

我们在前面介绍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方法,如下

@Override
public 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

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

标签:煎蛋   高仿   赵凯强   

原文地址:http://blog.csdn.net/zhaokaiqiang1992/article/details/45306313

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!