之前做了一个聊天室模块,除了能发送文字、图片,还需要发送语音,就涉及到了语音的手势,手势就和微信的是一样的,现在大部分聊天App都是采用这套。
其实难点在于计算手势距离来切换不同的状态,录制和提示弹窗都是很简单的事情。当然经过一段时间折腾和修Bug后,也完成了,特此记录一下。
需求分析
UI分析:
- 闲置状态,不录制
- 按钮文字为:按住 说话
- 提示弹窗:不显示
- 按下状态,录制中
- 按钮文字为:松开 结束
- 提示弹窗:显示,提示文字:手指上滑,取消发送
- 滑动到取消区域状态,松手则取消发送
- 按钮文字为:松开 结束
- 提示弹窗:显示,提示文字:松开手指,取消发送
- 滑动到正常区域,松手则马上发送
- 按钮文字为:松开 结束
- 提示弹窗:显示,提示文字:手指上滑,取消发送
- 录制时间小于指定时间,取消录制
- 按钮文字为:松开 结束
- 提示弹窗:显示,提示文字:录制时间太短
注意点
Android6.0开始录制语音,需要动态申请权限,注意适配
需要提供的回调
- 是否拦截,用于判断权限是否获取,如果未获取,返回false,按钮不处理事件
- 开始回调(手指按下)
- 取消回调(滑动到取消区域后松手)
- 结束回调(在取消区域和正常区域松手)
- 准备退出回调(手机抬起,松手)
- 间隔时间太小时回调(按下和松手时的时间差小于指定值)
- 手指移动到区域区域回调
- 手指移动到正常区域回调
按钮自定义步骤
一般我们按钮都继承于TextView,设置背景来做,所以我们的按钮继承TextView。
- 定义取消区域距离,我这里获取屏幕高度,以3分之一的距离作为取消区域,测试过发现是比较合适的,如有不合适再做调整。
- 定义最短录制时间,我这里定义为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;
}
}
- 定义回调接口,具体就是上面的分析
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;
}
- 手势判断,大部分的代码都在这里,我们把这里理解了,就能做出来了
- 首先,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中的一项难点,需要多练,即使再复杂,也可以从一步步拆解并分析,最后组合起来完成。