最近因为须要作一个录音功能(/嘘 悄悄透露一下,千万别告诉红薯,就是新版本的OSC客户端噢),起初打算采用仿微信的录音方式,最后又改为了QQ的录音方式,以前的微信录音控件也就白写了[大哭]。以前有不少朋友在问我自定义控件应该怎么学习,遂正好拿出来说讲喽,没来得及截效果图,你们就本身脑补一下微信发语音时的样子吧。java
所谓自定义控件其实就是因为系统SDK没法完成须要的功能时,经过本身扩展系统组件达到完成所需功能作出的控件。
微信
Android自定义控件有两种实现方式,一种是经过继承View类,其中的所有界面经过画布和画笔本身建立,这种控件通常多用于游戏开发中;另外一种则是经过继承已有控件,或采用包含关系包含一个系统控件达到目的,这也是接下来本文所要讲到的方法。
ide
先看代码(篇幅有限,仅保留重要方法)
工具
/** * 录音专用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,我采用从外部获取的方式,方便之后修改这个弹窗,也方便代码阅读的时候更加清晰。再好比根据话筒音量改变录音图标这样的方法,设置成外部之后,就算之后更换其余图片,更换其余显示方式,对自定义控件自己来讲,不须要改任何代码。this
对于录音和放音的功能实现,采用包含关系单独写在一个新类里面,这样方便之后作更多扩展,好比将来采用私有的录音编码加密,好比播放录音以前先放一段音乐(谁特么这么无聊)等等。。。编码
再来看一下Thread与Handle的交互,这里我设计的并非很好,其实不该该将两种消息放在同一个msg中发出的,这里主要是考虑到消息简单,使用一个空msg仅仅经过一个int值区分信息就好了。加密
Handle中采用了一个软引用包含外部类,这种方式在网上有不少讲解,以后我也会单独再写一篇博客讲解,这里你们知道目的是为了防止对象间的互相引用形成内存泄露就能够了。spa
以上即是对仿微信录音界面的一个讲解,其实微信的录音效果实现起来比起QQ的效果仍是比较简单的,之后我也会再讲QQ录音控件的实现方法。设计