Android 那些事– 小米手环 测量心率 动画实现

Android 那些事– 小米手环 测量心率 动画实现

双11的时候,买了一枚小米手环2,据说有测量心率的功能,如下图:

mi

觉得这个心跳图挺好玩,然后琢磨琢磨实现了一下,先上效果图:
mi3

第一步:分解动画


整体分为两个部分:圆环转动和心跳图。最外面那一层就是一个带缺口的圆环加一张图片,这个不是动画,嵌一个背景就行;里面一层是有动画的圆环,最里面一层是心跳图动画,那么还原主要也就是圆环动画和心率图动画两个部分。

1. 圆环动画

一开始琢磨着圆环怎么画的时候,想着用path去画,写一写发现需要添加的路径太多,放弃,然后突然想到通过canvas.drawArc这个API直接画弧就好了,把paint的宽度设大一点就可以实现圆弧效果,至于带有间距的实现,在网上查有人说用canvas.clipPath可以实现,不过其实通过间隔画弧是可以实现的,每隔一度画一度的弧,然后循环画慢360度就OK。

如果不用drawArc,通过drawCircle也可以,只要将paint设置为空心就行,不过这样一来就需要同clipPath来切图,就稍微太麻烦一点。

2. 心率图动画

根据原图发现先是一条直线慢慢从左边出来,然后开始有规律的上下震动。一开始不知道怎么去画,有一次无意间看了一篇通过sin函数画水波纹特效的博客,恍然大悟,原来这货就是一个正弦曲线。

第二步:动画实现


1. 圆环动画实现

通过间隔角度画圆弧就可以达到效果

for(int i=0;i<360;i+=3){
    canvas.drawArc(mRectf, i, 1,false, mRingPaint);
}

一开始用drawArc方法时,以为第三个参数是要画到的角度,但测试才发现这个参数是从第i度开始画N个角度的圆弧,不是FromDegree -> ToDegree,而是FromeDegree -> NDegree 的关系。通过调整mRingPaint的宽度调整圆弧的宽度。在子线程中调整 i 的范围就可以动态的画出圆弧了。

最终的效果要的是底层圆环是灰色的圆环,动画执行的时候,是白色的圆弧在转,从1度一直到360度。这个好办,画两个圆弧。

for (int i = 0; i < 360; i += 3) {
    canvas.drawArc(mRectf, i, 1, false, mRingPaint);
}
if (mAnimAngle != -1) {// 如果开启了动画
    for (int i = -90; i < mAnimAngle-90; i += 3) {
        canvas.drawArc(mRectf, i, 1, false, mRingAnimPaint);
    }
}

首先默认画底层那个灰色的圆弧(间隔3度),然后当开始执行动画的时候,在子线程里动态的修改mAnImAngle的值,从0~360,postInvalidate异步刷新就可以实现效果了。

2. 心率动画实现

这个稍微复杂一点点,首先第一步是画出静态的带一个完整周期的正选曲线图。
这里画的只是一条线而非一片区域,因此不用drawLine,通过drawPoint就可以实现想要的结果。在view的中心画3/4的直线,最后1/4画一条正弦曲线。

for(int i=start;i<end;i++){
    canvas.drawPoint(i,Y,paint);
}

x左边就是从view的左边到右边,然后只要确定好了Y轴的坐标就OK,通过一个数组存储这条线的Y坐标,在for循环中替换掉Y就可以了。
计算法方法如下:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mTotalHeight = h;
    mTotalWidth = w;
    mHeartBeatWidth = w - mHeartPaintWidth * 2 - 40; //内圆宽度
    AmplitudeA = (mTotalHeight-2*mHeartPaintWidth)/4;
    mOriginalYPositon = new float[mHeartBeatWidth];//正弦曲线 Y坐标
    Arrays.fill(mOriginalYPositon, 0);
    // 周期定位总宽度的1/3
    mPeriodFraction = (float) (Math.PI * 2 / mHeartBeatWidth * 3);
    for (int i =  mHeartBeatWidth/3*2; i < mHeartBeatWidth; i++) {
        mOriginalYPositon[i] = (float) (AmplitudeA * Math.sin(mPeriodFraction * i) + OFFSET_Y);
    }
}

在onSizeChanged回调中计算view的宽高,因为线条是包含在圆环内的,圆环是有宽度的(mHeartPaintWidth ),需要避开,为了避免正弦函数上下振动时覆盖圆环,加上40个偏移量。设置好振幅,周期,然后替换掉上一段中Y的值就好了。
为了达到连续振动的动画效果,稍微修改下就可以了:

if(StartHeartBeatAnmiFlag){
//绘制心率线 
    int interval = mHeartBeatWidth - mOffset;
    for(int i=mOffset,j=mHeartPaintWidth+20;i<mHeartBeatWidth;i++,j++){
        canvas.drawPoint(j, mTotalHeight/2-mOriginalYPositon[i], mHeartBeatPaint);
    }
    for(int i=0,j=interval+mHeartPaintWidth+20;i<mOffset;i++,j++){
        canvas.drawPoint(j, mTotalHeight/2-mOriginalYPositon[i], mHeartBeatPaint);
    }
    canvas.drawCircle(mHeartBeatWidth+20+mHeartPaintWidth, mTotalHeight/2-mOriginalYPositon[mOffset], 10, mHeartBeatPaint);
}

mHeartBeatWidth是心率线的总宽度,mOffset是当前动画时的偏移量,这里注意是通过两个for来绘制的,解释一下。
原始心率图Y轴坐标假设为 {0,0,0,0 … 0,1,2,3,2,1,0,-1,-2,-3,-2,-1,0}最后那个起伏假定是正弦曲线,那么下一帧动画Y的坐标就是把这个数组向左平移一位,然后把左边挤掉的数字黏在右边。

有两个方法可以解决,第一,可以通过一个数组,先通过System.arraycopy copy后一部分数据,然后在copy前一段数据,放到临时数组中,处理好数据,这样在onDraw函数中一个for就可以了,简单明了,但是这样多一个数组,而且数组被copy2次;第二,来回拷贝数据显然可以通过偏移量offset解决,先遍历后一部分数据,再遍历前一部分数据就可以,省去复制数组的消耗。这样就下onDraw里边需要两次for,分段画point。

最后那个canvas.drawCircle啊,是因为线条的最右边有一个黄色的小点,所以每次在最后一个坐标上再画一个小圆圈就OK。

心率图基本完成,还差动画的最后一步,就是慢慢将心率图从右到左平移出来,然后再循环移动,
再加一个偏移量,在onDraw里面再过一个for就完事。另一个方法,就是再加一个临时数组,考虑这个平移出来只会执行一次,并不会反复执行,因此加一个也行(实际跑出来这一段也比较流畅)。

System.arraycopy(mOriginalYPositon,0,mDefaultYPostion, mFirstFrameOffset,mOriginalYPositon.length - mFirstFrameOffset );

mDefaultYPostion是临时数组,默认值-1,保存从左到右平移出心率图的Y的坐标,在执行动画时,不断修改mFirstFrameOffset偏移量就可以了,在onDraw函数中简单调用:

if(StartFirstFrameFlag){
    for(int i=0,j=mHeartPaintWidth+20;i<mHeartBeatWidth;i++,j++){
        if(mDefaultYPostion[i]==-1)
            continue;
        else
            canvas.drawPoint(j, mTotalHeight/2-mDefaultYPostion[i], mHeartBeatPaint);
    }
}

恩,基本就这样了,模拟了下心率动画,再调整振幅,以及偏移基本就模拟完成了。

可以分开做两个view,然后叠加两个view,或者把这两个做到一起,我是把两个写在一个view中了,然后命名有点乱,搞混了变量,调试了半天。

3. 优化

有一个比较严重的问题是,当心率图完全出现之后,明显感觉有卡顿,不如刚出现的那一段流畅。
打开GPU绘制数据显示,跟手环对比发现:
duibi
duibitu

想想到底是哪里出了问题,注释掉圆环动画代码,先从心率线动画找起。如下图所示:
_point_

发现画心率图动画已经严重超标。回头看代码,每次仅仅只是遍历一遍坐标数组,画point上去,这块没法再优化了,那么应该是连续drawPoint绘制效率太低,改为drawPath:

private void resetPath(){
    path.reset();
    path.moveTo(mHeartPaintWidth+20, mTotalHeight/2-mOriginalYPositon[mOffset]);
    int interval = mHeartBeatWidth - mOffset;
    for(int i=mOffset+1,j=mHeartPaintWidth+20;i<mHeartBeatWidth;i++,j++){
        path.lineTo(j, mTotalHeight/2-mOriginalYPositon[i]);
    }
    for(int i=0,j=interval+mHeartPaintWidth+20;i<mOffset;i++,j++){
        path.lineTo(j, mTotalHeight/2-mOriginalYPositon[i]);
    }
}

如下图所示:

drawLine_

左边是调用resetPath之后的效果,右边是对drawPath优化之后的效果,也就是把连续的0通过drawLine先画出来,然后对于正弦曲线通过drawPath画,避免对于直线也用连续使用drawPath,效果其实差不多,稍微好一点点,至少大部分已经在标准线以下了。

不过跟手环对比还是差好多,想想是不是可以把采样率降低一点,在测试机中view宽度为460px,默认采样率为460,降低一半,画path的时候默认将距离*2,效果如下:
_

貌似好一点点,不过观察几个周期发现有时候峰值也会突然抖动一下,可能跟系统有关系,对比之后发现效果其实差不多。

然后将全部动画开启发现又不正常了,如下图,
_

这尼玛不太科学啊,那么只剩下圆弧了。看看单独画圆弧是什么样子:
_

从左到右依次是,完整圆弧动画,背景绘制,白色圆弧动画3个场景的数据,发现绘制背景的时候已经严重超标了,那么要消除这个就需要提供一个静态的png图片了,白色圆弧动画绘制时也有将近一半超标,不过还没有想到好的优化方法,还请大神指点一下。

代码地址

最终的效果如下:
mi4

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值