1. 程式人生 > >仿網易新聞評論“蓋樓”效果實現

仿網易新聞評論“蓋樓”效果實現

前言

各位應該對黃易新聞比較熟悉,其中評論區一般都會出現一些蓋樓的神評論,今天的主題就是仿照做一個有蓋樓效果的評論列表。

首先上圖給大家看下效果:

實現效果

思路

資料結構設計

首先分析看下評論的結構,仔細觀摩下發現,有的評論是簡單一條,只有使用者頭像,暱稱,評論內容等;有的評論是回覆別人的評論,這樣就不只是有使用者頭像,評論內容等基礎資訊,還有回覆的別人的內容,這就是所謂“蓋樓”了。那麼怎麼設計評論的model可以包含評論的所有資料呢?

首先滿足簡單評論的model很容易,如下:

public class Comment {
    private int id;
    private
String mReplyTime; private String mAvaterUrl; private String mUsername; private String mUserArea; private String mCommentContent; private int mFavorCount; //getter setter ... }

以上包括了一條簡單評論需要展示項的所有資訊,可是如果是回覆其他評論的評論,這種存在“蓋樓”的評論項,這種結構就無法滿足了。吃根辣條冷靜一下,再分析蓋樓中的內容,發現蓋樓中的一項內容其實是被回覆的評論,其中包括了使用者暱稱,評論內容。
總結來說,一條評論中可能包括了另一條評論,那麼在Comment中新增一個欄位:

private Comment replyTo;

這個欄位儲存了其回覆的評論,這樣就能形成一個類似單向連結串列的結構了,一條評論項中能夠與它回覆的評論關聯起來,並且能夠依次連結,那麼所有回覆的評論內容都能在連結串列中找到,結構清晰明瞭,那麼model就已經設計好了。

介面設計

很明顯這是一個列表結構,最外層咱們可以使用Recyclerview來實現,每個評論項可以使用一個自定義View封裝起來,這個自定義View來處理資料和UI的適配。我們來分析下這個View的構成:
1. 使用者頭像,暱稱,回覆內容等都是基礎內容,每個評論項都會展示出這些資訊。
2. 有的回覆其他評論的評論項中間會多出一個評論“樓層”。

這樣簡單一分析,我們就有了思路了,這個自定義的View可以分解成兩個部分,一個部分就是基礎資訊展示,另個就是評論“樓層”展示。佈局檔案如下:

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

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <io.geek.myapplication.view.CircleView2
            android:id="@+id/iv_avater"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_alignParentTop="true"/>

        <TextView
            android:id="@+id/tv_username"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/iv_avater"
            android:text="Geek"/>

        <TextView
            android:id="@+id/tv_user_area"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/tv_username"
            android:layout_toEndOf="@+id/iv_avater"
            android:layout_toRightOf="@+id/iv_avater"
            android:text="來自火星的網友"/>

        <TextView
            android:id="@+id/tv_reply_time"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBottom="@+id/tv_user_area"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="5dp"
            android:layout_marginTop="5dp"
            android:layout_toRightOf="@+id/tv_user_area"
            android:text="33分鐘前"/>

        <TextView
            android:id="@+id/favor_count"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:text="2896"
            android:textSize="8dp"/>


    </RelativeLayout>

    <io.geek.myapplication.comment.CommentFloorView
        android:id="@+id/comment_floor"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"/>

    <TextView
        android:id="@+id/tv_comment_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="我只是一個普通的評論。"/>
</merge>

tips: 使用merge標籤能夠減少佈局層級,減少介面渲染次數,優化效能。

以上是我們自定義CommentItemView的佈局檔案,現在我們來編寫CommentItemView,這個自定義View繼承LinearLayout,然後構造方法中初始化各個需要繫結資料的view,暴露一個繫結資料的bind方法,這樣就能把資料展示在對應的view上了。程式碼如下:

public class CommentItemView extends LinearLayout {
    CircleImageView avater;
    TextView tvUsername;
    TextView tvUserArea;
    TextView tvReplyTime;
    TextView tvFavorCount;
    CommentFloorView commentFloorView;
    TextView tvCommentContent;
    Comment mComment;

    public CommentItemView(Context context) {
        this(context, null);
    }

    public CommentItemView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CommentItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public CommentItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr);

        initView(context);
    }

    private void initView(Context context) {
        LayoutInflater.from(context).inflate(R.layout.list_item_view_comment, this,true);
        int padding = getResources().getDimensionPixelOffset(R.dimen.activity_horizontal_margin);
        setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        setOrientation(VERTICAL);
        setPadding(padding, padding, padding, padding);

        avater = (CircleImageView) findViewById(R.id.iv_avater);
        tvUsername = (TextView) findViewById(R.id.tv_username);
        tvUserArea = (TextView) findViewById(R.id.tv_user_area);
        tvReplyTime = (TextView) findViewById(R.id.tv_reply_time);
        commentFloorView = (CommentFloorView) findViewById(R.id.comment_floor);
        tvCommentContent = (TextView) findViewById(R.id.tv_comment_content);
    }

    public void bind(Comment comment) {
        mComment = comment;
        tvUsername.setText(comment.getUsername());
        tvUserArea.setText(comment.getUserArea());
        tvReplyTime.setText(comment.getReplyTime());
        tvCommentContent.setText(comment.getCommentContent());
        commentFloorView = (CommentFloorView) findViewById(R.id.comment_floor);

        setupCommentFloorView();
    }

    private void setupCommentFloorView() {
        List<Comment> replyFloor = getReplyFloor();
        if (replyFloor != null) {
            commentFloorView.setVisibility(VISIBLE);
            commentFloorView.updateData(replyFloor);
        } else {
            commentFloorView.setVisibility(GONE);
        }
    }


    //通過comment中的replyTo欄位找到其回覆的評論鏈,按照回覆的順序排列放入list中
    private List<Comment> getReplyFloor( ) {
        List<Comment> floorData = new ArrayList<>();
        if (mComment == null || mComment.getReplyTo() == null) {
            return null;
        }
        Comment reply = mComment.getReplyTo();
        while (reply != null) {
            floorData.add(reply);
            reply = reply.getReplyTo();
        }
        //按照回覆的順序來排列
        Collections.reverse(floorData);
        return floorData;
    }
}

上面的程式碼結構很簡單,沒有啥複雜的邏輯,就是一個簡單點的封裝了各個控制元件的容器,然後有一個繫結資料的方法。可能你看到一個陌生的view:CommentFloorView,以下就是本文需要講的重點!!和難點!!

CommentFloorView也是我們一個自定義view,這個view負責的是評論“樓層”展示的,主體構成是Recylerview,我們來仔細分析下這個評論“樓層”,本質就是一個列表,列表中展示的是評論使用者名稱和評論的內容,如果這個列表超過了一定的數量就會摺疊展示,點選展開樓層會全部展示,並且每個項會有邊框,並不是簡單的每個列表項套一個邊框,而是看起來會有層疊效果的邊框。通過這樣一分析,總結出實現這個view的兩個難點:
1. 展開和隱藏樓層。
2. 邊框的繪製。

  • 難點一

這個recylerview中展示的有兩種view,其一就是評論項,其二就是展開/隱藏樓層的按鈕。熟悉recylerview功能應該會知道重寫adapter中的getItemType方法可以實現這種一個recylerview展示不同的型別item。

首先解析展開和隱藏樓層功能,重寫getItemCount方法,程式碼如下,解析在註釋:

        @Override
        public int getItemCount() {
            if (hasHideFloor()) {
                if (isFloorExpanded) {
                    //如果有有隱藏樓層,並且樓層是展開的狀態
                    //加的1是因為最後還有一個隱藏樓層的項
                    return mComments.size() + 1;
                } else {
                    //如果有隱藏樓層,並且沒有展開,那麼只有4項,分別是第一二項資料,展開樓層按鈕和最後一項資料
                    return 4;
                }
            } else {
                //如果沒有隱藏樓層,則返回資料的數量
                return mComments.size();
            }
        }

        //如果資料量大於一個預設值(這裡設定的是5個),那麼預設會有隱藏樓層,小於的話就無需隱藏
        private boolean hasHideFloor() {
            return mComments.size() > EXPAND_LIMIT_COMMENT_COUNT;
        }

然後重現getItemType方法,只要把邏輯理清楚了其實也很簡單,程式碼如下:

        @Override
        public int getItemViewType(int position) {
            if (hasHideFloor()) {
                if (isFloorExpanded) {
                    if (position == mComments.size()) {
                        //如果有隱藏樓層,並且樓層是展開狀態,並且是最後一項,那麼返回展開/收起按鈕佈局ID
                        return R.layout.list_item_hide_expand_floor;
                    } else {
                        //如果有隱藏樓層,並且樓層是展開狀態,不是最後一項,返回評論項佈局ID
                        return R.layout.list_item_simple_reply_comment;
                    }
                } else {
                    if (position == EXPAND_FLOOR_ITEM_POSITION) {
                        //如果有隱藏樓層,並且樓層是隱藏狀態,並且是指定的展開樓層按鈕位置,那麼返回展開/收起按鈕佈局ID
                        return R.layout.list_item_hide_expand_floor;
                    } else {
                        //如果有隱藏樓層,並且樓層是隱藏狀態,不是展開樓層按鈕位置,那麼評論項佈局ID
                        return R.layout.list_item_simple_reply_comment;
                    }
                }
            } else {
                //如果沒有隱藏樓層,那麼都是返回評論佈局ID
                return R.layout.list_item_simple_reply_comment;
            }
        }

重寫好了以上兩個方法,就能配合onCreateViewHolder和onBindViewHolder方法來實現列表中不同位置生成不同的View,並且繫結響應的資料項。

這樣就能實現UI效果了,但是有個棘手的問題來了,怎麼做到點選展開/隱藏樓層後真的能展開和隱藏部分item呢?不要慌,吃根辣條冷靜分析下,我們已經重寫了getItemCount和getItemType方法,判斷邏輯中有利用判定樓層展開和隱藏的欄位isFloorExpaned來返回不同的item個數和控制在不同位置返回不同的view,其實實現展開和隱藏樓層的核心部分就寫完了,現在我們只需要控制這個isFloorExpanded的值就行了,具體實現就是編寫一個監聽點選展開/隱藏樓層按鈕的介面,然後在adapter構造方法中建立一個監聽器,傳入到展開/隱藏樓層按鈕的ViewHolder中,在點選這個view的時候觸發監聽器,修改isFloorExpanded的值,並且notifyDataSetChanged()方法,重新重新整理recylerview。核心程式碼如下:

 //這個adapter是編寫在CommentFloorView中的,所以使用了static來修飾
 public static class CommentFloorAdapter extends RecyclerView.Adapter {
        private List<Comment> mComments = new ArrayList<>();

        private OnFloorExpandListener mOnFloorExpandListener;
        private boolean isFloorExpanded;

        public CommentFloorAdapter() {
            mOnFloorExpandListener = new OnFloorExpandListener() {
                @Override
                public void onFloorExpand(boolean isExpand) {
                    isFloorExpanded = isExpand;
                    notifyDataSetChanged();
                }
            };

        }



       //.....

        public interface OnFloorExpandListener {
            void onFloorExpand(boolean isExpand);
        }


        static class HideExpandFloorVH extends RecyclerView.ViewHolder {
            FrameLayout mContentView;
            TextView tips;
            OnFloorExpandListener mOnFloorExpandListener;

            public HideExpandFloorVH(View itemView, OnFloorExpandListener onFloorExpandListener, final boolean isFloorExpanded) {
                super(itemView);
                mOnFloorExpandListener = onFloorExpandListener;
                mContentView = (FrameLayout) itemView;
                tips = (TextView) mContentView.findViewById(R.id.tv_is_show_all);

                mContentView.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mOnFloorExpandListener.onFloorExpand(!isFloorExpanded);
                    }
                });
            }

            public void changeTips(boolean isFloorExpanded) {
                tips.setText(isFloorExpanded ? "收起展開樓層" : "展開隱藏樓層");
            }
        }
    }

tips:內部類最好使用static修飾,這樣內部類不會持有外部類的強應用,防止記憶體洩露,優化效能。

這樣核心的功能就已經實現啦~此時的實現效果如下圖:

實現效果
這樣看著和原版的還是有點差距的,這是沒有繪製邊框的原因,吃根辣條休息下,接下來開始解析難點二了。

  • 難點二

還是先要分析下這個邊框是個啥玩意,看著是有層疊的效果。細細看了下,發現它的規律是第一個繪製一個邊框,然後第一個和第二個作為整體外層再繪製一個邊框,再前三個作為一個整體繪製邊框,依次類推。這樣就產生了一種層疊的效果。繪製的規律被我們發現了,那具體如何繪製呢?recyclerview中有一個方法是addItemDecoration,利用ItemDecoration我們可以給每個item繪製邊框或者分割線,這裡我們定義一個CommentFloorItemDecoration類,繼承自ItemDecoration,重寫其中的onDraw和getItemOffsets方法,程式碼如下:

public class CommentFloorItemDecoration extends RecyclerView.ItemDecoration {
    private static float BORDER_OFFSET;
    private static float BORDER_WIDTH;
    private Context mContext;
    private Paint mBorderPaint;

    public CommentFloorItemDecoration(Context context) {
        mContext = context;

        BORDER_OFFSET = context.getResources().getDisplayMetrics().density * 1;
        BORDER_WIDTH = context.getResources().getDisplayMetrics().density * 1;
        mBorderPaint = new Paint();
        mBorderPaint.setAntiAlias(true);
        mBorderPaint.setColor(Color.LTGRAY);
        mBorderPaint.setStyle(Paint.Style.STROKE);
        mBorderPaint.setStrokeWidth(BORDER_WIDTH);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        int itemCount = parent.getChildCount();

        int top = parent.getChildAt(0).getTop();
        for (int i = 0; i < itemCount; i++) {
            View child = parent.getChildAt(i);
            int left = child.getLeft();
            int right = child.getRight();
            int bottom = child.getBottom();
            c.drawRect(left, top, right, bottom, mBorderPaint);
            top -= (BORDER_WIDTH + BORDER_OFFSET);
        }
    }


    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);


        RecyclerView.Adapter adapter = parent.getAdapter();
        int itemCount = adapter.getItemCount();
        int i = parent.getChildAdapterPosition(view);
        int top = (int) ((itemCount - i) * BORDER_WIDTH + (itemCount - i - 1) * BORDER_OFFSET);
        int left = top;
        int right = top;
        int bottom = (int) BORDER_WIDTH;
        if (i == 0) {
            outRect.set(left, top, right, bottom);
        } else {
            outRect.set(left, 0, right, bottom);
        }
    }
}

getItemOffset的作用是得到每個item的偏移量,重寫此方法可以給每個item設定偏移量,在這裡我們按照上面總結出的規律給不同的item設定的不同的offset值,然後再重寫onDraw方法,給item繪製邊框,具體邏輯參看程式碼。最後我們就完整實現蓋樓效果了。

全文就此完畢~( 注意: 此文主要是提供一個思路,和原版細節方面有一定出入,並且沒有運用到實際專案中過,需要使用的同學請自行測試)

相關推薦

仿新聞評論效果實現

前言 各位應該對黃易新聞比較熟悉,其中評論區一般都會出現一些蓋樓的神評論,今天的主題就是仿照做一個有蓋樓效果的評論列表。 首先上圖給大家看下效果: 思路 資料結構設計 首先分析看下評論的結構,仔細觀摩下發現,有的評論是簡單一條,只有使用者頭

仿評論列表效果之介面生成

                前兩節我們分別分析了網易評論列表介面和生成一些我們需要的測試資料,生成測試資料那段如果大家看著看得頭疼沒關係,直接調業務物件中的方法生成資料即可不必理會我是怎麼處理的,接下來的對於大家來說才是讓各位感興趣的東西。介面分析了、資料也有了,那我們如何實現這樣的一個介面呢?首先我們來

Android漂亮的音樂歌詞控制元件,仿雲音樂滑動效果

前言: 專案有個音樂播發器功能,實現音樂線上播放,同時需要帶有歌詞顯示功能。網上也找過,在github找到勉強能用的控制元件,只是效果還是差強人意,不是特別好。於是趁有空的時間,參考了網上的部分demo,自己也寫了個歌詞控制元件。 只要demo可以拉到最底

仿雲音樂滑動效果

前言: 專案有個音樂播發器功能,實現音樂線上播放,同時需要帶有歌詞顯示功能。網上也找過,在github找到勉強能用的控制元件,只是效果還是差強人意,不是特別好。於是趁有空的時間,參考了網上的部分demo,自己也寫了個歌詞控制元件。 只要demo可以拉到最底部。 一.歌詞控制元件效果。 目前的歌詞控制元件效果

Android中RecyclerView學習(二)----高仿新聞欄目動畫效果

之前用TabLayout+RecyclerView實現了CSDN客戶端首頁搭建與Tabs的排序。今天準備用RecyclerView來實現網易新聞Tabs的動態效果。先看效果圖:  點選下面的RecyclerView的item,會有一個view的移動的動畫;動畫完成以後,

自反+遞迴 實現評論的無限引用 效果實現

引言 大家每天都在看部落格,發表評論,實現一個評論系統也是一名Web開發者的基本要求。雖然評論只是一個很普通的功能,但是實現評論的引用,尤其是無限引用,卻有一定的困難。本文不是本人原創,而是引自一名“網易工程隊”的正規軍,向大家展示一下“蓋樓”的方法。下面是他的文章: NO

Android實現仿首頁選項卡動態滑動效果

本文會實現一個類似網易新聞(不說網易新聞大家可能不知道大概是什麼樣子)點選超多選項卡,選項卡動態滑動的效果。  首先來看看佈局,就是用HorizontalScrollView控制元件來實現滑動的效果,裡面包含了一個佈局。 <code class="hljs xml has-numbering" st

微信小程式 --- CSS實現仿雲音樂播放介面效果(黑膠唱片與唱針純CSS實現

下面程式碼的效果是網易雲音樂唱針和黑膠唱片的CSS效果實現方式,播放等並沒貼出來 實現效果的範圍 動態圖效果預覽: stylusW,panW是獲取系統寬度計算後的引數 w

C# WPF 仿雲音樂(PC)左側菜單右側內容效果

ups tle onu urn mar mat 難點 -c nat 我們要做的效果是這樣的,左側是可折疊的菜單欄,右側是內容區域,點擊左側的菜單項右側內容區域則相應地切換。這篇博客標題起得比較隨意了,因為很多軟件、網站都有這種布局效果,所以請忽略。 wpf實現的話,我的辦

Android 仿雲首頁來回滑那種效果

思路:中間ViewPager 畫廊,底部背景ImageView 高斯模糊   過程很曲折:參考了很多 比如網易雲音樂播放器頁面 也有類似效果 底部做成ViewPager 也可以 但是切換動畫 pagertransform 很難弄到合適的 所以最後決定還是ImageVi

Android—(實現仿新聞的頂部導航指示器)

public class MainActivity extends AppCompatActivity implements View.OnClickListener {       private List<View> listViews;       private ImageView

安卓仿雲介面的本地音樂播放器的實現

前言:讀研究生之前,老闆讓學IOS,老老實實看了一個學期的IOS(在這裡強烈安利一下騰訊課堂的小碼哥IOS視訊,基本剛開始只需要10塊買第一部,後面的就可以用可視幣來兌換了,質量還不錯,能學到很多東西,看三部基本IOS就能入門了),結果開學提前兩個月被叫去幹活,一開會,說IO

[iOS開發]關於仿新聞中詳細頁圖文混排功能的實現

{"B4A39DDB00964LQ9":{"body":"   給你們講一個恐怖的故事:聽說從今天開始2015年只剩下100天了!<\/p>   2015年快過去了,年初定下的目標都達成了嗎?時間總是不知不覺匆匆地過去,不留下一絲痕跡。<\/p>   俗語有云:生命很短,我們真的沒必

關於仿新聞中詳細頁圖文混排功能的實現

{"B4A39DDB00964LQ9":{"body":"   給你們講一個恐怖的故事:聽說從今天開始2015年只剩下100天了!<\/p>   2015年快過去了,年初定下的目標都達成了嗎?時間總是不知不覺匆匆地過去,不留下一絲痕跡。<\/p>   俗語有云:生命很短,我們真的沒必

android 使用Scroller實現美團懸浮框,左右滑動選單效果

Scroller類其實就是對View的scrollTo()以及ScrollBy()方法封裝以及優化,比如我們熟悉的網易客戶端主介面的側滑選單,如果要實現當滑動超過一半的時候回彈的效果,而不是一下子就回到了最終點,view的scrollTo(),scrollBy()方法是沒

Android5.0以上實現全透明的狀態列(仿雲介面)

Android4.4以上要實現沉浸式狀態列可以通過在style檔案設定 <style name="AppTheme.NoActionBar"> <item name="windowActionBar">false</item&g

仿雲音樂的滑動效果

附上demo工程下載連結: 其中: <span style="font-family:SimHei;font-size:18px;">android.support.v7.app.ActionBarDrawerToggle:選單 android.sup

基於Node.js+MySQL開發的開源微信小程序B2C商城(頁面高仿嚴選)

收貨地址 lec load alt conf print 商品列表 pac data 高仿網易嚴選的微信小程序商城(微信小程序客戶端) 界面高仿網易嚴選商城(主要是2016年wap版) 測試數據采集自網易嚴選商城 功能和數據庫參考ecshop 服務端api基於Node.j

JS之選項卡-仿新聞

com selected ons utf-8 put adding mouse ext 網易 1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8">

原生js仿輪播圖

win cli font inline move 創建 ica tle 默認 <!-- HTML部分 --> <div id="wrap"> <div class="picBox"> <!-- 圖片區域 -->