Android自定义控件(神级)+MediaRecoder录音
零、前言
总算想到一个神级的自定义控件了
前方高能预警,萌新自带零食饮料
本文的前置知识你需简单了解:Android绘制函数图象及正弦函数的介绍
没错,今天玩自定义控件,和函数、录音有什么关系?用脚趾头稍微想一下就知道了...
废话不多说,看待仿
效果:
效果.gif别激动...这只是
待仿
的效果(OPPOR15X录音自带),至于能仿成什么样我心里也没底
二、正式开战
1.截两张图分析一下
[1]--上下镜像有没有,做一条,另一条镜像一下就行了
[2]--颜色渐变色,Paint支持颜色渐变
[3]--一条深,一条浅,就拿深的开刀吧
[4]--两端线较细,这个得琢磨一下
[5]--算上驻点两条线一共有5个交点,一共两个周期
分析图.png
2.正弦函数的绘制
先别看别的,先画一个正弦函数再说
那一篇用点拼的,现在想想可以用path,这篇用path来画
2.1--分析
A:振幅---默认200
φ:初相,默认0
曲线总长:测试阶段:-600~600 共1200
T:周期--600
ω:2π/T
2.2--绘制正弦函数
正弦函数.png/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/16 0016:9:04<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:旋律视图
*/
public class RhythmView2 extends View {
private Point mCoo = new Point(800, 500);//原点坐标
private double mMaxHeight = 200;//最到点
private double min = -600;//最小x
private double max = 600;//最大x
private double φ = 0;//初相
private double A = mMaxHeight;//振幅
private double ω;//角频率
private Paint mPaint;//主画笔
private Path mPath;//主路径
private Path mReflexPath;//镜像路径
public RhythmView2(Context context) {
this(context, null);
}
public RhythmView2(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();//初始化
}
private void init() {
//初始化主画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(6);
//初始化主路径
mPath = new Path();
mReflexPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
mPath.reset();
mReflexPath.reset();
super.onDraw(canvas);
canvas.save();
canvas.translate(mCoo.x, mCoo.y);
formPath();
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
/**
* 对应法则
*
* @param x 原像(自变量)
* @return 像(因变量)
*/
private double f(double x) {
double len = max - min;
ω = 2 * Math.PI / (rad(len) / 2);
double y = A * Math.sin(ω * rad(x) - φ);
return y;
}
private void formPath() {
mPath.moveTo((float) min, (float) f(min));
for (double x = min; x <= max; x++) {
double y = f(x);
mPath.lineTo((float) x, (float) y);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mAnimator.start();
break;
}
return true;
}
private double rad(double deg) {
return deg / 180 * Math.PI;
}
}
3.正弦函数的动起来
横向位移.gif什么影响正弦函数的横向位移--相位:φ
那还等什么,ValueAnimator走起,从0~2 * Math.PI
//数字时间流
mAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
mAnimator.setDuration(1000);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(a -> {
φ = (float) a.getAnimatedValue();
invalidate();
});
就这么简单?---是的
4.让凸起的部分渐渐平息
减息.gif不就是对A值进行渐变嘛...非常简单
mAnimator.addUpdateListener(a -> {
φ = (float) a.getAnimatedValue();
A = (float) (mMaxHeight* (1 - (float) a.getAnimatedValue() / (2 * Math.PI)));
invalidate();
});
二、加入衰减函数与渐变色
1.加入衰减函数
加入衰减函数.gif虽然有那么点感觉,但是还是差很多,关键在对应法则,说起来也简单
但是操作起来挺费劲,衰减函数凑了好一会...
/**
* 对应法则
*
* @param x 原像(自变量)
* @return 像(因变量)
*/
private double f(double x) {
double len = max - min;
double a = 4 / (4 + Math.pow(rad(x / Math.PI * 800 / len), 4));
double aa = Math.pow(a, 2.5);
ω = 2 * Math.PI / (rad(len) / 2);
double y = aa * A * Math.sin(ω * rad(x) - φ);
return y;
}
2.加渐变色
加颜色渐变.gif什么颜色好呢,好吧,我计较懒,搭条彩虹吧(以前实现过)
int[] colors = new int[]{
Color.parseColor("#F60C0C"),//红
Color.parseColor("#F3B913"),//橙
Color.parseColor("#E7F716"),//黄
Color.parseColor("#3DF30B"),//绿
Color.parseColor("#0DF6EF"),//青
Color.parseColor("#0829FB"),//蓝
Color.parseColor("#B709F4"),//紫
};
float[] pos = new float[]{
1.f / 7, 2.f / 7, 3.f / 7, 4.f / 7, 5.f / 7, 6.f / 7, 1
};
mPaint.setShader(
new LinearGradient(
(int) min, 0, (int) max, 0,
colors, pos,
Shader.TileMode.CLAMP
));
三、第二条曲线的绘制
两条曲线.gif1.路径的形成
会了一个,另一个Y镜像一下就行了(y坐标边-y)
private void formPath() {
mPath.moveTo((float) min, (float) f(min));
mReflexPath.moveTo((float) min, (float) f(min));
for (double x = min; x <= max; x++) {
double y = f(x);
mPath.lineTo((float) x, (float) y);
mReflexPath.lineTo((float) x, -(float) y);
}
}
2.绘制第二条曲线:onDraw
第二条淡一点
mPaint.setAlpha(255);
canvas.drawPath(mPath, mPaint);
mPaint.setAlpha(66);
canvas.drawPath(mReflexPath, mPaint);
3.高度设置
设置高度.gif我的用意是在录音是监听音量大小,然后让图象波动
暴漏设置高度的方法,在设置时执行动画,下面是点击设置随机高度效果
/**
* 设置高度
* @param maxHeight
*/
public void setMaxHeight(double maxHeight) {
mMaxHeight = maxHeight;
mAnimator.start();
invalidate();
}
四、扫尾--封装
该dp的dp,该删的删,该封装的封装,该优化的优化,直接贴代码
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/16 0016:9:04<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:贝塞尔三次曲线--旋律视图
*/
public class RhythmView extends View {
private double mMaxHeight = 0;//最到点
private double mPerHeight = 0;//最到点
private double min;//最小x
private double max;//最大x
private double φ = 0;//初相
private double A = mMaxHeight;//振幅
private double ω;//角频率
private Paint mPaint;//主画笔
private Path mPath;//主路径
private Path mReflexPath;//镜像路径
private ValueAnimator mAnimator;
private int mHeight;
private int mWidth;
public RhythmView(Context context) {
this(context, null);
}
public RhythmView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();//初始化
}
private void init() {
//初始化主画笔
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(dp(2));
//初始化主路径
mPath = new Path();
mReflexPath = new Path();
//数字时间流
mAnimator = ValueAnimator.ofFloat(0, (float) (2 * Math.PI));
mAnimator.setDuration(1000);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(a -> {
φ = (float) a.getAnimatedValue();
A = (float) (mMaxHeight * mPerHeight * (1 - (float) a.getAnimatedValue() / (2 * Math.PI)));
invalidate();
});
}
public void setPerHeight(double perHeight) {
mPerHeight = perHeight;
mAnimator.start();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
mMaxHeight = mHeight / 2 * 0.9;
min = -mWidth / 2;
max = mWidth / 2;
handleColor();
setMeasuredDimension(mWidth, mHeight);
}
private void handleColor() {
int[] colors = new int[]{
Color.parseColor("#33F60C0C"),//红
Color.parseColor("#F3B913"),//橙
Color.parseColor("#E7F716"),//黄
Color.parseColor("#3DF30B"),//绿
Color.parseColor("#0DF6EF"),//青
Color.parseColor("#0829FB"),//蓝
Color.parseColor("#33B709F4"),//紫
};
float[] pos = new float[]{
1.f / 10, 2.f / 7, 3.f / 7, 4.f / 7, 5.f / 7, 9.f / 10, 1
};
mPaint.setShader(
new LinearGradient(
(int) min, 0, (int) max, 0,
colors, pos,
Shader.TileMode.CLAMP
));
}
@Override
protected void onDraw(Canvas canvas) {
mPath.reset();
mReflexPath.reset();
super.onDraw(canvas);
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
formPath();
mPaint.setAlpha(255);
canvas.drawPath(mPath, mPaint);
mPaint.setAlpha(66);
canvas.drawPath(mReflexPath, mPaint);
canvas.restore();
}
/**
* 对应法则
*
* @param x 原像(自变量)
* @return 像(因变量)
*/
private double f(double x) {
double len = max - min;
double a = 4 / (4 + Math.pow(rad(x / Math.PI * 800 / len), 4));
double aa = Math.pow(a, 2.5);
ω = 2 * Math.PI / (rad(len) / 2);
double y = aa * A * Math.sin(ω * rad(x) - φ);
return y;
}
private void formPath() {
mPath.moveTo((float) min, (float) f(min));
mReflexPath.moveTo((float) min, (float) f(min));
for (double x = min; x <= max; x++) {
double y = f(x);
mPath.lineTo((float) x, (float) y);
mReflexPath.lineTo((float) x, -(float) y);
}
}
private double rad(double deg) {
return deg / 180 * Math.PI;
}
protected float dp(float dp) {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
五、MediaRecode实现录音
效果.png第一天用
AudioTrack
实现了录音,MediaRecode可以录音也可以录视频
两者的区别AudioTrack
麻烦一点,需要自己去操作字节流,但可以精致操作
MediaRecode相当于给你封装好了,你一步步走,给个文件就行了
1.录音的辅助类
/**
* 作者:张风捷特烈
* 时间:2018/4/16:10:33
* 邮箱:1981462002@qq.com
* 说明:MediaRecorder录音帮助类
*/
public class MediaRecorderTask {
private MediaRecorder mRecorder;
private long mStartTime;//开始的时间
private int mAllTime;//总共耗时
private boolean isRecording;//是否正在录音
private File mFile;//文件
private Timer mTimer;
private final Handler mHandler;
public MediaRecorderTask() {
mTimer = new Timer();//创建Timer
mHandler = new Handler();//创建Handler
}
/**
* 开始录音
*/
public void start(File file) {
mAllTime = 0;
mFile = file;
if (mRecorder == null) {
// [1]获取MediaRecorder类的实例
mRecorder = new MediaRecorder();
}
//配置MediaRecorder
// [2]设置音频的来源
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
// [3]设置音频的输出格式
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// [4]采样频率
mRecorder.setAudioSamplingRate(44100);
// [5]设置音频的编码方式
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
//[6]音质编码频率:96Kbps
mRecorder.setAudioEncodingBitRate(96000);
//[7]设置录音文件位置
mRecorder.setOutputFile(file.getAbsolutePath());
try {
mRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mStartTime = System.currentTimeMillis();
if (mRecorder != null) {
mRecorder.start();
isRecording = true;
cbkVolume();
}
}
/**
* 每隔1秒回调一次音量
*/
private void cbkVolume() {
mTimer.schedule(new TimerTask() {
@Override
public void run() {
if (isRecording) {
float per;
try {
//获取音量大小
per = mRecorder.getMaxAmplitude() / 32767f;//最大32767
} catch (IllegalStateException e) {
e.printStackTrace();
per = (float) Math.random();
}
if (mOnVolumeChangeListener != null) {
float finalPer = per;
mHandler.post(() -> {
mOnVolumeChangeListener.volumeChange(finalPer);
});
}
}
}
}, 0, 1000);
}
public void pause() {
mAllTime += System.currentTimeMillis() - mStartTime;
mRecorder.pause(); // [7]暂停录
isRecording = false;
mStartTime = System.currentTimeMillis();
}
public void resume() {
mRecorder.resume(); // [8]恢复录
isRecording = true;
}
/**
* 停止录音
*/
public void stop() {
try {
mAllTime += System.currentTimeMillis() - mStartTime;
mRecorder.stop(); // [7]停止录
isRecording = false;
mRecorder.release();
mRecorder = null;
} catch (RuntimeException e) {
mRecorder.reset();//[8] You can reuse the object by going back
mRecorder.release(); //[9] Now the object cannot be reused
mRecorder = null;
isRecording = false;
if (mFile.exists())
mFile.delete();
}
}
public int getAllTime() {
return mAllTime / 1000;
}
//---------设置音量改变监听-------------
public interface OnVolumeChangeListener {
void volumeChange(float per);
}
private OnVolumeChangeListener mOnVolumeChangeListener;
public void setOnVolumeChangeListener(OnVolumeChangeListener onVolumeChangeListener) {
mOnVolumeChangeListener = onVolumeChangeListener;
}
}
2.使用--Activity中
基本套路和第一篇的录音一致,下面只给出核心的步骤
不明白参见第一篇或源码
//初始化MediaRecorderTask
mMediaRecorderTask = new MediaRecorderTask();
//设置监听---效果的核心
mMediaRecorderTask.setOnVolumeChangeListener(per -> {
mIdRth.setPerHeight(per);
});
/**
* 开启录音
*/
private void startRecord() {
//创建录音文件---这里创建文件不是重点,我直接用了
mFile = FileHelper.get().createFile("MediaRecorder录音/" + StrUtil.getCurrentTime_yyyyMMddHHmmss() + ".m4a");
mMediaRecorderTask.start(mFile);
}
/**
* 停止录制
*/
private void stopRecode() {
mMediaRecorderTask.stop();
mIdTvState.setText("录制" + mMediaRecorderTask.getAllTime() + "秒");
}
音频软件打开.png
3.播放音频
昨天已经实现了MediaPlayer播放音频,不废话了,直接拿来用
mMusicPlayer = new MusicPlayer();
mMusicPlayer.start("/sdcard/MediaRecorder录音/20190104195319.m4a");
关于音频的编码,压缩,格式这三者感觉挺烦人的,下一篇把它们捋一下
再玩一下音频的变速和变声操作,今天就到这里
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
V0.1-github | 2018-1-5 | Android自定义控件(神级)+MediaRecode录音 |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的掘金 | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
icon_wx_200.png