Android如何實現(xiàn)社交應用中的評論與回復功能詳解
前言
在Android的日常開發(fā)中,評論與回復功能是我們經(jīng)常遇到的需求之一,其中評論與回復列表的展示一般在功能模塊中占比較大。對于需求改動和迭代較頻繁的公司來說,如何快速開發(fā)一個二級界面來適應我們的功能需求無疑優(yōu)先級更高一些。首先我們來看看其他社交類app的評論與回復列表如何展示的:
Twitter不用說了,全球知名社交平臺,上億用戶量,他們的評論回復都只展示一級數(shù)據(jù)(評論數(shù)據(jù)),其他更多內(nèi)容(回復內(nèi)容),是需要頁面跳轉(zhuǎn)去查看,知乎也類似。第一張圖是我們設計給我找的,他說要按照這個風格來,盡量將評論和回復內(nèi)容在一個頁面展示。好吧,沒辦法,畢竟我們做前端的,UI要看設計臉色,數(shù)據(jù)要看后臺臉色��??吹皆O計圖,我們腦海肯定第一時間聯(lián)想一下解決方案:用recyclerview?listview?不對,分析一下它的層級發(fā)現(xiàn),評論是一個列表,里面的回復又是一個列表,難道用recyclerview或者listview的嵌套?抱著不確定的態(tài)度,立馬去網(wǎng)上查一下,果不其然,搜到的實現(xiàn)方式大多都是用嵌套實現(xiàn)的,來公司之前,其中一個項目里的評論回復功能就是用的嵌套listview,雖然處理了滑動沖突問題,但效果不佳,而且時??D,所以,這里我肯定要換個思路。
網(wǎng)上還有說用自定義view實現(xiàn)的,但我發(fā)現(xiàn)大多沒有處理view的復用,而且開發(fā)成本大,暫時不予考慮。那怎么辦?無意中看到expandable這個關鍵詞,我突然想到谷歌很早之前出過一個擴展列表的控件 - ExpandableListView,但聽說比較老,存在一些問題。算了,試試再說,順便熟悉一下以前基礎控件的用法。
先來看一下最終的效果圖吧:
這只是一個簡單的效果圖,你可以在此基礎上來完善它。好了,廢話不多說,下面讓我們來看看效果具體如何實現(xiàn)的吧。大家應該不難看出來,頁面整體采用了CoordinatorLayout來實現(xiàn)詳情頁的頂部視差效。同時,這里我采用ExpandableListView來實現(xiàn)多級列表,然后再解決它們的嵌套滑動問題。OK,我們先從ExpandableListView開始動手。
ExpandableListView
官方對于ExpandableListView給出這樣的解釋:A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can individually be expanded to show its children. The items come from the ExpandableListAdapter associated with this view.
簡單來說,ExpandableListView是一個用于垂直方向滾動的二級列表視圖,ExpandableListView與listview不同之處在于,它可以實現(xiàn)二級分組,并通過ExpandableListAdapter來綁定數(shù)據(jù)和視圖。下面我們來一起實現(xiàn)上圖的效果。
布局中定義
首先,我們需要在xml的布局文件中聲明ExpandableListView:
<ExpandableListView android:id="@+id/detail_page_lv_comment" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="@null" android:layout_marginBottom="64dp" android:listSelector="@android:color/transparent" android:scrollbars="none"/>
這里需要說明兩個問題:
- ExpandableListView默認為它的item加上了點擊效果,由于item里面還包含了childItem,所以,點擊后,整個item里面的內(nèi)容都會有點擊效果。我們可以取消其點擊特效,避免其影響用戶體驗,只需要設置如上代碼中的listSelector即可。
- ExpandableListView具有默認的分割線,可以通過divider屬性將其隱藏。
設置Adapter
正如使用listView那樣,我們需要為ExpandableListView設置一個適配器Adapter,為其綁定數(shù)據(jù)和視圖。ExpandableListView的adapter需要繼承自ExpandableListAdapter,具體代碼如下:
/** * Author: Moos * E-mail: moosphon@gmail.com * Date: 18/4/20. * Desc: 評論與回復列表的適配器 */ public class CommentExpandAdapter extends BaseExpandableListAdapter { private static final String TAG = "CommentExpandAdapter"; private List<CommentDetailBean> commentBeanList; private Context context; public CommentExpandAdapter(Context context, List<CommentDetailBean> commentBeanList) { this.context = context; this.commentBeanList = commentBeanList; } @Override public int getGroupCount() { return commentBeanList.size(); } @Override public int getChildrenCount(int i) { if(commentBeanList.get(i).getReplyList() == null){ return 0; }else { return commentBeanList.get(i).getReplyList().size()>0 ? commentBeanList.get(i).getReplyList().size():0; } } @Override public Object getGroup(int i) { return commentBeanList.get(i); } @Override public Object getChild(int i, int i1) { return commentBeanList.get(i).getReplyList().get(i1); } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return getCombinedChildId(groupPosition, childPosition); } @Override public boolean hasStableIds() { return true; } boolean isLike = false; @Override public View getGroupView(final int groupPosition, boolean isExpand, View convertView, ViewGroup viewGroup) { final GroupHolder groupHolder; if(convertView == null){ convertView = LayoutInflater.from(context).inflate(R.layout.comment_item_layout, viewGroup, false); groupHolder = new GroupHolder(convertView); convertView.setTag(groupHolder); }else { groupHolder = (GroupHolder) convertView.getTag(); } Glide.with(context).load(R.drawable.user_other) .diskCacheStrategy(DiskCacheStrategy.RESULT) .error(R.mipmap.ic_launcher) .centerCrop() .into(groupHolder.logo); groupHolder.tv_name.setText(commentBeanList.get(groupPosition).getNickName()); groupHolder.tv_time.setText(commentBeanList.get(groupPosition).getCreateDate()); groupHolder.tv_content.setText(commentBeanList.get(groupPosition).getContent()); groupHolder.iv_like.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(isLike){ isLike = false; groupHolder.iv_like.setColorFilter(Color.parseColor("#aaaaaa")); }else { isLike = true; groupHolder.iv_like.setColorFilter(Color.parseColor("#FF5C5C")); } } }); return convertView; } @Override public View getChildView(final int groupPosition, int childPosition, boolean b, View convertView, ViewGroup viewGroup) { final ChildHolder childHolder; if(convertView == null){ convertView = LayoutInflater.from(context).inflate(R.layout.comment_reply_item_layout,viewGroup, false); childHolder = new ChildHolder(convertView); convertView.setTag(childHolder); } else { childHolder = (ChildHolder) convertView.getTag(); } String replyUser = commentBeanList.get(groupPosition).getReplyList().get(childPosition).getNickName(); if(!TextUtils.isEmpty(replyUser)){ childHolder.tv_name.setText(replyUser + ":"); } childHolder.tv_content.setText(commentBeanList.get(groupPosition).getReplyList().get(childPosition).getContent()); return convertView; } @Override public boolean isChildSelectable(int i, int i1) { return true; } private class GroupHolder{ private CircleImageView logo; private TextView tv_name, tv_content, tv_time; private ImageView iv_like; public GroupHolder(View view) { logo = view.findViewById(R.id.comment_item_logo); tv_content = view.findViewById(R.id.comment_item_content); tv_name = view.findViewById(R.id.comment_item_userName); tv_time = view.findViewById(R.id.comment_item_time); iv_like = view.findViewById(R.id.comment_item_like); } } private class ChildHolder{ private TextView tv_name, tv_content; public ChildHolder(View view) { tv_name = (TextView) view.findViewById(R.id.reply_item_user); tv_content = (TextView) view.findViewById(R.id.reply_item_content); } } }
一般情況下,我們自定義自己的ExpandableListAdapter后,需要實現(xiàn)以下幾個方法:
- 構(gòu)造方法,這個應該無需多說了,一般用來初始化數(shù)據(jù)等操作。
- getGroupCount,返回group分組的數(shù)量,在當前需求中指代評論的數(shù)量。
- getChildrenCount,返回所在group中child的數(shù)量,這里指代當前評論對應的回復數(shù)目。
- getGroup,返回group的實際數(shù)據(jù),這里指的是當前評論數(shù)據(jù)。
- getChild,返回group中某個child的實際數(shù)據(jù),這里指的是當前評論的某個回復數(shù)據(jù)。
- getGroupId,返回分組的id,一般將當前group的位置傳給它。
- getChildId,返回分組中某個child的id,一般也將child當前位置傳給它,不過為了避免重復,可以使用getCombinedChildId(groupPosition, childPosition);來獲取id并返回。
- hasStableIds,表示分組和子選項是否持有穩(wěn)定的id,這里返回true即可。
- isChildSelectable,表示分組中的child是否可以選中,這里返回true。
- getGroupView,即返回group的視圖,一般在這里進行一些數(shù)據(jù)和視圖綁定的工作,一般為了復用和高效,可以自定義ViewHolder,用法與listview一樣,這里就不多說了。
- getChildView,返回分組中child子項的視圖,比較容易理解,第一個參數(shù)是當前group所在的位置,第二個參數(shù)是當前child所在位置。
這里的數(shù)據(jù)是我自己做的模擬數(shù)據(jù),不過應該算是較為通用的格式了,大體格式如下:
一般情況下,我們后臺會通過接口返回給我們一部分數(shù)據(jù),如果想要查看更多評論,需要跳轉(zhuǎn)到“更多頁面”去查看,這里為了方便,我們只考慮加載部分數(shù)據(jù)。
Activity中使用
接下來,我們就需要在activity中顯示評論和回復的二級列表了:
private ExpandableListView expandableListView; private CommentExpandAdapter adapter; private CommentBean commentBean; private List<CommentDetailBean> commentsList; ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { expandableListView = findViewById(R.id.detail_page_lv_comment); initExpandableListView(commentsList); } /** * 初始化評論和回復列表 */ private void initExpandableListView(final List<CommentDetailBean> commentList){ expandableListView.setGroupIndicator(null); //默認展開所有回復 adapter = new CommentExpandAdapter(this, commentList); expandableListView.setAdapter(adapter); for(int i = 0; i<commentList.size(); i++){ expandableListView.expandGroup(i); } expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) { boolean isExpanded = expandableListView.isGroupExpanded(groupPosition); Log.e(TAG, "onGroupClick: 當前的評論id>>>"+commentList.get(groupPosition).getId()); // if(isExpanded){ // expandableListView.collapseGroup(groupPosition); // }else { // expandableListView.expandGroup(groupPosition, true); // } return true; } }); expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long l) { Toast.makeText(MainActivity.this,"點擊了回復",Toast.LENGTH_SHORT).show(); return false; } }); expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() { @Override public void onGroupExpand(int groupPosition) { //toast("展開第"+groupPosition+"個分組"); } }); } /** * by moos on 2018/04/20 * func:生成測試數(shù)據(jù) * @return 評論數(shù)據(jù) */ private List<CommentDetailBean> generateTestData(){ Gson gson = new Gson(); commentBean = gson.fromJson(testJson, CommentBean.class); List<CommentDetailBean> commentList = commentBean.getData().getList(); return commentList; }
就以上代碼作一下簡單說明:
1、ExpandableListView在默認情況下會為我們自帶分組的icon(▶️),當前需求下,我們根本不需要展示,可以通過expandableListView.setGroupIndicator(null)來隱藏。
2、一般情況下,我們可能需要默認展開所有的分組,我就可以通過循環(huán)來調(diào)用expandableListView.expandGroup(i);方法。
3、ExpandableListView為我們提供了group和child的點擊事件,分別通過setOnGroupClickListener和setOnChildClickListener來設置。值得注意的是,group的點擊事件里如果我們返回的是false,那么我們點擊group就會自動展開,但我這里碰到一個問題,當我返回false時,第一條評論數(shù)據(jù)會多出一條。通過百度查找方法,雖然很多類似問題,但終究沒有解決,最后我返回了ture,并通過以下代碼手動展開和收縮就可以了:
if(isExpanded){ expandableListView.collapseGroup(groupPosition); }else { expandableListView.expandGroup(groupPosition, true); }
4、此外,我們還可以通過setOnGroupExpandListener和setOnGroupCollapseListener來監(jiān)聽ExpandableListView的分組展開和收縮的狀態(tài)。
評論和回復功能
為了模擬整個評論和回復功能,我們還需要手動插入收據(jù)并刷新數(shù)據(jù)列表。這里我就簡單做一下模擬,請忽略一些UI上的細節(jié)。
插入評論數(shù)據(jù)
插入評論數(shù)據(jù)比較簡單,只需要在list中插入一條數(shù)據(jù)并刷新即可:
String commentContent = commentText.getText().toString().trim(); if(!TextUtils.isEmpty(commentContent)){ //commentOnWork(commentContent); dialog.dismiss(); CommentDetailBean detailBean = new CommentDetailBean("小明", commentContent,"剛剛"); adapter.addTheCommentData(detailBean); Toast.makeText(MainActivity.this,"評論成功",Toast.LENGTH_SHORT).show(); }else { Toast.makeText(MainActivity.this,"評論內(nèi)容不能為空",Toast.LENGTH_SHORT).show(); }
adapter中的addTheCommentData方法如下:
/** * by moos on 2018/04/20 * func:評論成功后插入一條數(shù)據(jù) * @param commentDetailBean 新的評論數(shù)據(jù) */ public void addTheCommentData(CommentDetailBean commentDetailBean){ if(commentDetailBean!=null){ commentBeanList.add(commentDetailBean); notifyDataSetChanged(); }else { throw new IllegalArgumentException("評論數(shù)據(jù)為空!"); } }
代碼比較容易理解,就不多做說明了。
插入回復數(shù)據(jù)
首先,我們需要實現(xiàn)點擊某一條評論,然后@ta,那么我們需要在group的點擊事件里彈起回復框:
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { @Override public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) { showReplyDialog(groupPosition); return true; } }); ...... /** * by moos on 2018/04/20 * func:彈出回復框 */ private void showReplyDialog(final int position){ dialog = new BottomSheetDialog(this); View commentView = LayoutInflater.from(this).inflate(R.layout.comment_dialog_layout,null); final EditText commentText = (EditText) commentView.findViewById(R.id.dialog_comment_et); final Button bt_comment = (Button) commentView.findViewById(R.id.dialog_comment_bt); commentText.setHint("回復 " + commentsList.get(position).getNickName() + " 的評論:"); dialog.setContentView(commentView); bt_comment.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { String replyContent = commentText.getText().toString().trim(); if(!TextUtils.isEmpty(replyContent)){ dialog.dismiss(); ReplyDetailBean detailBean = new ReplyDetailBean("小紅",replyContent); adapter.addTheReplyData(detailBean, position); Toast.makeText(MainActivity.this,"回復成功",Toast.LENGTH_SHORT).show(); }else { Toast.makeText(MainActivity.this,"回復內(nèi)容不能為空",Toast.LENGTH_SHORT).show(); } } }); commentText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { if(!TextUtils.isEmpty(charSequence) && charSequence.length()>2){ bt_comment.setBackgroundColor(Color.parseColor("#FFB568")); }else { bt_comment.setBackgroundColor(Color.parseColor("#D8D8D8")); } } @Override public void afterTextChanged(Editable editable) { } }); dialog.show(); }
插入回復的數(shù)據(jù)與上面插入評論類似,這里貼一下adapter中的代碼:
/** * by moos on 2018/04/20 * func:回復成功后插入一條數(shù)據(jù) * @param replyDetailBean 新的回復數(shù)據(jù) */ public void addTheReplyData(ReplyDetailBean replyDetailBean, int groupPosition){ if(replyDetailBean!=null){ Log.e(TAG, "addTheReplyData: >>>>該刷新回復列表了:"+replyDetailBean.toString() ); if(commentBeanList.get(groupPosition).getReplyList() != null ){ commentBeanList.get(groupPosition).getReplyList().add(replyDetailBean); }else { List<ReplyDetailBean> replyList = new ArrayList<>(); replyList.add(replyDetailBean); commentBeanList.get(groupPosition).setReplyList(replyList); } notifyDataSetChanged(); }else { throw new IllegalArgumentException("回復數(shù)據(jù)為空!"); } }
需要注意一點,由于不一定所有的評論都有回復數(shù)據(jù),所以在插入數(shù)據(jù)前我們要判斷ReplyList是否為空,如果不為空,直接獲取當前評論的回復列表,并插入數(shù)據(jù);如果為空,需要new一個ReplyList,插入數(shù)據(jù)后還要為評論set一下ReplyList。
解決CoordinatorLayout與ExpandableListView嵌套問題
如果你不需要使用CoordinatorLayout或者NestedScrollView,可以跳過本小節(jié)。一般情況下,我們產(chǎn)品為了更好的用戶體驗,還需要我們加上類似的頂部視差效果或者下拉刷新等,這就要我們處理一些常見的嵌套滑動問題了。
由于CoordinatorLayout實現(xiàn)NestedScrollingParent接口,RecycleView實現(xiàn)了NestedScrollingChild接口,所以就可以在NestedScrollingChildHelper的幫助下實現(xiàn)嵌套滑動,那么我們也可以通過自定義的ExpandableListView實現(xiàn)NestedScrollingChild接口來達到同樣的效果:
/** * Author: Moos * E-mail: moosphon@gmail.com * Date: 18/4/20. * Desc: 自定義ExpandableListView,解決與CoordinatorLayout滑動沖突問題 */ public class CommentExpandableListView extends ExpandableListView implements NestedScrollingChild{ private NestedScrollingChildHelper mScrollingChildHelper; public CommentExpandableListView(Context context, AttributeSet attrs) { super(context, attrs); mScrollingChildHelper = new NestedScrollingChildHelper(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setNestedScrollingEnabled(true); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); } @Override public void setNestedScrollingEnabled(boolean enabled) { mScrollingChildHelper.setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return mScrollingChildHelper.isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return mScrollingChildHelper.startNestedScroll(axes); } @Override public void stopNestedScroll() { mScrollingChildHelper.stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return mScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); } }
代碼就不解釋了,畢竟這不是本篇文章等重點,大家可以去網(wǎng)上查閱NestedScrollView相關文章或者源碼去對照理解。
完整的布局代碼比較多,這里就不貼了,大家可以去github上面查看:https://github.com/Moosphan/CommentWithReplyView-master (本地下載)
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關文章
Android中recyclerView底部添加透明漸變效果
這篇文章主要給大家介紹了關于Android中recyclerView如何實現(xiàn)底部添加透明漸變效果的相關資料,文中通過示例代碼介紹的非常詳細,對各位Android開發(fā)者們具有一定的參考學習價值,需要的朋友們下面來一起看看吧。2018-04-04ListView實現(xiàn)頂部和底部內(nèi)容指示器的方法
這篇文章主要介紹了ListView實現(xiàn)頂部和底部內(nèi)容指示器的方法,需要的朋友可以參考下2015-09-09Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術
這篇文章主要為大家介紹了Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術運用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12