自定义语音录制按钮

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

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

需求分析

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("按住 说话");
        //回调外部,决定是否拦截
        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中的一项难点,需要多练,即使再复杂,也可以从一步步拆解并分析,最后组合起来完成。

你可能感兴趣的:(自定义语音录制按钮)