上一篇自定义Drawable实现智能红鲤鱼动画(上)我们画了一条可以摆动身体的小鱼。本文将分享如何让小鱼游到手指点击的地方。主要使用的技巧如下:1)、三阶贝塞尔曲线2)、Path'sMeasure1、动画分析小鱼的行走不是简单的位移。前进方向变化,所以本文将解决以下两部分:1)、鱼体位移2)、鱼体旋转3)、点击时的水波纹2、技术分析1)、鱼身的位移Part1在介绍自定义Drawable时,分析到Drawable需要作为ImageView的drawable资源展示,或者作为View的背景,那么我们可以通过将自定义Drawable与ImageView关联起来ImageView.setImageDrawable(),通过移动鱼来移动小图视图。为了让鱼游动的轨迹更加逼真,位移路径中只有一条直线是不够的。当鱼需要掉头时,行走的路线应该是弯曲的曲线。只要涉及到曲线,就少不了贝塞尔曲线。说到贝塞尔曲线就会涉及到贝塞尔曲线控制点的确定。这里我们将重点放在控制点的确定上。三阶贝塞尔曲线的确定过程。关键点在上图中简单标出。控制点确定过程如下:1):利用头部圆心、鱼体重心、点击点坐标,唯一确定一个特征三角形。2):判断鱼身需要左转还是右转,这是一个很关键的问题。我们知道对于同一个目的地,右转或者右转都可以到达,但是一定有一个完美的解决方案。假设我们的小鱼有鱼智商,转45°肯定是不可能达到的,可以转315°。结合这个理论和1)的特征三角形,我们可以知道三角形的内角AOB就是我们想要的旋转角度。如果我们知道了旋转的角度,那么旋转的方向自然也就知道了。现在我们只有AOB三个点的坐标,怎么求角度呢?我们可以用矢量角度公式来计算角度cosAOB=(OA*OB)/(|OA|*|OB|)其中OA*OB是一个矢量的量积,计算过程如下OA=(Ax-Ox,Ay-Oy)OB=(Bx-Ox,By-Oy)OAOB=(Ax-Ox)(Bx-Ox)+(Ay-Oy)*(By-Oy)Oy)|OA|表示线段OA的模数就是OA的长度。对向量不太了解的请自行百度。3):知道是左转还是右转,就可以确定曲线的控制点。上图中的控制点是我根据经验和多次实践确定的比较好的方案。第一个控制点是头部的中心,第二个控制点是旋转方向1/2上的点。上述控制点确定后,可以通过A点、A点、C点和M点确定三阶贝塞尔曲线。4):那么问题来了,我们如何得到移动ImageView的贝塞尔曲线呢?我们经常看到各大直播平台给主播送礼,小礼物是如何不规律上涨的?原理都差不多,无非就是让控件跟着路径走。传统的方法是使用自定义估计器来计算动画行走路径。还有另一种方法不需要自定义估算器。LOLLIPOP版本发布后,在属性动画中加入了新的路径动画。我们只需要丢入一个控件和一个路径和模板参数就可以让控件沿着这条路径走,方法如下ObjectAnimatoranimator=ObjectAnimator.ofFloat(ivFish,"x","y",path);需要明确的是这里的位移只是平移,也就是说鱼的角度不会因为控件的旋转而改变。如果想让鱼在转弯的时候沿着路径的切线方向转弯,请听我继续分析。是的,数学中的切线和导数是联系在一起的。在第一个版本中,我通过自定义估算器确定了路径。在自定义estimator的时候,我可以找到当前时刻三阶贝塞尔曲线的导数。那是一个痛苦的过程,公式代码写了十几行,效果并不好。后来,我们发现了一个强大的类PathMeasure,我们可以通过getLength()计算出一条Path的总长度,还可以计算出路径中某一点的坐标和切线方向,简直就是为我们量身定做。参数distance是切线的点到我们需要计算的Path的起点的距离。通过在AnimatorUpdateListener中获取Animator当前的进度,乘以路径的总长度,就得到了当前动画走过的路径的距离。接下来通过Enter两个长度>=2的非空数组pos和tan数据,得到坐标和切角的相关参数。pos数组的前两个值是x和y的坐标值。tan的前两个值是角度的对边和邻边的相对长度值(也可能是绝对长度,因为看不到原生源码,但是不管是Relative还是Relative)absolute,这两个值的比值已知,就可以算出对应的角度)3),点击时的水波纹效果比较简单,改变圆环的大小和透明度即可,代码部分会详细说明。分析完位移和旋转,大家做个效果图就更清楚了。为了让大家更清楚的看到效果,我把ImageView的背景设置为蓝色。可以看出蓝色的ImageView只负责平移不负责旋转。旋转效果由Drawable中的小鱼完成。ImageView的翻译3.代码实现文中只贴出了主要代码,文末提供了最重要的特征三角角度计算代码的链接。注:1)、变量abc是向量ab和ac的乘积2)、angleCos是弧度值表示的,真正的角度需要通过Math.toDegrees/***转成角度制使用角度计算角度的向量公式*cosBAC=(AB*AC)/(|AB|*|AC|)*其中AB*AC是向量AB=(Bx-Ax,By-Ay)AC的量积=(Cx-Ax,Cy-Ay),AB*AC=(Bx-Ax)*(Cx-Ax)+(By-Ay)*(Cy-Ay)**@paramcenter顶点A*@paramhead点1B*@paramtouchpoint2C*@return*/publicstaticfloatincludedAngle(PointFcenter,PointFhead,PointFtouch){floatabc=(head.x-center.x)*(touch.x-center.x)+(head.y-center.y)*(touch.y-center.y);floatangleCos=(float)(abc/((Math.sqrt((head.x-center.x)*(head.x-center.x)+(head.y-center.y)*(head.y-center.y)))*(Math.sqrt((touch.x-center.x)*(touch.x-center.x)+(touch.y-center.y)*(touch.y-center.y)))));floattemAngle=(float)Math.toDegrees(Math.acos(angleCos));//判断方向positive:leftnegative:right0:ontheline,butthe安卓坐标系Y是朝下的,所以左右颠倒x);//需要判断是同方向还是反方向if(direction==0){if(abc>=0){return0;}elsereturn180;}else{if(direction>0){//顺时针右边是负回报rn-temAngle;}else{returntemAngle;}}}三阶贝塞尔曲线生成代码其中:1)、fishMiddle为确定鱼体重心2)、fishHead获取鱼头圆心3),angle通过夹角计算方法求出特征三角形的夹角4),delta是鱼体的夹角,angle/2+delta可以得到鱼体夹角的中线与夹角特征三角形和x轴的正方向。以fishMiddle为起点,旋转长度为1.6R,旋转角度(angle/2+delta)可以通过calculatePoint()方法(Part1)计算出控制点的坐标。有了控制点,可以通过cubicTo函数得到三阶贝塞尔曲线。Pathpath=newPath();PointFfishMiddle=newPointF(ivFish.getX()+fishDrawable.getMiddlePoint().x,ivFish.getY()+fishDrawable.getMiddlePoint().y);PointFfishHead=newPointF(ivFish.getX()+fishDrawable.getHeadPoint().x,ivFish.getY()+fishDrawable.getHeadPoint().y);path.moveTo(ivFish.getX(),ivFish.getY());finalfloatangle=includedAngle(fishMiddle,fishHead,touch);floatdelta=calcultatAngle(fishMiddle,fishHead);PointFcontrolF=calculatPoint(fishMiddle,1.6f*fishDrawable.HEAD_RADIUS,angle/2+delta);path.cubicTo(fishHead.x,fishHead.y,controlF.x,controlF.y,touch.x-fishDrawable.getHeadPoint().x,touch.y-fishDrawable.getHeadPoint().y);鱼体旋转代码其中:1)、tan数组变量是我们访问切线值的两侧的信息数组,通过publicstaticnatived双atan2(双y,双x);得到切角的弧度值,转换成角度计算旋转角度细心的朋友发现Math.atan2(-tan[1],tan[0])中的y值前面加了一个负号"-",这是为了适应Android坐标Y的正方向与自然直角左侧的Y轴方向相反的情况。2)、因为我们不需要坐标点信息,getPosTan(floatdistance,floatpos[],floattan[])传入的pos数组为null3),在动画监控中获取实时角度anglecallback=(float)(Math.toDegrees(Math.atan2(-tan[1],tan[0])))finalfloat[]tan=newfloat[2];//设置为false表示不强制关闭PathfinalPathMeasurepathMeasure=newPathMeasure(path,false);animator=ObjectAnimator.ofFloat(ivFish,"x","y",path);animator.setDuration(2*1000);animator.setInterpolator(newAccelerateDecelerateInterpolator());animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){@OverridepublicvoidonAnimationUpdate(ValueAnimatoranimation){floatfraction=animation.getAnimatedFraction();pathMeasure.getPosTan(pathMeasure.getLength()*fraction,null,tan);floatangle=(float)(Math.toDegrees(Math.atan2)(-tan[1],tan[0])));fishDrawable.setMainAngle(angle);}});水波纹代码代码比较简单。需要注意的是ofFloat中的“radius”关键字,我们知道默认的属性动画关键字有“alpha”、“scaleX”、“scaleY”、“rotationX”、“rotationY”、“Y”等,仅没有“半径”关键字。是的,我们自己定义的。ObjectAnimator的ofFloat(Objecttarget,StringpropertyName,float...values)方法会通过反射技术在参数target中找到关键字对应的set方法,即我们需要在“this”类,其中参数为我们定义的浮点数0~1中的过程值,通过setRadius方法改变水波纹的alpha和半径值,形成水波纹扩散和淡化效果rippleAnimator=ObjectAnimator.ofFloat(this,"radius",0f,1f).setDuration(1000);publicvoidsetRadius(floatcurrentValue){alpha=(int)(100*(1-currentValue)/2);radius=DEFAULT_RADIUS*currentValue;invalidate();}***需要注意的是,上面的代码是写在继承RelativeLayout的自定义ViewGroup。ViewGroup中onDraw的触发和View中的不同。绘制前需要写一句setWillNotDraw(false)开启强制绘制功能,否则不会显示水波纹。动画是一个非常灵活的东西。其实你可以找不同的思路来实现。本文对小鱼的旋转并不完美,但我还没有找到更好的旋转方法。希望有更好想法的朋友多交流。.上一篇文章地址:自定义Drawable实现智能红鲤鱼动画(上)github地址:Fish_2文章补丁(2017-07-1812:27:15):低版本(5.0以下)问题,谢谢@八阿哥_带来up低版本手机死机的问题。前面说过,属性动画中的路径动画是5.0以后才支持的。看来要回避的问题还是要解决的。我挖出了我很久以前写的版本,使用自定义估算器。估价师的作用正如其名“估价”。用最通俗的语言来说,就是估计动画当前时刻的值。虽然是预估,但计算出的数据并非凭空捏造,无法控制,因此需要对其进行评估。局限性,有三个重要的问题我们需要了解:1),估计值是什么类型2),值的取值范围3),用什么规则来估计,我们先贴代码再分析importandroid。一部一部的动画。TypeEvaluator;importandroid.graphics.PointF;publicclassBezierEvaluatorimplementsTypeEvaluator
