Dessin personnalisé - Dessinez un cas de poisson nageant pour apprendre l'idée d'une vue personnalisée.

Dessins personnalisés.

Dessins personnalisés. Ce n'est pas très différent de la vue personnalisée. Il est facile à utiliser et moins coûteux que la vue personnalisée. Les
drawables sans image prennent moins de place, ce qui peut réduire la taille de l'apk, et il n'est pas nécessaire de prendre en compte les problèmes de mesure et de mise en page.
Un Drawable personnalisé équivaut à une image, puis l'image peut y être définie.

Remplacements de méthode de base pour les Drawables personnalisés.

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

insérez la description de l'image ici
insérez la description de l'image ici
insérez la description de l'image ici
insérez la description de l'image ici
En règle générale, lorsque vous personnalisez le drawable et que vous placez ensuite l'imageview, l'imageview utilisera sa propre taille. donc.

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

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

Ces deux fonctions indiquent la taille de l'image et la taille de l'image vue.

Ensuite, dessinons un poisson nageur. C'est un bon exemple typique. Chaque fois que vous oubliez la revue, vous pouvez la revoir. Ce n'est pas pour vous dire comment dessiner, mais pour savoir comment obtenir cet effet de 0 à 1 tous les temps Une réflexion. Après avoir appris cet exemple, de nombreuses pratiques peuvent être réalisées avec cette réflexion :

L'effet obtenu est le suivant :

insérez la description de l'image ici

Tout d'abord, notre première étape est de dessiner le poisson sur une imaview,
puis de réaliser le balancement du poisson en place,
et ensuite c'est simple, et ensuite de réaliser le mouvement de la commande et la rotation du poisson selon le chemin.

La première étape consiste à dessiner le poisson.
insérez la description de l'image ici
La chose la plus importante à apprendre est la réflexion. Pour dessiner une image, aussi compliquée soit-elle, trouvez d'abord les coordonnées du centre de gravité de l'image. Notez que lorsque vous dessinez l'image in situ, toutes les coordonnées sont calculées avec le coin supérieur gauche du contrôle comme origine, et non avec une valeur absolue. coordonnées, c'est-à-dire le système de coordonnées C'est le système de coordonnées du contrôle. Après avoir trouvé les coordonnées du centre de gravité du graphique, en fonction du centre de gravité, un graphique est dessiné un par un. Lors du dessin, la pensée est basé sur une unité de valeur variable. Les modules suivants sont tous basés sur cette variable, donc si la valeur de la variable change, toutes les modifications seront apportées.

Par exemple, le poisson prend ici le centre de gravité du corps comme coordonnée et le rayon de la tête du poisson comme unité. Lors du réglage de la taille des autres blocs suivants, le rayon de la tête de poisson est utilisé comme unité.

 // 绘制鱼头的半径
    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;

Et lors du dessin un par un, la pensée est basée sur les coordonnées du centre de gravité de l'image. Connaissant les coordonnées du centre de gravité, la distance du centre de gravité au centre de gravité des autres modules, et le l'angle entre les deux points peut être calculé. Les coordonnées d'un autre point, puis les graphiques peuvent être dessinés avec les coordonnées d'un autre point.

La fonction suivante peut transmettre les coordonnées du point connu A et la distance du point A à B. Angle du point A au point B, renvoie les coordonnées du point 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);
    }

Le principe est :

insérez la description de l'image ici

insérez la description de l'image ici
Obtenez la longueur de ab et utilisez les coordonnées du point A pour trouver les coordonnées de B.
Les paramètres de Math.sin() et Math.cos() sont des radians. Les coordonnées sont comme en mathématiques.
Math.toRadians() Convertit les angles en radians.
Un cercle est de 360 ​​degrés, qui est également de 2π radians, soit 360°=2π

Par exemple. Après avoir dessiné le cercle de la tête de poisson. Si je veux dessiner le cercle de l'articulation 1, alors j'ai seulement besoin de connaître les coordonnées du cercle de l'articulation 1 pour dessiner le cercle de l'articulation 1, et à ce moment, je peux connaître les coordonnées du centre
de le cercle de la tête de poisson selon la fonction trigonométrique ci-dessus, la tête de poisson L'angle obtenu du centre du cercle au centre de l'articulation 1, et la distance entre les deux peuvent être obtenus à partir des coordonnées du cercle du joint 1, et le cercle du joint 1 peut être tracé.

Ce qui suit est de dessiner un poisson statique.

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

insérez la description de l'image ici

La deuxième étape consiste à réaliser que le poisson peut se balancer sur place. L'idée apprise ici est d'utiliser une animation de valeur pour contrôler les angles et les périodes d'oscillation de la tête et de la queue et d'autres parties. Par exemple, la tête d'un poisson tourne de -30 à 30 une fois par seconde, tandis que la queue d'
un le poisson tourne de -50 à 50 tourne 5 fois, non seulement l'angle de rotation est différent, mais aussi le cycle des temps est différent.
Le cycle de mise en œuvre est différent, on utilise ici des fonctions trigonométriques.


Nous savons que sin (0~360)=-1 à 1 dans les fonctions trigonométriques est un cycle ici. sin(0-720)=-1 à 1 voici 2 cycles alors
sin((0-360)*t)=-1 à 1. Voici -1 à 1 pour t cycles. Donc

sin (0~360)*k signifie qu'il y a un changement de cycle entre -k et k. Alors
sin((0-360)*t))*k change t périodes entre -k et k.

Parce que nous n'avons besoin que de définir deux variables t et k. Vous pouvez utiliser une animation de valeur pour vous rendre compte que différents départements ont des angles de rotation et des cycles différents.

comme suit

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

Cette animation de valeur est effectuée une fois de 0 à 360 en 1s. Donc

Math.sin(Math.toRadians(currentValue) est fait une fois de -1 à 1 en 1s.
Puis
Math.sin(Math.toRadians(currentValue) k est fait une fois de -k à k en 1s.
Enfin
Math.sin(Math .toRadians(currentValue
t) *k est de faire t fois de -k à k en 1s.

Par conséquent, on peut se rendre compte que l'angle du balancement de la tête est différent de celui de la queue, et la fréquence entre les deux est également différente

le code s'affiche comme ci-dessous ;

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

insérez la description de l'image ici

Ci-dessus, nous avons terminé le poisson nageant sur place.
Nous venons de personnaliser un drawable, puis de définir le drawable sur l'imageview dans la mise en page active. La vue d'image est ensuite placée dans la mise en page de l'activité.

Ensuite, nous devons personnaliser une mise en page et mettre une vue d'image dans ce contrôle pour réaliser l'ondulation de l'eau du poisson, puis définir ce dessin personnalisé sur la vue d'image.

code afficher comme ci-dessous:

insérez la description de l'image ici

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

L'effet est le suivant :
insérez la description de l'image ici
nous avons personnalisé une disposition de cadre, puis nous avons placé une vue d'image dans la disposition de cadre et défini la couleur d'arrière-plan de cette vue d'image sur vert. Ensuite, placez-y le poisson pouvant être dessiné.

Il est très important de faire attention ici
que la mise en page personnalisée hérite de FrameLayout ou de la mise en page fournie par d'autres systèmes. Il n'est pas appelé par défaut la méthode ondraw, vous devez donc utiliser

setWillNotDraw(false);

Définissez l'appel de méthode ondraw.

Réalisez ensuite l'effet des ondulations de l'eau lorsque vous cliquez.
Notre pensée conventionnelle l'est. Obtenez les coordonnées du point de contact. Utilisez ensuite l'animation d'attribut pour créer deux attributs pour le faire,
c'est-à-dire utilisez

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

Mais vous ne pouvez pas le faire du tout ici, l'objet que vous voulez utiliser est un cercle de points, qui est FrameLayout ici. Aucun objet ne peut être transmis à objectAnimator.
Bien sûr, vous pouvez définir l'animation ValueAnimator pour la définir, mais vous pouvez toujours utiliser l'animation ObjectAnimator ici. La raison pour laquelle l'animation d'attribut est utilisée ici est d'apprendre
l'essence d'ObjectAnimator. L'essence d'ObjectAnimator est d'utiliser la méthode de réflexion. C'est-à-dire appeler en permanence la méthode set de l'objet. Même si l'objet n'a pas cet attribut

Par exemple.

Dans ce FrameLayout, il n'y a pas d'attribut CircleRadius, mais vous n'avez que la méthode setCircleRadius dans cette classe. Ensuite vous pouvez

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

}

Ensuite, nous savons que l'essence d'objectAnimator est d'appeler en permanence la méthode set et de transmettre la valeur modifiée, il sera donc difficile de faire changer plusieurs attributs ensemble. Nous pouvons le faire comme ceci :

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

Le rayon a des incréments de 0 à 250.
La transparence passe de 100 à 0, c'est-à-dire de visible à invisible.

Le code complet est :

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

Effet:

insérez la description de l'image ici

Vient ensuite la dernière étape.
Contrôle le mouvement et la rotation du poisson.
Il est très important de se souvenir d'une pensée, le mouvement et la rotation du contrôle doivent utiliser le chemin, et la rotation consiste à utiliser le PathMeasure du chemin. Il est très pratique d'utiliser le chemin.

Vous n'avez qu'à définir les coordonnées du point du chemin, et le reste peut être résolu directement avec l'animation des attributs.

c'est

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

Pourquoi est-ce possible. Parce qu'il existe des méthodes setX et setY dans le contrôle. De cette façon, leur système de coordonnées est relatif à celui du père, qui est la coordonnée absolue à ce moment.
Ensuite, il vous suffit de déterminer le point du chemin du chemin pour réaliser le décalage du contrôle.

Encore une fois, souvenez-vous d'une pensée.
Le point de départ du contrôle est en fait le getX et le getY du contrôle, qui est le point dans le coin supérieur gauche du contrôle.
Le point final du contrôle est les coordonnées du point touché par le point de la main moins les coordonnées du centre de gravité du contrôle.

insérez la description de l'image ici

Prenez ce poisson par exemple.

          // 鱼的重心:相对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();

La plus grande difficulté dans la nage des poissons est d'obtenir les coordonnées du deuxième point de contrôle à partir du chemin de Bézier de troisième ordre du chemin de nage des poissons.
Après avoir obtenu cette coordonnée. Il est très simple de faire le mouvement ci-dessus en fonction du chemin.

Alors comment trouver le deuxième point de contrôle de Bézier ?
Voici d'autres formules mathématiques.
insérez la description de l'image ici

Ici, connu. Le point o est le centre de gravité de la commande, le point A est le centre de gravité de la tête de poisson, qui est le point de commande 1, et le point B est le point de contact. Ces 3 points sont tous connus, et ce que nous voulons savoir est maintenant le deuxième point de contrôle des coordonnées C, et les coordonnées de C tant que nous connaissons ∠cod, nous pouvons utiliser les coordonnées o du point connu ci-dessus, la distance de o à c. Trouver les coordonnées de C à partir de l'angle de o à c.
C'est-à-dire que la clé est de trouver le degré de ∠cod.

Selon les deux sociétés suivantes, tant que nous connaissons ∠AOB et ∠AOD ou ∠AOE, nous pouvons obtenir le degré de ∠cod et connaître la coordonnée du point C.

Et lorsque nous connaissons les coordonnées des trois points et que nous voulons trouver l'angle entre les trois points,
nous pouvons utiliser la formule mathématique
insérez la description de l'image ici

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

    }

En utilisant la fonction ci-dessus, nous savons

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



Alors ∠COD=∠AOE+∠AOC. Notez que les valeurs positives et négatives obtenues par la fonction ci-dessus ont été déterminées.
Après avoir connu ∠COD, vous pouvez le connaître avec des fonctions trigonométriques.

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

Ainsi, à l'avenir, le chemin est utilisé pour décaler l'objet, puis le deuxième point de contrôle de la courbe de Bézier doit être obtenu, et l'angle inclus est obtenu en fonction des coordonnées des 3 points ci-dessus, puis le deuxième contrôle point est obtenu par calcul d'angle.

Ensuite, il y a la rotation de la tête, la rotation de la tête et le PathMeasure du chemin. Utilisez le PathMeasur du chemin pour obtenir la valeur de tangente et utilisez la fonction pour obtenir l'angle et réglez-le sur l'angle de la tête de poisson. Étant donné que les autres parties du poisson sont dessinées en fonction de la tête de poisson, l'angle de la tête de poisson changera et d'autres parties changeront également en conséquence.

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

Enfin, à cause du temps pour nager. Le cycle d'oscillation de chaque partie du poisson s'accélérera, donc

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

        }
    });

Tous les codes à la fin

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

insérez la description de l'image ici

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43836998/article/details/102647617
conseillé
Classement