文档章节

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

短短的歼击机
 短短的歼击机
发布于 2014/12/09 14:14
字数 1875
阅读 2663
收藏 9

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

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

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

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/**
 * 录音专用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);
    }
}
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 * {@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主要是为了多一点缓冲,防止误操作。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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录音控件的实现方法。

© 著作权归作者所有

短短的歼击机

短短的歼击机

粉丝 82
博文 268
码字总数 269797
作品 0
武汉
高级程序员
私信 提问
Android仿微信录音功能,自定义控件的设计技巧

最近由于需要做一个录音功能(/嘘 悄悄透露一下,千万别告诉红薯,就是新版本的OSC客户端噢),起初打算采用仿微信的录音方式,最后又改成了QQ的录音方式,之前的微信录音控件也就白写了[大哭...

kymjs张涛
2014/12/05
4.2K
9
仿MIUI音量变化环形进度条实现

Android中使用环形进度条的业务场景其实蛮多的,比如下载文件的时候使用环形进度条,会给用户眼前一亮的感觉;再比如我大爱的MIUI系统,它的音量进度条就是使用环形进度条,尽显小米"为发烧而...

Jack_1900
2014/07/25
760
0
Android部分源码资源共享(视屏转GIF图片工具、仿抖音、仿朋友圈、仿红包、饼状图、引导图,图灵源码等)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。 https://blog.csdn.net/generallizhong/article/details/90202879 视屏转为gif图片工具: 下载地...

generallizhong
05/14
0
0
2017 我所分享的技术文章总结(下)

> 对下半年所分享的文章进行整理,上半年总结的 98 篇好文请点击这里,很多读者当时忘记了收藏,以致于查找一篇历史文章很费劲,因此在这里顺便做下记录。目前就分下下面几个大类,没有更多细...

你未读
2018/01/01
0
0
android经典源码,很不错的开源框架

高仿最美应用项目源码 项目介绍 这是仿最美应用开发的基于mvp+rxjava+retrofit的项目,很值得学 github地址: github.com/JJOGGER/Bea… Musicoco 完整项目:音乐播放器 项目介绍 功能:通过...

codeGoogle
2018/10/30
0
0

没有更多内容

加载失败,请刷新页面

加载更多

02.日志系统:一条SQL更新语句是如何执行的?

我们还是从一个表的一条更新语句说起,我们创建下面一张表: create table T(ID int primary key, c int); 如果要将ID=2这一行c的值加1,SQL可以这么写: update T set c=c+1 where ID=2; 前...

scgaopan
今天
7
0
【五分钟系列】掌握vscode调试技巧

调试前端js 准备一个前端项目 index.html <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1......

aoping
今天
6
0
PhotoShop 高级应用:USM锐化/S锐化/防抖

、 高反差锐化+混合模式:叠加模式 【将更多的边缘细节添加到图像中】

东方墨天
今天
7
0
Python数据可视化之matplotlib

常用模块导入 import numpy as npimport matplotlibimport matplotlib.mlab as mlabimport matplotlib.pyplot as pltimport matplotlib.font_manager as fmfrom mpl_toolkits.mplot3d i......

松鼠大帝
昨天
5
0
我用Bash编写了一个扫雷游戏

我在编程教学方面不是专家,但当我想更好掌握某一样东西时,会试着找出让自己乐在其中的方法。比方说,当我想在 shell 编程方面更进一步时,我决定用 Bash 编写一个扫雷游戏来加以练习。 我在...

老孟的Linux私房菜
昨天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部