RecyclerView实现语音聊天,仿微信
语音主要涉及到的就是MediaRecorder和MediaPlayer。其实就是对于两个过程录制和播放。
这两块其实和业务以及UI是分开的,我认为最好单独写成工具类调用。减少代码耦合,又能提高复用。
1.语音录制VoiceRecordMannager
public class VoiceRecordMannager {
private Handler handler;
private File file;
private MediaRecorder recorder;
private String currentVoicePath;
private boolean isRecording; //是否在录音
private boolean isCancel = false; //到达可取消的边界时变为true
private long startTime;
public String getCurrentVoicePath() {
return currentVoicePath;
}
public VoiceRecordMannager(Handler handler) {
this.handler = handler;
}
/**
* 开启录音
*/
public void startRecord() {
file = null;
try {
if (recorder != null) {
recorder.release();
recorder = null;
}
recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC); //设置音频源为麦克风
recorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB); //设置输出格式
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置编码格式
String voiceFilePath = PathUtil.getVoiceFilePath();
file = new File(voiceFilePath);
currentVoicePath = file.getAbsolutePath();
recorder.setOutputFile(currentVoicePath);
recorder.prepare();
isRecording = true;
recorder.start();
} catch (IOException e) {
e.printStackTrace();
}
//开启线程用于记录音量显示
new Thread(new Runnable() {
@Override
public void run() {
try {
while (isRecording) {
if (!isCancel) {
Message msg = new Message();
msg.what = recorder.getMaxAmplitude() / 3000;
handler.sendMessage(msg);
SystemClock.sleep(100);
}
}
} catch (Exception e) {
// from the crash report website, found one NPE crash from
// one android 4.0.4 htc phone
// maybe handler is null for some reason
}
}
}).start();
startTime = new Date().getTime();
}
public void setCancel(boolean cancel) {
this.isCancel = cancel;
}
/**
* 取消录音
*/
public void cancelRecord() {
if (recorder != null) {
try {
recorder.stop();
recorder.release();
recorder = null;
//删除该文件
if (file != null && file.exists() && !file.isDirectory()) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
isRecording = false;
currentVoicePath = null;
}
}
/**
* @return 结束录音返回录音时长
*/
public int stopRecord() {
if (recorder != null) {
isRecording = false;
recorder.stop();
recorder.release();
recorder = null;
if (file == null || !file.exists() || !file.isFile()) {
return 401;
}
if (file.length() == 0) {
file.delete();
return 401;
}
// int seconds = (int) (new Date().getTime() - startTime) / 1000; //得到录音时长 秒
int seconds = VoiceUtil.getVoiceLength(currentVoicePath);
return seconds;
}
return 0;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (recorder != null) {
recorder.release();
}
}
public boolean isRecording() {
return isRecording;
}
}
2.语音播放MediaPlayerHelper
public class MediaPlayerHelper {
private static MediaPlayer mMediaPlayer;
//是否暂停
private static boolean isPause;
/**
* 播放
*/
public static void play(String filePath, MediaPlayer.OnCompletionListener onCompletionListener) {
if (!(new File(filePath).exists())) {
return;
}
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
mMediaPlayer.reset();
return false;
}
});
} else {
mMediaPlayer.reset();
}
try {
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(filePath);
mMediaPlayer.setOnCompletionListener(onCompletionListener);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 暂停
*/
public static void pause() {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
isPause = true;
}
}
public static void resume() {
if (mMediaPlayer != null && isPause) {
mMediaPlayer.start();
isPause = false;
}
}
public static void release() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
public static boolean isPlaying() {
if (mMediaPlayer!=null) {
return mMediaPlayer.isPlaying();
}else {
return false;
}
}
}
关于界面布局和数据加载方式,我是参照微信来做的。实现分页加载,向上可拉取数据,数据均是本地化存储,与微信类似。数据存储采用GreenDAO
聊天界面xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/btn_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/data_bg_talk_nor1"
/>
<Button
android:id="@+id/btn_talk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/data_btn_talk_nor1"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
/>
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/srl_fresh"
android:layout_above="@+id/btn_send"
android:layout_marginBottom="12dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="vertical"
/>
</android.support.v4.widget.SwipeRefreshLayout>
<com.example.administrator.oldvoicechat.VoiceRecordView
android:id="@+id/voice_recorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="invisible"
/>
</RelativeLayout>
item子布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:orientation="vertical"
android:gravity="center_horizontal"
>
<TextView
android:id="@+id/timestamp"
style="@style/chat_text_date_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
>
<ImageView
android:id="@+id/iv_icon"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentRight="true"
android:src="@drawable/icon" />
<ImageView
android:id="@+id/iv_voice_bg"
android:layout_toLeftOf="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:minWidth="100dp"
android:background="@drawable/chat_bg_myself"
/>
<TextView
android:id="@+id/tv_voice_length"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_alignTop="@id/iv_voice_bg"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:layout_toLeftOf="@+id/iv_voice_bg"
android:text="3''"
android:gravity="center_vertical"
android:textColor="#333333"
android:textSize="16sp" />
<RelativeLayout
android:layout_toLeftOf="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_marginRight="20dp"
>
<View
android:id="@+id/voice_anim"
android:layout_centerVertical="true"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/data_ico_left_voice_three1"/>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="15dp"
android:gravity="center_horizontal"
android:orientation="vertical"
>
<TextView
android:id="@+id/timestamp"
style="@style/chat_text_date_style"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
>
<ImageView
android:id="@+id/iv_icon"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentLeft="true"
android:src="@drawable/icon" />
<TextView
android:id="@+id/tv_name"
android:layout_toRightOf="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:text="朋友"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="center"
android:textSize="10sp"
/>
<ImageView
android:id="@+id/iv_voice_bg"
android:layout_toRightOf="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:background="@drawable/chat_bg_other"
android:layout_below="@+id/tv_name"
/>
<TextView
android:id="@+id/tv_voice_length"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_alignTop="@id/iv_voice_bg"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:layout_toRightOf="@+id/iv_voice_bg"
android:text="3''"
android:gravity="center_vertical"
android:textColor="#333333"
android:textSize="16sp" />
<View
android:id="@+id/unread_flag"
android:layout_width="8dp"
android:layout_height="8dp"
android:layout_toRightOf="@id/iv_voice_bg"
android:layout_marginLeft="5dp"
android:layout_above="@+id/iv_voice_bg"
android:background="@drawable/selecter_voice_unread"
/>
<RelativeLayout
android:layout_toRightOf="@+id/iv_icon"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_marginLeft="20dp"
android:layout_marginTop="20dp"
>
<View
android:id="@+id/voice_anim"
android:layout_centerVertical="true"
android:layout_width="30dp"
android:layout_height="30dp"
android:background="@drawable/data_ico_right_voice_three1"/>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
布局写完了下面就是实现的代码,说说实现思路,语音录制:点击录音按钮,录音开始,向上滑动可取消,可以重写录音按钮的touch事件,在对应状态做状态改变,以及录音。
@Override
public boolean onTouch(View v, MotionEvent event) {
toBottom();
//获取TouchEvent状态
int action = event.getAction();
// 获得x轴坐标
int x = (int) event.getX();
// 获得y轴坐标
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: //按下
changeState(STATE_RECORDING); //手指按下开始记录
break;
case MotionEvent.ACTION_MOVE: //移动
if (voiceRecordView.isRecording()) {
if (y < 0) {
changeState(STATE_CANCEL);
} else {
changeState(STATE_RECORDING);
}
}
break;
case MotionEvent.ACTION_UP: //抬起
changeState(STATE_NORMAL);
break;
default:
changeState(STATE_NORMAL);
break;
}
return voiceRecordView.onPressVoiceButton(v, event, new VoiceRecordView.VoiceRecordListener() {
//录音结束回调
@Override
public void onVoiceRecordComplete(String voiceFilePath, int voiceTimeLength) {
VoiceMsg voiceMsg = new VoiceMsg(null, System.currentTimeMillis(), voiceFilePath, voiceTimeLength, 0, null);
VoiceDbUtil.getInstance().insert(voiceMsg);
mAdapter.addData(voiceMsg);
toBottom();
}
});
}
其中button的touch事件又与录音和声音显示的view息息相关这里用voiceRecordView.onPressVoiceButton把touch事件传入
到voiceRecordView,voiceRecordView中才是我们的主要逻辑,我们来看看它的逻辑
public class VoiceRecordView extends FrameLayout {
private Context context;
private ImageView ivVoiceRecorder;
private PowerManager mPowerManager;
private PowerManager.WakeLock mWakeLock;
private int duration = 60;//默认最长录音时长60秒
private VoiceRecordMannager voiceRecorder;
private int[] pics;
private VoiceCountTimer timer;
protected Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what > 6) {
ivVoiceRecorder.setImageResource(pics[6]);
} else {
ivVoiceRecorder.setImageResource(pics[msg.what]);
}
}
};
public void setDuration(int duration) {
this.duration = duration;
}
public VoiceRecordView(Context context) {
this(context, null);
}
public VoiceRecordView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VoiceRecordView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
this.context = context;
LayoutInflater.from(context).inflate(R.layout.widget_voice_recorder, this);
ivVoiceRecorder = (ImageView) findViewById(R.id.iv_voice_recorder);
voiceRecorder = new VoiceRecordMannager(mHandler);
pics = new int[]{R.drawable.voice_bg_upglide_1,
R.drawable.voice_bg_upglide_2,
R.drawable.voice_bg_upglide_3,
R.drawable.voice_bg_upglide_4,
R.drawable.voice_bg_upglide_5,
R.drawable.voice_bg_upglide_6,
R.drawable.voice_bg_upglide_7,
};
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = mPowerManager.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "pat");
}
//把按住说话按钮的touch事件拿过来
public boolean onPressVoiceButton(View v, MotionEvent event, VoiceRecordListener listener) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// TODO: 2018/4/24 这里如果有语音在播放就要停掉
//开始录音
startRecording();
timer = new VoiceCountTimer(1000 * duration, 1000, listener, event);
timer.start();
return true;
case MotionEvent.ACTION_MOVE:
if (event.getY() < 0) {
//取消发送
voiceRecorder.setCancel(true);
ivVoiceRecorder.setImageResource(R.drawable.data_ico_cancel);
} else {
voiceRecorder.setCancel(false);
}
return true;
case MotionEvent.ACTION_UP:
if (timer != null) {
timer.onFinish();
timer.cancel();
timer = null;
return true;
}
if (event.getY() < 0) {
cancelRecord();
} else {
try {
int length = stopRecord();
if (length > 0) {
if (listener != null) {
listener.onVoiceRecordComplete(getVoicePath(), length);
}
} else if (length == 401) {
Toast.makeText(context, "无录音权限", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "录音时间太短", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, "发送失败,请检测服务器是否连接", Toast.LENGTH_SHORT).show();
}
}
return true;
default:
cancelRecord();
return false;
}
}
/**
* @return 返回录音文件的路径
*/
public String getVoicePath() {
return voiceRecorder.getCurrentVoicePath();
}
public boolean isRecording() {
return voiceRecorder.isRecording();
}
/**
* 停止录音
*
* @return
*/
private int stopRecord() {
this.setVisibility(View.INVISIBLE);
if (mWakeLock.isHeld())
mWakeLock.release();
return voiceRecorder.stopRecord();
}
/**
* 取消录音
*/
private void cancelRecord() {
if (mWakeLock.isHeld())
mWakeLock.release();
try {
if (voiceRecorder.isRecording()) {
voiceRecorder.cancelRecord();
this.setVisibility(View.INVISIBLE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 开始录音
*/
private void startRecording() {
if (!PathUtil.isSdcardExit()) {
Toast.makeText(context, "录音需要SD卡支持", Toast.LENGTH_SHORT).show();
return;
}
try {
mWakeLock.acquire();
this.setVisibility(View.VISIBLE);
voiceRecorder.startRecord();
} catch (Exception e) {
e.printStackTrace();
if (mWakeLock.isHeld())
mWakeLock.release();
if (voiceRecorder != null)
voiceRecorder.cancelRecord();
this.setVisibility(View.INVISIBLE);
Toast.makeText(context, "录音失败,请重试!", Toast.LENGTH_SHORT).show();
return;
}
}
class VoiceCountTimer extends CountDownTimer {
private MotionEvent event;
private VoiceRecordListener vListener;
/**
* @param millisInFuture The number of millis in the future from the call
* to {@link #start()} until the countdown is done and {@link #onFinish()}
* is called.
* @param countDownInterval The interval along the way to receive
* {@link #onTick(long)} callbacks.
*/
public VoiceCountTimer(long millisInFuture, long countDownInterval, VoiceRecordListener vListener, MotionEvent event) {
super(millisInFuture, countDownInterval);
this.vListener = vListener;
this.event = event;
}
@Override
public void onTick(long millisUntilFinished) {
//这里可以做个时间提示
}
@Override
public void onFinish() {
if (event.getY() < 0) {
// discard the recorded audio.
cancelRecord();
} else {
// stop recording and send voice file
try {
int length = stopRecord();
if (length > 0) {
if (vListener != null) {
vListener.onVoiceRecordComplete(getVoicePath(), length);
}
} else if (length == 401) {
Toast.makeText(context, "无录音权限", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "录音时间太短", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, "发送失败,请检测服务器是否连接", Toast.LENGTH_SHORT).show();
}
}
}
}
public interface VoiceRecordListener {
/**
* @param voiceFilePath 录音文件路径
* @param voiceTimeLength 录音时间长度
*/
void onVoiceRecordComplete(String voiceFilePath, int voiceTimeLength);
}
}
录音结束后通过onVoiceRecordComplete回调,在回调中我们得到音频文件的路径和它的时长
创建一个消息实体VoiceMsg
@Entity
public class VoiceMsg {
@Id
private Long id;
private long time;//时间长度
private String filePath;//文件路径
private float voiceTime;//
private int deriction;//0 send 1 receive
private String name;
@Generated(hash = 749010677)
public VoiceMsg(Long id, long time, String filePath, float voiceTime,
int deriction, String name) {
this.id = id;
this.time = time;
this.filePath = filePath;
this.voiceTime = voiceTime;
this.deriction = deriction;
this.name = name;
}
@Generated(hash = 809488364)
public VoiceMsg() {
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public long getTime() {
return this.time;
}
public void setTime(long time) {
this.time = time;
}
public String getFilePath() {
return this.filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public float getVoiceTime() {
return this.voiceTime;
}
public void setVoiceTime(float voiceTime) {
this.voiceTime = voiceTime;
}
public int getDeriction() {
return this.deriction;
}
public void setDeriction(int deriction) {
this.deriction = deriction;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
可以看到有GreenDao的注解,刚才提到过采用GreenDao做数据存储,对GreenDao不了解的朋友可以看我的另一个博客
https://www.jianshu.com/p/bbd032ba2029
接下来看Adapter的实现
public class VoiceMsgAdapter extends RecyclerView.Adapter<VoiceMsgAdapter.ViewHolder> {
private static final int TYPE_SEND = 0x01;
private static final int TYPE_RECEIVE = 0x02;
private final int duration;
private List<VoiceMsg> msgs;
private Context ctx;
private final LayoutInflater mInflater;
private int mMaxWidth;
private int mMinWidth;
private View currentAnimView = null;
private AnimationDrawable animation;
public VoiceMsgAdapter(Context ctx, List<VoiceMsg> msgs, int duration) {
this.ctx = ctx;
this.duration = duration;
if (msgs == null) this.msgs = new ArrayList<VoiceMsg>();
else this.msgs = msgs;
mInflater = LayoutInflater.from(ctx);
//获取屏幕的宽度
WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
//最大宽度为屏幕宽度的百分之56
mMaxWidth = (int) (outMetrics.widthPixels * 0.56f);
//最小宽度为屏幕宽度的百分之16
mMinWidth = (int) (outMetrics.widthPixels * 0.16f);
}
/**
* 第一次加载
*
* @param datas
*/
public void setData(List<VoiceMsg> datas) {
msgs.clear();
msgs.addAll(datas);
notifyDataSetChanged();
}
public void loadMore(List<VoiceMsg> datas) {
msgs.addAll(0,datas);
notifyDataSetChanged();
}
/**
* 添加一条
*
* @param data
*/
public void addData(VoiceMsg data) {
msgs.add(data);
if (msgs.size() > 0) notifyItemInserted(msgs.size() - 1);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_SEND:
View view1 = mInflater.inflate(R.layout.chat_recycle_item_send_right, parent, false);
return new ViewHolder(view1);
case TYPE_RECEIVE:
View view2 = mInflater.inflate(R.layout.chat_recycle_item_receive_left, parent, false);
return new ViewHolder(view2);
default:
View view0 = mInflater.inflate(R.layout.chat_recycle_item_send_right, parent, false);
return new ViewHolder(view0);
}
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
final int itemViewType = getItemViewType(position);
final VoiceMsg msg = msgs.get(position);
Log.e("ll", "onBindViewHolder(VoiceMsgAdapter.java:102---------)" + msg.getId());
if (position == 0) {
holder.time.setText(VoiceDateUtils.getTimestampString(new Date(msg.getTime())));
holder.time.setVisibility(View.VISIBLE);
} else {
// 两条消息时间离得如果稍长,显示时间
if (VoiceDateUtils.isCloseEnough(msg.getTime(), msgs.get(position - 1).getTime())) {
holder.time.setVisibility(View.GONE);
} else {
holder.time.setText(VoiceDateUtils.getTimestampString(new Date(
msg.getTime())));
holder.time.setVisibility(View.VISIBLE);
}
}
ViewGroup.LayoutParams lp = holder.voiceBg.getLayoutParams();
lp.width = (int) (mMinWidth + ((float) (mMaxWidth - mMinWidth) / duration) * msg.getVoiceTime());
holder.voiceTime.setText(Math.round(msg.getVoiceTime()) + "\"");
holder.voiceBg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentAnimView != null && currentAnimView == holder.voiceAnim && MediaPlayerHelper.isPlaying()) {
MediaPlayerHelper.release();
if (itemViewType == TYPE_SEND) {
currentAnimView.setBackgroundResource(R.drawable.data_ico_left_voice_three1);
} else {
currentAnimView.setBackgroundResource(R.drawable.data_ico_right_voice_three1);
}
currentAnimView = null;
return;
}
if (animation != null && animation.isRunning()) {
animation.stop();
animation = null;
}
if (currentAnimView != null) {
if (itemViewType == TYPE_SEND) {
currentAnimView.setBackgroundResource(R.drawable.data_ico_left_voice_three1);
} else {
currentAnimView.setBackgroundResource(R.drawable.data_ico_right_voice_three1);
}
currentAnimView = null;
}
currentAnimView = holder.voiceAnim;
if (itemViewType == TYPE_SEND) {
currentAnimView.setBackgroundResource(R.drawable.voice_play_send_anim);
MediaPlayerHelper.play(msg.getFilePath(), new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
MediaPlayerHelper.release();
currentAnimView.setBackgroundResource(R.drawable.data_ico_left_voice_three1);
}
});
} else {
currentAnimView.setBackgroundResource(R.drawable.voice_play_receive_anim);
MediaPlayerHelper.play(msg.getFilePath(), new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
MediaPlayerHelper.release();
currentAnimView.setBackgroundResource(R.drawable.data_ico_right_voice_three1);
}
});
}
animation = (AnimationDrawable) currentAnimView.getBackground();
animation.start();
}
});
if (itemViewType == TYPE_RECEIVE) {
holder.tvName.setText(msg.getName());
}
}
@Override
public int getItemViewType(int position) {
VoiceMsg msg = msgs.get(position);
int deriction = msg.getDeriction();
if (deriction == 0) {
return TYPE_SEND;
} else if (deriction == 1) {
return TYPE_RECEIVE;
}
return 0;
}
@Override
public int getItemCount() {
return msgs.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
//共有
TextView time;
ImageView ivIcon;
ImageView voiceBg;
TextView voiceTime;
View voiceAnim;
//接收特有
TextView tvName;
View unread;
public ViewHolder(View itemView) {
super(itemView);
time = (TextView) itemView.findViewById(R.id.timestamp); //时间
ivIcon = (ImageView) itemView.findViewById(R.id.iv_icon); //头像
voiceBg = (ImageView) itemView.findViewById(R.id.iv_voice_bg); //消息长度条
voiceTime = (TextView) itemView.findViewById(R.id.tv_voice_length); //消息时长
voiceAnim = (View) itemView.findViewById(R.id.voice_anim); //消息播放动画
tvName = itemView.findViewById(R.id.tv_name); //名字
unread = itemView.findViewById(R.id.unread_flag); //未读标记
}
}
}
主要注意多条播放,和播放动画问题的处理,数据采用局部加载,减少性能消耗。
主界面VoiceChatActivity
public class VoiceChatActivity extends AppCompatActivity implements View.OnTouchListener {
private Button btnSend;
private Button btnTalk;
private SwipeRefreshLayout srlFresh;
private RecyclerView recyclerView;
private VoiceMsgAdapter mAdapter;
// 按钮正常状态(默认状态)
private static final int STATE_NORMAL = 1;
//正在录音状态
private static final int STATE_RECORDING = 2;
//录音取消状态
private static final int STATE_CANCEL = 3;
//记录当前状态
private int mCurrentState = STATE_NORMAL;
private VoiceRecordView voiceRecordView;
//判断在Button上滑动距离,以判断 是否取消
private static final int DISTANCE_Y_CANCEL = 50;
private int page;
private static final int DURATION = 15;
private LinearLayoutManager layoutManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_voice_chat);
btnSend = (Button) findViewById(R.id.btn_send);
btnTalk = (Button) findViewById(R.id.btn_talk);
srlFresh = (SwipeRefreshLayout) findViewById(R.id.srl_fresh);
recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
voiceRecordView = (VoiceRecordView) findViewById(R.id.voice_recorder);
if (Build.VERSION.SDK_INT >= 23) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_CODE);
}
}
voiceRecordView.setDuration(DURATION);
srlFresh.setColorSchemeColors(ContextCompat.getColor(this, R.color.color_1)
, ContextCompat.getColor(this, R.color.color_2)
, ContextCompat.getColor(this, R.color.color_3));
srlFresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
++page;
loadData(page);
//下拉
srlFresh.setRefreshing(false);
}
});
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
mAdapter = new VoiceMsgAdapter(this, null, DURATION);
recyclerView.setAdapter(mAdapter);
toBottom();
btnTalk.setOnTouchListener(this);
btnSend.setEnabled(false);
page = 1;
loadData(page);
toBottom();
}
private void loadData(int page) {
List<VoiceMsg> wxTwentyMsg = VoiceDbUtil.getInstance().getWXTwentyMsg(page);
if (wxTwentyMsg != null) {
if (wxTwentyMsg.size() > 0) {
mAdapter.loadMore(wxTwentyMsg);
} else {
if (page>1) {
Toast.makeText(this, "没有更多数据了", Toast.LENGTH_SHORT).show();
}
}
}
}
private void toBottom() {
if (mAdapter.getItemCount() > 0) {
recyclerView.scrollToPosition(mAdapter.getItemCount() - 1);
}
}
private static final int REQUEST_CODE = 482;
/*
* 语音按键的touch事件
*
* */
@Override
public boolean onTouch(View v, MotionEvent event) {
toBottom();
//获取TouchEvent状态
int action = event.getAction();
// 获得x轴坐标
int x = (int) event.getX();
// 获得y轴坐标
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: //按下
changeState(STATE_RECORDING); //手指按下开始记录
break;
case MotionEvent.ACTION_MOVE: //移动
if (voiceRecordView.isRecording()) {
if (y < 0) {
changeState(STATE_CANCEL);
} else {
changeState(STATE_RECORDING);
}
}
break;
case MotionEvent.ACTION_UP: //抬起
changeState(STATE_NORMAL);
break;
default:
changeState(STATE_NORMAL);
break;
}
return voiceRecordView.onPressVoiceButton(v, event, new VoiceRecordView.VoiceRecordListener() {
//录音结束回调
@Override
public void onVoiceRecordComplete(String voiceFilePath, int voiceTimeLength) {
VoiceMsg voiceMsg = new VoiceMsg(null, System.currentTimeMillis(), voiceFilePath, voiceTimeLength, 0, null);
VoiceDbUtil.getInstance().insert(voiceMsg);
mAdapter.addData(voiceMsg);
toBottom();
}
});
}
private boolean wantToCancle(View v, int x, int y) {
// 超过按钮的宽度
if (x < 0 || x > v.getWidth()) {
return true;
}
// 超过按钮的高度
if (y < -DISTANCE_Y_CANCEL || y > v.getHeight() + DISTANCE_Y_CANCEL) {
return true;
}
return false;
}
/**
* 根据触摸状态改变button的显示
*
* @param state
*/
private void changeState(int state) {
if (mCurrentState != state) {
mCurrentState = state;
switch (state) {
case STATE_NORMAL: //普通状态
btnSend.setBackgroundResource(R.drawable.data_bg_talk_nor1);
btnTalk.setBackgroundResource(R.drawable.data_btn_talk_nor1);
break;
case STATE_RECORDING: //录音状态
btnSend.setBackgroundResource(R.drawable.data_bg_talk_up);
btnTalk.setBackgroundResource(R.drawable.data_btn_talk_up);
break;
case STATE_CANCEL: //取消状态
btnSend.setBackgroundResource(R.drawable.data_bg_talk_nor1);
btnTalk.setBackgroundResource(R.drawable.data_btn_talk_nor1);
break;
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
MediaPlayerHelper.release();
}
}
如果需要clone代码进行研究请移步https://github.com/liu20160703/voiceChat
这里只写了发送的,由于接收消息使用的是公司的sdk,这里不方便透露,实现上已经和微信很接近如未读消息连读功能,消息发送状态,重发等