Android实现长按录音松开保存、播放及根据声贝动画展示

前言:做一个有梦想的程序猿!

最近公司需要本人负责开发一款app,其中有个实现录音、播放等功能,作为以java后台开发为主的我,以前没怎么接触过安卓开发,也罢,为了努力提升自身技术水平实现人生理想,只能咬牙接下重任。
在做这个功能之前,在网上也查阅了些资料,实现过程中确实遇到了一些坑,所以在此记录下来分享给各位,废话不多说,直接步入正题:

Android提供了两个API用于实现录音功能:android.media.AudioRecord、android.media.MediaRecorder,这两个各有各的优缺点 :

简单来说就是AudioRecord主要实现边录边播,也可以实时的进行音频处理,而它的缺点是输出的是PCM语音数据,如果保存成音频文件,是不能够被播放器播放的,所以必须先写代码实现数据编码以及压缩.

而MediaRecorder优点是大部分以及集成,直接调用相关接口即可,代码量小,缺点是无法实时处理音频,输出的音频格式不是很多,例如没有输出mp3格式文件,当然AudioRecord是可以实现输出mp3格式的,不过需要工具转码,这里就不多说了。
接下来主要介绍MediaRecorder的使用方法:

1、准备两张需要动态展示的图片

normal.pngrecoding.png

2、布局文件popup_window.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/popup"
    android:layout_width="match_parent"
	android:layout_height="match_parent"
    android:layout_alignParentBottom="true"
    android:orientation="vertical">
    
    	 <ImageView
    	        android:id="@+id/iv_pro"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="30dp"
                android:gravity="center"
                android:src="@drawable/popup" />

            <TextView
                android:id="@+id/recording_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@+id/iv_pro"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="10dp"
                android:textColor="#FFFFFF" 
                android:padding="2dp"
                android:textSize="17sp" />  
                
              <TextView
                android:id="@+id/recording_tip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_below="@id/recording_time"
                android:layout_centerHorizontal="true"
                android:layout_marginTop="10dp"
                android:textColor="#FFFFFF" 
                android:text="正在录音,松开保存" 
                android:padding="2dp"
                android:textSize="17sp" />  
</RelativeLayout>

popup_window.xml便是动画展示的界面

3、popup.xml 文件

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@android:id/background" android:drawable="@drawable/normal"/>
    <item android:id="@android:id/progress" >
        <clip android:drawable="@drawable/recoding" android:gravity="bottom" android:clipOrientation="vertical" />
    </item>
</layer-list>

popup.xml文件根据声贝大小动态剪切recoding.png到normal.png上,这样就实现了动画音频效果

4、封装MediaRecorder初始化及相关操作AudioRecoderUtils

package com.gtlxkj.cn.util;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import com.gtlxkj.cn.activity.FaultActivity;

import android.content.Context;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Handler;
import android.util.Log;

public class AudioRecoderUtils extends FaultActivity{
	 
    //文件路径
    private String filePath;
    //文件夹路径
    private String FolderPath;
 
    private MediaRecorder mMediaRecorder;
    
    private MediaPlayer player;
    private final String TAG = "fan";
    public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大录音时长1000*60*10;
 
    private OnAudioStatusUpdateListener audioStatusUpdateListener;
 
    /**
     * 文件存储默认sdcard/record
     */
    public AudioRecoderUtils(){
        //默认保存路径为/sdcard/record/下
        this(ConfigurationUtil.RECORD_PATH_ABSOULT);
    }
 
    public AudioRecoderUtils(String filePath) {
 
        File path = new File(filePath);
        if(!path.exists())
            path.mkdirs();
 
        this.FolderPath = filePath;
    }
 
    private long startTime;
    private long endTime;
 
 
 
    /**
     * 开始录音 使用mp4格式
     *      录音文件
     * @return
     */
    public void startRecord() {
        // 开始录音
        /* ①Initial:实例化MediaRecorder对象 */
        if (mMediaRecorder == null)
            mMediaRecorder = new MediaRecorder();
        try {
            /* ②setAudioSource/setVedioSource */
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);// 设置麦克风
            /* ②设置音频文件的编码:AAC/AMR_NB/AMR_MB/Default 声音的(波形)的采样 */
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
            /*
             * ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
             * ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)
             */
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
 
            filePath = FolderPath + System.currentTimeMillis() + ".mp4" ;
            /* ③准备 */
            mMediaRecorder.setOutputFile(filePath);
            mMediaRecorder.setMaxDuration(MAX_LENGTH);
            mMediaRecorder.prepare();
            /* ④开始 */
            mMediaRecorder.start();
            // AudioRecord audioRecord.
            /* 获取开始时间* */
            startTime = System.currentTimeMillis();
            updateMicStatus();
            Log.i("fan", "startTime" + startTime);
        } catch (IllegalStateException e) {
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        } catch (IOException e) {
            Log.i(TAG, "call startAmr(File mRecAudioFile) failed!" + e.getMessage());
        }
    }
 
    /**
     * 停止录音
     */
    public long stopRecord() {
        if (mMediaRecorder == null)
            return 0L;
        endTime = System.currentTimeMillis();
 
        //有一些网友反应在5.0以上在调用stop的时候会报错,翻阅了一下谷歌文档发现上面确实写的有可能会报错的情况,捕获异常清理一下就行了,感谢大家反馈!
        try {
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
            audioStatusUpdateListener.onStop(filePath);
            filePath = "";
 
        }catch (RuntimeException e){
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
 
            File file = new File(filePath);
            if (file.exists())
                file.delete();
 
            filePath = "";
 
        }
        return endTime - startTime;
    }
 
    /**
     * 播放录音
     * @param filepath
     */
    public MediaPlayer playRecord(String filepath) {
    	player = getMediaPlayer(this);
		if(player != null){
			player.reset();
			try {
				//设置语言的来源
				player.setDataSource(filepath);
				//初始化
				player.prepare();
				//开始播放
				player.start();
			}catch (IOException e) {
				e.printStackTrace();
			}
		}				
		return player;
	}
    
    
    /**
     * 取消录音
     */
    public void cancelRecord(){
 
        try {
 
            mMediaRecorder.stop();
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
 
        }catch (RuntimeException e){
            mMediaRecorder.reset();
            mMediaRecorder.release();
            mMediaRecorder = null;
        }
        File file = new File(filePath);
        if (file.exists())
            file.delete();
 
        filePath = "";
 
    }
 
    private final Handler mHandler = new Handler();
    private Runnable mUpdateMicStatusTimer = new Runnable() {
        public void run() {
            updateMicStatus();
        }
    };
 
 
    private int BASE = 1;
    private int SPACE = 100;// 间隔取样时间
 
    public void setOnAudioStatusUpdateListener(OnAudioStatusUpdateListener audioStatusUpdateListener) {
        this.audioStatusUpdateListener = audioStatusUpdateListener;
    }
 
    /**
     * 更新麦克状态
     */
    private void updateMicStatus() {
 
        if (mMediaRecorder != null) {
            double ratio = (double)mMediaRecorder.getMaxAmplitude() / BASE;
            double db = 0;// 分贝
            if (ratio > 1) {
                db = 20 * Math.log10(ratio);
                if(null != audioStatusUpdateListener) {
                    audioStatusUpdateListener.onUpdate(db,System.currentTimeMillis()-startTime);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }
 
    public interface OnAudioStatusUpdateListener {
        /**
         * 录音中...
         * @param db 当前声音分贝
         * @param time 录音时长
         */
        public void onUpdate(double db,long time);
 
        /**
         * 停止录音
         * @param filePath 保存路径
         */
        public void onStop(String filePath);
        
    }
 
	
	/**
     *  </br> This code is trying to do the following from the hidden API
     * </br> SubtitleController sc = new SubtitleController(context, null, null);
     * </br> sc.mHandler = new Handler();
     * </br> mediaplayer.setSubtitleAnchor(sc, null)</p>
     */
    private MediaPlayer getMediaPlayer(Context context) {
        MediaPlayer mediaplayer = new MediaPlayer();
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
            return mediaplayer;
        }
        try {
            Class<?> cMediaTimeProvider = Class.forName("android.media.MediaTimeProvider");
            Class<?> cSubtitleController = Class.forName("android.media.SubtitleController");
            Class<?> iSubtitleControllerAnchor = Class.forName("android.media.SubtitleController$Anchor");
            Class<?> iSubtitleControllerListener = Class.forName("android.media.SubtitleController$Listener");
            Constructor constructor = cSubtitleController.getConstructor(
                    new Class[]{Context.class, cMediaTimeProvider, iSubtitleControllerListener});
            Object subtitleInstance = constructor.newInstance(context, null, null);
            Field f = cSubtitleController.getDeclaredField("mHandler");
            f.setAccessible(true);
            try {
                f.set(subtitleInstance, new Handler());
            } catch (IllegalAccessException e) {
                return mediaplayer;
            } finally {
                f.setAccessible(false);
            }
            Method setsubtitleanchor = mediaplayer.getClass().getMethod("setSubtitleAnchor",
                    cSubtitleController, iSubtitleControllerAnchor);
            setsubtitleanchor.invoke(mediaplayer, subtitleInstance, null);
        } catch (Exception e) {
           Log.d(TAG,"getMediaPlayer crash ,exception = "+e);
        }
        return mediaplayer;
    }
    
}


相关工具类:

package com.gtlxkj.cn.util;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import android.util.Log;

public class TimeUtils {
	/** 
	 * 获取当前时间 
	 * @return 
	 */  
	public static String getNowTime(){  
	    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");  
	    Date date = new Date(System.currentTimeMillis());  
	    return simpleDateFormat.format(date);  
	}  
	/** 
	 * 获取时间戳 
	 * 
	 * @return 获取时间戳 
	 */  
	public static String getTimeString() {  
	    SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");  
	    Calendar calendar = Calendar.getInstance();  
	    return df.format(calendar.getTime());  
	}  
	/** 
	 * 时间转换为时间戳 
	 * @param time:需要转换的时间 
	 * @return 
	 */  
	public static String dateToStamp(String time)  {  
	    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
	    Date date = null;  
	    try {  
	        date = simpleDateFormat.parse(time);  
	    } catch (Exception e) {  
	        e.printStackTrace();  
	    }  
	    long ts = date.getTime();  
	    return String.valueOf(ts);  
	}  

	/** 
	 * 时间戳转换为字符串 
	 * @param time:时间戳 
	 * @return 
	 */  
	public static String getDateToString(long time) {  
	    Date d = new Date(time);  
	    SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");  
	    return sf.format(d);  
	}  
	
	/** 
	 * 时间戳转换为字符串分秒 
	 * @param time:时间戳 
	 * @return 
	 */  
	public static String getDateCoverString(long time) {  
	    Date d = new Date(time);  
	    SimpleDateFormat sf = new SimpleDateFormat("mm:ss");  
	    return sf.format(d);  
	}  
	/** 
	 *获取距现在某一小时的时刻 
	 * @param hour hour=-1为上一个小时,hour=1为下一个小时 
	 * @return 
	 */  
	public static String getLongTime(int hour){  
	    Calendar c = Calendar.getInstance(); // 当时的日期和时间  
	    int h; // 需要更改的小时  
	    h = c.get(Calendar.HOUR_OF_DAY) - hour;  
	    c.set(Calendar.HOUR_OF_DAY, h);  
	    SimpleDateFormat df = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");  
	    Log.v("time",df.format(c.getTime()));  
	    return df.format(c.getTime());  
	}  
}

5、Activity代码实现

@SuppressLint("NewApi")
@ContentView(R.layout.vw_head_fault)
public class FaultActivity extends BaseActivity{

    private View view ;
    @ViewInject(R.id.buttonPressToSpeak)
    private Button mButton;
    @ViewInject(R.id.buttonBroadcast)
    private Button cButton;
    @ViewInject(R.id.head_broadcast)
    private TextView head_broadcast;
    @ViewInject(R.id.ivAnim)
    private ImageView ivAnim;
    private AnimationDrawable drawable;
    private ImageView micImage;
    private TextView recordingTime;
    private long ltime;
    
    private MediaPlayer player;
  
	
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.context=FaultActivity.this;
        Log.i("XListViewActivity", "onCreate");   
        initVedio();
    }

    //录音功能 初始化 
    private void initVedio(){
    	  view = View.inflate(this, R.layout.popup_window, null);
    	   //设置空白的背景色
    	  final WindowManager.LayoutParams lp = FaultActivity.this.getWindow().getAttributes();
    	  final PopupWindow mPop  = new PopupWindow(view);
    	  micImage=(ImageView)view.findViewById(R.id.iv_pro);
    	  recordingTime=(TextView)view.findViewById(R.id.recording_time);
    	  mAudioRecoderUtils = new AudioRecoderUtils();
    	  //录音回调
          mAudioRecoderUtils.setOnAudioStatusUpdateListener(new AudioRecoderUtils.OnAudioStatusUpdateListener() {
   
              //录音中....db为声音分贝,time为录音时长
              @Override
              public void onUpdate(double db, long time) {
                  //根据分贝值来设置录音时话筒图标的上下波动
            	  ltime=time;
             	  micImage.getDrawable().setLevel((int) (3000 + 6000 * db / 100));
            	  recordingTime.setText(TimeUtils.getDateCoverString(time));
              }
   
              //录音结束,filePath为保存路径
              @Override
              public void onStop(String filePath) {
            	  if(ltime<1500){//判断,如果录音时间小于1.5秒,则删除文件提示,过短
          			File file = new File(filePath);
          			if(file.exists()){//判断文件是否存在,如果存在删除文件
          				file.delete();//删除文件
          				Toast.makeText(FaultActivity.this, "录音时间过短",Toast.LENGTH_SHORT).show();
          			}
                  }else{
                	  try {
                		  //保存录音路径前先删除旧的文件
                		  if(!Utils.StringEx(fault.getSoundpath())){
                			  File file = new File(fault.getSoundpath());
                    		  if(file.exists()){//判断文件是否存在,如果存在删除文件
                    				file.delete();//删除文件
                    			}
                		  }
                		  fault.setSoundpath(filePath);
                		  getDBManager().saveOrUpdate(fault);
                		  Toast.makeText(FaultActivity.this, "录音保存在:" + filePath, Toast.LENGTH_SHORT).show();
    	                  recordingTime.setText("00:00");
    	                  ltime=0;
					} catch (DbException e) {
						e.printStackTrace();
					}
                  }
              }
          });
          
		  //Button的touch监听
		  mButton.setOnTouchListener(new View.OnTouchListener() {
			  @Override
			  public boolean onTouch(View v, MotionEvent event) {
   
				  switch (event.getAction()){
   
					  case MotionEvent.ACTION_DOWN:
						  lp.alpha = 0.4f;
						  FaultActivity.this.getWindow().setAttributes(lp);
						  mPop.setWidth(500);
						  mPop.setHeight(500);
						  mPop.showAtLocation(rl,Gravity.CENTER,0,0);
						  mAudioRecoderUtils.startRecord();
   
						  break;
   
					  case MotionEvent.ACTION_UP:
						//恢复背景色
						  lp.alpha = 1f;
						  FaultActivity.this.getWindow().setAttributes(lp);
						  mAudioRecoderUtils.stopRecord();        //结束录音(保存录音文件)
						  mPop.dismiss();
   
						  break;
				  }
				  return true;
			  }
		  });
          
          //播放录音
          cButton.setOnClickListener(new OnClickListener() {
				@Override
				public void onClick(View v) {
					if(Utils.StringEx(fault.getSoundpath())){
						Toast.makeText(FaultActivity.this, "此故障信息暂无录音内容", Toast.LENGTH_SHORT).show();
					}else{
						if(player==null){
							player = mAudioRecoderUtils.playRecord(fault.getSoundpath());//播放
							 cButton.setBackgroundResource(R.drawable.zanting);
				    		 head_broadcast.setText("正在播放");
				    		 ivAnim.setBackgroundResource(R.drawable.anim);
				        	 drawable = (AnimationDrawable)ivAnim.getBackground();
				    		 drawable.start();
				    		 player.setOnCompletionListener(new OnCompletionListener() {
				 		    	@Override
				 		    	public void onCompletion(MediaPlayer mp) {//监听是否播放完毕
				 		    		 cButton.setBackgroundResource(R.drawable.play);
				 		    		 head_broadcast.setText("点击播放");
				 		    		 player.release();//释放资源
				 		    		 player=null;
				 		    		 drawable.stop();
				 		    		 ivAnim.setBackgroundResource(R.drawable.bofang3);
				 		    	}
				 		   });
						}else if(player.isPlaying()){
							 player.stop();
							 player.release();//释放资源
							 player=null;
				    		 cButton.setBackgroundResource(R.drawable.play);
				    		 head_broadcast.setText("点击播放");
				    		 drawable.stop();
				    		ivAnim.setBackgroundResource(R.drawable.bofang3);
						}
					}
				}
		});
    }
}

此Activity同样实现了播放喇叭动画展示以及录音中背景变暗等效果

6、录音及播放权限

最后就是加权限了,在AndroidManifest.xml文件中加入下面两句

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

7、总结

其实安卓实现录音功能并不复杂,明白了其中的调用流程就会感觉简单许多,根据声贝展动画界面需要在代码中指定PopupWindow的大小,否则可能会出现无法展示问题,后期也实现了批量上传录音及图片到服务器功能,而用这种方法上传的录音经过测试有wav和mp4格式录音文件上传到服务器可以播放,mp3格式会提示播放失败,其他格式暂时还没测试。
以上叙述有误的地方还请各位及时提出改正

8、效果图

界面没重新设计就没那么美观了
录音:

录音

播放:
播放

最后,如果本篇文章对您有所帮助,可以评论或点赞支持一下哦,感谢感谢!
在这里插入图片描述

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值