自定义语音录制按钮

作者: h2coder | 来源:发表于2019-07-27 14:08 被阅读2次

    之前做了一个聊天室模块,除了能发送文字、图片,还需要发送语音,就涉及到了语音的手势,手势就和微信的是一样的,现在大部分聊天App都是采用这套。

    其实难点在于计算手势距离来切换不同的状态,录制和提示弹窗都是很简单的事情。当然经过一段时间折腾和修Bug后,也完成了,特此记录一下。

    需求分析

    语音录制状态.png

    UI分析:

    1. 闲置状态,不录制
      • 按钮文字为:按住 说话
      • 提示弹窗:不显示
    2. 按下状态,录制中
      • 按钮文字为:松开 结束
      • 提示弹窗:显示,提示文字:手指上滑,取消发送
    3. 滑动到取消区域状态,松手则取消发送
      • 按钮文字为:松开 结束
      • 提示弹窗:显示,提示文字:松开手指,取消发送
    4. 滑动到正常区域,松手则马上发送
      • 按钮文字为:松开 结束
      • 提示弹窗:显示,提示文字:手指上滑,取消发送
    5. 录制时间小于指定时间,取消录制
      • 按钮文字为:松开 结束
      • 提示弹窗:显示,提示文字:录制时间太短

    注意点

    Android6.0开始录制语音,需要动态申请权限,注意适配

    需要提供的回调

    • 是否拦截,用于判断权限是否获取,如果未获取,返回false,按钮不处理事件
    • 开始回调(手指按下)
    • 取消回调(滑动到取消区域后松手)
    • 结束回调(在取消区域和正常区域松手)
    • 准备退出回调(手机抬起,松手)
    • 间隔时间太小时回调(按下和松手时的时间差小于指定值)
    • 手指移动到区域区域回调
    • 手指移动到正常区域回调

    按钮自定义步骤

    一般我们按钮都继承于TextView,设置背景来做,所以我们的按钮继承TextView。

    1. 定义取消区域距离,我这里获取屏幕高度,以3分之一的距离作为取消区域,测试过发现是比较合适的,如有不合适再做调整。
    2. 定义最短录制时间,我这里定义为700,测试过为比较合适的,如有不合适再做调整。
    public class VoiceRecordButton extends android.support.v7.widget.AppCompatTextView {
        /**
         * 最短录音时间
         **/
        private static final int MIN_INTERVAL_TIME = 700;
    
        public VoiceRecordButton(Context context) {
            super(context);
            init();
        }
    
        public VoiceRecordButton(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public VoiceRecordButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            //定义取消区域距离,设置默认状态。
            mCancelDistance = (int) (getScreenHeight(getContext()) / 3);
            setText("按住 说话");
        }
    
        public static DisplayMetrics getDisplayMetrics(Context context) {
            return context.getResources().getDisplayMetrics();
        }
        
        //获取屏幕高度
        public static float getScreenHeight(Context context) {
            return (float) getDisplayMetrics(context).heightPixels;
        }
    }
    
    1. 定义回调接口,具体就是上面的分析
    public interface ButtonTouchCallback {
        /**
         * 是否拦截,如果返回true,代表拦截,则不进行触摸事件的监听
         *
         * @return true为拦截,false为不拦截
         */
        boolean isIntercept();
    
        /**
         * 开始录制
         */
        void onStart();
    
        /**
         * 手动取消录制
         */
        void onCancel();
    
        /**
         * 结束录制
         */
        void onFinish();
    
        /**
         * 准备退出,在手指抬起时回调
         */
        void onTerminate();
    
        /**
         * 间隔时间太小
         */
        void onTouchIntervalTimeSmall();
    
        /**
         * 当触碰到取消区域
         */
        void onTouchCancelArea();
    
        /**
         * 当恢复到正常区域
         */
        void onRestoreNormalTouchArea();
    }
    
    public void setButtonTouchCallback(ButtonTouchCallback callback) {
        this.mCallback = callback;
    }
    
    1. 手势判断,大部分的代码都在这里,我们把这里理解了,就能做出来了
    • 首先,View按下事件,先回调我们接口回调,是否拦截,如果不拦截才拦截掉Down时间。这个拦截是提供给外部使用,一般是判断权限是否获取,未获取就返回拦截。
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //只关心上下移动
        if (action == MotionEvent.ACTION_DOWN) {
            mDownY = event.getY();
            mLastY = event.getY();
            mDownTime = System.currentTimeMillis();
            setText(R.string.answer_ask_voice_normal);
            //回调外部,决定是否拦截
            if (mCallback != null) {
                if (mCallback.isIntercept()) {
                    return super.onTouchEvent(event);
                }
                mCallback.onStart();
            }
            return true;
        }
    }
    
    • 第二步,不断获取移动事件,判断移动的位置在我们定义的取消范围之上(滑动到了区域,松手为取消)还是之下(滑动到正常区域,松手为发送),主要是为了移动时,不断根据移动区域,去切换弹窗样式
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //只关心上下移动
        if (action == MotionEvent.ACTION_DOWN) {
            mDownY = event.getY();
            mLastY = event.getY();
            mDownTime = System.currentTimeMillis();
            setText("按住 说话");
            //回调外部,决定是否拦截
            if (mCallback != null) {
                if (mCallback.isIntercept()) {
                    return super.onTouchEvent(event);
                }
                mCallback.onStart();
            }
            return true;
        } else if (action == MotionEvent.ACTION_MOVE) {
            //获取移动的位置
            float moveY = event.getY();
            //计算滑动距离
            float changeY = moveY - mLastY;
            //判断是否在取消范围之上(到达了取消区域)
            if (moveY < 0 && Math.abs(changeY) >= mCancelDistance) {
                //上滑到了取消区域
                setText("松开手指,取消发送");
                if (mCallback != null) {
                    mCallback.onTouchCancelArea();
                }
            } else {
                //不在取消区域范围内(正常区域)
                setText("松开 结束");
                if (mCallback != null) {
                    mCallback.onRestoreNormalTouchArea();
                }
            }
        }
    }
    
    • 第三步,获取松手事件。
      • 判断松手时间,如果不够我们定义的最短时间,回调外部 -> 录制太短
      • 判断松手坐标位置,如果在取消范围之下,为录制成功,回调外部 -> 录制成功
      • 判断到如果是在取消范围之上,则为取消,回调外部 -> 取消录制
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //只关心上下移动
        if (action == MotionEvent.ACTION_DOWN) {
            mDownY = event.getY();
            mLastY = event.getY();
            mDownTime = System.currentTimeMillis();
            setText("按住 说话");
            //回调外部,决定是否拦截
            if (mCallback != null) {
                if (mCallback.isIntercept()) {
                    return super.onTouchEvent(event);
                }
                mCallback.onStart();
            }
            return true;
        } else if (action == MotionEvent.ACTION_MOVE) {
            //获取移动的位置
            float moveY = event.getY();
            //计算滑动距离
            float changeY = moveY - mLastY;
            //判断是否在取消范围之上(到达了取消区域)
            if (moveY < 0 && Math.abs(changeY) >= mCancelDistance) {
                //上滑到了取消区域
                setText("松开手指,取消发送");
                if (mCallback != null) {
                    mCallback.onTouchCancelArea();
                }
            } else {
                //不在取消区域范围内(正常区域)
                setText("松开 结束");
                if (mCallback != null) {
                    mCallback.onRestoreNormalTouchArea();
                }
            }
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                float upY = event.getY();
                float distanceY = Math.abs(upY - mDownY);
                setText("按住说话");
                long intervalTime = System.currentTimeMillis() - mDownTime;
                //不够最短时间
                if (intervalTime <= MIN_INTERVAL_TIME) {
                    if (mCallback != null) {
                        mCallback.onTouchIntervalTimeSmall();
                    }
                } else if (upY >= 0 || distanceY < mCancelDistance) {
                    //在按钮以下或者还没到取消线,并且录制时间大于最小要求。录制成功
                    if (mCallback != null) {
                        mCallback.onFinish();
                    }
                } else if (upY < 0 && distanceY >= mCancelDistance) {
                    //上滑了到取消区域
                    if (mCallback != null) {
                        mCallback.onCancel();
                    }
                }
                if (mCallback != null) {
                    mCallback.onTerminate();
                }
                mLastY = event.getY();
            }
            return super.onTouchEvent(event);
    }
    

    外部使用

    • Xml布局我就不贴了,大家都会。

    • 配置回调,弹窗和录制(使用MediaRecorder)很简单,也不是本次重点,所以就不写到本篇文章了。

    //设置语音按钮事件
        vVoiceButton.setButtonTouchCallback(new VoiceRecordButton.ButtonTouchCallback() {
            @Override
            public boolean isIntercept() {
                //这里判断录音权限,如果没有,则返回false,并请求获取权限
                boolean isAcceptPermission = ...
                return !isAcceptPermission;
            }
    
            @Override
            public void onStart() {
                //1.按钮按下了,显示弹窗,弹窗样式切换到:录制中样式
                //2.开始录制
            }
    
            @Override
            public void onCancel() {
                //1.滑动到了取消,弹窗样式切换到:取消录制样式
                //2.取消录制
            }
    
            @Override
            public void onFinish() {
                //停止录制
            }
    
            @Override
            public void onTerminate() {
                //松手,隐藏弹窗
            }
    
            @Override
            public void onTouchIntervalTimeSmall() {
                //1.录制时间太小,弹窗样式切换到:录制时间太少样式
            }
    
            @Override
            public void onTouchCancelArea() {
                //滑动到了取消区域,弹窗样式切换到:松手取消的样式
            }
    
            @Override
            public void onRestoreNormalTouchArea() {
                //滑动到正常区域,弹窗样式切换到:松手发送的样式
            }
        });
    

    总结

    要做一个录制按钮、弹窗、录制功能其实不难,手势一直是自定义View中的一项难点,需要多练,即使再复杂,也可以从一步步拆解并分析,最后组合起来完成。

    相关文章

      网友评论

        本文标题:自定义语音录制按钮

        本文链接:https://www.haomeiwen.com/subject/lkqerctx.html