我们先来看看To圈(QQ,微信等其他大部分软件也是大同小异)的注册录音界面运行截图:
(⊙o⊙)…为了实现这个效果还是花了一番功夫的,
主要难点有以下方面:
1、跟随音量变化的话筒。
这个话筒一开始感觉是最头痛的部分,完全不知道从何开始实现。首先直接用图片肯定是不行的,想实现我最后达到的效果需要12张图片,这太占资源了直接GG。然后又想到用遮罩实现,但是仔细观察可以发现话筒的圆形进度条周围是透明的,也就是说如果用遮罩,除非完美重合(重合就得考虑屏幕适配问题了,这就是个大难题了)。最后我想出的办法有点取巧吧:用一个竖向进度条实现话筒的进度部分,然后下面的Y字形直接用canvas画(为了使其看起来像个话筒花费了大量代码进行计算...)。这样一来看起来差不多,而且规避了屏幕适配问题,也不需要加载那么多图片然后轮着换了。但这肯定不是最好的方法,所以跪求更高雅的实现的方法!!!
2、圈内的蓝色圆弧计时部分。
3、手势判断。
这么一总结感觉1比2和3加起来都难得多..
实现过程:
首先是界面实现:
activity_register.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <span style="white-space:pre"></span><pre name="code" class="html"> <!-- ...不重要的部分... -->
<!-- 录音显示UI层 --> <include android:id="@+id/layout_register_popup" android:layout_width="fill_parent" android:layout_height="fill_parent" layout="@layout/layout_recode_popwindow" android:gravity="center" android:visibility="gone" /></RelativeLayout>
不重要的部分没有贴上去,按下完成注册会出现黑色录音框的布局在<include>标签里引用。这里暂时设置为不可见。
下面是黑色录音框布局:
layout_recode_popwindow.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <RelativeLayout android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:layout_width="180dp" android:layout_height="180dp" android:background="@drawable/shape_register_recoderlayout" android:orientation="vertical"> <com.whale.nangua.toquan.view.RecodePopWindowCircle android:id="@+id/circleview_register_microphone" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerHorizontal="true" android:layout_marginTop="20dp" android:background="@drawable/shape_register_recodepopwindow" ></com.whale.nangua.toquan.view.RecodePopWindowCircle> <LinearLayout android:layout_width="wrap_content" android:layout_height="50dp" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true"> <TextView android:layout_marginBottom="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:id="@+id/tv_register_show" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="手指上划,取消发送" android:textColor="@android:color/white" android:textSize="16sp"/> </LinearLayout> </RelativeLayout> </RelativeLayout>
实现效果如下:
代码实现:
从上面的界面中可以看到有一个自定义的View:RecodePopWindowCircle.java
package com.whale.nangua.toquan.view; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ProgressBar; import android.widget.RelativeLayout; import com.whale.nangua.toquan.R; /** * Created by nangua on 2016/7/29. */ public class RecodePopWindowCircle extends RelativeLayout { ProgressBar progressbar_register_recode;//进度条 int width; //控件总高 int height; //控件总宽 boolean IS_SHOW_RECODING = true; //默认设置为true float scale = this.getResources().getDisplayMetrics().density; //获得像素 public RecodePopWindowCircle(Context context, AttributeSet attrs) { super(context, attrs); //在构造函数中将Xml中定义的布局解析出来。 LayoutInflater.from(context).inflate(R.layout.layout_recode_circlepopwindow, this, true); init(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); progressbar_register_recode = (ProgressBar) this.findViewById(R.id.progressbar_register_recode); width = getWidth(); height = getHeight(); } private void init() { } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint = new Paint(); //画笔 if (IS_SHOW_RECODING) { progressbar_register_recode.setVisibility(View.VISIBLE); //画话筒下面的Y形,整体下移5个像素点5*scale paint.setColor(Color.WHITE); paint.setStrokeWidth(6); //宽度 paint.setAntiAlias(true); //抗锯齿 paint.setStyle(Paint.Style.STROKE); //设置空心 RectF oval = new RectF(); //RectF对象 int xandy = width / 2; int r = (int) ( 8 * scale); //进度条底部半径 int space = (int) (5 * scale); //圆弧与底部进度条的间隔 oval.left = xandy - r - space; //左边 oval.top = xandy - 2 * r - space + 5 * scale; //上边 oval.right = xandy + r + space; //右边 oval.bottom = xandy + space + 5 * scale; //画弧形 canvas.drawArc(oval, 20, 140, false, paint); //画线 int lineLength = (int) (10 * scale); canvas.drawLine(xandy, xandy + space + 5 * scale, xandy, xandy + space + lineLength + 5 * scale, paint); //画弧形的圆点 //因为位置计算没有精确化所以做了些微调 int pointx = (int) (Math.cos(xandy) + r + space); paint.setStyle(Paint.Style.FILL); //设置实心 //右边缘圆点 canvas.drawCircle(xandy + pointx - 1 * scale, xandy + 2 * scale, 3, paint); //左边缘圆点 canvas.drawCircle(xandy - pointx + 1 * scale, xandy + 2 * scale, 3, paint); //直线下边缘圆点 canvas.drawCircle(xandy, xandy + space + lineLength + 5 * scale, 3, paint); } //否则显示垃圾桶界面 else { progressbar_register_recode.setVisibility(View.INVISIBLE); Bitmap bitmap = BitmapFactory.decodeResource(this.getContext().getResources(), R.drawable.ic_trash); canvas.drawBitmap(bitmap,null, new Rect((int) (width/2 - 20*scale), (int) (height/2 - 25*scale), (int) (width/2 + 20*scale), (int) (height/2 + 25*scale)),null); } //画外围的蓝色录音圆弧 paint.setColor(Color.parseColor("#0CA6D9")); paint.setStrokeWidth(2); //宽度 paint.setAntiAlias(true); //抗锯齿 paint.setStyle(Paint.Style.STROKE); //设置空心 canvas.drawArc(new RectF(0 + 2, 0 + 2, width - 2, height - 2), -90, endArc, false, paint); } /** * 设置是否显示录音话筒在录音 */ public void setIsShowRecoding(boolean IS_SHOW_RECODING) { this.IS_SHOW_RECODING = IS_SHOW_RECODING; postInvalidate(); } //设置时间圆弧终止角度 public void setEndArc(int endArc) { this.endArc = endArc; postInvalidate(); } //设置进度 public void setProgress(int progress) { progressbar_register_recode.setProgress(progress); } int endArc = 0; //圆弧终止角度 }
实现的过程注释说明得很清楚了,这里再概括一下,主要是在试图中绘制Y字形话筒的"把手",以及外围蓝色录音弧,并提供了设置方法以便在Activity中改变视图。
这里再Y字形三个点处加画了白色小圈圈,以使其更加Q弹...
最后就是控制代码的实现了:
RegisterActivity.java
package com.whale.nangua.toquan; import android.app.Activity; import android.media.MediaPlayer; import android.os.Bundle; import android.os.Handler; import android.view.MotionEvent; import android.view.View; import android.widget.Button; import android.widget.ImageButton; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; import com.whale.nangua.toquan.view.RecodePopWindowCircle; import com.whale.nangua.toquan.voice.SoundMeter; import java.io.IOException; /** * Created by nangua on 2016/7/26. */ public class RegisterActivity extends Activity implements View.OnClickListener, RadioGroup.OnCheckedChangeListener, SoundMeter.onStartRecoder { //性别选择的radiobutton private RadioButton radiobtn_register_man; private RadioGroup radiogroup_register_sexcheck; private View layout_register_popup; //播放录音按钮 private Button btn_register_play; //录音按钮 private ImageButton imgbtn_register_recode; private RecodePopWindowCircle circleview_register_microphone; private long startVoiceT; //开始录音的时间 private String voiceName; //音频名 //录音组件 private SoundMeter mSensor; private Handler mHandler = new Handler(); private Runnable ampTask = new Runnable() { public void run() { double amp = mSensor.getAmplitude(); //得到音频图 updateDisplay(amp); mHandler.postDelayed(ampTask, POLL_INTERVAL); } }; private int endArc = 0; private Runnable updateArcTask = new Runnable() { @Override public void run() { endArc += 1; circleview_register_microphone.setEndArc(endArc); //更新时间圈圈 mHandler.postDelayed(updateArcTask, UPDATE_ARC_INTERVAL); } }; private Runnable mSleepTask = new Runnable() { public void run() { stop(); } }; //录音延迟 private static final int POLL_INTERVAL = 300; //时间圆弧更新延迟,默认一秒 private static final int UPDATE_ARC_INTERVAL = 200; //录音提示文字 TextView tv_register_show; //是否取消发送 private boolean IF_CANCLE_SEND = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_register); initView(); } float scale; //像素密度 int screenHeight; //屏幕高度 private void initView() { screenHeight = this.getWindowManager().getDefaultDisplay().getHeight(); //屏幕高度 scale = this.getResources().getDisplayMetrics().density; //初始化录音提示文字 tv_register_show = (TextView) findViewById(R.id.tv_register_show); //初始化播放录音按钮 btn_register_play = (Button) findViewById(R.id.btn_register_play); btn_register_play.setOnClickListener(this); //初始化录音组件 mSensor = new SoundMeter(); mSensor.setonStartRecoderCallback(this); circleview_register_microphone = (RecodePopWindowCircle) findViewById(R.id.circleview_register_microphone); //初始化性别选择rbtn radiobtn_register_man = (RadioButton) findViewById(R.id.radiobtn_register_man); radiobtn_register_man.setChecked(true); radiogroup_register_sexcheck = (RadioGroup) findViewById(R.id.radiogroup_register_sexcheck); radiogroup_register_sexcheck.setOnCheckedChangeListener(this); layout_register_popup = findViewById(R.id.layout_register_popup); layout_register_popup.setVisibility(View.INVISIBLE); imgbtn_register_recode = (ImageButton) findViewById(R.id.imgbtn_register_recode); imgbtn_register_recode.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int Y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: btn_register_play.setVisibility(View.VISIBLE); layout_register_popup.setVisibility(View.VISIBLE); circleview_register_microphone.setEndArc(0);//初始化时间圆弧 startVoiceT = System.currentTimeMillis(); voiceName = startVoiceT + ".amr"; /** * 开始录音方法,传入音频名字为事件+.amr */ start(voiceName); break; case MotionEvent.ACTION_UP: layout_register_popup.setVisibility(View.GONE); endArc = 0; stop(); //如果取消发送 if (IF_CANCLE_SEND) { //TODO 取消发送 } //如果发送 else { //TODO 发送 } break; case MotionEvent.ACTION_MOVE: //位置判断在方框范围以内 if (Y <= screenHeight / 2 + 90 * scale) { tv_register_show.setText("手指松开,取消发送"); tv_register_show.setBackground(getResources().getDrawable(R.drawable.shape_register_recodetv)); circleview_register_microphone.setIsShowRecoding(false); } else { tv_register_show.setText("手指上划,取消发送"); tv_register_show.setBackground(null); circleview_register_microphone.setIsShowRecoding(true); } break; } return true; } }); } private void stop() { mHandler.removeCallbacks(mSleepTask); mHandler.removeCallbacks(ampTask); mHandler.removeCallbacks(updateArcTask); mSensor.stop(); circleview_register_microphone.setProgress(0); } /** * 更新显示音频高低图 * * @param signalEMA */ private void updateDisplay(double signalEMA) { int temp = 100 / 12; switch ((int) signalEMA) { case 0: circleview_register_microphone.setProgress(temp); break; case 1: circleview_register_microphone.setProgress(2 * temp); break; case 2: circleview_register_microphone.setProgress(3 * temp); break; case 3: circleview_register_microphone.setProgress(4 * temp); break; case 4: circleview_register_microphone.setProgress(5 * temp); break; case 5: circleview_register_microphone.setProgress(6 * temp); break; case 6: circleview_register_microphone.setProgress(7 * temp); break; case 7: circleview_register_microphone.setProgress(8 * temp); break; case 8: circleview_register_microphone.setProgress(9 * temp); break; case 9: circleview_register_microphone.setProgress(10 * temp); break; case 10: circleview_register_microphone.setProgress(11 * temp); break; case 11: circleview_register_microphone.setProgress(12 * temp); break; default: break; } } /** * 开始录音 * * @param name */ private void start(String name) { mSensor.start(name); mHandler.postDelayed(ampTask, POLL_INTERVAL); mHandler.postDelayed(updateArcTask, UPDATE_ARC_INTERVAL); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_register_play: MediaPlayer mediaPlayer = new MediaPlayer(); try { mediaPlayer.setDataSource(soundFilePath); mediaPlayer.prepare(); } catch (IOException e) { e.printStackTrace(); } mediaPlayer.start(); break; } } String soundFilePath;//录音文件路径 @Override public void setVoicePath(String path) { soundFilePath = path; } /** * 性别选择改变的监听方法 * * @param group * @param checkedId */ @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.radiobtn_register_women: //TODO 选择了男性 break; case R.id.radiobtn_register_man: //TODO 选择了女性 break; } } }
其中主要是对MediaRecoder类的各种调用,比较简单就不再赘述了。
最后实现的效果如下:
总的来说功能还是比较好实现的,就是目前经验还是不是太足做起来比较费力。
需要源码的留评论哈~
继续加油~