Android仿微信录音功能,自定义控件的设计技巧



对本文有任何问题,可加我的个人微信询问:kymjs666

最近由于需要做一个录音功能(/嘘 悄悄透露一下,千万别告诉红薯,就是新版本的OSC客户端噢),起初打算采用仿微信的录音方式,最后又改成了QQ的录音方式,之前的微信录音控件也就白写了[大哭]。之前有很多朋友在问我自定义控件应该怎么学习,遂正好拿出来讲讲喽,没来得及截效果图,大家就自己脑补一下微信发语音时的样子吧。

原理

所谓自定义控件其实就是由于系统SDK无法完成需要的功能时,通过自己扩展系统组件达到完成所需功能做出的控件。

Android自定义控件有两种实现方式,一种是通过继承View类,其中的全部界面通过画布和画笔自己创建,这种控件一般多用于游戏开发中;另一种则是通过继承已有控件,或采用包含关系包含一个系统控件达到目的,这也是接下来本文所要讲到的方法

代码

先看代码(篇幅有限,仅保留重要方法)

    /**
     * 录音专用Button,可弹出自定义的录音dialog。需要配合{@link #RecordButtonUtil}使用
     * @author kymjs(kymjs123@gmail.com)
     */
    public class RecordButton extends Button {
        private static final int MIN_INTERVAL_TIME = 700; // 录音最短时间
        private static final int MAX_INTERVAL_TIME = 60000; // 录音最长时间
        private RecordButtonUtil mAudioUtil;
        private Handler mVolumeHandler; // 用于更新录音音量大小的图片

        public RecordButton(Context context) {
            super(context);
            mVolumeHandler = new ShowVolumeHandler(this);
            mAudioUtil = new RecordButtonUtil();
            initSavePath();
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (mAudioFile == null) {
                return false;
            }
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                initlization();
                break;
            case MotionEvent.ACTION_UP:
                if (event.getY() < -50) {
                    cancelRecord();
                } else {
                    finishRecord();
                }
                break;
            case MotionEvent.ACTION_MOVE:
               //做一些UI提示
                break;
            }
            return true;
        }

        /** 初始化 dialog和录音器 */
        private void initlization() {
            mStartTime = System.currentTimeMillis();
            if (mRecordDialog == null) {
                mRecordDialog = new Dialog(getContext());
                mRecordDialog.setOnDismissListener(onDismiss);
            }
            mRecordDialog.show();
            startRecording();
        }

        /** 录音完成(达到最长时间或用户决定录音完成) */
        private void finishRecord() {
            stopRecording();
            mRecordDialog.dismiss();
            long intervalTime = System.currentTimeMillis() - mStartTime;
            if (intervalTime < MIN_INTERVAL_TIME) {
                AppContext.showToastShort(R.string.record_sound_short);
                File file = new File(mAudioFile);
                file.delete();
                return;
            }
            if (mFinishedListerer != null) {
                mFinishedListerer.onFinishedRecord(mAudioFile,
                        (int) ((System.currentTimeMillis() - mStartTime) / 1000));
            }
        }
        // 用户手动取消录音
        private void cancelRecord() {
            stopRecording();
            mRecordDialog.dismiss();
            File file = new File(mAudioFile);
            file.delete();
            if (mFinishedListerer != null) {
                mFinishedListerer.onCancleRecord();
            }
        }

        // 开始录音
        private void startRecording() {
            mAudioUtil.setAudioPath(mAudioFile);
            mAudioUtil.recordAudio();
            mThread = new ObtainDecibelThread();
            mThread.start();

        }
        // 停止录音
        private void stopRecording() {
            if (mThread != null) {
                mThread.exit();
                mThread = null;
            }
            if (mAudioUtil != null) {
                mAudioUtil.stopRecord();
            }
        }

        /******************************* inner class ****************************************/
        private class ObtainDecibelThread extends Thread {
            private volatile boolean running = true;

            public void exit() {
                running = false;
            }
            @Override
            public void run() {
                while (running) {
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (System.currentTimeMillis() - mStartTime >= MAX_INTERVAL_TIME) {
                        // 如果超过最长录音时间
                        mVolumeHandler.sendEmptyMessage(-1);
                    }
                    if (mAudioUtil != null && running) {
                        // 如果用户仍在录音
                        int volumn = mAudioUtil.getVolumn();
                        if (volumn != 0)
                            mVolumeHandler.sendEmptyMessage(volumn);
                    } else {
                        exit();
                    }
                }
            }
        }
        private final OnDismissListener onDismiss = new OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                stopRecording();
            }
        };
        static class ShowVolumeHandler extends Handler {
            private final WeakReference<RecordButton> mOuterInstance;
            public ShowVolumeHandler(RecordButton outer) {
                mOuterInstance = new WeakReference<RecordButton>(outer);
            }
            @Override
            public void handleMessage(Message msg) {
                RecordButton outerButton = mOuterInstance.get();
                if (msg.what != -1) {
                    // 大于0时 表示当前录音的音量
                    if (outerButton.mVolumeListener != null) {
                        outerButton.mVolumeListener.onVolumeChange(mRecordDialog,
                                msg.what);
                    }
                } else {
                    // -1 时表示录音超时
                    outerButton.finishRecord();
                }
            }
        }

        /** 音量改变的监听器 */
        public interface OnVolumeChangeListener {
            void onVolumeChange(Dialog dialog, int volume);
        }
        public interface OnFinishedRecordListener {
            /** 用户手动取消 */
            public void onCancleRecord();
            /** 录音完成 */
            public void onFinishedRecord(String audioPath, int recordTime);
        }
    }
    /**
     * {@link #RecordButton}需要的工具类
     * 
     * @author kymjs(kymjs123@gmail.com)
     */
    public class RecordButtonUtil {
        public static final String AUDOI_DIR = Environment
                .getExternalStorageDirectory().getAbsolutePath() + "/oschina/audio"; // 录音音频保存根路径

        private String mAudioPath; // 要播放的声音的路径
        private boolean mIsRecording;// 是否正在录音
        private boolean mIsPlaying;// 是否正在播放
        private OnPlayListener listener;
     
        // 初始化 录音器
        private void initRecorder() {
            mRecorder = new MediaRecorder();
            mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            mRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);
            mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            mRecorder.setOutputFile(mAudioPath);
            mIsRecording = true;
        }

        /** 开始录音,并保存到文件中 */
        public void recordAudio() {
            initRecorder();
            try {
                mRecorder.prepare();
            } catch (IOException e) {
                e.printStackTrace();
            }
            mRecorder.start();
        }

        /** 获取音量值,只是针对录音音量 */
        public int getVolumn() {
            int volumn = 0;
            // 录音
            if (mRecorder != null && mIsRecording) {
                volumn = mRecorder.getMaxAmplitude();
                if (volumn != 0)
                    volumn = (int) (10 * Math.log(volumn) / Math.log(10)) / 7;
            }
            return volumn;
        }

        /** 停止录音 */
        public void stopRecord() {
            if (mRecorder != null) {
                mRecorder.stop();
                mRecorder.release();
                mRecorder = null;
                mIsRecording = false;
            }
        }

        public void startPlay(String audioPath) {
            if (!mIsPlaying) {
                if (!StringUtils.isEmpty(audioPath)) {
                    mPlayer = new MediaPlayer();
                    try {
                        mPlayer.setDataSource(audioPath);
                        mPlayer.prepare();
                        mPlayer.start();
                        if (listener != null) {
                            listener.starPlay();
                        }
                        mIsPlaying = true;
                        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                            @Override
                            public void onCompletion(MediaPlayer mp) {
                                if (listener != null) {
                                    listener.stopPlay();
                                }
                                mp.release();
                                mPlayer = null;
                                mIsPlaying = false;
                            }
                        });
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } else {
                    AppContext.showToastShort(R.string.record_sound_notfound);
                }
            } // end playing
        }
        public interface OnPlayListener {
            /** 播放声音结束时调用 */
            void stopPlay();

            /**  播放声音开始时调用 */
            void starPlay();
        }
    }

核心代码讲解

作为控件界面控制逻辑,我们主要看一下onTouchEvent方法:当手指按下的时候,初始化录音器。手指在屏幕上移动的时候如果滑到按钮之上的时候,event.getY会返回一个负值(因为滑出控件了嘛)。这里我写的是-50主要是为了多一点缓冲,防止误操作。

    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            initlization();
            break;
        case MotionEvent.ACTION_UP:
            if (mIsCancel && event.getY() < -50) {
                cancelRecord();
            } else {
                finishRecord();
            }
            mIsCancel = false;
            break;
        case MotionEvent.ACTION_MOVE:
            // 当手指移动到view外面,会cancel
            //做一些UI提示
            break;
        }
        return true;
    }

设计技巧

一些设计技巧:比如通过回调解耦,使控件变得通用。虽说自定义控件一般不需要多么的通用,但是像录音控件这种很多应用都会用到的功能,还是做得通用一点要好。像录音时弹出的dialog,我采用从外部获取的方式,方便以后修改这个弹窗,也方便代码阅读的时候更加清晰。再比如根据话筒音量改变录音图标这样的方法,设置成外部以后,就算以后更换其他图片,更换其他显示方式,对自定义控件本身来说,不需要改任何代码。

对于录音和放音的功能实现,采用包含关系单独写在一个新类里面,这样方便以后做更多扩展,比如未来采用私有的录音编码加密,比如播放录音之前先放一段音乐(谁特么这么无聊)等等。。。

再来看一下Thread与Handle的交互,这里我设计的并不是很好,其实不应该将两种消息放在同一个msg中发出的,这里主要是考虑到消息简单,使用一个空msg仅仅通过一个int值区分信息就行了。

Handle中采用了一个软引用包含外部类,这种方式在网上有很多讲解,之后我也会单独再写一篇博客讲解,这里大家知道目的是为了防止对象间的互相引用造成内存泄露就可以了。

以上便是对仿微信录音界面的一个讲解,其实微信的录音效果实现起来比起QQ的效果还是比较简单的,以后我也会再讲QQ录音控件的实现方法。