[转]自定义Drawable实现灵动的红鲤鱼动画(下篇)

 
上篇文章自定义Drawable实现灵动的红鲤鱼动画(上篇)我们绘制了可以摆动身体的小鱼,本篇就分享一下如何让小鱼游到手指点击的位置。用到的主要技术如下:
1)、三阶贝塞尔曲线
2)、Path的Measure
一、动画分析
小鱼的行走不是简单的位移,不难看出在小鱼位移的同时身体的角度还随着前进的方向而变化,所以本篇要解决如下两部分:
1)、鱼身的位移
2)、鱼身的旋转
3)、点击处的水波纹
二、技术分析
1)、鱼身的位移
上篇介绍自定义Drawable的时候分析了Drawable需要作为ImageView的drawable资源或者作为View的background才可以显示出来,那么我们就可以通过ImageView.setImageDrawable()将自定义Drawable和ImageView关联起来,通过位移ImageView来移动小鱼。
  为了让鱼游动的轨迹更真实,位移路径只有直线是不行的,在鱼需要转身的时候行走路线应该是有弧度的曲线 ,只要涉及到曲线就少不了贝塞尔曲线,涉及到贝塞尔曲线就要涉及到贝塞尔曲线控制点的确定,这里重点介绍一下控制点的确定问题

上图对关键点都做了简单标注,控制点确定过程如下:
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)
|OA|表示线段OA的模即OA的长度,如果对向量不太了解的朋友请自行百度一下。
3):知道了向左转还是向右转就可以确定曲线的控制点,上图控制点是我凭经验和多次实践确定的比较好的方案第一个控制点就是头部的圆心处,第二个控制点就是转动方向的1/2上的一点
好了,上述的控制点确定之后就可以实用A点、A点、C点、M点来确定一条三阶贝塞尔曲线了
4):那么问题来了我们拿到贝塞尔曲线如何让
ImageView移动呢?我们经常看到各大直播平台送主播礼物时那些小礼物不规则地向上升是怎么实现呢?原理都差不多,无非就是让控件跟着路径走。传统的做法是利用自定义估值器来计算出动画行走路径,还有一种方法可以不用自定义估值器,LOLLIPOP版本出来之后属性动画里新增了一个路径动画,我们只用丢进去一个控件和一条路径以及模板参数就能让控件跟着这个路径走,方法如下
        ObjectAnimator animator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
需要明确一点,这里的位移只是平移,也就是说鱼的角度不会因为控件转动而改变,要想让鱼在转弯的时候沿路径切线方向转动请听我继续分析.
2)、鱼身的旋转
计算鱼身的旋转角度只用计算出路径切线方向即可,数学里的切线和导数是挂钩的,初代版本我是通过自定义估值器来确定路径的,自定义估值器的时候可以求出当前时刻三阶贝塞尔曲线的导数,那是一个痛苦的过程,公式代码写了十几行,而且效果不好。后来发现一个强大的类PathMeasure,我们可以通过
getLength()
计算出一条Path的总长度,还可以通过
getPosTan(float distance, float pos[], float tan[])  
根据传入的长度计算出路径的某点坐标和切线方向,简直就是为我们量身定做的。其中参数distance就是我们需要计算切线的点距Path的起点的距离,通过在AnimatorUpdateListener中获取Animator的当前进度,再与路径总长度相乘,就得到了当前动画已行走的路径长度distance,接下来传入两个长度>=2的非空数组pos和tan数据就可以得到坐标和切线角度的相关参数了。
pos数组的前两个值就是x,y的坐标值
tan前两个值就是所求角的对边和临边的相对长度值(也有可能是绝对长度,因为无法看到native源码,但是不管是相对的还是绝对的这两个值的比例知道就可以求出对应的角度了)
3)、点击处的水波纹
水波纹效果比较简单,只需改变圆环的大小和透明度即可,代码部分会详细说明。
分析完位移和旋转,做一个效果图看看大家就更清楚了。为了让大家更清晰地看出效果我把ImageView背景设置成蓝色,可以看出蓝色的ImageView只负责平移并没有旋转,旋转效果是Drawable中的小鱼执行的。

三、代码实现
文章只贴出主要代码,完整代码文末提供链接
最重要的特征三角夹角计算代码
注意点:
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)
     *
     * @param center 顶点 A
     * @param head   点1  B
     * @param touch  点2  C
     * @return
     */
    public static float includedAngle(PointF center, PointF head, PointF touch) {
        float abc = (head.x - center.x) * (touch.x - center.x) + (head.y - center.y) * (touch.y - center.y);
        float angleCos = (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)))));
        float temAngle = (float) Math.toDegrees(Math.acos(angleCos));
        //判断方向  正:左侧  负:右侧 0:线上,但是Android的坐标系Y是朝下的,所以左右颠倒一下
        float direction = (center.x - touch.x) * (head.y - touch.y) - (center.y - touch.y) * (head.x - touch.x);
        //线上还要判断是同向还是逆向
        if (direction == 0) {
            if (abc >= 0) {
                return 0;
            } else
                return 180;
        } else {
            if (direction > 0) {//右侧顺时针为负
                return -temAngle;
            } else {
                return temAngle;
            }
        }
    }
三阶贝塞尔曲线生成代码
其中:
1)、fishMiddle 是确定鱼身重心
2)、fishHead获取鱼头圆心
3)、angle即通过夹角计算方法计算出特征三角形的夹角
4)、delta是鱼身的角度,angle/2+delta就可以得出特征三角形夹角中线跟x轴正方向的角度了
有了起点fishMiddle,转动的长度1.6R以及转动的角度(angle/2+delta)就可以通过(上篇)的calculatPoint()方法计算出控制点的坐标了,有了控制点就可以通过cubicTo函数得到三阶贝塞尔曲线了
        Path path = new Path();
        PointF fishMiddle = new PointF(ivFish.getX() + fishDrawable.getMiddlePoint().x, ivFish.getY() + fishDrawable.getMiddlePoint().y);
        PointF fishHead = new PointF(ivFish.getX() + fishDrawable.getHeadPoint().x, ivFish.getY() + fishDrawable.getHeadPoint().y);
        path.moveTo(ivFish.getX(), ivFish.getY());
        final float angle = includedAngle(fishMiddle, fishHead, touch);
        float delta = calcultatAngle(fishMiddle, fishHead);
        PointF controlF = 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数组变量就是我们存取正切值的两个边的信息数组,通过public static native double atan2(double y, double x);得到切角的弧度值,转换为角度即可算出转动角度。细心的朋友发现Math.atan2(-tan[1], tan[0])中的y值前边有一个负号“-”,这是为了适配Android坐标Y的正方向和自然直角左边系Y轴方向相反的情况。
2)、因为我们用不到坐标点信息所以在getPosTan(float distance, float pos[], float tan[])中传入的pos数组是null
3)、在动画监听回调中获取到实时角度angle = (float) (Math.toDegrees(Math.atan2(-tan[1], tan[0])))
        final float[] tan = new float[2];
        //设置为false代表不强制把Path闭合
        final PathMeasure pathMeasure = new PathMeasure(path, false);
        animator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
        animator.setDuration(2 * 1000);
        animator.setInterpolator(new AccelerateDecelerateInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                pathMeasure.getPosTan(pathMeasure.getLength() * fraction, null, tan);
                float angle = (float) (Math.toDegrees(Math.atan2(-tan[1], tan[0])));
                fishDrawable.setMainAngle(angle);
            }
        });
水波纹代码
代码比较简单,需要注意的是ofFloat中的“radius”关键字,我们知道默认的属性动画关键字有"alpha"、"scaleX"、"scaleY"、"rotationX"、"rotationY"、"Y"等等,唯独没有“radius”关键字,对的我们自己定义的,ObjectAnimator的ofFloat(Object target, String propertyName, float... values)方法会通过反射技术在参数target中寻找关键字对应的set方法,即我们需要在“this”类中定义一个setRadius(参数)方法,其中的参数是我们定义的浮点数0~1中的过程值,通过setRadius方法改变水波纹的alpha和半径值,形成水波纹扩散和渐隐的效果
rippleAnimator = ObjectAnimator.ofFloat(this, "radius", 0f, 1f).setDuration(1000);
public void setRadius(float currentValue) {
        alpha = (int) (100 * (1 - currentValue) / 2);
        radius = DEFAULT_RADIUS * currentValue;
        invalidate();
    }
最后需要注意一点如上代码都是写在一个继承了RelativeLayout的自定义ViewGroup中,ViewGroup中onDraw的触发和View中不一样,需要在绘制前写上一句setWillNotDraw(false)来打开强制绘制功能,否则水波纹无法显示。
四、结语
(上篇)得到了很多朋友的支持,非常感动,谢谢大家给予我的鼓励。 动画是个很灵活的事情其实大家可以找找不同的思路来实现,本篇小鱼的转动并不完美,但是我还没找到更好的转弯方法,希望有有更好思路的朋友多多交流。
上篇地址: 自定义Drawable实现灵动的红鲤鱼动画(上篇)
github地址:Fish_2  
csdn同步地址:[自定义Drawable实现灵动的红鲤鱼动画(下篇)
文章补丁(2017年7月18日12:27:15):低版本(5.0以下)的问题,谢谢@八阿哥_提出了低版本手机崩溃的问题。前文提到过属性动画中的路径动画是5.0之后才支持的,看来想逃避的问题还是要解决的。翻出了N久前写的版本,用的是自定义估值器。
  估值器的作用正如其名字"估值",用最通俗的语言描述就是估算出动画当前时刻的值,虽说是估算但是计算出的数据也不是凭空捏造不可控制的,所以我们要对它进行限制,有三个重要的问题需要我们搞明白:
1)、估算出的值是什么类型
2)、值的取值范围
3)、用什么规则去估算
我们先贴代码再一 一分析
import android.animation.TypeEvaluator;
import android.graphics.PointF;
public class BezierEvaluator implements TypeEvaluator<PointF> {
    //三阶贝塞尔曲线的两个控制点
    private PointF pointF1;
    private PointF pointF2;
    private FishView fishView;
    /**
     *
     * @param pointF1 控制点1
     * @param pointF2 控制点2
     * @param fishView
     */
    public BezierEvaluator(PointF pointF1, PointF pointF2, FishView fishView) {
        this.pointF1 = pointF1;
        this.pointF2 = pointF2;
        this.fishView = fishView;
    }
    /**
     * 三阶贝塞尔曲线
     *
     * @param time
     * @param startValue 运动开始点
     * @param endValue 运动结束点
     * @return
     */
    @Override
    public PointF evaluate(float time, PointF startValue, PointF endValue) {
        float timeLeft = 1.0f - time;
        float slopeX;
        float slopeY;
        float angle;
        PointF point = new PointF();// 结果
        point.x = timeLeft * timeLeft * timeLeft * (startValue.x) + 3
                * timeLeft * timeLeft * time * (pointF1.x) + 3 * timeLeft
                * time * time * (pointF2.x) + time * time * time * (endValue.x);
        point.y = timeLeft * timeLeft * timeLeft * (startValue.y) + 3
                * timeLeft * timeLeft * time * (pointF1.y) + 3 * timeLeft
                * time * time * (pointF2.y) + time * time * time * (endValue.y);
        //鱼儿身体角度
        slopeX = (-3 * startValue.x * timeLeft * timeLeft) +
                (3 * pointF1.x * timeLeft * timeLeft - 6 * pointF1.x * time * timeLeft) +
                (6 * pointF2.x * time * timeLeft - 3 * pointF2.x * time * time) +
                (3 * endValue.x * time * time);
        slopeY = (-3 * startValue.y * timeLeft * timeLeft) +
                (3 * pointF1.y * timeLeft * timeLeft - 6 * pointF1.y * time * timeLeft) +
                (6 * pointF2.y * time * timeLeft - 3 * pointF2.y * time * time) +
                (3 * endValue.y * time * time);
        angle = (float) (Math.atan2(slopeX, slopeY) * 180.0 / Math.PI);
        fishView.setMainAngle(angle-90);
        return point;
    }
}
自定义估值器思路十分简单,只用实现TypeEvaluator<T>接口复写public T evaluate(float fraction, T startValue, T endValue),其中泛型类型T就代表起始值以及返回的过程值类型,这里我们用的是PointF,即浮点型的坐标这就是我们要解决的第一个问题估算出的值是什么类型,startValue和endValue就是我们要解决的第二个问题值的取值范围,相对比较难解决的是第三个问题用什么规则去估算,这里我们的估算方案当然还是三阶贝塞尔曲线了,上公式:

B(t)即当前时刻的计算结果,P0、P1、P2、P3分别是起点、控制点1、控制点2、终点。这样大家是不是就可以理解我上边代码里那个point.x和point.y是如何计算的了吧。
代码里还有slopeX和slopeY,这是我对上述公式做了处理,分别让X坐标和Y坐标对时间t做了偏导(Tips:之所以不喜欢这种方法并不是因为需要用到数学知识,而是曲线切线角度只能在这里计算,计算之后还要通过持有小鱼的对象才能将角度结果赋值给小鱼),好了 估值器定义完毕,别急,差点忘记了,大部分没接触过自定义估值器的朋友可能要问:说了这么多,我只看到了两个控制点是构造函数的时候传递进来的,那这个startValue和endValue是哪里来的?接下来看看这个估值器是如何使用的,就豁然开朗了
        // 初始化贝塞尔估值器- - 传入两个控制点
        BezierEvaluator evaluator = new BezierEvaluator(listPoint.get(2),
                listPoint.get(3), fishView);
        //估值器  传入起点终点
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, listPoint.get(0),
                listPoint.get(1));
        animator.addUpdateListener(new BezierListenr(target));
                //绑定动画目标View
        animator.setTarget(target);
        animator.setDuration(animatorDuration);
核心代码已经提供了,有兴趣的朋友可以把原来的代码改造改造
补丁打完,完结撒花!
作者:Jics
链接:https://www.jianshu.com/p/54f78c38a0f0
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
[转]自定义Drawable实现灵动的红鲤鱼动画(下篇)的更多相关文章
- [转]自定义Drawable实现灵动的红鲤鱼动画(上篇)
		此篇中的小鱼动画是模仿国外一个大牛做的flash动画,第一眼就爱上它了,简约灵动又不失美学,于是抽空试着尝试了一下,如下是我用Android实现的效果图: 小鱼儿 由于整个绘制分析过程比较繁琐所以 ... 
- Canvas 实现灵动的红鲤鱼动画(上)
		前言 上一篇文章<Canvas 仿百度贴吧客户端 loading 小球>实现了百度贴吧客户端的 loading 小球效果,同时还留下了一个任务:实现灵动的红鲤鱼动画. 这个动画效果实现起来 ... 
- 自定义Drawable
		本文由 伯乐在线 - treesouth 翻译,toolate 校稿.未经许可,禁止转载! 英文出处:ryanharter.com.欢迎加入翻译小组. 我们看过一些博客文章,讲述了为什么要适时地使用自 ... 
- P4773 红鲤鱼与绿鲤鱼
		P4773 红鲤鱼与绿鲤鱼 暑假比赛的一个水题 总情况数:\(\dfrac{(a+b)!}{a!b!}\) 就是\(a+b\)条鲤鱼中选\(a\) or \(b\)的情况 反正我们会用完鲤鱼,则红鲤鱼 ... 
- Android APK开发 Drawable文件夹下的自定义Drawable文件
		版本:2018/2/11 Drawable的分类 自定义Drawable SVG矢量图 个人总结的知识点外,部分知识点选自<Android开发艺术探索>-第六章 Drawable 1.Dr ... 
- Android特效专辑(五)——自定义圆形头像和仿MIUI卸载动画—粒子爆炸
		Android特效专辑(五)--自定义圆形头像和仿MIUI卸载动画-粒子爆炸 好的,各位亲爱的朋友,今天讲的特效还是比较炫的,首先,我们会讲一个自定义圆形的imageView,接着,我们会来实现粒子爆 ... 
- Android自定义View 画弧形,文字,并增加动画效果
		一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类 B ... 
- Android 自定义Drawable
		1.使用BitmapShader实现图片圆角 public class CornerDrawable extends Drawable { private Paint mPaint; private ... 
- Android自定义drawable(Shape)详解
		在Android开发过程中,经常需要改变控件的默认样式, 那么通常会使用多个图片来解决.不过这种方式可能需要多个图片,比如一个按钮,需要点击时的式样图片,默认的式样图片. 这样就容易使apk变大. 那 ... 
随机推荐
- POJ 2356  Find a multiple 抽屉原理
			从POJ 2356来体会抽屉原理的妙用= =! 题意: 给你一个n,然后给你n个数,让你输出一个数或者多个数,让这些数的和能够组成n: 先输出一个数,代表有多少个数的和,然后再输出这些数: 题解: 首 ... 
- no device found for connection ‘ System eth0′
			解决办法: 1.删除/etc/udev/rules.d/70-persistent-net.rules文件,重启系统. 2.如果上面的不起作用,那么去看ifcfg-eth0文件中的HWADDR是否正确 ... 
- 三个实例演示 Java Thread Dump 日志分析(转)
			原文链接:http://www.cnblogs.com/zhengyun_ustc/archive/2013/01/06/dumpanalysis.html 转来当笔记^_^ jstack Dump ... 
- ubuntu 13.04 编译 安装 升级 gcc 4.9.0 address sanitizer
			@ 前记: 最近查一个线上项目的crash,review代码无果,crash几率低,不可在本地环境重现.之后在线上好几个服务器跑valgrind就不crash了.个人猜测可能是跑valgrind后性能 ... 
- Android证书有效性验证方案
			1.前言: 1.1.SSL劫持攻击: 目前虽然很多Android APP使用了https通信方式,但是只是简单的调用而已,并未对SSL证书有效性做验证.在攻击者看来,这种漏洞让htt ... 
- Latest SQLite binary for January 2015
			Latest SQLite binary for January 2015 Well I went through quite a few threads to find an updated, de ... 
- linux内核源码中常见宏定义
			http://blog.csdn.net/yangdelong/article/details/5508057 
- Java基础加强总结(二)——泛型
			一.体验泛型 JDK1.5之前的集合类中存在的问题——可以往集合中加入任意类型的对象,例如下面代码: package cn.gacl.generic.summary; import java.util ... 
- 线程系列06,通过CLR代码查看线程池及其线程
			在"线程系列04,传递数据给线程,线程命名,线程异常处理,线程池"中,我们已经知道,每个进程都有一个线程池.可以通过TPL,ThreadPool.QueueUserWorkItem ... 
- 用最简单的例子理解迭代器模式(Iterator Pattern)
			迭代器模式的需求来自:需要对一些集合进行迭代,而迭代的方式可能有很多种. 说到迭代,动作大致包括设置第一个位置,获取下一个位置元素,判断是否迭代结束,获取当前位置元素,大致就这么些.把这些迭代动作封装 ... 
