커스텀 드로어블--커스텀 뷰의 아이디어를 배우기 위해 물고기가 헤엄치는 경우를 그려보세요.

커스텀 드로어블.

커스텀 드로어블. 커스텀 뷰와 크게 다르지 않고 사용이 간편하고 비용이 저렴하며
Non-picture Drawable은 공간을 덜 차지하여 apk 크기를 줄일 수 있으며 측정 및 레이아웃 문제를 고려할 필요가 없습니다.
사용자 지정 Drawable은 그림과 동일하며 imagview를 설정할 수 있습니다.

커스텀 드로어블에 대한 기본 메서드 재정의.

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;
    }
}

여기에 이미지 설명 삽입
여기에 이미지 설명 삽입
여기에 이미지 설명 삽입
여기에 이미지 설명 삽입
일반적으로 드로어블을 사용자 정의한 다음 이미지 뷰를 넣으면 이미지 뷰는 자체 크기를 사용합니다. 그래서.

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

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

이 두 함수는 그림의 크기와 이미지 뷰의 크기를 나타냅니다.

다음으로 헤엄치는 물고기를 그려보자 좋은 전형적인 예이다 복습을 잊을 때마다 복습하면 된다 그리는 방법을 알려주는 것이 아니라 매 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과 관절 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);
    }
}

여기에 이미지 설명 삽입

두 번째 단계는 물고기가 제자리에서 흔들릴 수 있다는 것을 깨닫는 것입니다.
여기서 배운 아이디어는 값 애니메이션을 사용하여 머리와 꼬리 및 기타 부분의 스윙 각도와 주기를 제어하는 ​​것입니다.예를 들어 물고기의 머리 는 1초에 한 번 -30에서 30으로 회전하는 반면 물고기는 -50에서 50까지 5번 회전하는데, 회전 각도가 다를 뿐만 아니라 시간 주기도 다릅니다.
구현주기가 다르며 여기서는 삼각 함수가 사용됩니다.

우리는 삼각함수에서
sin(0~360)=-1 to 1이 여기서 순환이라는 것을 알고 있습니다. sin(0-720)=-1에서 1은 여기서 2주기이고
sin((0-360)*t)=-1에서 1입니다. 다음은 t 주기에 대해 -1에서 1입니다. 그래서

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();

이 값 애니메이션은 1초 내에 0에서 360까지 한 번 수행됩니다. 그래서

Math.sin(Math.toRadians(currentValue)는 1초 내에 -1에서 1까지 한 번 수행됩니다.
그런 다음
Math.sin(Math.toRadians(currentValue) k는 1초 내에 -k에서 k까지 한 번 수행됩니다.
마지막으로
Math.sin(Math .toRadians(currentValue
t) *k는 1초 내에 -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);
    }
}

여기에 이미지 설명 삽입

위에서 우리는 물고기가 제자리에서 헤엄치는 것을 완성했습니다.
그냥 드로어블을 커스터마이즈한 다음 활성 레이아웃에서 드로어블을 이미지 뷰로 설정했습니다. 그러면 이미지 뷰가 활동의 레이아웃에 배치됩니다.

다음으로 레이아웃을 사용자 정의하고 이미지 뷰를 이 컨트롤에 넣어 물고기의 물결을 구현한 다음 이 사용자 정의 드로어블을 이미지 뷰로 설정해야 합니다.

아래와 같이 코드 쇼:

여기에 이미지 설명 삽입

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);
    }
}

효과는 다음과 같습니다.
여기에 이미지 설명 삽입
프레임 레이아웃을 사용자 정의한 다음 프레임 레이아웃에 이미지 보기를 넣고 이 이미지 보기의 배경색을 녹색으로 설정했습니다. 그런 다음 그 안에 물고기 드로어블을 넣습니다.


여기에서 사용자 정의 레이아웃이 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);
    }
}

효과:

여기에 이미지 설명 삽입

그런 다음 마지막 단계가 온다.
물고기의 움직임과 회전을 제어합니다.
생각을 기억하는 것이 매우 중요하며 컨트롤의 이동과 회전은 경로를 사용하는 것이고 회전은 경로의 PathMeasure를 사용하는 것입니다. 경로를 사용하는 것이 매우 편리합니다.

경로 지점의 좌표만 설정하면 되고 나머지는 속성 애니메이션으로 직접 해결할 수 있습니다.

그건

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

이것이 가능한 이유. 컨트롤에 setX 및 setY 메서드가 있기 때문입니다. 이와 같이 이들의 좌표계는 이때의 절대좌표인 아버지를 기준으로 한다.
그런 다음 컨트롤의 오프셋을 실현하기 위해 경로의 경로 지점을 결정하기만 하면 됩니다.

다시, 생각을 기억하십시오.
컨트롤의 시작점은 실제로 컨트롤의 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();

물고기 수영에서 가장 큰 어려움은 물고기 수영 경로의 3차 베지어 경로에서 두 번째 제어점의 좌표를 구하는 것입니다.
이 좌표를 얻은 후. 경로에 따라 위의 움직임을 수행하는 것은 매우 간단합니다.

그렇다면 베지어의 두 번째 제어점을 찾는 방법은
다음과 같습니다.
여기에 이미지 설명 삽입

여기, 알려진. 점 o는 컨트롤의 무게 중심이고, 점 A는 컨트롤 포인트 1인 물고기 머리의 무게 중심이며, 점 B는 터치 포인트입니다. 지금은 C 좌표의 두 번째 제어점이며 C의 좌표는 우리가 ∠cod를 알고 있는 한 위에서 알려진 점 o 좌표, o에서 c까지의 거리를 사용할 수 있습니다. o에서 c까지의 각도에서 C의 좌표를 찾으십시오.
즉, ∠cod의 정도를 찾는 것이 핵심이다.

다음 두 회사에 따르면 ∠AOB와 ∠AOD 또는 ∠AOE를 알면 ∠cod의 차수를 얻을 수 있고 점 C의 좌표를 알 수 있습니다.

세 점의 좌표를 알고 세 점 사이의 각도를 찾으려면
수학 공식을 사용할 수 있습니다.
여기에 이미지 설명 삽입

 // 这个函数能够求得已知的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);

그래서 앞으로 경로를 이용하여 물체를 옵셋한 다음 베지어 곡선의 두 번째 제어점을 구해야 하고, 위 세 점의 좌표에 따라 끼인각을 구한 다음 두 번째 제어점을 구해야 합니다. 포인트는 각도 계산으로 얻습니다.

그런 다음 머리의 회전, 머리의 회전 및 경로의 PathMeasure가 있습니다. 경로의 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;
    }
}

여기에 이미지 설명 삽입

Supongo que te gusta

Origin blog.csdn.net/weixin_43836998/article/details/102647617
Recomendado
Clasificación