请选择 进入手机版 | 继续访问电脑版

默认
打赏 发表评论 4
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
腾讯团队分享:手机QQ中的人脸识别酷炫动画效果实现详解
微信扫一扫关注!

1、前言


有图有真相,先来看下手机QQ中的运行效果吧:
1.gif

看到这么酷炫的效果图,不得不赞叹一下我们的设计师。然而,站在程序员的角度上看,除了酷炫之外更多的是复杂。但是,上面我们所看到的还只是最简单的一种形态而已,更加复杂的情况是当存在多个人脸的时候进行主次脸动画的切换,摄像头移动的时候动画的追踪,多个动画的之间的时序控制等问题。总之,UI展示加上各种业务逻辑使得这个动画变得异常复杂。

今天我们要讲解的是剔除业务逻辑之外的单纯UI动画效果上的实现,希望能给你带来启发。

2、为什么是SurfaceView


选择一种方案的同时要给出为什么不选择另一种的理由是什么。

没错,为什么这里不用自定义View来完成绘图呢?既然自定义View也可以实现一般的动画效果,为什么还要引入SurfaceView呢?可以把View理解为一个经过系统优化的,可以用来高效执行一些帧数比较低动画的对象,但是对于灵活性更高的动画来说,View并不是最好的选择。

同时,对于普通的View它们都是在应用程序的主线程中进行绘制的,我们知道在Android系统上我们不能够在主线程做一些耗时的操作,否则会引起ANR。对于一些游戏画面,或者摄像头预览、视频播放来说,它们的UI都比较复杂,而且要求能够进行高效的绘制。因此,它们的UI就不适合在应用程序的主线程中进行绘制。这时候就必须要给那些需要复杂而高效UI的视图生成一个独立的绘图表面,以及使用一个独立的线程来绘制这些视图的UI,所以SurfaceView华丽登场了。

SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面。由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。

SurfaceView 一般与SurfaceHolder.Callback配合使用,需要重写以下三个方法:
@Override
public void surfaceCreated(SurfaceHolder holder) {
}    
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}    
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}

我们可以通过SurfaceView的getHolder()函数可以获取SurfaceHolder对象,然后在子线程中通过 mHolder.lockCanvas()来获得 canvas,绘制完毕之后调用mHolder.unlockCanvasAndPost(canvas)来释放canvas,并把绘制的内容显示出来。

3、你所知道的ObjectAnimator


所谓属性动画:改变一切能改变的对象的属性值。

ObjectAnimator内部的工作机制是通过寻找特定属性的get和set方法,然后通过方法不断地对值进行改变,从而实现动画效果的。ObjectAnimator提供了ofInt、ofFloat、ofObject等几个方法来对属性进行插值操作,这几个方法都是设置动画作用的元素、作用的属性、动画开始、结束、以及中间的任意个属性值。动画更新的过程中,会不断调用setPropName更新元素的属性,所以使用ObjectAnimator更新某个属性,必须得有setter方法。

ObjectAnimator经常配合AnimatorSet进行使用:
if (null == mBCRotateAnimator1) {
    mBCRotateAnimator1 = ObjectAnimator.ofFloat(this, "bCRotate", 0f, 360f);
    mBCRotateAnimator1.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator1.setDuration(866);
}        
if (null == mBCRotateAnimator2) {
    mBCRotateAnimator2 = ObjectAnimator.ofFloat(this, "bCRotate", 360f, 720f);
    mBCRotateAnimator2.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator2.setDuration(334);
}        
if (null == mBCRotateAnimator3) {
    mBCRotateAnimator3 = ObjectAnimator.ofFloat(this, "bCRotate", 720f, 360f);
    mBCRotateAnimator3.setInterpolator(new LinearInterpolator());
    mBCRotateAnimator3.setDuration(12000);
    mBCRotateAnimator3.setRepeatCount(ValueAnimator.INFINITE);
}        
if (null == mBCRotateAnimatorSet) {
    mBCRotateAnimatorSet = new AnimatorSet();
    ArrayList<Animator> animatorArrayList = new ArrayList<Animator>();
    animatorArrayList.add(mBCRotateAnimator1);
    animatorArrayList.add(mBCRotateAnimator2);
    animatorArrayList.add(mBCRotateAnimator3);
    mBCRotateAnimatorSet.playSequentially(animatorArrayList);
    mBCRotateAnimatorSet.setStartDelay(800);
}

4、实用又强大的三角函数


大家有没有注意到转圈动画里面有一对小三角形呢?再细心点你会发现,这是个正三角形,并且其中的一个顶点正对着大圆的圆心,然后随着大圆一起转动,有木有?标题也说了,这里所有的元素都是自绘的,所以这两个三角形它不是设计同学给的icon资源,而是在Canvas上面绘制出来的。

那么这里问题就来了:

  • 如何确定三角形所在的位置?(总不能把它画到圆外面去吧)
  • 如何确定三角形三个顶点的位置?(总不能把它画歪了吧)

2.jpg

这里先抛一下数学思路,后面会进行更详细的讲解:
//绘制三角形
/**
 * 数学问题:已知圆心(a, b),半径r,和角度m,求圆周上点的坐标?
 * 解:假设圆心的坐标为(a, b),那么圆的方程是(x-a)^2+(y-b)^2=r^2
 * 根据方程可以求出圆上的各点坐标
 * 又已知角度m,则圆上点的坐标分别是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b)
 *
 * 拓展:已知正三角形中心点的坐标和其外接圆的半径,求其他三个顶点的坐标?
 * 解:可画出该三角形的外接圆,然后可把问题转化为求圆上三个点的坐标,又因为是正三角形,所以每个点的角度已知。解法同上。
 */

5、人脸识别动画完全解析


所有的动画元素可以分解为以下几种,这里我们主要讲解第一种——扫描控件,因为这个是难度最大的。

先来粗略看下扫描控件的设计稿(这还不是全部的,一共有好几张,看不清的同学可以放大来看):
3.jpg

4.jpg

5.jpg

6.jpg

转圈动画是整个动画的核心。我们先来看下,其实它是分了好几层圆圈:圆弧、圆环、圆点,然后组合在一起根据各自不同的方向和速率进行转动,同时还伴随着alpha值的淡入淡出以及scale值的放大缩小,还有颜色的改变。so,几乎平时接触到的动画里所有能改变的元素都掺和了进来:alpha、scale、rotate、color、shadow、speed、direction。

重点来分析下那个大蓝圈(暂且叫它为大蓝圈):由两个半圆弧组成,颜色为渐变蓝(最后变为白色),中间夹着对称的两个三角形,然后在不同的时间段里以不同的速率进行旋转和缩放。嘿嘿,画圆弧是件很简单的事情,调用canvas.drawArc()方法就可以了,圆弧的起始结束角度设计稿都有给了。这里的难点是在圆弧的不同部位绘制出渐变蓝色以及阴影效果。对于渐变颜色的填充,我们可以使用API提供的 来实现,参数填入圆心的坐标、颜色数组、颜色的比例,最后调用Paint.setShader()方法即可。
public SweepGradient(float cx, float cy, int colors[], float positions[]) {}

int[] colors = new int[]{Color.argb(255, 0x1E, 0xFF, 0xEC), Color.argb(255, 0x00, 0xBF, 0xFF)}; //蓝圈的渐变颜色float[] positions = new float[]{0f, 0.5f}; //渐变颜色占的位置Matrix matrix = new Matrix();
SweepGradient sweepGradient = new SweepGradient(scanningData.centerX, scanningData.centerY, colors, positions);
matrix.setRotate(scanningData.bCRightAngleStart+scanningData.bCRotate - 5, scanningData.centerX, scanningData.centerY);//这里居然需要减5°来矫正?sweepGradient.setLocalMatrix(matrix);
scanningData.mBCPaint.setShader(sweepGradient);

阴影效果可以调用API提供的 来实现,参数填入阴影半径以及Blur type。最后调用Paint.setMaskFilter()方法即可。
public BlurMaskFilter(float radius, Blur style) {}

现在来看下大蓝圈上的三角形如何绘制出来:我们只分析左上角那个,右下角那个处于对称位置。以顺时针X轴正方向为0°角,那么根据设计稿的初始状态,可计算出左上角三角形的初始角度是位于225°左右,右下角三角形的初始角度是45°左右。这里我们的最终目标是求出三角形三个顶点的坐标,然后用线条连起来使其成为一个三角形,但是根据这些条件我们直接计算三角形的顶点坐标是做不到的。现在我们来分解下:先根据三角函数求出三角形的中心坐标,然后又由于这个是正三角形,再根据三角函数求出各个顶点的坐标。

由于三角形是在圆周上,假设圆心(a, b),半径r,和三角形所在的角度m,其实这几个变量都是知道的,圆心坐标(a,b)则是人脸的中心点,可以通过人脸识别后的矩形坐标返回,半径r则是设计稿给的初始半径,角度m就是刚刚我们计算出来的225°,那么根据三角函数可得该三角形的圆心坐标则是 x = rcos(mMath.PI/180)+a, y = rsin(mMath.PI/180)+b。

下一步计算三角形三个顶点的坐标:思路还是一样的,画出该三角形的外接圆,三角形的中心坐标即是外接圆的圆心,问题可转化为求外接圆上三个点的坐标,是不是又回到了上面的求解过程?是的。我们已知了这个外接圆的半径(设计稿给出),圆心坐标,现在要知道的是三角形三个顶点的角度,然后我们就可以分别算出它们的坐标了。我们知道这是个正三角形,而且其中一个顶点指向大圆圈的圆心,暂且把这个顶点命名为P。画出三角形的外接圆,即顶点P相对于外接圆的位置就是右下角那个三角形相对于大蓝圈的位置,因为这两个三角形是对称的,顶点是相对的。也就是说顶点P相对于外接圆的角度是45°。

如图:
7.png

那么这样就好办了,第一个顶点的角度为45°,第二个顶点的角度为45°+120° = 165°,第三个顶点的坐标为165°+120°=285°,再次运用三角函数可求得三个顶点的坐标,然后调用Path.moveTo(),Path.lineTo(),Path.close()等方法把三角形的路径描述出来,最后调用Canvas.drawPath()把三角形绘制出来,三角形的绘制过程就到此结束了。
//绘制三角形
/**
 * 数学问题:已知圆心(a, b),半径r,和角度m,求圆周上点的坐标?
 * 解:假设圆心的坐标为(a, b),那么圆的方程是(x-a)^2+(y-b)^2=r^2
 * 根据方程可以求出圆上的各点坐标
 * 又已知角度m,则圆上点的坐标分别是(r*cos(m*Math.PI/180)+a, r*sin(m*Math.PI/180)+b)
 *
 * 拓展:已知正三角形中心点的坐标和其外接圆的半径,求其他三个顶点的坐标?
 * 解:可画出该三角形的外接圆,然后可把问题转化为求圆上三个点的坐标,又因为是正三角形,所以每个点的角度已知。解法同上。
 */
scanningData.bTLeftTopX = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.cos((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerX);
scanningData.bTLeftTopY = (float)((scanningData.bCRadius * scanningData.bCScale) * Math.sin((scanningData.bTLeftTopAngle + scanningData.bCRotate) * Math.PI / 180) + scanningData.centerY);
float tempAngle1 = scanningData.bTRightBottomAngle + scanningData.bCRotate; //取对称的角度,因为这是在三角形小圆里面的角度,不是大圆的角度
float tempAngle2 = tempAngle1 + 120f; //正三角形,所以加120°
float tempAngle3 = tempAngle2 + 120f;

if (tempAngle1 >= 360) {
    tempAngle1 = tempAngle1 - 360;}  
if (tempAngle2 >= 360) {
    tempAngle2 = tempAngle2 - 360;}  
if (tempAngle3 >= 360) {
    tempAngle3 = tempAngle3 - 360;}  

//问题转化,求三角形三个顶点的坐标(看上面的数学解析)
float vertex1_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopX); 
float vertex1_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180) + scanningData.bTLeftTopY);  
float vertex2_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopX);  
float vertex2_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180) + scanningData.bTLeftTopY);  
float vertex3_x = (float) (scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopX);  
float vertex3_y = (float) (scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180) + scanningData.bTLeftTopY);  

Path path = new Path();
path.moveTo(vertex1_x, vertex1_y);
path.lineTo(vertex2_x, vertex2_y);
path.lineTo(vertex3_x, vertex3_y);
path.close();

scanningData.mBTPaint.setColor(scanningData.bTLeftTopColor);  
scanningData.mBTPaint.setAlpha((int) (scanningData.bTAlpha * 255));
canvas.drawPath(path, scanningData.mBTPaint);

好了,最难的一块绘制完了,其他的外圈,内圈,透明圈等等也都问题不大了。嗯,接下来,我们应该让它动起来。为了让动画旋转起来,我们可以用一个变量rotate运用到ObjectAnimator上面,然后在绘制的时候加上rotate的变化值即可。scale、alpha,等等都一样,需要在绘制的时候把这些值给加上去。
if (null == mBCScaleAnimator1) {
    mBCScaleAnimator1 = ObjectAnimator.ofFloat(this, "bCScale", 0f, 1.04f);
    mBCScaleAnimator1.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator1.setDuration(200);
}
if (null == mBCScaleAnimator2) {
    mBCScaleAnimator2 = ObjectAnimator.ofFloat(this, "bCScale", 1.04f, 1f);
    mBCScaleAnimator2.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator2.setDuration(66);
}
if (null == mBCScaleAnimator3) {
    mBCScaleAnimator3 = ObjectAnimator.ofFloat(this, "bCScale", 1f, 1.02f);
    mBCScaleAnimator3.setInterpolator(new LinearInterpolator());
    mBCScaleAnimator3.setDuration(66);
}

scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle1 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle2 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.cos(tempAngle3 * Math.PI / 180)
scanningData.bTRadius * scanningData.bCScale * Math.sin(tempAngle3 * Math.PI / 180)

人物名称的绘制以及指示线条以及下面的资料浮窗就不说了,调用Canvas.drawLine(),Canvas.drawText(),Canvas.drawCircle()等等就可以实现(其实线条的位置以及角度也需要运用三角函数进行计算)。

下面简单分析下右边小图片的波纹效果:
8.jpg

其实这里又有一个需要自绘的三角形,有没有感觉到崩溃?没关系,还是按照我们上面的套路,改变下初始角度就可以了,算法在手,三角形我有!其实波纹效果的绘制也比较简单,调用Canvas.drawCircle(),然后通过ObjectAnimator不断地去改变圆圈的alpha值和scale值,从而达到波纹的效果。

见如下代码:
if (null == mFPOutCircleAlphaAnimator) {
    mFPOutCircleAlphaAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleAlpha", 1f, 0f, 0f);
    mFPOutCircleAlphaAnimator.setInterpolator(new LinearInterpolator());
    mFPOutCircleAlphaAnimator.setDuration(1500);
    mFPOutCircleAlphaAnimator.setRepeatCount(ValueAnimator.INFINITE);
}
if (null == mFPOutCircleAlphaAnimatorSet) {
    mFPOutCircleAlphaAnimatorSet = new AnimatorSet();
    mFPOutCircleAlphaAnimatorSet.play(mFPOutCircleAlphaAnimator);
    mFPOutCircleAlphaAnimatorSet.setStartDelay(2200);
}
if (null == mFPOutCircleScaleAnimator) {
     mFPOutCircleScaleAnimator = ObjectAnimator.ofFloat(this, "fPOutCircleScale", 1f, 1.3f, 1.3f);
     mFPOutCircleScaleAnimator.setInterpolator(new LinearInterpolator());
     mFPOutCircleScaleAnimator.setRepeatCount(ValueAnimator.INFINITE);
     mFPOutCircleScaleAnimator.setDuration(1500);
}
if (null == mFPOutCircleScaleAnimatorSet) {
      mFPOutCircleScaleAnimatorSet = new AnimatorSet();
      mFPOutCircleScaleAnimatorSet.play(mFPOutCircleScaleAnimator);
      mFPOutCircleScaleAnimatorSet.setStartDelay(2200);
}

scanningData.mFPOutCirclePaint.setAlpha((int)(scanningData.fPOutCircleAlpha * 255 * 0.5));
canvas.drawCircle(scanningData.fPCenterX, scanningData.fPCenterY, scanningData.fPOutCircleRadius * scanningData.fPOutCircleScale, scanningData.mFPOutCirclePaint);

到此,整个动画绘制完毕。

(原文链接:点此进入,来自QQ空间开发团队)

附录:微信、QQ原创文章汇总


[1] 有关QQ、微信的技术文章:
腾讯团队分享:手机QQ中的人脸识别酷炫动画效果实现详解
腾讯团队分享 :一次手Q聊天界面中图片显示bug的追踪过程分享
微信团队分享:微信Android版小视频编码填过的那些坑
微信手机端的本地数据全文检索优化之路
企业微信客户端中组织架构数据的同步更新方案优化实战
微信团队披露:微信界面卡死超级bug“15。。。。”的来龙去脉
QQ 18年:解密8亿月活的QQ后台服务接口隔离技术
月活8.89亿的超级IM微信是如何进行Android端兼容测试的
以手机QQ为例探讨移动端IM中的“轻应用”
一篇文章get微信开源移动端数据库组件WCDB的一切!
微信客户端团队负责人技术访谈:如何着手客户端性能监控和优化
微信后台基于时间序的海量数据冷热分级架构设计实践
微信团队原创分享:Android版微信的臃肿之困与模块化实践之路
微信后台团队:微信后台异步消息队列的优化升级实践分享
微信团队原创分享:微信客户端SQLite数据库损坏修复实践
腾讯原创分享(一):如何大幅提升移动网络下手机QQ的图片传输速度和成功率
腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(下篇)
腾讯原创分享(二):如何大幅压缩移动网络下APP的流量消耗(上篇)
微信Mars:微信内部正在使用的网络层封装库,即将开源
如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源
开源libco库:单机千万连接、支撑微信8亿用户的后台框架基石 [源码下载]
微信新一代通信安全解决方案:基于TLS1.3的MMTLS详解
微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
Android版微信从300KB到30MB的技术演进(PPT讲稿) [附件下载]
微信团队原创分享:Android版微信从300KB到30MB的技术演进
微信技术总监谈架构:微信之道——大道至简(演讲全文)
微信技术总监谈架构:微信之道——大道至简(PPT讲稿) [附件下载]
如何解读《微信技术总监谈架构:微信之道——大道至简》
微信海量用户背后的后台系统存储架构(视频+PPT) [附件下载]
微信异步化改造实践:8亿月活、单机千万连接背后的后台解决方案
微信朋友圈海量技术之道PPT [附件下载]
微信对网络影响的技术试验及分析(论文全文)
一份微信后台技术架构的总结性笔记
架构之道:3个程序员成就微信朋友圈日均10亿发布量[有视频]
快速裂变:见证微信强大后台架构从0到1的演进历程(一)
快速裂变:见证微信强大后台架构从0到1的演进历程(二)
微信团队原创分享:Android内存泄漏监控和优化技巧总结
全面总结iOS版微信升级iOS9遇到的各种“坑”
微信团队原创资源混淆工具:让你的APK立减1M
微信团队原创Android资源混淆工具:AndResGuard [有源码]
Android版微信安装包“减肥”实战记录
iOS版微信安装包“减肥”实战记录
移动端IM实践:iOS版微信界面卡顿监测方案
微信“红包照片”背后的技术难题
移动端IM实践:iOS版微信小视频功能技术方案实录
移动端IM实践:Android版微信如何大幅提升交互性能(一)
移动端IM实践:Android版微信如何大幅提升交互性能(二)
移动端IM实践:实现Android版微信的智能心跳机制
移动端IM实践:WhatsApp、Line、微信的心跳策略分析
移动端IM实践:谷歌消息推送服务(GCM)研究(来自微信)
移动端IM实践:iOS版微信的多设备字体适配方案探讨
信鸽团队原创:一起走过 iOS10 上消息推送(APNS)的坑
腾讯信鸽技术分享:百亿级实时消息推送的实战经验
>> 更多同类文章 ……

[2] 有关QQ、微信的技术故事:
腾讯开发微信花了多少钱?技术难度真这么大?难在哪?
技术往事:创业初期的腾讯——16年前的冬天,谁动了马化腾的代码
技术往事:史上最全QQ图标变迁过程,追寻IM巨人的演进历史
技术往事:“QQ群”和“微信红包”是怎么来的?
开发往事:深度讲述2010到2015,微信一路风雨的背后
开发往事:微信千年不变的那张闪屏图片的由来
开发往事:记录微信3.0版背后的故事(距微信1.0发布9个月时)
一个微信实习生自述:我眼中的微信开发团队
首次揭秘:QQ实时视频聊天背后的神秘组织
>> 更多同类文章 ……

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:腾讯团队分享:一次手Q聊天界面中图片显示bug的追踪过程分享下一篇:求教关于 IM 应用的服务器性能和带宽的选择

本帖已收录至以下技术专辑

推荐方案
评论 4
手机QQ老是做这种华而不实的效果,然而有什么卵用,能跟微信一下至少用起来不卡吗?
签名: 该会员没有填写今日想说内容.
引用:vincentfutux 发表于 2017-11-06 21:48
手机QQ老是做这种华而不实的效果,然而有什么卵用,能跟微信一下至少用起来不卡吗?

哈哈,手q的产品经理可能真是脑子有水。。。
厉害厉害了
感谢分享
签名: 记录
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部