RecyclerView实现语音聊天,仿微信

2018-04-27  本文已影响0人  bruce1990

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

上一篇下一篇

猜你喜欢

热点阅读