Android如何實(shí)現(xiàn)社交應(yīng)用中的評(píng)論與回復(fù)功能詳解
前言
在Android的日常開(kāi)發(fā)中,評(píng)論與回復(fù)功能是我們經(jīng)常遇到的需求之一,其中評(píng)論與回復(fù)列表的展示一般在功能模塊中占比較大。對(duì)于需求改動(dòng)和迭代較頻繁的公司來(lái)說(shuō),如何快速開(kāi)發(fā)一個(gè)二級(jí)界面來(lái)適應(yīng)我們的功能需求無(wú)疑優(yōu)先級(jí)更高一些。首先我們來(lái)看看其他社交類(lèi)app的評(píng)論與回復(fù)列表如何展示的:


Twitter不用說(shuō)了,全球知名社交平臺(tái),上億用戶量,他們的評(píng)論回復(fù)都只展示一級(jí)數(shù)據(jù)(評(píng)論數(shù)據(jù)),其他更多內(nèi)容(回復(fù)內(nèi)容),是需要頁(yè)面跳轉(zhuǎn)去查看,知乎也類(lèi)似。第一張圖是我們?cè)O(shè)計(jì)給我找的,他說(shuō)要按照這個(gè)風(fēng)格來(lái),盡量將評(píng)論和回復(fù)內(nèi)容在一個(gè)頁(yè)面展示。好吧,沒(méi)辦法,畢竟我們做前端的,UI要看設(shè)計(jì)臉色,數(shù)據(jù)要看后臺(tái)臉色��??吹皆O(shè)計(jì)圖,我們腦海肯定第一時(shí)間聯(lián)想一下解決方案:用recyclerview?listview?不對(duì),分析一下它的層級(jí)發(fā)現(xiàn),評(píng)論是一個(gè)列表,里面的回復(fù)又是一個(gè)列表,難道用recyclerview或者listview的嵌套?抱著不確定的態(tài)度,立馬去網(wǎng)上查一下,果不其然,搜到的實(shí)現(xiàn)方式大多都是用嵌套實(shí)現(xiàn)的,來(lái)公司之前,其中一個(gè)項(xiàng)目里的評(píng)論回復(fù)功能就是用的嵌套listview,雖然處理了滑動(dòng)沖突問(wèn)題,但效果不佳,而且時(shí)??D,所以,這里我肯定要換個(gè)思路。
網(wǎng)上還有說(shuō)用自定義view實(shí)現(xiàn)的,但我發(fā)現(xiàn)大多沒(méi)有處理view的復(fù)用,而且開(kāi)發(fā)成本大,暫時(shí)不予考慮。那怎么辦?無(wú)意中看到expandable這個(gè)關(guān)鍵詞,我突然想到谷歌很早之前出過(guò)一個(gè)擴(kuò)展列表的控件 - ExpandableListView,但聽(tīng)說(shuō)比較老,存在一些問(wèn)題。算了,試試再說(shuō),順便熟悉一下以前基礎(chǔ)控件的用法。
先來(lái)看一下最終的效果圖吧:

這只是一個(gè)簡(jiǎn)單的效果圖,你可以在此基礎(chǔ)上來(lái)完善它。好了,廢話不多說(shuō),下面讓我們來(lái)看看效果具體如何實(shí)現(xiàn)的吧。大家應(yīng)該不難看出來(lái),頁(yè)面整體采用了CoordinatorLayout來(lái)實(shí)現(xiàn)詳情頁(yè)的頂部視差效。同時(shí),這里我采用ExpandableListView來(lái)實(shí)現(xiàn)多級(jí)列表,然后再解決它們的嵌套滑動(dòng)問(wèn)題。OK,我們先從ExpandableListView開(kāi)始動(dòng)手。
ExpandableListView
官方對(duì)于ExpandableListView給出這樣的解釋?zhuān)篈 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.
簡(jiǎn)單來(lái)說(shuō),ExpandableListView是一個(gè)用于垂直方向滾動(dòng)的二級(jí)列表視圖,ExpandableListView與listview不同之處在于,它可以實(shí)現(xiàn)二級(jí)分組,并通過(guò)ExpandableListAdapter來(lái)綁定數(shù)據(jù)和視圖。下面我們來(lái)一起實(shí)現(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"/>
這里需要說(shuō)明兩個(gè)問(wèn)題:
- ExpandableListView默認(rèn)為它的item加上了點(diǎn)擊效果,由于item里面還包含了childItem,所以,點(diǎn)擊后,整個(gè)item里面的內(nèi)容都會(huì)有點(diǎn)擊效果。我們可以取消其點(diǎn)擊特效,避免其影響用戶體驗(yàn),只需要設(shè)置如上代碼中的listSelector即可。
- ExpandableListView具有默認(rèn)的分割線,可以通過(guò)divider屬性將其隱藏。
設(shè)置Adapter
正如使用listView那樣,我們需要為ExpandableListView設(shè)置一個(gè)適配器Adapter,為其綁定數(shù)據(jù)和視圖。ExpandableListView的adapter需要繼承自ExpandableListAdapter,具體代碼如下:
/**
* Author: Moos
* E-mail: moosphon@gmail.com
* Date: 18/4/20.
* Desc: 評(píng)論與回復(fù)列表的適配器
*/
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后,需要實(shí)現(xiàn)以下幾個(gè)方法:
- 構(gòu)造方法,這個(gè)應(yīng)該無(wú)需多說(shuō)了,一般用來(lái)初始化數(shù)據(jù)等操作。
- getGroupCount,返回group分組的數(shù)量,在當(dāng)前需求中指代評(píng)論的數(shù)量。
- getChildrenCount,返回所在group中child的數(shù)量,這里指代當(dāng)前評(píng)論對(duì)應(yīng)的回復(fù)數(shù)目。
- getGroup,返回group的實(shí)際數(shù)據(jù),這里指的是當(dāng)前評(píng)論數(shù)據(jù)。
- getChild,返回group中某個(gè)child的實(shí)際數(shù)據(jù),這里指的是當(dāng)前評(píng)論的某個(gè)回復(fù)數(shù)據(jù)。
- getGroupId,返回分組的id,一般將當(dāng)前group的位置傳給它。
- getChildId,返回分組中某個(gè)child的id,一般也將child當(dāng)前位置傳給它,不過(guò)為了避免重復(fù),可以使用getCombinedChildId(groupPosition, childPosition);來(lái)獲取id并返回。
- hasStableIds,表示分組和子選項(xiàng)是否持有穩(wěn)定的id,這里返回true即可。
- isChildSelectable,表示分組中的child是否可以選中,這里返回true。
- getGroupView,即返回group的視圖,一般在這里進(jìn)行一些數(shù)據(jù)和視圖綁定的工作,一般為了復(fù)用和高效,可以自定義ViewHolder,用法與listview一樣,這里就不多說(shuō)了。
- getChildView,返回分組中child子項(xiàng)的視圖,比較容易理解,第一個(gè)參數(shù)是當(dāng)前group所在的位置,第二個(gè)參數(shù)是當(dāng)前child所在位置。
這里的數(shù)據(jù)是我自己做的模擬數(shù)據(jù),不過(guò)應(yīng)該算是較為通用的格式了,大體格式如下:

一般情況下,我們后臺(tái)會(huì)通過(guò)接口返回給我們一部分?jǐn)?shù)據(jù),如果想要查看更多評(píng)論,需要跳轉(zhuǎn)到“更多頁(yè)面”去查看,這里為了方便,我們只考慮加載部分?jǐn)?shù)據(jù)。
Activity中使用
接下來(lái),我們就需要在activity中顯示評(píng)論和回復(fù)的二級(jí)列表了:
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);
}
/**
* 初始化評(píng)論和回復(fù)列表
*/
private void initExpandableListView(final List<CommentDetailBean> commentList){
expandableListView.setGroupIndicator(null);
//默認(rèn)展開(kāi)所有回復(fù)
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: 當(dāng)前的評(píng)論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,"點(diǎn)擊了回復(fù)",Toast.LENGTH_SHORT).show();
return false;
}
});
expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
@Override
public void onGroupExpand(int groupPosition) {
//toast("展開(kāi)第"+groupPosition+"個(gè)分組");
}
});
}
/**
* by moos on 2018/04/20
* func:生成測(cè)試數(shù)據(jù)
* @return 評(píng)論數(shù)據(jù)
*/
private List<CommentDetailBean> generateTestData(){
Gson gson = new Gson();
commentBean = gson.fromJson(testJson, CommentBean.class);
List<CommentDetailBean> commentList = commentBean.getData().getList();
return commentList;
}
就以上代碼作一下簡(jiǎn)單說(shuō)明:
1、ExpandableListView在默認(rèn)情況下會(huì)為我們自帶分組的icon(▶️),當(dāng)前需求下,我們根本不需要展示,可以通過(guò)expandableListView.setGroupIndicator(null)來(lái)隱藏。
2、一般情況下,我們可能需要默認(rèn)展開(kāi)所有的分組,我就可以通過(guò)循環(huán)來(lái)調(diào)用expandableListView.expandGroup(i);方法。
3、ExpandableListView為我們提供了group和child的點(diǎn)擊事件,分別通過(guò)setOnGroupClickListener和setOnChildClickListener來(lái)設(shè)置。值得注意的是,group的點(diǎn)擊事件里如果我們返回的是false,那么我們點(diǎn)擊group就會(huì)自動(dòng)展開(kāi),但我這里碰到一個(gè)問(wèn)題,當(dāng)我返回false時(shí),第一條評(píng)論數(shù)據(jù)會(huì)多出一條。通過(guò)百度查找方法,雖然很多類(lèi)似問(wèn)題,但終究沒(méi)有解決,最后我返回了ture,并通過(guò)以下代碼手動(dòng)展開(kāi)和收縮就可以了:
if(isExpanded){
expandableListView.collapseGroup(groupPosition);
}else {
expandableListView.expandGroup(groupPosition, true);
}
4、此外,我們還可以通過(guò)setOnGroupExpandListener和setOnGroupCollapseListener來(lái)監(jiān)聽(tīng)ExpandableListView的分組展開(kāi)和收縮的狀態(tài)。
評(píng)論和回復(fù)功能
為了模擬整個(gè)評(píng)論和回復(fù)功能,我們還需要手動(dòng)插入收據(jù)并刷新數(shù)據(jù)列表。這里我就簡(jiǎn)單做一下模擬,請(qǐng)忽略一些UI上的細(xì)節(jié)。
插入評(píng)論數(shù)據(jù)
插入評(píng)論數(shù)據(jù)比較簡(jiǎn)單,只需要在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,"評(píng)論成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this,"評(píng)論內(nèi)容不能為空",Toast.LENGTH_SHORT).show();
}
adapter中的addTheCommentData方法如下:
/**
* by moos on 2018/04/20
* func:評(píng)論成功后插入一條數(shù)據(jù)
* @param commentDetailBean 新的評(píng)論數(shù)據(jù)
*/
public void addTheCommentData(CommentDetailBean commentDetailBean){
if(commentDetailBean!=null){
commentBeanList.add(commentDetailBean);
notifyDataSetChanged();
}else {
throw new IllegalArgumentException("評(píng)論數(shù)據(jù)為空!");
}
}
代碼比較容易理解,就不多做說(shuō)明了。
插入回復(fù)數(shù)據(jù)
首先,我們需要實(shí)現(xiàn)點(diǎn)擊某一條評(píng)論,然后@ta,那么我們需要在group的點(diǎn)擊事件里彈起回復(fù)框:
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:彈出回復(fù)框
*/
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("回復(fù) " + commentsList.get(position).getNickName() + " 的評(píng)論:");
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,"回復(fù)成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this,"回復(fù)內(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();
}
插入回復(fù)的數(shù)據(jù)與上面插入評(píng)論類(lèi)似,這里貼一下adapter中的代碼:
/**
* by moos on 2018/04/20
* func:回復(fù)成功后插入一條數(shù)據(jù)
* @param replyDetailBean 新的回復(fù)數(shù)據(jù)
*/
public void addTheReplyData(ReplyDetailBean replyDetailBean, int groupPosition){
if(replyDetailBean!=null){
Log.e(TAG, "addTheReplyData: >>>>該刷新回復(fù)列表了:"+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("回復(fù)數(shù)據(jù)為空!");
}
}
需要注意一點(diǎn),由于不一定所有的評(píng)論都有回復(fù)數(shù)據(jù),所以在插入數(shù)據(jù)前我們要判斷ReplyList是否為空,如果不為空,直接獲取當(dāng)前評(píng)論的回復(fù)列表,并插入數(shù)據(jù);如果為空,需要new一個(gè)ReplyList,插入數(shù)據(jù)后還要為評(píng)論set一下ReplyList。
解決CoordinatorLayout與ExpandableListView嵌套問(wèn)題
如果你不需要使用CoordinatorLayout或者NestedScrollView,可以跳過(guò)本小節(jié)。一般情況下,我們產(chǎn)品為了更好的用戶體驗(yàn),還需要我們加上類(lèi)似的頂部視差效果或者下拉刷新等,這就要我們處理一些常見(jiàn)的嵌套滑動(dòng)問(wèn)題了。
由于CoordinatorLayout實(shí)現(xiàn)NestedScrollingParent接口,RecycleView實(shí)現(xiàn)了NestedScrollingChild接口,所以就可以在NestedScrollingChildHelper的幫助下實(shí)現(xiàn)嵌套滑動(dòng),那么我們也可以通過(guò)自定義的ExpandableListView實(shí)現(xiàn)NestedScrollingChild接口來(lái)達(dá)到同樣的效果:
/**
* Author: Moos
* E-mail: moosphon@gmail.com
* Date: 18/4/20.
* Desc: 自定義ExpandableListView,解決與CoordinatorLayout滑動(dòng)沖突問(wèn)題
*/
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);
}
}
代碼就不解釋了,畢竟這不是本篇文章等重點(diǎn),大家可以去網(wǎng)上查閱NestedScrollView相關(guān)文章或者源碼去對(duì)照理解。
完整的布局代碼比較多,這里就不貼了,大家可以去github上面查看:https://github.com/Moosphan/CommentWithReplyView-master (本地下載)
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android實(shí)現(xiàn)朋友圈評(píng)論回復(fù)列表
- Android評(píng)論功能的實(shí)現(xiàn)過(guò)程
- Android 仿微信朋友圈點(diǎn)贊和評(píng)論彈出框功能
- Android實(shí)現(xiàn)評(píng)論欄隨Recyclerview滑動(dòng)左右移動(dòng)
- Android studio導(dǎo)入項(xiàng)目的方法詳解(簡(jiǎn)單快速)
- Android 仿抖音的評(píng)論列表的UI和效果的實(shí)現(xiàn)代碼
- Android中使用PopupWindow 仿微信點(diǎn)贊和評(píng)論彈出
- Android模擬登錄評(píng)論CSDN實(shí)現(xiàn)代碼
- Android評(píng)論圖片可移動(dòng)順序選擇器(推薦)
- Android實(shí)現(xiàn)跑馬燈效果的代碼詳解
相關(guān)文章
Android中recyclerView底部添加透明漸變效果
這篇文章主要給大家介紹了關(guān)于Android中recyclerView如何實(shí)現(xiàn)底部添加透明漸變效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)各位Android開(kāi)發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2018-04-04
ListView實(shí)現(xiàn)頂部和底部?jī)?nèi)容指示器的方法
這篇文章主要介紹了ListView實(shí)現(xiàn)頂部和底部?jī)?nèi)容指示器的方法,需要的朋友可以參考下2015-09-09
Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術(shù)
這篇文章主要為大家介紹了Flutter框架解決盒約束widget和assets里加載資產(chǎn)技術(shù)運(yùn)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

