由于最近项目开发需要用到自定义SeekBar,于是又对android下的各种类型drawable进行了一个全面系统的认识,只能感慨drawable的功能还是很强大的。通过自定义SeekBar有感而发,尝试用layer-list实现音频录制过程的一个麦克风录制效果,具体效果图如下:

在实现开发之前先让我们一起认识两种类型的Drawable:

LayerDrawable

LayerDrawable可以包含一个drawable数组,而且系统会根据drawable数组的前后顺序来绘制所有的drawable,索引最大的drawable也就相应的会被绘制在最上面。使用过PhotoShop的朋友应该会比较容易理解,LayerDrawable和PhotoShop中图层的概念很相似,这里drawable数组中的每一个drawable就相当于PhotoShop中的一个图层,上一个图层会遮住之后所有图层与之重叠的部分。

定义LayerDrawable对象的XML文件的根元素为

ClipDrawable

ClipDrawable,顾名思义这就是一个可以进行裁切的drawable,在XML文件中定义ClipDrawable对象使用的根元素是< clip … />元素,该元素包含以下几个重要的属性:

  • android:drawable:指定将要被截取的Drawable对象。
  • android:clipOrientation:指定Drawable对象的截取方向可以是水平和竖直方向。
  • android:gravity:表示Drawable对象的对齐方式,例如:left 可以理解为左边部分为保留部分,右边部分为剪切部分,则我们可以看到的就是截取的左边部分。

注意,使用ClipDrawable对象时可以调用setLevel(int level)方法来控制截取区域的大小,而level的取值区间在0~10000之间,则level为0时,表示图片截取部分为空,当了level为10000时,截取整张图片。

了解完毕,下面我们就要用这两种Drawable结合使用开发我们今天的麦克风说话效果:

首先,准备两张位图

然后,在XML中新建一个拥有两个Drawable的LayerDrawable文件layer-microphone.xml,在顶层显示的是可以裁切的ClipDrawable,设置剪切方向为竖直方向,设置对其方式为bottom,底部的则是不通的Drawable作为背景。

<?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/icon_microphone_normal" />
    <item android:id="@android:id/progress" >
        <clip android:drawable="@drawable/icon_microphone_recoding" android:gravity="bottom" android:clipOrientation="vertical" />
    </item>
</layer-list>

然后,再自定义一个PopupWindow用于音频录制过程显示麦克风动画效果,关于自定义popupWindow有疑问的朋友可以参考我的上一篇文章“2016-05-10 浅谈PopupWindow在Android开发中的使用”

窗口布局如下layout_microphone.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:background="@drawable/shape_window_background"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="16dp" >

        <ImageView
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:id="@android:id/progress"
            android:src="@drawable/layer-microphone" />

        <TextView
            android:layout_height="wrap_content"
            android:layout_marginTop="6dp"
            android:id="@android:id/text1"
            android:layout_width="114dp"
            android:textColor="#FFFFFF"
            android:gravity="center"
            android:textSize="16sp"
            android:text="00:00" />
    </LinearLayout>
</LinearLayout>

从代码中可以看出,我把ImageVie的资源设为layer-microphone.xml,我们获取ImageView的Drawable对象并设置Level值就能实现想要的效果:

imageView.getDrawable().setLevel(5400);

从这里我们已经可以看到裁切效果。最后一步,我们只要能够实时获取音频录制过程中的分贝值的变化,再将分贝值变化转换到相应的Level值,就能实现音频录制说话效果啦,于是百度,在网上看到一篇文章“Android中实时获取音量分贝值详解” ,首先,感谢作者的分享,于是我也就照着方法写了一个AudioRecoderUtil.java类,稍加改进,添加了一个监听事件,代码如下:

package com.mariostudio.audiorecoder;

import java.io.File;
import java.io.IOException;

import android.media.MediaRecorder;
import android.os.Handler;
import android.util.Log;

/**
 * Created by MarioStudio on 2016/5/12.
 */

public class AudioRecoderUtils {

    private String filePath;
    private MediaRecorder mMediaRecorder;
    private final String TAG = "MediaRecord";
    public static final int MAX_LENGTH = 1000 * 60 * 10;// 最大录音时长1000*60*10;

    private OnAudioStatusUpdateListener audioStatusUpdateListener;

    public AudioRecoderUtils(){
        this.filePath = "/dev/null";
    }

    public AudioRecoderUtils(File file) {
        this.filePath = file.getAbsolutePath();
    }

    private long startTime;
    private long endTime;

    /**
     * 开始录音 使用amr格式
     *      录音文件
     * @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.DEFAULT);
            /*
             * ②设置输出文件的格式:THREE_GPP/MPEG-4/RAW_AMR/Default THREE_GPP(3gp格式
             * ,H263视频/ARM音频编码)、MPEG-4、RAW_AMR(只支持音频且音频编码要求为AMR_NB)
             */
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            /* ③准备 */
            mMediaRecorder.setOutputFile(filePath);
            mMediaRecorder.setMaxDuration(MAX_LENGTH);
            mMediaRecorder.prepare();
            /* ④开始 */
            mMediaRecorder.start();
            // AudioRecord audioRecord.
            /* 获取开始时间* */
            startTime = System.currentTimeMillis();
            updateMicStatus();
            Log.i("ACTION_START", "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();
        Log.i("ACTION_END", "endTime" + endTime);
        mMediaRecorder.stop();
        mMediaRecorder.reset();
        mMediaRecorder.release();
        mMediaRecorder = null;
        Log.i("ACTION_LENGTH", "Time" + (endTime - startTime));
        return endTime - startTime;
    }

    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);
                }
            }
            mHandler.postDelayed(mUpdateMicStatusTimer, SPACE);
        }
    }

    public interface OnAudioStatusUpdateListener {
        public void onUpdate(double db);
    }
}

进行到这一步,已经基本完成了这个效果,最后只需要在自定义PopupWindow的时候提供方法setLevel(int level)就可以轻松实现PopupWindow实时刷新分贝值啦!!

public void setLevel(int level) {
    imageView.getDrawable().setLevel(3000 + 6000 * level / 100);
}

至于为什么设置level的时候要3000 + 6000 level / 100*以及计时效果,就都留给聪明的你去探索咯!!