Android仿微信语音对讲录音功能

作者:Joe_c 时间:2021-10-12 22:13:36 

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

效果图:

Android仿微信语音对讲录音功能

实现思路:

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;
 }

}

源码下载:Android仿微信语音对讲录音

来源:https://blog.csdn.net/smbroe/article/details/43953195

标签:Android,语音,录音
0
投稿

猜你喜欢

  • 给C语言初学者的学习建议

    2023-06-14 17:23:44
  • Android应用中仿今日头条App制作ViewPager指示器

    2023-02-01 15:45:24
  • 关于Eureka的概念作用以及用法详解

    2023-08-23 15:17:29
  • Android实现时钟特效

    2022-09-08 01:43:22
  • 浅谈Java多线程实现及同步互斥通讯

    2022-11-17 17:50:53
  • SpringBoot和Vue.js实现的前后端分离的用户权限管理系统

    2023-09-03 14:37:33
  • Android编程实现的短信编辑器功能示例

    2022-09-08 12:58:44
  • 通过Java实现bash命令过程解析

    2023-01-07 17:38:25
  • 浅谈C#中堆和栈的区别(附上图解)

    2022-02-15 14:05:09
  • Java SpringBoot 使用拦截器作为权限控制的实现方法

    2023-04-18 01:41:58
  • SpringBoot中属性赋值操作的实现

    2022-05-04 18:10:30
  • 深入理解Java 类加载全过程

    2023-10-20 12:34:23
  • Android 利用ViewPager+GridView实现首页导航栏布局分页效果

    2021-08-05 09:39:07
  • Android批量插入数据到SQLite数据库的方法

    2022-09-28 06:18:06
  • Java线程池并发执行多个任务方式

    2023-08-14 16:26:03
  • Android实现图片拖拉功能

    2023-03-27 14:57:51
  • 浅谈shiro的SecurityManager类结构

    2022-08-25 14:13:32
  • C#实现在前端网页弹出警告对话框(alert)的方法

    2022-02-17 11:05:12
  • java图片色阶调整和亮度调整代码示例

    2023-02-24 16:35:33
  • java利用数组随机抽取幸运观众

    2023-11-11 11:14:33
  • asp之家 软件编程 m.aspxhome.com