Android 全仿To圈儿录音界面实现

我们先来看看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类的各种调用,比较简单就不再赘述了。

最后实现的效果如下:

总的来说功能还是比较好实现的,就是目前经验还是不是太足做起来比较费力。

需要源码的留评论哈~

继续加油~

时间: 2024-10-22 11:22:03

Android 全仿To圈儿录音界面实现的相关文章

Android 全仿To圈儿个人资料界面层叠淡入淡出显示效果

前几天做的一个仿To圈个人资料界面的实现效果 下面是To圈的效果Gif图: 做这个东西其实也花了一下午的时间,一开始思路一直没理清楚,就开始盲目的去做,结果反而事倍功半. 以后要吸取教训,先详细思考清楚其中的逻辑关系,然后再开始动手写代码,这样比较容易理顺. 可以看到实现这个效果还是不难的,得分成以下三个步骤: 1:首先要有一个可拖动的详细资料布局(下半部分). 2:上半部分可跟随移动. 3:标题栏由隐藏到显示. 涉及到的技术点有: 1:屏幕像素密度DP转化. 2:自定义视图的OnTouchLi

Android 高仿微信6.0主界面 带你玩转切换图标变色

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/41087219,本文出自:[张鸿洋的博客] 1.概述 学习Android少不了模仿各种app的界面,自从微信6.0问世以后,就觉得微信切换时那个变色的Tab图标屌屌的,今天我就带大家自定义控件,带你变色变得飞起~~ 好了,下面先看下效果图: 清晰度不太好,大家凑合看~~有木有觉得这个颜色弱爆了了的,,,下面我动动手指给你换个颜色: 有没有这个颜色比较妖一点~~~好了~下面开始介绍

Android开发--仿微信语音对讲录音

自微信出现以来取得了很好的成绩,语音对讲的实现更加方便了人与人之间的交流.今天来实践一下微信的语音对讲的录音实现,这个也比较容易实现.在此,我将该按钮封装成为一个控件,并通过策略模式的方式实现录音和界面的解耦合,以方便我们在实际情况中对录音方法的不同需求(例如想要实现wav格式的编码时我们也就不能再使用MediaRecorder,而只能使用AudioRecord进行处理). 效果图: 实现思路 1.在微信中我们可以看到实现语音对讲的是通过点按按钮来完成的,因此在这里我选择重新自己的控件使其继承自

android实现仿QQ界面刷新

android实现仿QQ界面刷新 转载请注明出处:http://blog.csdn.net/wangpengfei_p/article/details/51420422 昨天想要实现一个下拉刷新的效果,本来想应该比较简单,因为之前在慕课网看见过类似的实现,记得是在listView里面添加footView或是添加headView,监听手指的点击滑动事件来控制view的显示或是隐藏,但是自己按照上面的代码来实现之后发现.这样做有一点不好的地方: 它判断是否刷新的依据是判断listView是不是滑动到

分享一个Android版 仿QQ局域网即时通信软件(可发文件、语音、录音)

一.支持的功能有文字信息交互.语音聊天.发送文件和录音 源码会在后面附上. 二.UI展示图 三.经过我的测试,是非常成功的.只是有一点不足就是语音实时通话的时候声音会回声甚至死机. 文件传送和文字,录音都比较成功. 四.本软件是用Java编码,在安卓平台上的应用.使用了UDP协议和TCP协议. 大家可以学习这两部分的代码. 里面注释还是比较多. 五.当然我只是个学生,这个只是学生版本,仅供大家学习借鉴之用.绝对不能用于商业拿去直接卖,或者改改就上架某市场. 六.宣传下本人的小制作: 单机斗地主-

Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO

距离上次发布(android高仿系列)今日头条 --新闻阅读器 (二) 相关的内容已经半个月了,最近利用空闲时间,把今日头条客户端完善了下.完善的功能一个一个全部实现后,就放整个源码.开发的进度就是按照一个一个功能的思路走的,所以开发一个小的功能,如果有用,就写一个专门的博客以便有人用到独立的功能可以方便使用. 这次实现的功能是很多新闻阅读器(网易,今日头条,360新闻等)以及腾讯视频等里面都会出现的频道管理功能. 下面先上这次实现功能的效果图:(注:这个效果图没有拖拽的时候移动动画,DEMO里

玩转Android Camera开发(四):预览界面四周暗中间亮,只拍摄矩形区域图片(附完整源码)

杂家前文曾写过一篇关于只拍摄特定区域图片的demo,只是比较简陋,在坐标的换算上不是很严谨,而且没有完成预览界面四周暗中间亮的效果,深以为憾,今天把这个补齐了. 在上代码之前首先交代下,这里面存在着换算的两种模式.第一种,是以屏幕上的矩形区域为基准进行换算.举个例子,屏幕中间一个 矩形框为100dip*100dip.这里一定要使用dip为单位,否则在不同的手机上屏幕呈现的矩形框大小不一样.先将这个dip换算成px,然后根据屏幕的宽和高的像素计算出矩形区域,传给Surfaceview上铺的一层Vi

Drawer Arrow Drawable(meun-icon-to-back-arrow)使用,仿知乎菜单栏界面

Drawer Arrow Drawable(meun-icon-to-back-arrow)使用,仿知乎菜单栏界面 一.什么是Drawer Arrow Drawable Drawer Arrow Drawable 其实就是一个抽屉侧滑菜单栏,只不过加入了很酷炫的meun-icon-to-back-arrow动画效果,如下图所示 二.Drawer Arrow Drawable的实现原理 设计方法: 我的想法是:如果我能生成每条线末端的移动曲线,我就能随抽屉(drawer)的滑动,根据参数t简单地计

高仿手机QQ5.0界面框架

这次的手机QQ更新从客观的角度来说,还是很好的,更加简约,控件也自定义了,界面也有了大的改动,但是最主要的框架还是它的左右滑动机制.让我们先来看看它的效果. 可以看到它是从左到右的一个滑动方法菜单的方式,最主要的就是这个控件类的实现吧.其他的感觉都没什么太大的问题,下面我就来看看这种效果应该怎么来实现. 第一拿到东西先分析这个效果是怎么出来的.我仔细的看了一下主要应该注意这几点. 1:菜单的出现有个放大效果而且伴随着一个apha的效果 2:主要的内容面板上面就是一个缩放的动画 3:可以看到这个菜