Android麦克风录音带音量大小动态显示的圆形自定义View

1、所谓无图无真相,先上效果图。我们要实现的就是中间那个录音的按钮,周边会显示一圈音量大小的波形

2、VolumCircleBar继承自View,我们进行了自定义,代码如下

package com.rdinfo.ccenglish.ui.ccprofile.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import com.panshi.xuexiao.R;
/**
 * 麦克风音量圆形按钮
 * @author [email protected]
 * @version [CCEnglish, 2014-7-25]
 */
public class VolumCircleBar extends View
{
    private double volumRate; // 音量百分比
    private boolean isRecording; // 录音标志
    private Object lock = new Object();
    private Thread uiThread;
    private Paint mPaint;
    private RectF arcRect;
    private Matrix matrix = new Matrix();
    private final int VOLUM_INDICATE_LENGTH = 5; // 音量大小线长度
    private final int CIRCLE_INNER_DISTANCE_TO_OUTSIDE = 5; // 内切圆距离外圆的距离
    public VolumCircleBar(Context context)
    {
        this(context, null);
    }

    public VolumCircleBar(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public VolumCircleBar(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VolumCircleBar, defStyle, 0);
        init(typedArray);
    }

    private int recordingColor; // 录音背景色
    private int stoppedColor; // 停止背景色
    private Bitmap centerRes; // 中间麦克风图片
    private int totalBlockCount; // 块数量
    private int spliteAngle; // 块之间的间隔角度大小
    private int circleWidth; // 直径
    /**
     * 初始化
     */
    private void init(TypedArray typedArray)
    {
        for (int i = 0; i < typedArray.length(); i++)
        {
            int attr = typedArray.getIndex(i);
            switch (attr)
            {
                case R.styleable.VolumCircleBar_recordingColor:
                    recordingColor = typedArray.getColor(i, Color.GREEN);
                    break;
                case R.styleable.VolumCircleBar_stoppedColor:
                    stoppedColor = typedArray.getColor(i, Color.GRAY);
                    break;
                case R.styleable.VolumCircleBar_centerRes:
                    centerRes = BitmapFactory.decodeResource(getContext().getResources(), typedArray.getResourceId(i, R.drawable.ic_launcher));
                    break;
                case R.styleable.VolumCircleBar_blockCount:
                    totalBlockCount = typedArray.getInt(i, 50);
                    break;
                case R.styleable.VolumCircleBar_splitAngle:
                    spliteAngle = typedArray.getInt(i, 2);
                    break;
            }
        }
        typedArray.recycle();
        uiThread = Thread.currentThread();
        mPaint = new Paint();
        if (spliteAngle * totalBlockCount > 360)
        {
            throw new IllegalArgumentException("spliteAngle * blockCount > 360, while the result should be less than 360.");
        }

        // debug for test
        isRecording = true;
        volumRate = 0.5;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        // 直径
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        circleWidth = width > height? width : height;
        if (arcRect == null)
        {
            arcRect = new RectF(CIRCLE_INNER_DISTANCE_TO_OUTSIDE, CIRCLE_INNER_DISTANCE_TO_OUTSIDE,
                    circleWidth - CIRCLE_INNER_DISTANCE_TO_OUTSIDE, circleWidth - CIRCLE_INNER_DISTANCE_TO_OUTSIDE); // 音量显示区域, 内偏移几个像素
            // 图片处理矩阵
            initBitmapMatrix();
        }
        // 强制设置view大小
        setMeasuredDimension(circleWidth, circleWidth);
    }

    /**
     * 中间图片压缩处理
     */
    private void initBitmapMatrix()
    {
        float innerCircleRadius = (circleWidth - 2 * (VOLUM_INDICATE_LENGTH + CIRCLE_INNER_DISTANCE_TO_OUTSIDE)) / 2f; // 内圆的半径
        float innerRectangleWidth = (float)Math.cos((Math.PI / 180) * 45) * innerCircleRadius * 2; // 内圆的内切正方形的边长
        float translateOffset = VOLUM_INDICATE_LENGTH + CIRCLE_INNER_DISTANCE_TO_OUTSIDE + innerCircleRadius - innerRectangleWidth / 2; // 偏移的offset
        if (centerRes.getWidth() > (innerRectangleWidth) || centerRes.getHeight() > (innerRectangleWidth))
        {
            // 图片宽度或高度大于(直径-内偏移), 等比压缩
            if (centerRes.getWidth() > centerRes.getHeight())
            {
                // 按照宽度压缩
                float ratio = innerRectangleWidth / centerRes.getWidth();
                matrix.postScale(ratio, ratio);
                float translateY = (innerRectangleWidth - (centerRes.getHeight() * ratio)) / 2f;
                // 在纵坐标方向上进行偏移,以保证图片居中显示
                matrix.postTranslate(translateOffset, translateY + translateOffset);
            }
            else
            {
                // 按照高度压缩
                float ratio = innerRectangleWidth / (centerRes.getHeight() * 1.0f);
                matrix.postScale(ratio, ratio);
                float translateX = (innerRectangleWidth - (centerRes.getWidth() * ratio)) / 2f;
                // 在横坐标方向上进行偏移,以保证图片居中显示
                matrix.postTranslate(translateX + translateOffset, translateOffset);
            }
        }
        else
        {
            // 当图片的宽高都小于屏幕宽高时,直接让图片居中显示
            float translateX = (innerRectangleWidth - centerRes.getWidth()) / 2f;
            float translateY = (innerRectangleWidth - centerRes.getHeight()) / 2f;
            matrix.postTranslate(translateX + translateOffset, translateY + translateOffset);
        }
    }

    /**
     * 设置音量百分比
     * @param rate
     */
    public void updateVolumRate(double rate)
    {
        synchronized (lock)
        {
            this.volumRate = rate;
            if (Thread.currentThread() != uiThread)
            {
                postInvalidate();
            }
            else
            {
                invalidate();
            }
        }
    }

    /**
     * 开始、停止录音
     */
    public void toggleRecord()
    {
        synchronized (lock)
        {
            isRecording = !isRecording;
            if (Thread.currentThread() != uiThread)
            {
                postInvalidate();
            }
            else
            {
                invalidate();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        canvas.drawColor(Color.TRANSPARENT);
        synchronized (lock)
        {
            if (isRecording) // 正在录音
            {
                //1.绘制绿色圆圈
                mPaint.setAntiAlias(true); //消除锯齿
                mPaint.setColor(recordingColor);
                mPaint.setStrokeWidth(1);
                mPaint.setStyle(Paint.Style.FILL); // 填充
                canvas.drawCircle(circleWidth / 2f, circleWidth / 2f, circleWidth / 2f, mPaint);
                //2.根据音量百分比、块数量、块间隔大小计算角度动态绘制音量大小
                // 计算块的角度
                float blockAngle = (360 * 1.0f - spliteAngle * totalBlockCount) / totalBlockCount;
                int drawBlockCount = (int)(totalBlockCount * volumRate); // 绘制的block数量
                mPaint.setStrokeWidth(VOLUM_INDICATE_LENGTH);
                mPaint.setColor(stoppedColor);
                mPaint.setStyle(Paint.Style.STROKE); // 空心
                for (int i = 0; i < drawBlockCount; i++)
                {
                    canvas.drawArc(arcRect, i * (blockAngle + spliteAngle) - 90, blockAngle, false, mPaint);
                }
            }
            else // 录音停止
            {
                //1.绘制灰色圆圈
                mPaint.setColor(stoppedColor);
                mPaint.setStrokeWidth(1);
                mPaint.setStyle(Paint.Style.FILL); // 填充
                canvas.drawCircle(circleWidth / 2f, circleWidth / 2f, circleWidth / 2f, mPaint);
            }
        }
        // 绘制中间话筒
        canvas.drawBitmap(centerRes, matrix, null);
    }
}

今天时间不多,没看明白的留言吧。

Android麦克风录音带音量大小动态显示的圆形自定义View

时间: 2024-08-24 02:23:09

Android麦克风录音带音量大小动态显示的圆形自定义View的相关文章

Android UI 绘制过程浅析(五)自定义View

前言 这已经是Android UI 绘制过程浅析系列文章的第五篇了,不出意外的话也是最后一篇.再次声明一下,这一系列文章,是我在拜读了csdn大牛郭霖的博客文章<带你一步步深入了解View>后进行的实践. 前面依次了解了inflate的过程,以及绘制View的三个步骤:measure, layout, draw.这一次来亲身实践一下,通过自定义View来加深对这几个过程的理解. 自定义View的分类 根据实现方式,自定义View可以分为以下3种类型. 自绘控件.View的绘制代码(onDraw

Android零基础入门第24节:自定义View简单使用

当我们开发中遇到Android原生的组件无法满足需求时,这时候就应该自定义View来满足这些特殊的组件需求. 一.概述 很多初入Android开发的程序员,对于Android自定义View可能比较恐惧,但这又是高手进阶的必经之路,这里先不做过多学习,只是简单了解.关于高阶的内容会在后续课程陆续进行学习,欢迎关注分享达人秀(ShareExpert)获取第一手教程. 如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种:自绘控件.组合控件.以及继承控件. 自绘控件:内容都是开发者自己绘

[转]Android自定义控件三部曲系列完全解析(动画, 绘图, 自定义View)

来源:http://blog.csdn.net/harvic880925/article/details/50995268 一.自定义控件三部曲之动画篇 1.<自定义控件三部曲之动画篇(一)——alpha.scale.translate.rotate.set的xml属性及用法>2.<自定义控件三部曲之动画篇(二)——Interpolator插值器>3.<自定义控件三部曲之动画篇(三)—— 代码生成alpha.scale.translate.rotate.set及插值器动画&g

android自定义view(一),打造绚丽的验证码

前言:我相信信念的力量,信念可以支撑起一个人,一个名族,一个国家.正如"人没有梦想和咸鱼有什么区别"一样,我有信念,有理想,所以我正在努力向着梦想前进~. 自定义view,如果是我,我首先要看到自定义view的效果图,然后再想想怎么实现这种效果或功能,所以先贴上自定义验证码控件的效果图: 怎么样,这种验证码是不是很常见呢,下面我们就自己动手实现这种效果,自己动手,丰衣足食,哈哈~ 一. 自定义view的步骤 自定义view一直被认为android进阶通向高手的必经之路,其实自定义vie

Android零基础入门第40节:自定义ArrayAdapter

ListView用起来还是比较简单的,也是Android应用程序中最重要的一个组件,但其他ListView可以随你所愿,能够完成很多想要的精美列表,而这正是我们接下来要学习的内容. 一.自定义ArrayAdapter 从上期自定义列表项示例知道,每个列表项的图标都一样,如果需要每个列表项的图标根据内容动态表示,Android系统的ArrayAdapter就无能为力了,就只能使用自定义ArrayAdapter来实现啦. 做法就是创建一个ArrayAdapter的子类,重写其getView()方法,

【Android应用开发技术:用户界面】自定义View类设计

作者:郭孝星 微博:郭孝星的新浪微博 邮箱:[email protected] 博客:http://blog.csdn.net/allenwells Github:https://github.com/AllenWells 设计良好的类总是相似的,它使用一个易用的接口来封装一个特定的功能,它能有效的使用CPU和内存,我们在设计View类时,通常会考虑以下因素: 遵循Android标准规则 提供自定义的风格属性值并能够被Android XML Layout所识别. 发出可访问的事件 能够兼容And

Android官方开发文档Training系列课程中文版:创建自定义View之View的绘制

原文地址:http://android.xsoftlab.net/training/custom-views/custom-drawing.html#draw 自定义View最重要的部分就是它的样子了.自定义View的绘制根据应用的需要或者简单亦或者复杂.这节课的内容涵盖了大多数通用的知识点. 重写onDraw()方法 绘制自定义View很重要的一个步骤就是重写它的onDraw()方法.该方法含有一个Canvas对象作为参数,用来使View绘制它本身的内容.Canvas类定义了用于绘制文本,线条

【Android】自定义View —— 滑动的次数选择器

[关键词] 自定义View 次数选择器 滑动 [问题] 实现一个可滑动的次数选择器: [效果图] 「原型图」 「实现图」 [分析] 对外提供简单的Change监听接口: 如果处于两者之间就需要做判断:大于一半就自动跳转到下一个,小于一半,则回到上一个: 通过Scroller及其startScroll()方法来实现回弹效果: 要灵活控制刻度的最小值和最大值,因为可能随着需求的更改,这个值很容易发生改变(即处理好对应关系): 在自定义View中只画刻度和文字,至于红色的指针和外面的透明渐变图层则可以

Android自定义view教程01-------------Android的Frame动画详解

本系列博文 最终的目的是能教会大家自己实现比较复杂的android 自定义控件.所以知识点不仅仅局促在自定义view本身上面.实际上现在github上一些做的比较出色的自定义控件 大部分都是由三个部分组成 第一:动画 第二:自定义view 第三:触摸滑动控制.所以我们这个系列也是由动画作为开篇.最终会带着大家分析几个github上比较出色的自定义控件. Android 的frame动画是比较简单基础的内容,在以往的2.x 3.x版本很多人都会去使用这个 来作为loading 图的实现方法.但是最