美文网首页
RecyclerView实现语音聊天,仿微信

RecyclerView实现语音聊天,仿微信

作者: bruce1990 | 来源:发表于2018-04-27 16:26 被阅读0次

    语音主要涉及到的就是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&apos;&apos;"
                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&apos;&apos;"
                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,这里不方便透露,实现上已经和微信很接近如未读消息连读功能,消息发送状态,重发等

    相关文章

      网友评论

          本文标题:RecyclerView实现语音聊天,仿微信

          本文链接:https://www.haomeiwen.com/subject/dwgblftx.html