介绍
这篇文章,主要介绍是,在项目中,开发一个评论功能,并且支持语音功能的评论。
直接看效果图:

功能分析与实现
简单的说,需求就是:
实现 文本 或者 语音 评论,回复只支持文本
为了下面更好的分析,这里标注了一些用词,如图:

Comment:通过下面输入框直接发布的评论
SubComment:Comment下的回复;
Public:在SubComment数据中,对Comment回复(即点击右上角留言图标),属于Public状态;
Private:在SubComment数据中,对SubComment回复(即在下面点击用户名回复),属于Private状态;
有了这些规则,开始吧…
首先,先来实现,只有文本的评论。
文本评论实现
源码后面给出
通过上面的效果图片,简单分析下(如果分析的不清楚,可以看源码):
-
首先,可以看到,评论是一个List,这里使用RecycleView来实现,而且后面还要添加语音的item,这里使用RecycleView会更方便;
-
其次,整体布局,主要是 SubComment (回复)的布局实现,其他比较简单。这里不饶弯子,如果使用过
SpannableStringBuilder
的童鞋,马上就知道了。这个类可以说TextView的花式用法。这个类可以将一行textView的字符串,切割成不同形式(局部有颜色、局部添加点击事件…),然后再拼接起来。(如果不懂具体的使用,我这里单独抽出来了,可以看看:SpannableString 和 SpannableStringBuilder的使用)但SubComment的数量是动态的,这里自定义一个类
CommentListView
,使用addView()
方法动态添加。主要代码如下:
/**
* 数据刷新
*/
public void notifyDataSetChanged() {
removeAllViews();
if (mDatas == null || mDatas.size() == 0) {
return;
}
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
for (int i = 0; i < mDatas.size(); i++) {
final int index = i;
/**
* 跟据SubComment的数量 ,生成多个的TextView
*/
View view = getView(index);
if (view == null) {
throw new NullPointerException("listview item layout is null, please check getView()...");
}
if (index != mDatas.size() - 1) {
layoutParams.bottomMargin = DensityUtil.dpTopx(mContext, 5);
}
/**
* 添加到viewGroup中
*/
addView(view, index, layoutParams);
}
}
/**
*根据SubComment数据,生成textView
*/
private View getView(final int position) {
if (layoutInflater == null) {
layoutInflater = LayoutInflater.from(getContext());
}
View convertView = layoutInflater.inflate(R.layout.item_sub_comment_layout, null, false);
TextView commentTv = convertView.findViewById(R.id.tv_sub_comment);
final SubCommentData bean = mDatas.get(position);
if (bean != null) {
// 谁 回复
User whoReplyUser = bean.getWhoReply();
if (whoReplyUser != null && !StringUtil.isNull(whoReplyUser.getUserId())) {
String whoReplyName = whoReplyUser.getNickname();
int id = bean.getPostId();
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.append(setClickableSpan(whoReplyName, whoReplyUser.getUserId()));
// 回复 谁
User replyWhoUser = bean.getReplyWho();
if (replyWhoUser != null && !StringUtil.isNull(replyWhoUser.getUserId())) {
String replyWhoName = replyWhoUser.getNickname();
builder.append(mReplayStr);
builder.append(setClickableSpan(replyWhoName, replyWhoUser.getUserId()));
}
builder.append(": ");
String contentBodyStr = bean.getContent();
builder.append(contentBodyStr);
commentTv.setText(builder);
final MovementMethod circleMovementMethod = new com.example.recordcomment.widget.MovementMethod(mItemSelectorBgColor, mItemSelectorBgColor);
commentTv.setMovementMethod(circleMovementMethod);
commentTv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (circleMovementMethod.isPassToTv()) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(position);
Toast.makeText(getContext(), "onClick", Toast.LENGTH_SHORT).show();
}
}
}
});
commentTv.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (circleMovementMethod.isPassToTv()) {
if (onItemLongClickListener != null) {
onItemLongClickListener.onItemLongClick(position);
Toast.makeText(getContext(), "onLongClick", Toast.LENGTH_SHORT).show();
}
}
/**
* 返回 false ,让他触发MovementMethod的 onTouch(),最终使背景消失
*/
return false;
}
});
}
}
return convertView;
}
-
然后,就是添加一些图标的功能,发布评论,点赞、回复、删除(如果是自己发表的评论),这些比较简单了,可以直接看源码。
-
大体界面功能实现之后,为了增强用户的体验:点击某条评论,键盘上移的同时,其对应的内容也上移或下移,使其刚好在键盘的上方。
主要是给跟布局添加布局监听,然后根据点击那个位置,计算recycleView的偏移量:
/**
* 监听布局的变化,键盘
*/
private void setViewTreeObserver() {
final ViewTreeObserver viewTreeObserver = mRootLayout.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect r = new Rect();
/**
* 获取当前窗口可视区域大小的
*/
mRootLayout.getWindowVisibleDisplayFrame(r);
int statusBarH = StatusUtil.getStatusBarHeight(getApplicationContext());// 状态栏高度
int screenH = mRootLayout.getRootView().getHeight();
if (r.top != statusBarH) {
/**
* 在沉浸式状态栏时r.top=0; 如果有显示状态栏,在不计算状态栏高度
* r.top代表的是状态栏高度
*/
r.top = statusBarH;
}
int keyboardH = screenH - (r.bottom - r.top);
if (keyboardH == mCurrentKeyboardHeight) { // 有变化时才处理,否则会陷入死循环
return;
}
mCurrentKeyboardHeight = keyboardH;
mScreenHeight = screenH;// 应用屏幕的高度
mInputCommentHeight = mInputCommentLayout.getHeight();//底部输入框高度
if (keyboardH < 150) {// 说明是隐藏键盘的情况
hideSoftInput();
return;
}
// 偏移listview
if (mLayoutManager != null && mCommentConfig != null) {
mLayoutManager.scrollToPositionWithOffset(mCommentConfig.commentPosition, getListViewOffset(mCommentConfig));
}
}
});
}
好了,大体上,文本评论的功能,主要就是这些了,具体细节,还是看源码。
语音评论实现
实现完了文本,接下来看看语音的实现。
语音评论,貌似比较少见,其实借鉴微信语音的节目功能,来模仿实现:

分析下:
使用录音功能,主要有2个可以实现:
- MediaRecorder
- AudioRecord
关于这2个类的使用,网上也很多,这里就不啰嗦了。主要简单分析下,在项目的选择。
MediaRecorder
已集成了录音,编码,压缩等,所以使用比较简单,代码量少,录制的音频大小相对小很多,官方还有小demo。但,正是,放大了优点,缺点也比较明显,无法方便处理音频,输出的音频格式少,目前支持 .aac
, .amr
,.3gp
AudioRecord
语音的实时处理,可以用代码实现各种音频的封装,可以转换为wav格式,并且可以使用 lamelib 工具装换为MP3
格式。但是,代码量很多,需要AudioTrack进行处理,最终才能播放,并且录制之后的音频大小比较大。
咋一看,那肯定选择 MediaRecorder?但是,最终我们选择了 AudioRecord。
主要一个原因就是(非常坑),需要兼容IOS,统一音频格式。MediaRecorder的格式,IOS播放不了,所以最终转换为大家共同的兼容的格式:MP3
。
Android端可以使用lamelib 工具装换,并且,最终的音频大小也很小,所以,最终就采取了。
当然,随着技术的改变,可能后面有不同的选择,如果,大家有更好的方案,欢迎提出!!!
源码里面,我把这个2个类的实现多添加上,是不是很nice???这里就不贴代码了。。。
记得添加权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
关于仿微信界面,主要参考:
https://blog.csdn.net/lhk147852369/article/details/78658055
文本、语音合并
评论的内容有2种,所以在Comment
这个位置的视图,需要切换。
当然,最简单的使用 setVisibility()
实现也是可以的,但在过程中,处理起来不太方便,后面放弃了…
前面有提到,使用RecycleView
,那么就是使用RecycleView
的 getItemViewType()的功能来实现动态切换。
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position)
BaseCommentViewHolder holder = (BaseCommentViewHolder) viewHolder;
CommentData commentData = mCommentDataList.get(position);
mCommentPosition = position;
if(commentData.getUser() == null) {
return;
}
/**
* 设置不同留言类型
*/
switch (commentData.getContentType()) {
case CONTENT_TYPE_TEXT :
if(holder instanceof TextCommentViewHolder) {
((TextCommentViewHolder)holder).contentText.setText(commentData.getContent());
}
break;
case CONTENT_TYPE_RECORD :
if(holder instanceof RecordCommentViewHolder) {
//todo
((RecordCommentViewHolder)holder).audioPlaybackView.setDuration(Integer.parseInt(commentData.getVoiceTime()));
((RecordCommentViewHolder)holder).audioPlaybackView.setRecordFile(commentData.getVoice());
setAudioPlaybackView((RecordCommentViewHolder) holder, position);
}
break;
default:
break;
}
//其他
......
}
......
/**
* @param position
* @return 返回留言内容的类型
*/
@Override
public int getItemViewType(int position) {
if (mCommentDataList != null && mCommentDataList.size() > 0) {
int type = mCommentDataList.get(position).getContentType();
if (type == CONTENT_TYPE_TEXT) {
return CONTENT_TYPE_TEXT;
} else if (type == CONTENT_TYPE_RECORD) {
return CONTENT_TYPE_RECORD;
}
}
return CONTENT_TYPE_TEXT;
}
好了,简单就是这样子,具体还是看源码。
总结
好了,总体下来,并没有很困难的地方,问题不大。
So,有什么问题,欢迎一起讨论呢。
遇到的一些问题:
0.如何实现格式转换,并且将音频大小压缩
2.添加文本、或者语音不同评论时,如何处理更方便(即文本、语音合并)
3.播放语音时,处理由recycleView视图复用,导致的动画混乱;
…
源码点 这里
参考:
https://developer.android.google.cn/guide/topics/media/mediarecorder#java
http://www.cnblogs.com/Amandaliu/archive/2013/02/04/2891604.html
https://blog.csdn.net/lhk147852369/article/details/78658055
…