1. 程式人生 > >實現支援語音的評論功能

實現支援語音的評論功能

介紹

這篇文章,主要介紹是,在專案中,開發一個評論功能,並且支援語音功能的評論。
直接看效果圖:

圖片描述

功能分析與實現

簡單的說,需求就是:

實現 文字 或者 語音 評論,回覆只支援文字

為了下面更好的分析,這裡標註了一些用詞,如圖:

圖片描述

Comment:通過下面輸入框直接釋出的評論
SubComment:Comment下的回覆;
Public:在SubComment資料中,對Comment回覆(即點選右上角留言圖示),屬於Public狀態;
Private:在SubComment資料中,對SubComment回覆(即在下面點選使用者名稱回覆),屬於Private狀態;

有了這些規則,開始吧…

首先,先來實現,只有文字的評論。

文字評論實現

原始碼後面給出

通過上面的效果圖片,簡單分析下(如果分析的不清楚,可以看原始碼):

  1. 首先,可以看到,評論是一個List,這裡使用RecycleView來實現,而且後面還要新增語音的item,這裡使用RecycleView會更方便;

  2. 其次,整體佈局,主要是 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; }
  1. 然後,就是新增一些圖示的功能,釋出評論,點贊、回覆、刪除(如果是自己發表的評論),這些比較簡單了,可以直接看原始碼。

  2. 大體介面功能實現之後,為了增強使用者的體驗:點選某條評論,鍵盤上移的同時,其對應的內容也上移或下移,使其剛好在鍵盤的上方。

主要是給跟佈局添加布局監聽,然後根據點選那個位置,計算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個可以實現:

  1. MediaRecorder
  2. 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