自定义drawable--画鱼游动的案例来学习自定义view的思想。

自定义Drawable。

自定义Drawable。跟自定义view的做法差别不多,使用简单,比自定义View成本低
非图片类的Drawable所占空间小,能减小apk大小,而且也不需要考虑测量和布局啥问题。
自定义Drawable就相当于一张图片,然后imagview设置进去就可以。

自定义Drawable 的基本方法重写。

public class TestDrawable extends Drawable {

    private Paint mPaint;
    public  TestDrawable(){
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setARGB(110, 244, 92, 71);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {

    }

    @Override
    public void setAlpha(int alpha) {
        mPaint.setAlpha(alpha);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    
    @Override
    public int getIntrinsicWidth() {
        return  100;
    }

    @Override
    public int getIntrinsicHeight() {
        return  100;
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一般在自定义drawable然后imageview放进去的时候,imageview都会是采用包含自身的大小。所以。

 @Override
    public int getIntrinsicWidth() {
        return  100;
    }

    @Override
    public int getIntrinsicHeight() {
        return  100;
    }

这两个函数就是表示图片多大,imageview多大。

下面我们来画一条游动的鱼,这是一个很好的典型例子,每次复习忘了就可以在复习一边,并不是叫你完全怎么画,而是知道,从0到1实现这个效果每一布的思维。学会这个例子后面很多做法都可以以这个思维做出来:

实现的效果如下:

在这里插入图片描述

首先,我们第一步是先在一个imaview上画出这条鱼,
接着实现,这条鱼的原地摆动,
然后就简单了,后面就是根据路径,实现这个控件的移动和鱼的旋转。

第一步先画鱼
在这里插入图片描述
这里面最重要的学习思维就是。画一个不管多么复杂的图,先找到这个图重心点坐标,注意,再原地画图的时候,所有的坐标都是以控件的左上角为原点计算,并不是以绝对坐标计算,也就是坐标系是 控件的坐标系 ,找到这个图重心点坐标后,基于这个重心点,一个图形一个图形去做画出来,在画的时候,思维采用的是以一个变量值为单位。后续的模块都以这个变量为单位这样,变量值改变就全部变。

打个比方这里的鱼以身体的重心点为坐标,它先鱼头的半径为单位。后续的其他图块在设置大小的时候都以鱼头的半径为单位。

 // 绘制鱼头的半径
    private float HEAD_RADIUS = 50;
    // 鱼身长度
    private float BODY_LENGTH = HEAD_RADIUS * 3.2f;
    // 寻找鱼鳍起始点坐标的线长
    private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
    // 鱼鳍的长度
    private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
    // 大圆的半径
    private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;

而且画一个个图行的时候,思维是以图片的重心点坐标基准,知道了重心点坐标,知道了重心点到其他模块的重心点的距离,以及两个点之间的角度就可以求出另外一个点的坐标,接着就可以以另外一个点的坐标画出图形。

下面的这个函数就是可以传入已知A点的坐标,A点到B的距离。A点到B 的角度,返回B点的坐标

public PointF calculatePoint(PointF startPoint, float length, float angle) {
        // 距离A点坐标的X偏移量
        float deltaX = (float) (Math.cos(Math.toRadians(angle)) * length);
        //距离A点坐标的Y偏移量
        float deltaY = (float) (-Math.sin(Math.toRadians(angle)) * length);
        //得到偏移量后再加上A点的坐标就可以得到B的坐标
        return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
    }

原理就是:

在这里插入图片描述

在这里插入图片描述
得到了ab长度再利用A点的坐标就可以求出B的坐标
Math.sin()、Math.cos()的参数是弧度。坐标是按数学中的坐标。
Math.toRadians() 将角度转成弧度。
圆是360度,也是2π弧度,即360°=2π

打个比方。当画出来了鱼头的圆之后。想话画节肢1的圆,那么我只需要知道节肢1的圆的坐标就可以画出节肢1的圆,
而这个时候就可以根据上面的三角函数,知道点鱼头的圆心的坐标,鱼头的圆心到节肢1的圆心得角度,以及两者距离就可以道节肢1的圆的坐标就可以画出节肢1的圆。

下面就是画出来一条静态得鱼。

package com.example.test;

import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class FishDrawable1  extends Drawable {

    private Path mPath;
    private Paint mPaint;

    private int OTHER_ALPHA = 110;
    private int BODY_ALPHA = 160;

    // 鱼的重心
    private PointF middlePoint;
    // 鱼的主要朝向角度
    private float fishMainAngle = 0;

    /**
     * 鱼的长度值
     */
    // 绘制鱼头的半径
    private float HEAD_RADIUS = 50;
    // 鱼身长度
    private float BODY_LENGTH = HEAD_RADIUS * 3.2f;
    // 寻找鱼鳍起始点坐标的线长
    private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
    // 鱼鳍的长度
    private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
    // 大圆的半径
    private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;
    // 中圆的半径
    private float MIDDLE_CIRCLE_RADIUS = 0.6f * BIG_CIRCLE_RADIUS;
    // 小圆半径
    private float SMALL_CIRCLE_RADIUS = 0.4f * MIDDLE_CIRCLE_RADIUS;
    // --寻找尾部中圆圆心的线长
    private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS * (0.6f + 1);
    // --寻找尾部小圆圆心的线长
    private final float FIND_SMALL_CIRCLE_LENGTH = MIDDLE_CIRCLE_RADIUS * (0.4f + 2.7f);
    // --寻找大三角形底边中心点的线长
    private final float FIND_TRIANGLE_LENGTH = MIDDLE_CIRCLE_RADIUS * 2.7f;


    public FishDrawable() {
        init();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setARGB(OTHER_ALPHA, 244, 92, 71);

        middlePoint = new PointF(4.19f * HEAD_RADIUS, 4.19f * HEAD_RADIUS);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        float fishAngle = fishMainAngle;

        // 鱼头的圆心坐标
        PointF headPoint = calculatePoint(middlePoint, BODY_LENGTH / 2, fishAngle);
        canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint);

        // 画右鱼鳍
        PointF rightFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle - 110);
        makeFins(canvas, rightFinsPoint, fishAngle, true);

        // 画左鱼鳍
        PointF leftFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle + 110);
        makeFins(canvas, leftFinsPoint, fishAngle, false);

        PointF bodyBottomCenterPoint = calculatePoint(headPoint, BODY_LENGTH, fishAngle - 180);
        // 画节肢1
        PointF middleCenterPoint = makeSegment(canvas, bodyBottomCenterPoint, BIG_CIRCLE_RADIUS, MIDDLE_CIRCLE_RADIUS,
                FIND_MIDDLE_CIRCLE_LENGTH, fishAngle, true);
        // 画节肢2
        makeSegment(canvas, middleCenterPoint, MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
                FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);

        // 尾巴
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH, BIG_CIRCLE_RADIUS, fishAngle);
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH - 10,
                BIG_CIRCLE_RADIUS - 20, fishAngle);

        // 身体
        makeBody(canvas, headPoint, bodyBottomCenterPoint, fishAngle);
    }

    private void makeBody(Canvas canvas, PointF headPoint, PointF bodyBottomCenterPoint, float fishAngle) {
        // 身体的四个点求出来
        PointF topLeftPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle + 90);
        PointF topRightPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle - 90);
        PointF bottomLeftPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle + 90);
        PointF bottomRightPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle - 90);

        // 二阶贝塞尔曲线的控制点 --- 决定鱼的胖瘦
        PointF controlLeft = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle + 130);
        PointF controlRight = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle - 130);

        // 绘制
        mPath.reset();
        mPath.moveTo(topLeftPoint.x, topLeftPoint.y);
        mPath.quadTo(controlLeft.x, controlLeft.y, bottomLeftPoint.x, bottomLeftPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.quadTo(controlRight.x, controlRight.y, topRightPoint.x, topRightPoint.y);
        mPaint.setAlpha(BODY_ALPHA);
        canvas.drawPath(mPath, mPaint);
    }

    private void makeTriangel(Canvas canvas, PointF startPoint, float findCenterLength,
                              float findEdgeLength, float fishAngle) {
        // 三角形底边的中心坐标
        PointF centerPoint = calculatePoint(startPoint, findCenterLength, fishAngle - 180);
        // 三角形底边两点
        PointF leftPoint = calculatePoint(centerPoint, findEdgeLength, fishAngle + 90);
        PointF rightPoint = calculatePoint(centerPoint, findEdgeLength, fishAngle - 90);

        mPath.reset();
        mPath.moveTo(startPoint.x, startPoint.y);
        mPath.lineTo(leftPoint.x, leftPoint.y);
        mPath.lineTo(rightPoint.x, rightPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float bigRadius, float smallRadius,
                               float findSmallCircleLength, float fishAngle, boolean hasBigCircle) {

        // 梯形上底圆的圆心
        PointF upperCenterPoint = calculatePoint(bottomCenterPoint, findSmallCircleLength,
                fishAngle - 180);
        // 梯形的四个点
        PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, bigRadius, fishAngle + 90);
        PointF bottomRightPoint = calculatePoint(bottomCenterPoint, bigRadius, fishAngle - 90);
        PointF upperLeftPoint = calculatePoint(upperCenterPoint, smallRadius, fishAngle + 90);
        PointF upperRightPoint = calculatePoint(upperCenterPoint, smallRadius, fishAngle - 90);

        if (hasBigCircle) {
            // 画大圆 --- 只在节肢1 上才绘画
            canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigRadius, mPaint);
        }
        // 画小圆
        canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, smallRadius, mPaint);

        // 画梯形
        mPath.reset();
        mPath.moveTo(upperLeftPoint.x, upperLeftPoint.y);
        mPath.lineTo(upperRightPoint.x, upperRightPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.lineTo(bottomLeftPoint.x, bottomLeftPoint.y);
        canvas.drawPath(mPath, mPaint);

        return upperCenterPoint;
    }

    /**
     * 画鱼鳍
     *
     * @param startPoint 起始坐标
     * @param isRight    是否是右鱼鳍
     */
    private void makeFins(Canvas canvas, PointF startPoint, float fishAngle, boolean isRight) {
        float controlAngle = 115;

        // 鱼鳍的终点 --- 二阶贝塞尔曲线的终点
        PointF endPoint = calculatePoint(startPoint, FINS_LENGTH, fishAngle - 180);
        // 控制点
        PointF controlPoint = calculatePoint(startPoint, FINS_LENGTH * 1.8f,
                isRight ? fishAngle - controlAngle : fishAngle + controlAngle);
        // 绘制
        mPath.reset();
        // 将画笔移动到起始点
        mPath.moveTo(startPoint.x, startPoint.y);
        // 二阶贝塞尔曲线
        mPath.quadTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * @param startPoint 起始点坐标
     * @param length     要求的点到起始点的直线距离 -- 线长
     * @param angle      鱼当前的朝向角度
     * @return
     */
    public PointF calculatePoint(PointF startPoint, float length, float angle) {
        // x坐标
        float deltaX = (float) (Math.cos(Math.toRadians(angle)) * length);
        // y坐标
        float deltaY = (float) (Math.sin(Math.toRadians(angle - 180)) * length);

        return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
    }


    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public int getIntrinsicWidth() {
        return (int) (8.38f * HEAD_RADIUS);
    }

    @Override
    public int getIntrinsicHeight() {
        return (int) (8.38f * HEAD_RADIUS);
    }
}

在这里插入图片描述

第二步骤是实现这条鱼得原地摆动。这里学到得思维是,利用一个值动画,去控制头尾以及其他部位他们摆动得角度不同和周期不同,比如
鱼一秒内头旋转从-30到30一次,而鱼尾从,-50到50旋转5次,不仅旋转角度不同,次数周期也不同。
实现周期不同这里利用的是三角函数。

我们知道在三角函数中
sin(0~360)=-1到1 这里是一个周期。sin(0-720)=-1到1 这里是2个周期 那么
sin((0-360)*t)=-1到1.。这里就是-1到1 做个t个周期。 那么

sin(0~360)*k 就是 -k到k之间变化了一个周期。那么
sin((0-360)*t) )*k 就是 在-k到k之间变化了t个周期。

因为我们只需要设置两个变量 t 和k 。就可以利用一个值动画,去实现不同部门旋转的角度不同周期也不同。

如下

 ValueAnimator valueAnimator =ValueAnimator.ofFloat(0,360);
        valueAnimator.setDuration(1000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) animation.getAnimatedValue();
                invalidateSelf();
            }
        });
valueAnimator.start();

这个值动画,在1s内从0到360做了一次。那么

Math.sin(Math.toRadians(currentValue) 就是在1s内从-1到1做了一次。
接着
Math.sin(Math.toRadians(currentValue) k 就是就是在1s内从-k到k做了一次。
最后
Math.sin(Math.toRadians(currentValue
t) *k 就是就是在1s内从-k到k做了t次。

因此就可以实现了,头部摆的角度跟尾巴不同,并且两者之间的频率也不同

代码如下;

package com.example.test;


import android.animation.ValueAnimator;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.view.animation.LinearInterpolator;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class FishDrawable1  extends Drawable {

    private Path mPath;
    private Paint mPaint;

    private int OTHER_ALPHA = 110;
    private int BODY_ALPHA = 160;

    // 鱼的重心
    private PointF middlePoint;
    // 鱼的主要朝向角度
    private float fishMainAngle = 0;



    /**
     * 鱼的长度值
     */
    // 绘制鱼头的半径
    private float HEAD_RADIUS = 50;
    // 鱼身长度
    private float BODY_LENGTH = HEAD_RADIUS * 3.2f;
    // 寻找鱼鳍起始点坐标的线长
    private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
    // 鱼鳍的长度
    private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
    // 大圆的半径
    private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;
    // 中圆的半径
    private float MIDDLE_CIRCLE_RADIUS = 0.6f * BIG_CIRCLE_RADIUS;
    // 小圆半径
    private float SMALL_CIRCLE_RADIUS = 0.4f * MIDDLE_CIRCLE_RADIUS;
    // --寻找尾部中圆圆心的线长
    private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS * (0.6f + 1);
    // --寻找尾部小圆圆心的线长
    private final float FIND_SMALL_CIRCLE_LENGTH = MIDDLE_CIRCLE_RADIUS * (0.4f + 2.7f);
    // --寻找大三角形底边中心点的线长
    private final float FIND_TRIANGLE_LENGTH = MIDDLE_CIRCLE_RADIUS * 2.7f;


    //-1到1的值变动
    private float currentValue;

    //头部的摆动值
    private int headK=10;
    //头部的摆动周期
    private int headT=1;

    //鱼鳍的摆动值
    private int finsK=10;
    //鱼鳍的摆动周期
    private int finsT=3;

    //节肢的摆动值
    private int segmentK=20;
    //节肢的摆动周期
    private int segmentT=3;

    //尾巴的摆动值
    private int triangeK=25;
    //尾巴的摆动周期
    private int triangeT=4;

    public FishDrawable1() {
        init();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setARGB(OTHER_ALPHA, 244, 92, 71);

        middlePoint = new PointF(4.19f * HEAD_RADIUS, 4.19f * HEAD_RADIUS);

        ValueAnimator valueAnimator =ValueAnimator.ofFloat(0,360);
        valueAnimator.setDuration(1000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) animation.getAnimatedValue();
                invalidateSelf();
            }
        });
        valueAnimator.start();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        float fishAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*headT)))*headK);;

        // 鱼头的圆心坐标
        PointF headPoint = calculatePoint(middlePoint, BODY_LENGTH / 2, fishAngle);
        canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint);

        // 画右鱼鳍
        PointF rightFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle - 110);
        makeFins(canvas, rightFinsPoint, fishAngle, true);

        // 画左鱼鳍
        PointF leftFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle + 110);
        makeFins(canvas, leftFinsPoint, fishAngle, false);

        PointF bodyBottomCenterPoint = calculatePoint(headPoint, BODY_LENGTH, fishAngle - 180);
        // 画节肢1
        PointF middleCenterPoint = makeSegment(canvas, bodyBottomCenterPoint, BIG_CIRCLE_RADIUS, MIDDLE_CIRCLE_RADIUS,
                FIND_MIDDLE_CIRCLE_LENGTH, fishAngle, true);
        // 画节肢2
        makeSegment(canvas, middleCenterPoint, MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
                FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);

        // 尾巴
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH, BIG_CIRCLE_RADIUS, fishAngle);
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH - 10,
                BIG_CIRCLE_RADIUS - 20, fishAngle);

        // 身体
        makeBody(canvas, headPoint, bodyBottomCenterPoint, fishAngle);
    }

    private void makeBody(Canvas canvas, PointF headPoint, PointF bodyBottomCenterPoint, float fishAngle) {
        // 身体的四个点求出来
        PointF topLeftPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle + 90);
        PointF topRightPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle - 90);
        PointF bottomLeftPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle + 90);
        PointF bottomRightPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle - 90);

        // 二阶贝塞尔曲线的控制点 --- 决定鱼的胖瘦
        PointF controlLeft = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle + 130);
        PointF controlRight = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle - 130);

        // 绘制
        mPath.reset();
        mPath.moveTo(topLeftPoint.x, topLeftPoint.y);
        mPath.quadTo(controlLeft.x, controlLeft.y, bottomLeftPoint.x, bottomLeftPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.quadTo(controlRight.x, controlRight.y, topRightPoint.x, topRightPoint.y);
        mPaint.setAlpha(BODY_ALPHA);
        canvas.drawPath(mPath, mPaint);
    }

    private void makeTriangel(Canvas canvas, PointF startPoint, float findCenterLength,
                              float findEdgeLength, float fishAngle) {

        float triangleAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*triangeT)))*triangeK);
        // 三角形底边的中心坐标
        PointF centerPoint = calculatePoint(startPoint, findCenterLength, triangleAngle - 180);
        // 三角形底边两点
        PointF leftPoint = calculatePoint(centerPoint, findEdgeLength, triangleAngle + 90);
        PointF rightPoint = calculatePoint(centerPoint, findEdgeLength, triangleAngle - 90);

        mPath.reset();
        mPath.moveTo(startPoint.x, startPoint.y);
        mPath.lineTo(leftPoint.x, leftPoint.y);
        mPath.lineTo(rightPoint.x, rightPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float bigRadius, float smallRadius,
                               float findSmallCircleLength, float fishAngle, boolean hasBigCircle) {
        float segmentAngle;
        if(hasBigCircle){
            segmentAngle = (float) (fishMainAngle+(Math.cos(Math.toRadians(currentValue*segmentT)))*segmentK);
        }else {
            segmentAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*segmentT)))*segmentK);
        }

        // 梯形上底圆的圆心
        PointF upperCenterPoint = calculatePoint(bottomCenterPoint, findSmallCircleLength,
                segmentAngle - 180);
        // 梯形的四个点
        PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, bigRadius, segmentAngle + 90);
        PointF bottomRightPoint = calculatePoint(bottomCenterPoint, bigRadius, segmentAngle - 90);
        PointF upperLeftPoint = calculatePoint(upperCenterPoint, smallRadius, segmentAngle + 90);
        PointF upperRightPoint = calculatePoint(upperCenterPoint, smallRadius, segmentAngle - 90);

        if (hasBigCircle) {
            // 画大圆 --- 只在节肢1 上才绘画
            canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigRadius, mPaint);
        }
        // 画小圆
        canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, smallRadius, mPaint);

        // 画梯形
        mPath.reset();
        mPath.moveTo(upperLeftPoint.x, upperLeftPoint.y);
        mPath.lineTo(upperRightPoint.x, upperRightPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.lineTo(bottomLeftPoint.x, bottomLeftPoint.y);
        canvas.drawPath(mPath, mPaint);

        return upperCenterPoint;
    }

    /**
     * 画鱼鳍
     *
     * @param startPoint 起始坐标
     * @param isRight    是否是右鱼鳍
     */
    private void makeFins(Canvas canvas, PointF startPoint, float fishAngle, boolean isRight) {
        float controlAngle = 115;

        // 鱼鳍的终点 --- 二阶贝塞尔曲线的终点
        PointF endPoint = calculatePoint(startPoint, FINS_LENGTH, fishAngle - 180);
        // 控制点
        PointF controlPoint = calculatePoint(startPoint, FINS_LENGTH * 1.8f,
                isRight ? fishAngle - controlAngle : fishAngle + controlAngle);
        // 绘制
        mPath.reset();
        // 将画笔移动到起始点
        mPath.moveTo(startPoint.x, startPoint.y);
        // 二阶贝塞尔曲线
        mPath.quadTo((float) (controlPoint.x+(Math.sin(Math.toRadians(currentValue*finsT))*finsK)), (float) (controlPoint.y+(Math.sin(Math.toRadians(currentValue*finsT))*finsK)), endPoint.x, endPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * @param startPoint 起始点坐标
     * @param length     要求的点到起始点的直线距离 -- 线长
     * @param angle      鱼当前的朝向角度
     * @return
     */
    public PointF calculatePoint(PointF startPoint, float length, float angle) {
        // x坐标
        float deltaX = (float) (Math.cos(Math.toRadians(angle)) * length);
        // y坐标
        float deltaY = (float) (Math.sin(Math.toRadians(angle - 180)) * length);

        return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
    }


    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public int getIntrinsicWidth() {
        return (int) (8.38f * HEAD_RADIUS);
    }

    @Override
    public int getIntrinsicHeight() {
        return (int) (8.38f * HEAD_RADIUS);
    }
}

在这里插入图片描述

上面我们完成了鱼的原地游动
我们只是自定义了一个drawable,然后再活动布局中把这个drawable设置到这个imageview。然后imageview放入到活动的布局中。

接下来我们实现鱼的水波纹需要自定义一个布局然后把一个imageview 放入到这个控件中,再把这个自定义了一个drawable设置到imageview中去。

代码如下:

在这里插入图片描述

public class FishFramelayout1 extends FrameLayout {


    ImageView ivFish;
    private FishDrawable1 fishDrawable;
    private Paint mPaint;



    public FishFramelayout1(@NonNull Context context) {
        super(context);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);

        ivFish = new ImageView(getContext());
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        ivFish.setLayoutParams(layoutParams);
        fishDrawable = new FishDrawable1();
        ivFish.setImageDrawable(fishDrawable);
        ivFish.setBackgroundColor(Color.GREEN);
        addView(ivFish);
        setWillNotDraw(false);
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}

效果如下:
在这里插入图片描述
我们自定了一个帧布局,然后再帧布局里面放入了一个imageview,这个imageview设置了背景颜色为绿色。接着把这个条鱼drawable放进去。

这里非常注意
自定义布局继承FrameLayout或者是其他系统提供的布局。是默认ondraw方法不调用的,所以需要用

setWillNotDraw(false);

设置ondraw方法调用。

接着实现点击出现水波纹的效果。
我们常规的思维就是。获取到触碰点的坐标。然后利用属性动画的制造两个属性去做
也就是利用

        PropertyValuesHolder propertyValuesHolder1 = PropertyValuesHolder.ofFloat("alpha", 150, 0);
        PropertyValuesHolder propertyValuesHolder2 = PropertyValuesHolder.ofFloat("circleRadius", 150, 0);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(propertyValuesHolder1, propertyValuesHolder2);

但是这里 完全不可以这么做,,你要操作的对象是个点圆,在这里可是FrameLayout。objectAnimator 内没法传入什么对象。
当然你可以设置ValueAnimator动画去设置,但是这里也依旧可以用ObjectAnimator动画。这里之所以要用属性动画,是为了学习
ObjectAnimator的本质。ObjectAnimator的本质是采用的是反射的方法。也就是不断的去调用对象的set方法。就连对象没有这个属性都可以

打个比方。

在这个FrameLayout中 ,并没有CircleRadius这个属性,但是你只有这个类里面有setCircleRadius这个方法。那么就可以

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this,“CircleRadius”,0,1f).setDuration(1000);
objectAnimator.start();

public class FishFramelayout1 extends FrameLayout {


    ImageView ivFish;
    private FishDrawable1 fishDrawable;
    private Paint mPaint;
    //0到1的值变动
    private float currentValue;
    //圆的透明度
    private float alpha;
    //触碰点
    float touchX, touchY;



    public FishFramelayout1(@NonNull Context context) {
        super(context);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);

        ivFish = new ImageView(getContext());
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        ivFish.setLayoutParams(layoutParams);
        fishDrawable = new FishDrawable1();
        ivFish.setImageDrawable(fishDrawable);
        ivFish.setBackgroundColor(Color.GREEN);
        addView(ivFish);
        setWillNotDraw(false);


        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this,"CircleRadius",0,1f).setDuration(1000);
        objectAnimator.start();

    }

    public void setCircleRadius(float circleRadius) {
        Log.d("TAG", "setCircleRadius: "+circleRadius);
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

}

那么我们知道了objectAnimator的本质就是不断去调用set方法并且传入变化值,那么想要造成多个属性一起变化还会难嘛。我们完全可以这样:

   ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this,"CurrentValue",0,1).setDuration(1000);
    objectAnimator.start();

    public void setCurrentValue(float currentValue) {
        circleRadius=currentValue*250;
        alpha=(1-currentValue)*100;
        invalidate();
    }

半径有0到250递增。
透明度为100到0递减也就是从看见到看不见。

全部代码为:

public class FishFramelayout1 extends FrameLayout {


    ImageView ivFish;
    private FishDrawable1 fishDrawable;
    private Paint mPaint;


    //圆的透明度
    private float alpha;
    //圆的半径
    private float circleRadius;
    //触碰点
    float touchX, touchY;


    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);
        mPaint.setAlpha((int) alpha);

        ivFish = new ImageView(getContext());
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        ivFish.setLayoutParams(layoutParams);
        fishDrawable = new FishDrawable1();
        ivFish.setImageDrawable(fishDrawable);
        ivFish.setBackgroundColor(Color.GREEN);
        addView(ivFish);
        setWillNotDraw(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        touchX = event.getX();
        touchY = event.getY();

        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this,"CurrentValue",0,1).setDuration(1000);
        objectAnimator.start();

        return super.onTouchEvent(event);
    }


    public void setCurrentValue(float currentValue) {
        circleRadius=currentValue*250;
        alpha=(1-currentValue)*100;
        invalidate();
    }

    public FishFramelayout1(@NonNull Context context) {
        super(context);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setAlpha((int) alpha);
        drawCircle(canvas);
    }

    private void drawCircle(Canvas canvas) {
        canvas.drawCircle(touchX, touchY, circleRadius , mPaint);
    }
}

效果:

在这里插入图片描述

接着就是最后一步骤了。
控件的移动和鱼的旋转。
记住一个思维非常重要,控件的移动和旋转就是用到path,而旋转就是利用path的PathMeasure。利用path非常方便。

你只需要设置好path路径点坐标,,剩下直接用用属性动画就可以解决。

也就是

        Path path = new Path();
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
        objectAnimator .start();

为什么可以这样呢。因为控件里面就有setX和setY的方法。而这个方式他们的坐标系是相对于父亲的在这个时候也就是绝对坐标。
那么你只需要确定好path的路径点就就可以实现控件的偏移。

再者记住一个思维。
控件的起始点其实就是控件的getX和getY,也就是控件的左上角的点开始。
控件的终点就是手点触碰的点坐标再减去控件的重心点坐标。

在这里插入图片描述

以这条鱼为例子。

          // 鱼的重心:相对ImageView坐标
        PointF fishRelativeMiddle = fishDrawable.getMiddlePoint();
        // 鱼的重心:绝对坐标.就是控件的左上角坐标+鱼重心相对ImageView坐标
        PointF fishMiddle = new PointF(ivFish.getX() + fishRelativeMiddle.x, ivFish.getY() + fishRelativeMiddle.y);

        //起始点 控件的左上角点
        PointF startPointF = new PointF(ivFish.getX(), ivFish.getY());

        // 结束点==触摸点-鱼重心相对ImageView坐标
        PointF endPointF = new PointF(touchX-fishRelativeMiddle.x, touchY-fishRelativeMiddle.y);

        // 触摸点
        PointF  touch = new PointF(touchX, touchY);


        // 鱼头圆心的坐标 -- 控制点1
        final PointF fishHead = new PointF(ivFish.getX() + fishDrawable.getHeadPoint().x,
                ivFish.getY() + fishDrawable.getHeadPoint().y);


        // 控制点2 的坐标
        float angle = includeAngle(fishMiddle, fishHead, touch) / 2;
        float delta = includeAngle(fishMiddle, new PointF(fishMiddle.x + 1, fishMiddle.y), fishHead);
        PointF controlPoint = fishDrawable.calculatePoint(fishMiddle,
                fishDrawable.getHEAD_RADIUS() * 1.6f, angle + delta);




        Path path = new Path();
        path.moveTo(startPointF.x, startPointF.y);
        path.cubicTo(fishHead.x - fishRelativeMiddle.x, fishHead.y - fishRelativeMiddle.y,
                controlPoint.x - fishRelativeMiddle.x, controlPoint.y - fishRelativeMiddle.y,
                endPointF.x, endPointF.y);


        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
        objectAnimator.setDuration(2000);
        objectAnimator.start();

鱼的游动最大的难点在于鱼游动的路径贝塞尔三阶的得到第二个控制点的坐标。
得到这个坐标后。在根据路径进行上面的移动非常简单。

那么贝塞尔的第二控制点如何求呢
这里更多的是数学的公式。
在这里插入图片描述

在这里,已知。o点是控件重心点,A点是鱼头的重心位置也就是控制点一,B点是触控点,这3个点都是知道的,而目前我们想知道的就是C第二个控制点的坐标,而C的坐标只要知道∠cod 我们就可以利用上面的已知道点坐o坐标,o到c的距离。o到c的角度求得C的坐标。
也就是关键就是求出∠cod 多少度。

根据下面的两条公司我们只要知道∠AOB和∠AOD或者∠AOE就可以得出∠cod多少度从而知道C点坐标。

而当我们知道3个点的坐标想要求出三个点夹角的时候
我们就可以利用数学公式
在这里插入图片描述

 // 这个函数能够求得已知的3个点坐标,求出他们的夹角的角度,并且角度的正负已经确定好了。
    public float includeAngle(PointF O, PointF A, PointF B) {

        // OA*OB=(Ax-Ox)(Bx-Ox)+(Ay-Oy)*(By-Oy)
        float AOB = (A.x - O.x) * (B.x - O.x) + (A.y - O.y) * (B.y - O.y);
        float OALength = (float) Math.sqrt((A.x - O.x) * (A.x - O.x) + (A.y - O.y) * (A.y - O.y));
        // OB 的长度
        float OBLength = (float) Math.sqrt((B.x - O.x) * (B.x - O.x) + (B.y - O.y) * (B.y - O.y));
        float cosAOB = AOB / (OALength * OBLength);

        // 反余弦
        float angleAOB = (float) Math.toDegrees(Math.acos(cosAOB));

        // AB连线与X的夹角的tan值 - OB与x轴的夹角的tan值
        float direction = (A.y - B.y) / (A.x - B.x) - (O.y - B.y) / (O.x - B.x);

        if (direction == 0) {
            if (AOB >= 0) {
                return 0;
            } else {
                return 180;
            }
        } else {
            if (direction > 0) {
                return -angleAOB;
            } else {
                return angleAOB;
            }
        }

    }

利用上面的函数我们就知道了

  float ∠AOE = includeAngle(fishMiddle, new PointF(fishMiddle.x + 1, fishMiddle.y), fishHead);
  float ∠AOC = includeAngle(fishMiddle, fishHead, touch) / 2;



那么∠COD=∠AOE+∠AOC。注意上面的函数得出来的值正负已经确定好。
知道∠COD后再有三角函数就可以知道。

//第二个控制点坐标
 PointF controlPoint = fishDrawable.calculatePoint(fishMiddle,
                fishDrawable.getHEAD_RADIUS() * 1.6f, ∠AOE+ ∠AOC);

所以以后物体得偏移就用path,然后需要求得贝塞尔曲线第二控制点就根据上面得3点坐标求得夹角再由角度运算得到第二控制点。

接着就是头得旋转 头得旋转利用path得PathMeasure。利用path得PathMeasur求得正切值在利用函数得到角度并设置到鱼头角度,由于,鱼的其他部位是根据鱼头来画的所以鱼头角度变其他部门也会跟着变。

  final PathMeasure pathMeasure = new PathMeasure(path, false);
        final float[] tan = new float[2];
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
//                animation.getAnimatedValue();
                // 执行了整个周期的百分之多少
                float fraction = animation.getAnimatedFraction();
                pathMeasure.getPosTan(pathMeasure.getLength() * fraction, null, tan);
                float angle = (float) Math.toDegrees(Math.atan2(-tan[1], tan[0]));
                fishDrawable.setFishMainAngle(angle);
            }
        });

最后因为游动得时候。鱼得各部位摆动周期会加快 所以

objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
fishDrawable.setTriangeT(4);
fishDrawable.setFinsT(3);
fishDrawable.setSegmentT(3);
}

        @Override
        public void onAnimationStart(Animator animation) {
            super.onAnimationStart(animation);
            fishDrawable.setFinsT(5);
            fishDrawable.setTriangeT(6);
            fishDrawable.setSegmentT(6);

        }
    });

最后所有得代码

public class FishFramelayout1 extends FrameLayout {


    ImageView ivFish;
    private FishDrawable1 fishDrawable;
    private Paint mPaint;


    //圆的透明度
    private float alpha;
    //圆的半径
    private float circleRadius;
    //触碰点
    float touchX, touchY;


    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);
        mPaint.setAlpha((int) alpha);

        ivFish = new ImageView(getContext());
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        ivFish.setLayoutParams(layoutParams);
        fishDrawable = new FishDrawable1();
        ivFish.setImageDrawable(fishDrawable);
        ivFish.setBackgroundColor(Color.GREEN);
        addView(ivFish);
        setWillNotDraw(false);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        touchX = event.getX();
        touchY = event.getY();

        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this,"CurrentValue",0,1).setDuration(1000);
        objectAnimator.start();

        makeTrail();

        return super.onTouchEvent(event);
    }


    public void setCurrentValue(float currentValue) {
        circleRadius=currentValue*250;
        alpha=(1-currentValue)*100;
        invalidate();
    }

    public FishFramelayout1(@NonNull Context context) {
        super(context);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public FishFramelayout1(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setAlpha((int) alpha);
        drawCircle(canvas);
    }

    private void drawCircle(Canvas canvas) {
        canvas.drawCircle(touchX, touchY, circleRadius , mPaint);
    }


    private void makeTrail() {
        // 鱼的重心:相对ImageView坐标
        PointF fishRelativeMiddle = fishDrawable.getMiddlePoint();
        // 鱼的重心:绝对坐标.就是控件的左上角坐标+鱼重心相对ImageView坐标
        PointF fishMiddle = new PointF(ivFish.getX() + fishRelativeMiddle.x, ivFish.getY() + fishRelativeMiddle.y);

        //起始点 控件的左上角点
        PointF startPointF = new PointF(ivFish.getX(), ivFish.getY());

        // 结束点==触摸点-鱼重心相对ImageView坐标
        PointF endPointF = new PointF(touchX-fishRelativeMiddle.x, touchY-fishRelativeMiddle.y);

        // 触摸点
        PointF  touch = new PointF(touchX, touchY);


        // 鱼头圆心的坐标 -- 控制点1
        final PointF fishHead = new PointF(ivFish.getX() + fishDrawable.getHeadPoint().x,
                ivFish.getY() + fishDrawable.getHeadPoint().y);


        // 控制点2 的坐标
        float angle = includeAngle(fishMiddle, fishHead, touch) / 2;
        float delta = includeAngle(fishMiddle, new PointF(fishMiddle.x + 1, fishMiddle.y), fishHead);
        PointF controlPoint = fishDrawable.calculatePoint(fishMiddle,
                fishDrawable.getHEAD_RADIUS() * 1.6f, angle + delta);


        Path path = new Path();
        path.moveTo(startPointF.x, startPointF.y);
        path.cubicTo(fishHead.x - fishRelativeMiddle.x, fishHead.y - fishRelativeMiddle.y,
                controlPoint.x - fishRelativeMiddle.x, controlPoint.y - fishRelativeMiddle.y,
                endPointF.x, endPointF.y);


        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
        objectAnimator.setDuration(2000);
        final PathMeasure pathMeasure = new PathMeasure(path, false);
        final float[] tan = new float[2];
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
//                animation.getAnimatedValue();
                // 执行了整个周期的百分之多少
                float fraction = animation.getAnimatedFraction();
                pathMeasure.getPosTan(pathMeasure.getLength() * fraction, null, tan);
                float angle = (float) Math.toDegrees(Math.atan2(-tan[1], tan[0]));
                fishDrawable.setFishMainAngle(angle);
            }
        });

        objectAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                fishDrawable.setTriangeT(4);
                fishDrawable.setFinsT(3);
                fishDrawable.setSegmentT(3);
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                fishDrawable.setFinsT(5);
                fishDrawable.setTriangeT(6);
                fishDrawable.setSegmentT(6);

            }
        });
        objectAnimator.start();

    }

    public float includeAngle(PointF O, PointF A, PointF B) {
        // cosAOB
        // OA*OB=(Ax-Ox)(Bx-Ox)+(Ay-Oy)*(By-Oy)
        float AOB = (A.x - O.x) * (B.x - O.x) + (A.y - O.y) * (B.y - O.y);
        float OALength = (float) Math.sqrt((A.x - O.x) * (A.x - O.x) + (A.y - O.y) * (A.y - O.y));
        // OB 的长度
        float OBLength = (float) Math.sqrt((B.x - O.x) * (B.x - O.x) + (B.y - O.y) * (B.y - O.y));
        float cosAOB = AOB / (OALength * OBLength);

        // 反余弦
        float angleAOB = (float) Math.toDegrees(Math.acos(cosAOB));

        // AB连线与X的夹角的tan值 - OB与x轴的夹角的tan值
        float direction = (A.y - B.y) / (A.x - B.x) - (O.y - B.y) / (O.x - B.x);

        if (direction == 0) {
            if (AOB >= 0) {
                return 0;
            } else {
                return 180;
            }
        } else {
            if (direction > 0) {
                return -angleAOB;
            } else {
                return angleAOB;
            }
        }

    }
}
public class FishDrawable1  extends Drawable {

    private Path mPath;
    private Paint mPaint;

    private int OTHER_ALPHA = 110;
    private int BODY_ALPHA = 160;



    // 鱼的重心
    private PointF middlePoint;




    // 鱼的主要朝向角度
    private float fishMainAngle = 0;




    /**
     * 鱼的长度值
     */
    // 绘制鱼头的半径
    private float HEAD_RADIUS = 50;
    //鱼头的点
    private PointF headPoint;
    // 鱼身长度
    private float BODY_LENGTH = HEAD_RADIUS * 3.2f;
    // 寻找鱼鳍起始点坐标的线长
    private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
    // 鱼鳍的长度
    private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
    // 大圆的半径
    private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;
    // 中圆的半径
    private float MIDDLE_CIRCLE_RADIUS = 0.6f * BIG_CIRCLE_RADIUS;
    // 小圆半径
    private float SMALL_CIRCLE_RADIUS = 0.4f * MIDDLE_CIRCLE_RADIUS;
    // --寻找尾部中圆圆心的线长
    private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS * (0.6f + 1);
    // --寻找尾部小圆圆心的线长
    private final float FIND_SMALL_CIRCLE_LENGTH = MIDDLE_CIRCLE_RADIUS * (0.4f + 2.7f);
    // --寻找大三角形底边中心点的线长
    private final float FIND_TRIANGLE_LENGTH = MIDDLE_CIRCLE_RADIUS * 2.7f;


    //-1到1的值变动
    private float currentValue;

    //头部的摆动值
    private int headK=10;
    //头部的摆动周期
    private int headT=1;

    //鱼鳍的摆动值
    private int finsK=10;



    //鱼鳍的摆动周期
    private int finsT=3;

    //节肢的摆动值
    private int segmentK=20;
    //节肢的摆动周期
    private int segmentT=3;

    //尾巴的摆动值
    private int triangeK=25;
    //尾巴的摆动周期
    private int triangeT=4;

    public FishDrawable1() {
        init();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setARGB(OTHER_ALPHA, 244, 92, 71);

        middlePoint = new PointF(4.19f * HEAD_RADIUS, 4.19f * HEAD_RADIUS);

        ValueAnimator valueAnimator =ValueAnimator.ofFloat(0,360);
        valueAnimator.setDuration(1000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setRepeatMode(ValueAnimator.RESTART);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentValue = (float) animation.getAnimatedValue();
                invalidateSelf();
            }
        });
        valueAnimator.start();
    }



    @Override
    public void draw(@NonNull Canvas canvas) {
        float fishAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*headT)))*headK);;

        // 鱼头的圆心坐标
        headPoint = calculatePoint(middlePoint, BODY_LENGTH / 2, fishAngle);
        canvas.drawCircle(headPoint.x, headPoint.y, HEAD_RADIUS, mPaint);

        // 画右鱼鳍
        PointF rightFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle - 110);
        makeFins(canvas, rightFinsPoint, fishAngle, true);

        // 画左鱼鳍
        PointF leftFinsPoint = calculatePoint(headPoint, FIND_FINS_LENGTH, fishAngle + 110);
        makeFins(canvas, leftFinsPoint, fishAngle, false);

        PointF bodyBottomCenterPoint = calculatePoint(headPoint, BODY_LENGTH, fishAngle - 180);
        // 画节肢1
        PointF middleCenterPoint = makeSegment(canvas, bodyBottomCenterPoint, BIG_CIRCLE_RADIUS, MIDDLE_CIRCLE_RADIUS,
                FIND_MIDDLE_CIRCLE_LENGTH, fishAngle, true);
        // 画节肢2
        makeSegment(canvas, middleCenterPoint, MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
                FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);

        // 尾巴
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH, BIG_CIRCLE_RADIUS, fishAngle);
        makeTriangel(canvas, middleCenterPoint, FIND_TRIANGLE_LENGTH - 10,
                BIG_CIRCLE_RADIUS - 20, fishAngle);

        // 身体
        makeBody(canvas, headPoint, bodyBottomCenterPoint, fishAngle);
    }

    private void makeBody(Canvas canvas, PointF headPoint, PointF bodyBottomCenterPoint, float fishAngle) {
        // 身体的四个点求出来
        PointF topLeftPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle + 90);
        PointF topRightPoint = calculatePoint(headPoint, HEAD_RADIUS, fishAngle - 90);
        PointF bottomLeftPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle + 90);
        PointF bottomRightPoint = calculatePoint(bodyBottomCenterPoint, BIG_CIRCLE_RADIUS,
                fishAngle - 90);

        // 二阶贝塞尔曲线的控制点 --- 决定鱼的胖瘦
        PointF controlLeft = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle + 130);
        PointF controlRight = calculatePoint(headPoint, BODY_LENGTH * 0.56f,
                fishAngle - 130);

        // 绘制
        mPath.reset();
        mPath.moveTo(topLeftPoint.x, topLeftPoint.y);
        mPath.quadTo(controlLeft.x, controlLeft.y, bottomLeftPoint.x, bottomLeftPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.quadTo(controlRight.x, controlRight.y, topRightPoint.x, topRightPoint.y);
        mPaint.setAlpha(BODY_ALPHA);
        canvas.drawPath(mPath, mPaint);
    }

    private void makeTriangel(Canvas canvas, PointF startPoint, float findCenterLength,
                              float findEdgeLength, float fishAngle) {

        float triangleAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*triangeT)))*triangeK);
        // 三角形底边的中心坐标
        PointF centerPoint = calculatePoint(startPoint, findCenterLength, triangleAngle - 180);
        // 三角形底边两点
        PointF leftPoint = calculatePoint(centerPoint, findEdgeLength, triangleAngle + 90);
        PointF rightPoint = calculatePoint(centerPoint, findEdgeLength, triangleAngle - 90);

        mPath.reset();
        mPath.moveTo(startPoint.x, startPoint.y);
        mPath.lineTo(leftPoint.x, leftPoint.y);
        mPath.lineTo(rightPoint.x, rightPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float bigRadius, float smallRadius,
                               float findSmallCircleLength, float fishAngle, boolean hasBigCircle) {
        float segmentAngle;
        if(hasBigCircle){
            segmentAngle = (float) (fishMainAngle+(Math.cos(Math.toRadians(currentValue*segmentT)))*segmentK);
        }else {
            segmentAngle = (float) (fishMainAngle+(Math.sin(Math.toRadians(currentValue*segmentT)))*segmentK);
        }

        // 梯形上底圆的圆心
        PointF upperCenterPoint = calculatePoint(bottomCenterPoint, findSmallCircleLength,
                segmentAngle - 180);
        // 梯形的四个点
        PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, bigRadius, segmentAngle + 90);
        PointF bottomRightPoint = calculatePoint(bottomCenterPoint, bigRadius, segmentAngle - 90);
        PointF upperLeftPoint = calculatePoint(upperCenterPoint, smallRadius, segmentAngle + 90);
        PointF upperRightPoint = calculatePoint(upperCenterPoint, smallRadius, segmentAngle - 90);

        if (hasBigCircle) {
            // 画大圆 --- 只在节肢1 上才绘画
            canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigRadius, mPaint);
        }
        // 画小圆
        canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, smallRadius, mPaint);

        // 画梯形
        mPath.reset();
        mPath.moveTo(upperLeftPoint.x, upperLeftPoint.y);
        mPath.lineTo(upperRightPoint.x, upperRightPoint.y);
        mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
        mPath.lineTo(bottomLeftPoint.x, bottomLeftPoint.y);
        canvas.drawPath(mPath, mPaint);

        return upperCenterPoint;
    }

    /**
     * 画鱼鳍
     *
     * @param startPoint 起始坐标
     * @param isRight    是否是右鱼鳍
     */
    private void makeFins(Canvas canvas, PointF startPoint, float fishAngle, boolean isRight) {
        float controlAngle = 115;

        // 鱼鳍的终点 --- 二阶贝塞尔曲线的终点
        PointF endPoint = calculatePoint(startPoint, FINS_LENGTH, fishAngle - 180);
        // 控制点
        PointF controlPoint = calculatePoint(startPoint, FINS_LENGTH * 1.8f,
                isRight ? fishAngle - controlAngle : fishAngle + controlAngle);
        // 绘制
        mPath.reset();
        // 将画笔移动到起始点
        mPath.moveTo(startPoint.x, startPoint.y);
        // 二阶贝塞尔曲线
        mPath.quadTo((float) (controlPoint.x+(Math.sin(Math.toRadians(currentValue*finsT))*finsK)), (float) (controlPoint.y+(Math.sin(Math.toRadians(currentValue*finsT))*finsK)), endPoint.x, endPoint.y);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * @param startPoint 起始点坐标
     * @param length     要求的点到起始点的直线距离 -- 线长
     * @param angle      鱼当前的朝向角度
     * @return
     */
    public PointF calculatePoint(PointF startPoint, float length, float angle) {
        // x坐标
        float deltaX = (float) (Math.cos(Math.toRadians(angle)) * length);
        // y坐标
        float deltaY = (float) (Math.sin(Math.toRadians(angle - 180)) * length);

        return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
    }


    @Override
    public void setAlpha(int i) {
        mPaint.setAlpha(i);
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public int getIntrinsicWidth() {
        return (int) (8.38f * HEAD_RADIUS);
    }

    @Override
    public int getIntrinsicHeight() {
        return (int) (8.38f * HEAD_RADIUS);
    }

    public PointF getMiddlePoint() {
        return middlePoint;
    }

    public float getHEAD_RADIUS() {
        return HEAD_RADIUS;
    }
    public PointF getHeadPoint() {
        return headPoint;
    }
    public float getFishMainAngle() {
        return fishMainAngle;
    }

    public void setFishMainAngle(float fishMainAngle) {
        this.fishMainAngle = fishMainAngle;
    }
    public void setHeadT(int headT) {
        this.headT = headT;
    }

    public void setFinsT(int finsT) {
        this.finsT = finsT;
    }

    public void setSegmentT(int segmentT) {
        this.segmentT = segmentT;
    }

    public void setTriangeT(int triangeT) {
        this.triangeT = triangeT;
    }
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43836998/article/details/102647617