文档章节

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

爱看博客
 爱看博客
发布于 2015/10/22 10:02
字数 1625
阅读 26
收藏 0
点赞 0
评论 0

        欢迎各位加入我的Android开发群[257053751]

最近由于需要做一个录音功能(/嘘 悄悄透露一下,千万别告诉红薯,就是新版本的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录音控件的实现方法。

版权声明:本文原创,转载请注明来自 http://kymjs.com/

本文转载自:http://blog.csdn.net/kymjs/article/details/41903565

共有 人打赏支持
爱看博客
粉丝 5
博文 103
码字总数 23887
作品 0
深圳

暂无相关文章

前台对中文编码,后台解码

前台:encodeURI(sbzt) 后台:String param = URLDecoder.decode(sbzt,"UTF-8");

west_coast ⋅ 38分钟前 ⋅ 0

VS2015配置并运行汇编(一步一步照图做)【vs2017的链接在最后】

前言 我是上学期学的汇编,因为有vs又不想用课上教的麻烦的dosbox以及masm32,但是一直没找到高亮插件和能调试的(难在运行不了而找不到答案上,出现的错误在最后放出,还请先达们不吝指点)...

simpower ⋅ 48分钟前 ⋅ 0

一起读书《深入浅出nodejs》-node模块机制

node 模块机制 前言 说到node,就不免得提到JavaScript。JavaScript自诞生以来,经历了工具类库、组件库、前端框架、前端应用的变迁。通过无数开发人员的努力,JavaScript不断被类聚和抽象,...

小草先森 ⋅ 51分钟前 ⋅ 0

Java桌球小游戏

其实算不上一个游戏,就是两张图片,不停的重画,改变ball图片的位置。一个左右直线碰撞的,一个有角度碰撞的。 左右直线碰撞 package com.bjsxt.test;import javax.swing.*;import j...

森林之下 ⋅ 58分钟前 ⋅ 0

你真的明白RPC 吗?一起来探究 RPC 的实质

你真的明白RPC 吗?一起来探究 RPC 的实质 不论你是科班出身还是半路转行,这么优秀的你一定上过小学语文,那么对扩句和缩句你一定不陌生。缩句就是去除各种修饰提炼出一句话的核心,而不失基...

AI9o後 ⋅ 59分钟前 ⋅ 0

z-index设置失效?

今天碰到了一个问题,就是在给li设置提示框的时候,有用到遮罩效果,本来想把对应的出现在最顶层,可是不管将li设置的z-index值设为多大,li都没有出现在遮罩层之上。 我在网上查了z-index设...

IrisHunag ⋅ 今天 ⋅ 0

CyclicBarrier、CountDownLatch以及Semaphore使用及其原理分析

CyclicBarrier、CountDownLatch以及Semaphore是Java并发包中几个常用的并发组件,这几个组件特点是功能相识很容易混淆。首先我们分别介绍这几个组件的功能然后再通过实例分析和源码分析其中设...

申文波 ⋅ 今天 ⋅ 0

Java对象的序列化与反序列化

Java对象的序列化与反序列化

Cobbage ⋅ 今天 ⋅ 0

Sqoop

1.Sqoop: 《=》 SQL to Hadoop 背景 1)场景:数据在RDBMS中,我们如何使用Hive或者Hadoop来进行数据分析呢? 1) RDBMS ==> Hadoop(广义) 2) Hadoop ==> RDBMS 2)原来可以通过MapReduce I...

GordonNemo ⋅ 今天 ⋅ 0

全量构建和增量构建的区别

1.全量构建每次更新时都需要更新整个数据集,增量构建只对需要更新的时间范围进行更新,所以计算量会较小。 2.全量构建查询时不需要合并不同Segment,增量构建查询时需要合并不同Segment的结...

无精疯 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部