前言:今天和大家分享一个项目中用到的录音及播放进度控件。
一 代码实现:
1.view:
public class RecordView extends View {
/**
* 进度条颜色
*/
private int progressColor;
/**
* 描边颜色
*/
private int strokeColor;
private int textColor;
/**
* 椭圆半径
*/
private float radius;
/**
* 图片距离左边的大小
*/
private int ivMarginLeft;
/**
* 图片距离右边的大小
*/
private int ivMarginRight;
/**
* 文字大小
*/
private int textSize;
private Context mContext;
/**
* 控件总宽度,同时也是进度条的宽度
*/
private int recordWith;
private int recordHeight;
public void setRecordText(String recordText) {
this.recordText = recordText;
invalidate();
}
/**
* 文字
*/
private String recordText;
/**
* 画笔
*/
private Paint mPaint;
private Paint mTextPaint;
private Paint mIvPaint;
private Paint mProgressPaint;
/**
* 默认录音时长
*/
private int mRecordTimeLength = 60;
/**
* 当前时间
*/
private int mTimeLength = 0;
/**
* 文件播放总时长
*/
private int mPlayTime = 0;
/**
* 当前进度位置
*/
private int mCurrentProgressPosition;
/**
* 当前进度
*/
private int mCurrentProgress;
/**
* 控件类型
*/
private String mRecordViewType;
/**
* 播放控件
*/
public static final String PLAY_TYPE = "play";
/**
* 录音控件
*/
public static final String RECORD_TYPE = "record";
/**
* 控件左边播放图片
*/
private Bitmap mVoiceBitmap;
/**
* 控件右边播放/录音图片
*/
private Bitmap mRecordBitmap;
private Bitmap mRecordBeginBm;
private Bitmap mRecordOnBm;
private Bitmap mVoicePlayBm;
private Bitmap mVoicePlusBm;
private Bitmap mVoicePlayOneBm;
private Bitmap mVoicePlayTwoBm;
private Bitmap mVoicePlayThreeBm;
private List<Bitmap> mPlayerIcons;
/**
* 播放及录音监听
*/
private RecordViewListener mRecordViewListener;
private Handler mHandler = new Handler();
/**
* 是否正在播放
*/
boolean isPlaying;
/**
* 是否正在录音
*/
boolean isRecording;
/**
* 是否正在准备播放
*/
boolean isLoadingFinish = true;
boolean isStartAnimation;
public RecordView(Context context) {
super(context);
}
public RecordView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public RecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context, attrs);
}
public RecordView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context, attrs);
}
private void initialize(Context context, AttributeSet attributeSet) {
mContext = context;
TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.RecordView);
progressColor = typedArray.getColor(R.styleable.RecordView_progressColor, Color.parseColor("#F0F3FF"));
strokeColor = typedArray.getColor(R.styleable.RecordView_strokeColor, Color.parseColor("#BAC6F5"));
textColor = typedArray.getColor(R.styleable.RecordView_textColor, Color.parseColor("#607DF0"));
radius = (int) typedArray.getDimension(R.styleable.RecordView_radius, dp2px(18));
ivMarginLeft = (int) typedArray.getDimension(R.styleable.RecordView_imgvMarginLeft, dp2px(14));
ivMarginRight = (int) typedArray.getDimension(R.styleable.RecordView_imgvMarginRight, dp2px(14));
textSize = (int) typedArray.getDimension(R.styleable.RecordView_textSize, dp2px(13));
recordWith = (int) typedArray.getDimension(R.styleable.RecordView_recordWith, dp2px(220));
recordHeight = (int) typedArray.getDimension(R.styleable.RecordView_recordHeight, dp2px(36));
mRecordViewType = typedArray.getString(R.styleable.RecordView_recordViewType);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (mRecordViewType.equals(PLAY_TYPE)) {
if (isLoadingFinish) {
startPlayerAnimation();
} else {
ToastUtils.showShort("加载中,请稍后");
startLoadingAnimation();
}
}
if (mRecordViewType.equals(RECORD_TYPE)) {
startRecordAnimation();
}
}
});
initPaint();
initBitmap(context);
}
/**
* 初始化画笔
*/
private void initPaint() {
mPaint = new Paint();
mTextPaint = new Paint();
mIvPaint = new Paint();
mProgressPaint = new Paint();
mPaint.setAntiAlias(true);
mTextPaint.setAntiAlias(true);
mIvPaint.setAntiAlias(true);
mProgressPaint.setAntiAlias(true);
}
private void initBitmap(Context context) {
mRecordBeginBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_begin_icon);
mRecordOnBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_icon);
mVoicePlayBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_play_icon);
mVoicePlusBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_plluse_icon);
mVoicePlayOneBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_one);
mVoicePlayTwoBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_two);
mVoicePlayThreeBm = BitmapFactory.decodeResource(context.getResources(), R.drawable.public_voice_on_three);
//重新压缩图片尺寸
mRecordBeginBm = Bitmap.createScaledBitmap(mRecordBeginBm, dp2px(12), dp2px(12), false);
mRecordOnBm = Bitmap.createScaledBitmap(mRecordOnBm, dp2px(12), dp2px(12), false);
mVoicePlayBm = Bitmap.createScaledBitmap(mVoicePlayBm, dp2px(14), dp2px(14), false);
mVoicePlusBm = Bitmap.createScaledBitmap(mVoicePlusBm, dp2px(14), dp2px(14), false);
mVoicePlayOneBm = Bitmap.createScaledBitmap(mVoicePlayOneBm, dp2px(18), dp2px(18), false);
mVoicePlayTwoBm = Bitmap.createScaledBitmap(mVoicePlayTwoBm, dp2px(18), dp2px(18), false);
mVoicePlayThreeBm = Bitmap.createScaledBitmap(mVoicePlayThreeBm, dp2px(18), dp2px(18), false);
mPlayerIcons = new ArrayList<>();
mPlayerIcons.add(mVoicePlayOneBm);
mPlayerIcons.add(mVoicePlayTwoBm);
mPlayerIcons.add(mVoicePlayThreeBm);
if (mRecordViewType.equals(RECORD_TYPE)) {
setRecordBitmap(mRecordBeginBm);
setRecordText("点击录音");
}
if (mRecordViewType.equals(PLAY_TYPE)) {
setRecordBitmap(mVoicePlayBm);
setRecordText("播放" + "\t" + mPlayTime + "''");
}
setVoiceBitmap(mVoicePlayThreeBm);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int minimumWidth = getSuggestedMinimumWidth();
final int minimumHeight = getSuggestedMinimumHeight();
int width = measureWidth(minimumWidth, widthMeasureSpec);
int height = measureHeight(minimumHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureHeight(int defaultHeight, int heightMeasureSpec) {
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
defaultHeight = specSize;
} else {
defaultHeight = recordHeight;
}
return defaultHeight;
}
private int measureWidth(int defaultWith, int widthMeasureSpec) {
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
if (specMode == MeasureSpec.EXACTLY) {
defaultWith = specSize;
} else {
defaultWith = recordWith;
}
return defaultWith;
}
@Override
protected void onDraw(Canvas canvas) {
//绘制最外圈到椭圆
mPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
mPaint.setAntiAlias(true);
mPaint.setColor(strokeColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(4.0f);
float left = 2.0f;
float top = 2.0f;
float right = recordWith - 2.0f;
float bottom = recordHeight - 2.0f;
RectF rectF = new RectF(left, top, right, bottom);
canvas.drawRoundRect(rectF, radius, radius, mPaint);
//绘制进度
canvas.translate(0, recordHeight / 2.0f);
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStyle(Paint.Style.FILL);
mProgressPaint.setColor(progressColor);
if (mRecordViewType.equals(RECORD_TYPE)) {
mCurrentProgressPosition = recordWith * mCurrentProgress / mRecordTimeLength;
} else {
if (mPlayTime == 0) {
mCurrentProgressPosition = 0;
} else {
mCurrentProgressPosition = recordWith * mCurrentProgress / mPlayTime;
}
}
if (mRecordViewType.equals(PLAY_TYPE) && mCurrentProgressPosition >= recordWith) {
mCurrentProgressPosition = recordWith;
setRecordText("播放" + "\t" + mPlayTime + "''");
}
if (mCurrentProgressPosition < radius) {
//单边角度
int angle = (int) Math.toDegrees(Math.acos((radius - mCurrentProgressPosition) / (float) radius));
//开始的位置
int startAngle = 180 - angle;
//扫过的角度
int sweetAngle = 2 * angle;
float startTop = (float) (Math.sin((Math.PI * angle / 180)) * radius);
RectF oval = new RectF(4, -startTop + 2, 2 * radius - 2, startTop - 2);
canvas.drawArc(oval, startAngle, sweetAngle, false, mProgressPaint);
} else if (mCurrentProgressPosition >= radius && mCurrentProgressPosition < recordWith - radius) {
//绘制整个半圆弧
RectF oval = new RectF(4, -(recordHeight / 2) + 6, radius * 2 - 2, recordHeight / 2 - 6);
canvas.drawArc(oval, 90, 180, false, mProgressPaint);
canvas.drawRect(radius, -(recordHeight / 2) + 6, mCurrentProgressPosition, recordHeight / 2 - 6, mProgressPaint);
} else if (mCurrentProgressPosition >= recordWith - radius && mCurrentProgressPosition <= recordWith) {
//绘制整个半圆弧
RectF oval = new RectF(4, -(recordHeight / 2) + 6, radius * 2 - 2, recordHeight / 2 - 6);
canvas.drawArc(oval, 90, 180, false, mProgressPaint);
//绘制矩形部分进度
canvas.drawRect(radius, -(recordHeight / 2) + 8, recordWith - radius + 4, recordHeight / 2 - 8, mProgressPaint);
Path leftPath = new Path();
RectF rectFLeft = new RectF(recordWith - ivMarginRight - radius, -(recordHeight / 2) + 6, recordWith - 6, recordHeight / 2 - 6);
leftPath.addArc(rectFLeft, -90, 180);
canvas.drawPath(leftPath, mProgressPaint);
int angle = (int) Math.toDegrees(Math.acos(((radius - (recordWith - mCurrentProgressPosition)) / (float) radius)));
float top1 = (float) ((Math.sin((Math.PI * angle / 180)) * radius));
float left1 = mCurrentProgressPosition - (radius - (recordWith - mCurrentProgressPosition)) - radius;
RectF rectFRight = new RectF(left1 - 2, -top1 + 2,
recordWith - 4, top1 - 2);
mProgressPaint.setColor(Color.WHITE);
canvas.drawArc(rectFRight, -angle, 2 * angle, false, mProgressPaint);
}
//绘制左边图片
canvas.translate(ivMarginLeft, 0);
if (mRecordViewType.equals(PLAY_TYPE)) {
int voiceIconWith = mVoiceBitmap.getWidth();
int voiceIconHeight = mVoiceBitmap.getHeight();
canvas.drawBitmap(mVoiceBitmap, 0, -voiceIconHeight / 2, mIvPaint);
}
//绘制中间文字
canvas.translate(-ivMarginLeft + recordWith / 2.0f, 0);
mTextPaint.setColor(textColor);
mTextPaint.setTextSize(textSize);
if (recordText != null) {
float measureText = mTextPaint.measureText(recordText);
float textX = -measureText / 2.0f;
float textY = textSize / 2.0f;
canvas.drawText(recordText, textX, textY, mTextPaint);
}
//绘制右边图片
canvas.translate(-ivMarginRight + recordWith / 2.0f, 0);
//录音
if (mRecordViewType.equals(RECORD_TYPE)) {
int radius = mRecordBitmap.getWidth() / 2;
canvas.drawBitmap(mRecordBitmap, -radius * 2, -radius, mIvPaint);
}
//播放
if (mRecordViewType.equals(PLAY_TYPE)) {
if (isStartAnimation) {
isStartAnimation = false;
drawLoadingAnimation(canvas);
} else {
int with = mRecordBitmap.getWidth();
int height = mRecordBitmap.getHeight();
canvas.drawBitmap(mRecordBitmap, -with / 2 - 4, -height / 2, mIvPaint);
}
}
}
private void drawLoadingAnimation(Canvas canvas) {
canvas.translate(-12, 0);
Path path = new Path();
int width = mVoicePlusBm.getWidth();
RectF rectF = new RectF();
rectF.left = -width / 2;
rectF.top = -width / 2;
rectF.right = width / 2;
rectF.bottom = width / 2;
path.addArc(rectF, 20 * loadingRote, 100);
mIvPaint.setStyle(Paint.Style.STROKE);
mIvPaint.setStrokeWidth(4);
mIvPaint.setColor(Color.parseColor("#607DF0"));
canvas.drawPath(path, mIvPaint);
}
int loadingRote = 0;
private void startLoadingAnimation() {
mRecordViewListener.loadingFinish(isLoadingFinish);
mHandler.postDelayed(loadingRunnable, 10);
}
private void startRecordAnimation() {
if (isRecording) {
if (mRecordViewListener != null) {
mRecordViewListener.record(isRecording);
}
isRecording = false;
// recordText = "播放";
setRecordViewType(PLAY_TYPE);
setLoadingFinish(true);
setVoiceBitmap(mVoicePlayThreeBm);
setRecordBitmap(mVoicePlayBm);
//进度为0
setCurrentProgress(0);
//停止计时
mHandler.removeCallbacks(timeRunnable);
mHandler.removeCallbacks(runnable);
mTimeLength = 0;
} else {
if (mRecordViewListener != null) {
mRecordViewListener.record(isRecording);
}
isRecording = true;
setRecordBitmap(mRecordOnBm);
mHandler.postDelayed(runnable, 100);
//计时器
mHandler.postDelayed(timeRunnable, 0);
}
}
private void startPlayerAnimation() {
if (isPlaying) {
if (mRecordViewListener != null) {
mRecordViewListener.play(isPlaying);
}
isPlaying = false;
setRecordBitmap(mVoicePlayBm);
setRecordText("播放" + "\t" + mPlayTime + "''");
//停止进度计时
mHandler.removeCallbacks(runnable);
//停止计时
mHandler.removeCallbacks(timeRunnable);
//恢复到播放的初始状态
setVoiceBitmap(mVoicePlayThreeBm);
//进度为0
setCurrentProgress(0);
mTimeLength = 0;
} else {
if (mRecordViewListener != null) {
mRecordViewListener.play(isPlaying);
}
isPlaying = true;
setRecordText("播放中" + "\t" + mTimeLength + "''");
setRecordBitmap(mVoicePlusBm);
mHandler.postDelayed(runnable, 0);
//计时器
setCurrentProgress(0);
mHandler.postDelayed(timeRunnable, 1000);
}
}
int iconIndex = 0;
Runnable runnable = new Runnable() {
@Override
public void run() {
setVoiceBitmap(mPlayerIcons.get(iconIndex));
iconIndex++;
if (iconIndex > mPlayerIcons.size() - 1) {
iconIndex = 0;
}
mHandler.postDelayed(this, 500);
}
};
Runnable timeRunnable = new Runnable() {
@Override
public void run() {
mTimeLength++;
setCurrentProgress(mTimeLength);
if (isPlaying) {
setRecordText("播放中" + "\t" + mTimeLength + "''");
}
if (isRecording) {
setRecordText("录音中" + "\t" + mTimeLength + "''");
}
mHandler.postDelayed(this, 1000);
if (mTimeLength > mRecordTimeLength) {
finishRecordOrPlay();
}
}
};
Runnable loadingRunnable = new Runnable() {
int sweetRote = 18;
@Override
public void run() {
isStartAnimation = true;
loadingRote++;
if (loadingRote == sweetRote) {
loadingRote = 0;
}
mHandler.postDelayed(this, 100);
invalidate();
}
};
/**
* 完成播放/录音
*/
private void finishRecordOrPlay() {
setCurrentProgress(mRecordTimeLength);
//停止更新进度的计时
mHandler.removeCallbacks(runnable);
//停止计时
mHandler.removeCallbacks(timeRunnable);
if (mRecordViewType.equals(PLAY_TYPE)) {
isPlaying = false;
setRecordBitmap(mVoicePlayBm);
setRecordText("播放" + "\t" + mPlayTime + "''");
//恢复到播放的初始状态
setVoiceBitmap(mVoicePlayThreeBm);
//恢复到播放状态之后,表示已经加载完成
setLoadingFinish(true);
}
if (mRecordViewType.equals(RECORD_TYPE)) {
isRecording = false;
setRecordViewType(PLAY_TYPE);
setRecordBitmap(mVoicePlayBm);
setVoiceBitmap(mVoicePlayThreeBm);
setRecordText("播放" + "\t" + mPlayTime + "''");
}
if (mRecordViewListener != null) {
mRecordViewListener.finishEvent();
}
mTimeLength = 0;
setCurrentProgress(0);
}
private int dp2px(final float dpValue) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
//************************************以下为对外提供的直接调用方法***************************************************************
public void setLoadingFinish(boolean loadingFinish) {
isLoadingFinish = loadingFinish;
if (isLoadingFinish) {
setRecordBitmap(mVoicePlayBm);
if (mHandler != null && loadingRunnable != null) {
mHandler.removeCallbacks(loadingRunnable);
}
}
}
/**
* 开始播放
*/
public void startPlay() {
if (mRecordViewType.equals(PLAY_TYPE) && isLoadingFinish) {
startPlayerAnimation();
} else {
return;
}
}
/**
* 停止播放
*/
public void stopPlay() {
if (mRecordViewType.equals(PLAY_TYPE) && isPlaying) {
finishRecordOrPlay();
}
}
public void setRecordViewType(String recordViewType) {
this.mRecordViewType = recordViewType;
invalidate();
}
public void setProgressColor(int progressColor) {
this.progressColor = progressColor;
invalidate();
}
public void setCurrentProgress(int mCurrentProgress) {
this.mCurrentProgress = mCurrentProgress;
invalidate();
}
public void setRecordTimeLength(int recordTimeLength) {
this.mRecordTimeLength = recordTimeLength;
}
public void setRecordBitmap(Bitmap recordBitmap) {
this.mRecordBitmap = recordBitmap;
invalidate();
}
public void setVoiceBitmap(Bitmap voiceBitmap) {
this.mVoiceBitmap = voiceBitmap;
invalidate();
}
public void setPlayTime(int playTime) {
this.mPlayTime = playTime;
}
/**
* 录音完成并添加或者删除当前录音之后,恢复录音控件的属性
*/
public void convertRecordToPlay() {
setRecordTimeLength(60);
setRecordText("点击录音");
setRecordBitmap(mRecordBeginBm);
setRecordViewType(RECORD_TYPE);
}
/**
* 录制完成之后,需要调用该方法更新文件录制长度,以便进行录音的预播放
*/
public void updatePlayTime(int playTime) {
setPlayTime(playTime);
//修改播放总时长
setRecordTimeLength(playTime);
setRecordText("播放" + "\t" + playTime + "''");
}
/**
* 设置控件的监听
*
* @param recordViewListener 监听接口
*/
public void setRecordViewListener(RecordViewListener recordViewListener) {
this.mRecordViewListener = recordViewListener;
}
/**
* 销毁计时器对象,防止内存溢出
*/
public void clear() {
finishRecordOrPlay();
if (mHandler != null) {
mHandler.removeCallbacks(runnable);
mHandler.removeCallbacks(loadingRunnable);
mHandler.removeCallbacks(timeRunnable);
}
}
}
2.attr.xml中的自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--录音控件属性-->
<declare-styleable name="RecordView">
<!--进度颜色-->
<attr name="progressColor" format="color" />
<!--描边颜色-->
<attr name="strokeColor" format="color" />
<!--文字颜色-->
<attr name="textColor" format="color" />
<!--半径-->
<attr name="radius" format="dimension" />
<!--图片距离左右的距离-->
<attr name="imgvMarginLeft" format="dimension" />
<attr name="imgvMarginRight" format="dimension" />
<attr name="textSize" format="dimension" />
<attr name="recordWith" format="dimension" />
<attr name="recordHeight" format="dimension" />
<attr name="recordViewType" format="string" />
</declare-styleable>
</resources>
3.控件监听:
public interface RecordViewListener {
/**
* 是否正处于播放状态
*
* @param isPlaying 是否为播放状态
*/
void play(boolean isPlaying);
/**
* 是否处于录音状态
*
* @param isRecording 是否为录音状态
*/
void record(boolean isRecording);
/**
* 完成录音或者播放
*/
void finishEvent();
/**
* 播放准备的监听
* @param isFinish 是否正在准备
*/
void loadingFinish(boolean isFinish);
}
二 使用:
需要指出,在使用时,由于录音及播放是两种不同的状态,因此,需要在recordViewType中进行声明其类型。此外,也可以通过提供的setRecordViewType方法改变控件的类型。
<views.record.RecordView
android:id="@+id/recorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="40dp"
recordWith:recordHeight="36dp"
recordWith:recordViewType="record"
recordWith:recordWith="220dp" />
至此,该控件的自定义基本已经完成。接下来可直接使用。需要指出的是,控件中使用的进度更新方法逻辑有点混乱,事实上可以使用valueanimator来实现,相应的,会在后期进行优化,目前基本功能是可以实现的。不足之处,欢迎大家指出。