仿陌陌发送语音控件

最近做一款即时通讯软件,一些UI效果参考陌陌实现,看到陌陌的语音发送控件比较不错,于是试着仿照实现了一个。

先看效果图:

这里写图片描述

以下是我实现时考虑的几个问题:
①圆形一缩一放的效果这么实现
②按下View的时候圆形由空心变实心,图片还在,动画开始后图片消失了,这个怎么做。
③什么时候回收不需要的资源文件


下面看代码:

1.

public class PressSpeakView extends View {
    /**
     * View宽度
     */
    private int mWidth;
    /**
     * View高度
     */
    private int mHeight;
    /**
     * 圆半径
     */
    private float mRadius;
    /**
     * 圆左边界
     */
    private float mCircleLeftBorder;
    /**
     * 圆右边界
     */
    private float mCircleRightBorder;
    /**
     * 圆上边界
     */
    private float mCircleTopBorder;
    /**
     * 圆下边界
     */
    private float mCircleBottomBorder;
    /**
     * 话筒Bitmap
     */
    private Bitmap mMicBitmap;
    /**
     * 话筒Bitmap绘制区域
     */
    private Rect mMicArea;
    /**
     * 垃圾箱Bitmap
     */
    private Bitmap mTrashCanBitmap;
    /**
     * 垃圾箱Bitmap绘制区域
     */
    private Rect mTrashCanArea;

    /**
     * 记录是否是按住状态
     */
    private boolean isPressed;
    /**
     * 记录是否是可取消语音状态
     */
    private boolean isCancelable;

    private Paint mCirclePaint;
    private ValueAnimator animator;

    private int mGrayColor;

    private MyOnTouchEventListener myTouchListener;

    public PressSpeakView(Context context) {
        this(context, null);
    }

    public PressSpeakView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PressSpeakView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        initDefaultValue(context);
        initPaint();
        initAnimator();
    }

这个类继承自View,在构造方法中执行一些初始化操作。其中mCirclePaint是画圆的画笔,animator是属性动画,为了实现圆形一收一放的动画效果,MyTouchListener是对外提供的接口,用于用户继续重写onTouchEvent方法。

2.

    private void initAnimator() {
        MyAnimatorListener animatorListener = new MyAnimatorListener();
        animator = ValueAnimator.ofFloat(1f, 1.5f, 1f);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                mRadius = (mWidth / 2 - mWidth / 5) * value;
                invalidate();
            }
        });
        animator.addListener(animatorListener);
        animator.setDuration(1000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
    }

    private void initDefaultValue(Context context) {
        isPressed = false;
        isCancelable = false;
        mGrayColor = ContextCompat.getColor(context, R.color.gray_f5f5f5);
        mWidth = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
        mHeight = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
        mMicBitmap = BitmapFactory.decodeResource(
            getResources(), R.drawable.ic_chat_audio_record_blue);
        mTrashCanBitmap = BitmapFactory.decodeResource(
            getResources(), R.drawable.ic_chat_audio_record_cancel);
        mMicArea = new Rect();
        mTrashCanArea = new Rect();
    }

    private void initPaint() {
        mCirclePaint = new Paint();
        mCirclePaint.setColor(mGrayColor);
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStrokeWidth(6);
        mCirclePaint.setStyle(Paint.Style.STROKE);
    }

这里是三个初始化方法。
① initDefaultValue方法初始化了一些属性值,View的宽高默认为100dp,拿到显示的Bitmap等等。
② initPaint方法初始化了画圆的画笔。
③ initAnimator方法初始化了属性动画,实现的效果是float值从1->1.5->1,持续1秒,无限循环。在这个方法里我们注册了两个listener,这两个listener都做了什么呢?
a.通过addUpdateListener方法,我们注册了一个AnimatorUpdateListener,用来监听动画的执行过程。在onAnimationUpdate方法中,我们得到当前的float值,用这个值动态计算圆半径(mRadius),执行invalidate重绘方法,这样就能实现圆的动画效果(解决了第一个考虑的问题)。
b.通过addListener,我们注册了一个AnimatorListner,代码如下:

private class MyAnimatorListener extends AnimatorListenerAdapter {
        @Override
        public void onAnimationStart(Animator animation) {
            mMicArea.setEmpty();
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            mMicArea.left = mWidth / 2 - 50;
            mMicArea.right = mWidth / 2 + 50;
            mMicArea.top = mHeight / 2 - 50;
            mMicArea.bottom = mHeight / 2 + 50;
        }
    }

原生的AnimatorListener接口有很多方法需要我们实现,但是我们用不上那么多,所以我们自定义一个类,继承了AnimatorListenerAdapter。
AnimatorListenerAdapter是一个抽象类,已经实现了AnimatorListener接口并实现了其中所有方法,但都是空方法,所以我们继承这个类,重写我们需要的方法就可以了。
在这里我们只需要监听动画的开始和停止状态,并且都操作了mMicArea变量,mMicArea是Rect类的对象,用于放置“话筒”图片的位置,在动画开始时,我们执行setEmpty()方法,其实就是把mMicArea四个顶点都置0,这样图片就消失了,而动画结束时,我们再把mMicArea的四个顶点恢复,这样图片就又显示出来了。

3.

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth = widthSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        }

        mMicArea.left = mTrashCanArea.left = mWidth / 2 - 50;
        mMicArea.right = mTrashCanArea.right = mWidth / 2 + 50;
        mMicArea.top = mTrashCanArea.top = mHeight / 2 - 50;
        mMicArea.bottom = mTrashCanArea.bottom = mHeight / 2 + 50;

        mRadius = mWidth / 2 - mWidth / 5;

        mCircleLeftBorder = mWidth / 2 - mRadius;
        mCircleRightBorder = mWidth / 2 + mRadius;
        mCircleTopBorder = mHeight / 2 - mRadius;
        mCircleBottomBorder = mHeight / 2 + mRadius;

        setMeasuredDimension(mWidth, mHeight);

    }

进入onMeasure方法,在这个方法里我们需要测量View的大小。
① 首先拿到widthMode、widthSize、heightMode、heightSize四个值
② 如果widthMode或heightMode为Exactly说明用户指定了宽高值,我们就使用widthSize或heightSize作为View的宽或高,并赋值给mWidth或mHeight,否则用户可能设置了WRAP_CONTENT,我们就使用默认的宽高值。
③ 根据上一步计算出的mWidth和mHeight设置放置图片矩形的位置、圆形的半径和圆形的四个边界(用于判定手指是否点在了圆形内部)。

4.

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (!isPressed) {
            mCirclePaint.setStyle(Paint.Style.STROKE);
        } else {
            mCirclePaint.setStyle(Paint.Style.FILL);
            if (isCancelable) {
                mCirclePaint.setColor(Color.RED);
                canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mCirclePaint);
                canvas.drawBitmap(mTrashCanBitmap, null, mTrashCanArea, null);
                return;
            }
        }
        mCirclePaint.setColor(mGrayColor);
        canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mCirclePaint);
        canvas.drawBitmap(mMicBitmap, null, mMicArea, null);
    }

在onDraw中执行绘制操作。
① 如果View不在按下状态,则绘制空心圆
② 如果View在按下状态,则绘制实心圆。
③ 在按下状态下进一步判断是否是可取消状态,如果是的话绘制红色实心圆,且显示“垃圾箱”图标,直接退出方法,不再往下进行。
④ 如果不是可取消状态,则绘制灰色实心圆,显示“话筒”图标。
⑤ 我们使用了drawBitmap(Bitmap bitmap, Rect src, Rect dst,Paint paint)这个方法绘制图片。源码如下:

public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
            @Nullable Paint paint) {
        if (dst == null) {
            throw new NullPointerException();
        }
        throwIfCannotDraw(bitmap);
        final long nativePaint = paint == null ? 0 : paint.getNativeInstance();

        int left, top, right, bottom;
        if (src == null) {
            left = top = 0;
            right = bitmap.getWidth();
            bottom = bitmap.getHeight();
        } else {
            left = src.left;
            right = src.right;
            top = src.top;
            bottom = src.bottom;
        }

        native_drawBitmap(mNativeCanvasWrapper, bitmap, left, top, right, bottom,
            dst.left, dst.top, dst.right, dst.bottom, nativePaint, mScreenDensity,
            bitmap.mDensity);
    }

第一个参数代表要绘制的原始图片,第二个参数代表要绘制原始图片的区域,第三个参数代表原始图片绘制到的区域,第四个参数是画笔。在这里我们第二个参数传入null,代表绘制整个图片。第三个参数是一个Rect对象且不能为null,前面我们已经得到了,如果我们不希望图片显示出来,就把这个Rect对象四个顶点设置成0,反之把四个顶点设置成有效值,这就是图片显示与消失的实现方式。

5.

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //判断点击区域是否在圆内
                if (event.getX() < mCircleRightBorder
                        && event.getX() > mCircleLeftBorder
                        && event.getY() < mCircleBottomBorder
                        && event.getY() > mCircleTopBorder) {
                    isPressed = true;
                    invalidate();
                    delayStartAnimator();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                float x = event.getX();
                float y = event.getY();
                isCancelable = (y < mCircleTopBorder)
                        || (y > mCircleBottomBorder)
                        || (x > mCircleRightBorder)
                        || (x < mCircleLeftBorder);
                if (isCancelable) {
                    if (animator.isStarted()) {
                        animator.end();
                    }
                } else {
                    if (!animator.isStarted()) {
                        instantStartAnimator();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isPressed = false;
                isCancelable = false;
                animator.end();
                break;
        }
        return myTouchListener == null || myTouchListener.onTouchEvent(event);
    }

在onTouchEvent中判断用户的操作。
① 在用户执行按下屏幕操作时(ACTION_DOWN),判断是否按在圆形内部,如果在的话,则设置按下状态,重绘一次View,再延时开启一个动画。
这里有个疑问,前面动画的监听事件里调用了 invalidate()重绘View的方法,为什么这里在开启动画前又调用了一次呢?
这里我们先调用了一次invalidate(),这时空心圆已经变成实心圆,但是圆中的图片还在,等动画开始时,圆中的图片就消失了,解决了第二点考虑的问题
② 在用户手指在屏幕上移动时(ACTION_MOVE),判断手指是否移到了圆外,如果到圆外了说明要取消发送语音,这时动画停止,显示删除图片,如果手指又从圆外移回圆内,则继续播放动画。(取消发送语音一般是上滑取消,但我看陌陌的取消方式就是以圆的边界作为判断,所以也使用了这种判断方式)。
③用户手指离开屏幕时(ACTION_UP),恢复初始值,在这里可以判断手指最后在圆内还是圆外,决定是发送语音还是放弃发送语音。
④ 在最后返回值判断上,如果没有设置MyTouchListener,则默认返回true,说明我们消费了这个事件,如果设置了MyTouchListener,以listener返回为准。

6.

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mMicBitmap.recycle();
        mTrashCanBitmap.recycle();
        mMicBitmap = null;
        mTrashCanBitmap = null;
        if (animator.isStarted()) {
            animator.cancel();
        }
        animator.removeAllListeners();
        animator.removeAllUpdateListeners();
        animator = null;
    }

通过在网上查阅资料知道,View生命周期的最后一步就是走onDetachedFromWindow方法,在这里我们执行了两个操作:
① 回收bitmap对象,释放内存。
② 判断动画是否还在执行,如果正在执行就取消,同时取消动画的监听事件,这样能够防止程序意外停止时动画还在一直执行,持有它的对象不能被回收,造成内存泄漏。
解决了第三点考虑的问题。


小结:

由于自身水平有限,写出的代码与文章难免有错误与疏漏,还请大家指正,谢谢!
Github:https://github.com/venfor/PressSpeakDemo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值