Android开发--仿微信语音对讲录音

自微信出现以来取得了很好的成绩,语音对讲的实现更加方便了人与人之间的交流。今天来实践一下微信的语音对讲的录音实现,这个也比较容易实现。在此,我将该按钮封装成为一个控件,并通过策略模式的方式实现录音和界面的解耦合,以方便我们在实际情况中对录音方法的不同需求(例如想要实现wav格式的编码时我们也就不能再使用MediaRecorder,而只能使用AudioRecord进行处理)。

效果图:

实现思路

1.在微信中我们可以看到实现语音对讲的是通过点按按钮来完成的,因此在这里我选择重新自己的控件使其继承自Button并重写onTouchEvent方法,来实现对录音的判断。

2.在onTouchEvent方法中,

当我们按下按钮时,首先显示录音的对话框,然后调用录音准备方法并开始录音,接着开启一个计时线程,每隔0.1秒的时间获取一次录音音量的大小,并通过Handler根据音量大小更新Dialog中的显示图片;

当我们移动手指时,若手指向上移动距离大于50,在Dialog中显示松开手指取消录音的提示,并将isCanceled变量(表示我们最后是否取消了录音)置为true,上移动距离小于20时,我们恢复Dialog的图片,并将isCanceled置为false;

当抬起手指时,我们首先关闭录音对话框,接着调用录音停止方法并关闭计时线程,然后我们判断是否取消录音,若是的话则删除录音文件,否则判断计时时间是否太短,最后调用回调接口中的recordEnd方法。

3.在这里为了适应不同的录音需求,我使用了策略模式来进行处理,将每一个不同的录音方法视为一种不同的策略,根据自己的需要去改写。

注意问题

1.在onTouchEvent的返回值中应该返回true,这样才能屏蔽之后其他的触摸事件,否则当手指滑动离开Button之后将不能在响应我们的触摸方法。

2.不要忘记为自己的App添加权限:

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

代码参考

RecordButton 类,我们的自定义控件,重新复写了onTouchEvent方法

package com.example.recordtest;

import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

public class RecordButton extends Button {

    private static final int MIN_RECORD_TIME = 1; // 最短录音时间,单位秒
    private static final int RECORD_OFF = 0; // 不在录音
    private static final int RECORD_ON = 1; // 正在录音

    private Dialog mRecordDialog;
    private RecordStrategy mAudioRecorder;
    private Thread mRecordThread;
    private RecordListener listener;

    private int recordState = 0; // 录音状态
    private float recodeTime = 0.0f; // 录音时长,如果录音时间太短则录音失败
    private double voiceValue = 0.0; // 录音的音量值
    private boolean isCanceled = false; // 是否取消录音
    private float downY;

    private TextView dialogTextView;
    private ImageView dialogImg;
    private Context mContext;

    public RecordButton(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
        init(context);
    }

    public RecordButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO Auto-generated constructor stub
        init(context);
    }

    public RecordButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
        init(context);
    }

    private void init(Context context) {
        mContext = context;
        this.setText("按住 说话");
    }

    public void setAudioRecord(RecordStrategy record) {
        this.mAudioRecorder = record;
    }

    public void setRecordListener(RecordListener listener) {
        this.listener = listener;
    }

    // 录音时显示Dialog
    private void showVoiceDialog(int flag) {
        if (mRecordDialog == null) {
            mRecordDialog = new Dialog(mContext, R.style.Dialogstyle);
            mRecordDialog.setContentView(R.layout.dialog_record);
            dialogImg = (ImageView) mRecordDialog
                    .findViewById(R.id.record_dialog_img);
            dialogTextView = (TextView) mRecordDialog
                    .findViewById(R.id.record_dialog_txt);
        }
        switch (flag) {
        case 1:
            dialogImg.setImageResource(R.drawable.record_cancel);
            dialogTextView.setText("松开手指可取消录音");
            this.setText("松开手指 取消录音");
            break;

        default:
            dialogImg.setImageResource(R.drawable.record_animate_01);
            dialogTextView.setText("向上滑动可取消录音");
            this.setText("松开手指 完成录音");
            break;
        }
        dialogTextView.setTextSize(14);
        mRecordDialog.show();
    }

    // 录音时间太短时Toast显示
    private void showWarnToast(String toastText) {
        Toast toast = new Toast(mContext);
        View warnView = LayoutInflater.from(mContext).inflate(
                R.layout.toast_warn, null);
        toast.setView(warnView);
        toast.setGravity(Gravity.CENTER, 0, 0);// 起点位置为中间
        toast.show();
    }

    // 开启录音计时线程
    private void callRecordTimeThread() {
        mRecordThread = new Thread(recordThread);
        mRecordThread.start();
    }

    // 录音Dialog图片随录音音量大小切换
    private void setDialogImage() {
        if (voiceValue < 600.0) {
            dialogImg.setImageResource(R.drawable.record_animate_01);
        } else if (voiceValue > 600.0 && voiceValue < 1000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_02);
        } else if (voiceValue > 1000.0 && voiceValue < 1200.0) {
            dialogImg.setImageResource(R.drawable.record_animate_03);
        } else if (voiceValue > 1200.0 && voiceValue < 1400.0) {
            dialogImg.setImageResource(R.drawable.record_animate_04);
        } else if (voiceValue > 1400.0 && voiceValue < 1600.0) {
            dialogImg.setImageResource(R.drawable.record_animate_05);
        } else if (voiceValue > 1600.0 && voiceValue < 1800.0) {
            dialogImg.setImageResource(R.drawable.record_animate_06);
        } else if (voiceValue > 1800.0 && voiceValue < 2000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_07);
        } else if (voiceValue > 2000.0 && voiceValue < 3000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_08);
        } else if (voiceValue > 3000.0 && voiceValue < 4000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_09);
        } else if (voiceValue > 4000.0 && voiceValue < 6000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_10);
        } else if (voiceValue > 6000.0 && voiceValue < 8000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_11);
        } else if (voiceValue > 8000.0 && voiceValue < 10000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_12);
        } else if (voiceValue > 10000.0 && voiceValue < 12000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_13);
        } else if (voiceValue > 12000.0) {
            dialogImg.setImageResource(R.drawable.record_animate_14);
        }
    }

    // 录音线程
    private Runnable recordThread = new Runnable() {

        @Override
        public void run() {
            recodeTime = 0.0f;
            while (recordState == RECORD_ON) {
                {
                    try {
                        Thread.sleep(100);
                        recodeTime += 0.1;
                        // 获取音量,更新dialog
                        if (!isCanceled) {
                            voiceValue = mAudioRecorder.getAmplitude();
                            recordHandler.sendEmptyMessage(1);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    @SuppressLint("HandlerLeak")
    private Handler recordHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            setDialogImage();
        }
    };

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: // 按下按钮
            if (recordState != RECORD_ON) {
                showVoiceDialog(0);
                downY = event.getY();
                if (mAudioRecorder != null) {
                    mAudioRecorder.ready();
                    recordState = RECORD_ON;
                    mAudioRecorder.start();
                    callRecordTimeThread();
                }
            }
            break;
        case MotionEvent.ACTION_MOVE: // 滑动手指
            float moveY = event.getY();
            if (downY - moveY > 50) {
                isCanceled = true;
                showVoiceDialog(1);
            }
            if (downY - moveY < 20) {
                isCanceled = false;
                showVoiceDialog(0);
            }
            break;
        case MotionEvent.ACTION_UP: // 松开手指
            if (recordState == RECORD_ON) {
                recordState = RECORD_OFF;
                if (mRecordDialog.isShowing()) {
                    mRecordDialog.dismiss();
                }
                mAudioRecorder.stop();
                mRecordThread.interrupt();
                voiceValue = 0.0;
                if (isCanceled) {
                    mAudioRecorder.deleteOldFile();
                } else {
                    if (recodeTime < MIN_RECORD_TIME) {
                        showWarnToast("时间太短  录音失败");
                        mAudioRecorder.deleteOldFile();
                    } else {
                        if (listener != null) {
                            listener.recordEnd(mAudioRecorder.getFilePath());
                        }
                    }
                }
                isCanceled = false;
                this.setText("按住 说话");
            }
            break;
        }
        return true;
    }

    public interface RecordListener {
        public void recordEnd(String filePath);
    }
}

Dialog布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:gravity="center"
    android:background="@drawable/record_bg"
    android:padding="20dp" >

    <ImageView
        android:id="@+id/record_dialog_img"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/record_dialog_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:layout_marginTop="5dp" />

</LinearLayout>

录音时间太短的Toast布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/record_bg"
    android:padding="20dp"
    android:gravity="center"
    android:orientation="vertical" >

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/voice_to_short" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="15sp"
        android:text="时间太短  录音失败" />

</LinearLayout>

自定义的Dialogstyle,对话框样式

    <style name="Dialogstyle">
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowFrame">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
        <!-- 显示对话框时当前的屏幕是否变暗 -->
        <item name="android:backgroundDimEnabled">false</item>
    </style>

RecordStrategy 录音策略接口

package com.example.recordtest;

/**
 * RecordStrategy 录音策略接口
 * @author acer
 */
public interface RecordStrategy {

    /**
     * 在这里进行录音准备工作,重置录音文件名等
     */
    public void ready();
    /**
     * 开始录音
     */
    public void start();
    /**
     * 录音结束
     */
    public void stop();

    /**
     * 录音失败时删除原来的旧文件
     */
    public void deleteOldFile();

    /**
     * 获取录音音量的大小
     * @return
     */
    public double getAmplitude();

    /**
     * 返回录音文件完整路径
     * @return
     */
    public String getFilePath();

}

个人写的一个录音实践策略

package com.example.recordtest;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import android.media.MediaRecorder;
import android.os.Environment;

public class AudioRecorder implements RecordStrategy {

    private MediaRecorder recorder;
    private String fileName;
    private String fileFolder = Environment.getExternalStorageDirectory()
            .getPath() + "/TestRecord";

    private boolean isRecording = false;

    @Override
    public void ready() {
        // TODO Auto-generated method stub
        File file = new File(fileFolder);
        if (!file.exists()) {
            file.mkdir();
        }
        fileName = getCurrentDate();
        recorder = new MediaRecorder();
        recorder.setOutputFile(fileFolder + "/" + fileName + ".amr");
        recorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置MediaRecorder的音频源为麦克风
        recorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);// 设置MediaRecorder录制的音频格式
        recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);// 设置MediaRecorder录制音频的编码为amr
    }

    // 以当前时间作为文件名
    private String getCurrentDate() {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy_MM_dd_HHmmss");
        Date curDate = new Date(System.currentTimeMillis());// 获取当前时间
        String str = formatter.format(curDate);
        return str;
    }

    @Override
    public void start() {
        // TODO Auto-generated method stub
        if (!isRecording) {
            try {
                recorder.prepare();
                recorder.start();
            } catch (IllegalStateException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            isRecording = true;
        }

    }

    @Override
    public void stop() {
        // TODO Auto-generated method stub
        if (isRecording) {
            recorder.stop();
            recorder.release();
            isRecording = false;
        }

    }

    @Override
    public void deleteOldFile() {
        // TODO Auto-generated method stub
        File file = new File(fileFolder + "/" + fileName + ".amr");
        file.deleteOnExit();
    }

    @Override
    public double getAmplitude() {
        // TODO Auto-generated method stub
        if (!isRecording) {
            return 0;
        }
        return recorder.getMaxAmplitude();
    }

    @Override
    public String getFilePath() {
        // TODO Auto-generated method stub
        return fileFolder + "/" + fileName + ".amr";
    }

}

MainActivity

package com.example.recordtest;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;

public class MainActivity extends Activity {

    RecordButton button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (RecordButton) findViewById(R.id.btn_record);
        button.setAudioRecord(new AudioRecorder());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

源码下载

点击下载源码

时间: 2024-10-09 20:25:00

Android开发--仿微信语音对讲录音的相关文章

Android开发仿微信下拉关闭图片11

图片会跟随手指移动,只有是下滑时才会退出查看页面,其他情况会复位,直接当做ImageView使用即可,setViewCall方法是在下滑完成后要执行的操作,上,左,右,可自行扩展 onTouchEvent 监听手指坐标,GestureDetector 监听滑动的惯性,ViewHelper设置图片位移动画 public class FriendCircleView extends android.support.v7.widget.AppCompatImageView implements Ges

Android仿微信语音聊天

完整代码下载地址: Android仿微信语音聊天 效果图: 分析: 1.自定义Button中要复写onTouchEvent的DOWN,MOVE,UP三种状态,对正常按下,想要取消发送,抬起三种动作进行侦听处理. 2.Dialog共有三种状态,除上图所示的两种外,还有一个录音时间过短的提示.其中录音状态中的音量可以变化. 3.显示录音的ListView的item中有一个录音时长(TextView),一个播放动画(View)和一个头像(ImageView). 4.录音类里有两个成员:录音长度,录音路

Android PopupWindow 仿微信弹出效果

项目中,我需要PopupWindow的时候特别多,这个东西也特别的好使,所以我今天给大家写一款PopupWindow 仿微信弹出效果,这样大家直接拿到项目里就可以用了!首先让我们先看效果: 那么我首先先看下布局代码非常简单:如下 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pop_layout" android:layout_

android开发实现微信三方登录

android开发实现微信三方登录 标签: 微信开放平台三方登录微信开放平台android开发HttpClient 2016-03-22 11:13 2077人阅读 评论(0) 收藏 举报  分类: android开发(26)  版权声明:本文为博主原创文章,未经博主允许不得转载. 需要了解的知识:OATH2的简单机制.http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html这篇文章写得很清晰(谢谢作者). 基本流程(从微信官方文档截图):(说的

[转]Android 超高仿微信图片选择器 图片该这么加载

快速加载本地图片缩略图的方法: Android 超高仿微信图片选择器 图片该这么加载 其示例下载: 仿微信图片选择器 ImageLoader

Android仿微信语音聊天界面

有段时间没有看视频了,昨天晚上抽了点空时间,又看了下鸿洋大神的视频教程,又抽时间写了个学习记录.代码和老师讲的基本一样,网上也有很多相同的博客.我只是在AndroidStudio环境下写的. --主界面代码-- public class MainActivity extends Activity { private ListView mListView; private ArrayAdapter<Recorder> mAdapter; private List<Recorder>

Android 超高仿微信图片选择器 图片该这么载入

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39943731,本文出自:[张鸿洋的博客] 1.概述 关于手机图片载入器,在当今像素随随便便破千万的时代.一张图片占领的内存都相当可观,作为高大尚程序员的我们.有必要掌握图片的压缩,缓存等处理,以到达纵使你有万张照片.纵使你的像素再高,我们也能正确的显示全部的图片.当然了,单纯显示图片没撒意思,我们决定高仿一下微信的图片选择器,在此,感谢微信!本篇博客将基于以下两篇博客: And

android高仿微信拍照、多选、预览、删除(去除相片)相册功能

先声明授人与鱼不如授人与渔,只能提供一个思路,当然需要源码的同学可以私下有偿问我要源码:QQ:508181017 工作了将近三年时间了,一直没正儿八经的研究系统自带的相册和拍照,这回来个高仿微信的拍照.多选.预览.删除(去除相片)相册功能,之前开发的所有应用都带有这需求,但是一直都不实用!废话就不多说了,先来捋一下思路: 1.拍照能实时保存到本地并实时查询(不必用广播或者服务) 2.拍照保存到到自定义路径并根据不同文件夹显示文件夹下的相片 3.多选规定张数图片 4.用到的集合有: (1).所有相

Android ActionBar仿微信界面

ActionBar仿微信界面 1.学习了别人的两篇关于ActionBar博客,在结合别人的文章来仿造一下微信的界面: 思路如下:1).利用ActionBar生成界面的头部,在用ActionBar的ActionProvider时候要注意引入的包一定是android.view.ActionProvider,不能是android.support.v4.view.ActionProvider 2),切换的Title可以参考之前之前一篇文章利用RadioGroup来做,这里是利用一个开源的项目PagerS