自定义View——语音发送按键

我尽量不打错别字,用词准确,不造成阅读障碍。

本文是仿照微信录制语音信息并发送的自定义Button,学习自慕客网上鸿神的视频,视频很老了,但是最近在整理自定义View文章,感觉这个最简单,所以先写出来。

我们有三个类:AudioRecorderButtonAudioManagerAudioDialogManager;

分别用来做自定义Button、麦克风管理、dialog管理;
效果图:

one

我用的模拟器,麦克风接受声音不灵敏,所以等级没显示出来,真机使用是没问题的。

AudioManager

先看AudioManager,因为这个类很独立,不需要访问其他两个类中的方法,只会被访问。这个类主要是用来做麦克风初始化参数和控制,包括打开、关闭、数据源、输出文件、编码格式、计算声音大小等等的设置;

public class AudioManager implements MediaRecorder.OnErrorListener{
    private MediaRecorder mMediaRecorder;
    private String mCurrentFilePath;       //最终的文件保存路径
    private boolean isPrepare;

    private static AudioManager mInstance;

    private AudioManager() {}

    public String getCurrentFilePath() {
        return mCurrentFilePath;
    }

  //当初出现一些系统错误的时候会回调
    @Override
    public void onError(MediaRecorder mediaRecorder, int i, int i1) {
        try {
            if (mediaRecorder != null)
                mediaRecorder.reset();
        } catch (Exception e) {
            Log.w("AudioManager", "stopRecord", e);
        }
    }


    //回调准备完毕
    public interface AudioStateListener {
        void wellPrepared();
    }

    private AudioStateListener mListener;

    public void setOnAudioStateListener(AudioStateListener listener) {
        mListener = listener;
    }

  //单例模式获取实例对象
    public static AudioManager getInstance() {
        if (mInstance == null) {
            synchronized (AudioManager.class) {
                if (mInstance == null) {
                    mInstance = new AudioManager();
                }
            }
        }
        return mInstance;
    }

  //对外暴露prepare方法,AudioRecorderButton会调用
    public void prepareAudio() {
        try {
            isPrepare = false;
          //为防止文件夹不存在而采取的判断,可根据实际情况删减
            File dir = new File(Constants.ChatFileSaveVoice);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            String filename = generateFileName();
            File file = new File(dir, filename);
            mCurrentFilePath = file.getAbsolutePath();
            mMediaRecorder = new MediaRecorder();
            mMediaRecorder.setOutputFile(file.getAbsolutePath());          //设置输出文件
            mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);  //设置声音源为麦克风
            mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR); //设置输出格式
            mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);  //设置编码格式
            mMediaRecorder.prepare();    //准备
            mMediaRecorder.start();      //开始
            isPrepare = true;     //准备结束
            if (mListener != null) {
                mListener.wellPrepared();  //通知外界已准备完毕
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //随机生成文件名称
    private String generateFileName() {
        return System.currentTimeMillis() + ".amr";
    }

    //根据maxLevel生成相应的声音等级
    public int getVoiceLevel(int maxLevel) {
        if (isPrepare && mMediaRecorder != null) {
            try {
              //mMediaRecorder.getMaxAmplitude()的范围是0~32767
                return maxLevel* mMediaRecorder.getMaxAmplitude() / 32768 + 1;
            } catch (IllegalStateException e) {
            }
        }
        return 1;
    }

  //当手指抬起时我们要停止录音并回收对象,AudioRecorderButton会调用
    public void release() {
        try {
            mMediaRecorder.stop();
            mMediaRecorder.release();
            mMediaRecorder = null;
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }
    }

  //当用户取消发送的时候,除了release外还需要删除文件,AudioRecorderButton会调用
    public void cancel() {
        release();
        if (mCurrentFilePath!=null) {
            File file = new File(mCurrentFilePath);
            file.delete();
            mCurrentFilePath = null;
        }
    }
}

我比较喜欢把原理在注释里写明白,所以请看看注释。
return maxLevel*mMediaRecorder.getMaxAmplitude() / 32768 + 1; 其中mMediaRecorder.getMaxAmplitude()为声音最大振幅,取值范围为[0~32767],如果我们maxLevel为7,那么最大振幅与32768做除法取整数会是[0~1),所以最后结果会是[0~7),+1后就是[1~7]了。没有0这个等级,所以不要用32767做分母。

AudioDialogManager

这个类是用来做dialog显示管理的,也是独立的,没有调用其他两个类;代码很简单;

public class AudioDialogManager {
    private Dialog mDialog;

    private ImageView mIcon;
    private TextView mToShort;

    private ImageView mCancel;

    private Context mContext;

    public AudioDialogManager(Context mContext) {
        this.mContext = mContext;
    }

  //显示dialog,AudioRecorderButton会调用
    public void showRecordingDialog() {
        mDialog = new Dialog(mContext, R.style.Theme_AudioDialog);
        LayoutInflater inflater = LayoutInflater.from(mContext);
        View view = inflater.inflate(R.layout.dialog_recorder, null);
        mDialog.setContentView(view);

        mIcon = (ImageView) mDialog.findViewById(R.id.id_recorder_dialog_icon);
        mToShort = (TextView) mDialog.findViewById(R.id.id_recorder_dialog_toshort);
        mCancel = (ImageView) mDialog.findViewById(R.id.id_recorder_dialog_cancel);

        mDialog.show();
    }

  //录音中时显示等级,AudioRecorderButton会调用
    public void recording() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.VISIBLE);
            mToShort.setVisibility(View.GONE);
            mCancel.setVisibility(View.GONE);
            mIcon.setImageResource(R.drawable.voice_message1_13x);
        }
    }

  //打算取消时显示取消,AudioRecorderButton会调用
    public void wantToCancel() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.GONE);
            mToShort.setVisibility(View.GONE);
            mCancel.setVisibility(View.VISIBLE);
            mIcon.setImageResource(R.drawable.voice_message23x);
        }
    }

  //录音时间太短时显示,AudioRecorderButton会调用
    public void tooShort() {
        if (mDialog != null && mDialog.isShowing()) {
            mIcon.setVisibility(View.GONE);
            mToShort.setVisibility(View.VISIBLE);
            mCancel.setVisibility(View.GONE);
            mIcon.setImageResource(R.drawable.voice_to_short);
        }
    }

  //关闭dialog,AudioRecorderButton会调用
    public void dimissDialog() {
        if (mDialog != null && mDialog.isShowing()) {
            mDialog.dismiss();
            mDialog = null;
        }
    }

    //通过level去更新Voice的图片,AudioRecorderButton会调用
    public void updateVoiceLevel(int level) {
        if (mDialog != null && mDialog.isShowing()) {
          //准备几张图片
            int resId = mContext.getResources().getIdentifier("voice_message1_"+level+"3x", "drawable", mContext.getPackageName());
            mIcon.setImageResource(resId);
        }
    }
}

AudioRecorderButton

重头戏是这个类,代码不短,也不太长。

public class AudioRecorderButton extends android.support.v7.widget.AppCompatButton implements AudioManager.AudioStateListener {

    private static final int DISTANCE_Y_CANCEL = 50;     //规定坐标绝对值超过50则表示想要取消发送
    private static final int STATE_NORMAL = 1;           //正常状态
    private static final int STATE_RECORDING = 2;        //录音状态
    private static final int STATE_WANT_TO_CANCEL = 3;   //想要取消状态

    private int mCurState = STATE_NORMAL;                //记录当前状态
   
    private boolean isRecording = false;                 //已经开始录音
    private AudioDialogManager mDialogManager;
    private AudioManager mAudioManager;
    private float mTime;

    private boolean mReady;                              //是否触发longClick
    private AlertDialog alertDialog;
    private Dialog showToast = null;                     //弹出窗口

    public AudioRecorderButton(Context context) {
        super(context, null);
    }

  //初始化Button时获取dialog和AudioManager对象等操作
    public AudioRecorderButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDialogManager = new AudioDialogManager(getContext());
        mAudioManager = AudioManager.getInstance();
        mAudioManager.setOnAudioStateListener(this);
        setOnLongClickListener(new OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                mReady = true;
                mAudioManager.prepareAudio();
                return false;
            }
        });
    }

    //录音完成后的回调
    public interface AudioFinishRecorderListener {
        void onFinish(float seconds, String filePath);
    }

    private AudioFinishRecorderListener mListener;

    public void setAudioFinishRecordListener(AudioFinishRecorderListener listener) {
        mListener = listener;
    }

  //监听手势变化
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                changeState(STATE_RECORDING);
                break;
            case MotionEvent.ACTION_MOVE:
                if (isRecording) {
                    if (wantToCancel(x, y)) {
                        changeState(STATE_WANT_TO_CANCEL);
                    } else {
                        changeState(STATE_RECORDING);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (!mReady) {
                    reset();
                    return super.onTouchEvent(event);
                }
                if (!isRecording || mTime < 0.6f) {
                    mDialogManager.tooShort();
                    mAudioManager.cancel();
                    mHandler.sendEmptyMessageDelayed(MSG_DIALOG_MISS, 1300);
                } else if (mCurState == STATE_RECORDING) {
                    mDialogManager.dimissDialog();
                    mAudioManager.release();
                    if (mListener != null) {
                        mListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
                    }
                } else if (mCurState == STATE_WANT_TO_CANCEL) {
                    mDialogManager.dimissDialog();
                    mAudioManager.cancel();
                }
                reset();
                break;
        }
        return super.onTouchEvent(event);
    }

    //获取音量大小
    private Runnable mGetVoiceLevelRunnable = new Runnable() {
        @Override
        public void run() {
            while (isRecording) {
                try {
                    Thread.sleep(100);
                    mTime += 0.1f;
                    if (mTime <= 30f) {
                        if (mTime >= 25f) {
                            if (mHandler != null) {
                                Message msg = mHandler.obtainMessage();
                                msg.what = HANDLER_REC_TIME;
                                float i = 30f - mTime;
                                msg.arg1 = (int) i;
                                mHandler.sendMessage(msg);
                            }
                        }
                        mHandler.sendEmptyMessage(MSG_VOICE_CHANGED);
                    }
                    if (mTime > 31f) {
                        if (mHandler != null) {
                            Message stopRecMsg = mHandler.obtainMessage();
                            stopRecMsg.what = HANDLER_STOP_REC;
                            mHandler.sendMessage(stopRecMsg);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    };


    //创建toast
    @SuppressLint("NewApi")
    private Dialog getToastDialog() {
        try {
            String s = android.os.Build.FINGERPRINT.toUpperCase();
            if (!s.contains("MIUI")) {
                if (alertDialog == null) {
                    alertDialog = new AlertDialog.Builder(getContext(), R.style.CustomProgressDialog).create();
                    alertDialog.getWindow().setGravity(Gravity.TOP);// 设置位置
                    WindowManager.LayoutParams lp = alertDialog.getWindow().getAttributes();
                    lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
                    alertDialog.getWindow().setAttributes(lp);
                }
                return alertDialog;
            } else {
                if (showToast == null) {
                    showToast = new Dialog(getContext());
                    showToast.getWindow().setGravity(Gravity.TOP);// 设置位置
                    WindowManager.LayoutParams lp = showToast.getWindow().getAttributes();
                    lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
                    showToast.getWindow().setAttributes(lp);
                }
                return showToast;
            }
        } catch (Exception e) {
            Log.e("AudioButton", "" + e.toString());
        }
        return null;
    }

    private static final int MSG_AUDIO_PREPARED = 0X110;
    private static final int MSG_VOICE_CHANGED = 0X111;
    private static final int MSG_DIALOG_MISS = 0X112;
    private static final int HANDLER_REC_TIME = 0X2010; // 录音时间计时
    private static final int HANDLER_STOP_REC = 0X2011; // 停止录音

  //其实应该避免在自定义view中使用Handler
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_AUDIO_PREPARED:
                    mDialogManager.showRecordingDialog();
                    isRecording = true;
                    new Thread(mGetVoiceLevelRunnable).start();
                    break;
                case MSG_VOICE_CHANGED:
                    mDialogManager.updateVoiceLevel(mAudioManager.getVoiceLevel(7));
                    break;
                case MSG_DIALOG_MISS:
                    mDialogManager.dimissDialog();
                    break;
                case HANDLER_REC_TIME:
                    if (!getToastDialog().isShowing() && isRecording) {
                        getToastDialog().setTitle("将在" + msg.arg1 + "秒后结束录音");
                        getToastDialog().show();
                    }
                    getToastDialog().setTitle("将在" + msg.arg1 + "秒后结束录音");
                    break;
                case HANDLER_STOP_REC:
                    if (!mReady) {
                        reset();
                        break;
                    }
                    if (mCurState == STATE_RECORDING) {
                        mDialogManager.dimissDialog();
                        if (getToastDialog().isShowing() && isRecording) {
                            getToastDialog().dismiss();
                        }
                        mAudioManager.release();
                        if (mListener != null) {
                            mListener.onFinish(mTime, mAudioManager.getCurrentFilePath());
                        }
                    }
                    reset();
                    break;
            }
        }
    };

    //回复状态及标识位
    private void reset() {
        isRecording = false;
        mReady = false;
        mTime = 0;
        changeState(STATE_NORMAL);
    }

    private boolean wantToCancel(int x, int y) {
        if (x < 0 || x > getWidth()) {
            return true;
        }
        if (y < -DISTANCE_Y_CANCEL || y > getHeight() + DISTANCE_Y_CANCEL) {
            return true;
        }
        return false;
    }

  //改变dialog及文字显示
    private void changeState(int stateRecording) {
        if (mCurState != stateRecording) {
            mCurState = stateRecording;
            switch (stateRecording) {
                case STATE_NORMAL:
                    setText(R.string.str_recorder_normal);
                    break;
                case STATE_RECORDING:
                    setText(R.string.str_recorder_recording);
                    if (isRecording) {
                        mDialogManager.recording();
                    }
                    break;
                case STATE_WANT_TO_CANCEL:
                    setText(R.string.str_recorder_want_cancel);
                    mDialogManager.wantToCancel();
                    break;
            }
        }
    }

    @Override
    public void wellPrepared() {
        mHandler.sendEmptyMessage(MSG_AUDIO_PREPARED);
    }
}

这是很久前写的代码,if…else用的比较多,意思明白了就好。

集合了一些简单自定义View的github地址:
https://github.com/longlong-2l/MySelfViewDemo
很简单,没有太多高深的用法,适合学习入门。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值