Android自定义控件绘制鱼,并且模拟鱼游动

整体代码鱼的游动还是不太明白,抄的享学老师的代码,先这样吧
先上效果图:
在这里插入图片描述

在这里插入图片描述
画鱼的代码

package com.lgj.xxkt.ui;

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;

import retrofit2.http.HEAD;

public class FishDrawable extends Drawable {
    public FishDrawable() {
        init();
    }

    private Path mPath;
    private Paint mPaint;
    private int OTHER_ALPHA = 110;
    // 鱼头的半径
    private float HEAD_RADIUS = 50;
    // 鱼的开始角度
    private float fishMainAngle = 90;
    // 身体长度
    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_radius = 0.7f * HEAD_RADIUS;
    // 中圆的半径
    private float middle_radius = 0.6f * big_radius;
    // 小圆的半径
    private float small_radius = 0.4f * middle_radius;
    // --寻找尾部中圆圆心的线长
    private final float FIND_MIDDLE_CIRCLE_LENGTH = big_radius * (0.6f + 1);
    // --寻找尾部小圆圆心的线长
    private final float FIND_SMALL_CIRCLE_LENGTH = middle_radius * (0.4f + 2.7f);
    // --寻找大三角形底边中心点的线长
    private final float FIND_TRIANGLE_LENGTH = middle_radius * 2.7f;

    private float currentValue;

    private PointF middlePoint;
    PointF headPoint;
    public PointF getCenterPoint(){
        return middlePoint;
    }
    public PointF getControlOne(){
        return headPoint;
    }
    public float getHeadRadius(){
        return HEAD_RADIUS;
    }
    private float frequence = 1f;
    public void setFrequence(float frequence) {
        this.frequence = frequence;
    }

    public float getFishMainAngle() {
        return fishMainAngle;
    }

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

    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);
        // 鱼的中心,PointF 和 Point 的区别是,PointF是float类型
        middlePoint = new PointF(4.19f * HEAD_RADIUS, 4.19f * HEAD_RADIUS);
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 3600f);
        animator.setDuration(15 * 1000);
        // 重复的模式: 重新开始
        animator.setRepeatMode(ValueAnimator.RESTART);
        // 重复的次数,无限次
        animator.setRepeatCount(ValueAnimator.INFINITE);
        // 插值器,匀速
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currentValue = (float) valueAnimator.getAnimatedValue();
                invalidateSelf();
            }
        });
        animator.start();
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        // 每一个部分都以鱼的中心位置的相对坐标来的
        // 鱼头的计算坐标,x = cosA * c   y = sinA * c
        // 角度转化成弧度 Math.toRadians(), 圆是360度,也就是2π,即 360度=2π
        float fishAngle = (float) (fishMainAngle + Math.sin(Math.toRadians(currentValue) * 10 ));// 左右10度摆动
        // 鱼头的圆心坐标
        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
        makeSegment(canvas,bodyBottomCenterPoint,fishAngle);

        PointF middleCenterPoint = calculatePoint(bodyBottomCenterPoint, FIND_MIDDLE_CIRCLE_LENGTH, fishAngle-180);
        //画节支 2
        makeSegment2(canvas,middleCenterPoint,fishAngle);
        // 画尾巴 也就是大三角形
        makeTriangel(canvas,middleCenterPoint,fishAngle);
        // 画尾巴 也就是小三角形
        makeTriangel2(canvas,middleCenterPoint,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_radius, fishAngle + 90);
        PointF bottomRightPoint = calculatePoint(bodyBottomCenterPoint, big_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);
        mPath.lineTo(topLeftPoint.x,topLeftPoint.y);
        canvas.drawPath(mPath,mPaint);
    }

    private void makeTriangel(Canvas canvas, PointF middleCenterPoint, float fishAngle) {
        float triangelAngle = (float) (fishAngle + Math.cos(Math.toRadians(currentValue * 1.5)) * 25);// 左右25度摆动
        // 三角形底边的中心坐标
        PointF centerPoint = calculatePoint(middleCenterPoint, FIND_TRIANGLE_LENGTH, triangelAngle - 180);
        // 底边其实就是大圆的半径
        PointF leftPoint = calculatePoint(centerPoint, big_radius, triangelAngle + 90);
        PointF rightPoint = calculatePoint(centerPoint, big_radius, triangelAngle - 90);
        mPath.reset();
        mPath.moveTo(leftPoint.x,leftPoint.y);
        mPath.lineTo(rightPoint.x,rightPoint.y);
        mPath.lineTo(middleCenterPoint.x,middleCenterPoint.y);
        canvas.drawPath(mPath,mPaint);
    }

    private void makeTriangel2(Canvas canvas, PointF middleCenterPoint, float fishAngle) {
        float triangelAngle = (float) (fishAngle + Math.cos(Math.toRadians(currentValue * 1.5)) * 25);// 左右25度摆动
        // 三角形底边的中心坐标
        PointF centerPoint = calculatePoint(middleCenterPoint, FIND_TRIANGLE_LENGTH-10, triangelAngle - 180);
        // 底边其实就是大圆的半径
        PointF leftPoint = calculatePoint(centerPoint, big_radius-20, triangelAngle + 90);
        PointF rightPoint = calculatePoint(centerPoint, big_radius-20, triangelAngle - 90);
        mPath.reset();
        mPath.moveTo(leftPoint.x,leftPoint.y);
        mPath.lineTo(rightPoint.x,rightPoint.y);
        mPath.lineTo(middleCenterPoint.x,middleCenterPoint.y);
        canvas.drawPath(mPath,mPaint);

    }

    private void makeSegment(Canvas canvas, PointF bottomCenterPoint, float fishAngle) {
        float segmentAngle = (float) (fishAngle + Math.cos(Math.toRadians(currentValue * 1.5)) * 15);// 左右15度摆动
        // 梯形上低圆心
        PointF upperCenterPoint =
                calculatePoint(bottomCenterPoint,FIND_MIDDLE_CIRCLE_LENGTH,segmentAngle-180);
        PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, big_radius, segmentAngle + 90);
        PointF bottomRightPoint = calculatePoint(bottomCenterPoint, big_radius, segmentAngle - 90);
        PointF upperLeftPoint = calculatePoint(upperCenterPoint, middle_radius, segmentAngle + 90);
        PointF upperRightPoint = calculatePoint(upperCenterPoint, middle_radius, segmentAngle - 90);
        // 画大圆
        canvas.drawCircle(bottomCenterPoint.x,bottomCenterPoint.y,big_radius,mPaint);
        // 画中圆
        canvas.drawCircle(upperCenterPoint.x,upperCenterPoint.y,middle_radius,mPaint);
        // 画梯形 reset 就是把 path 之前保存的路径清零
        mPath.reset();
        mPath.moveTo(bottomLeftPoint.x,bottomLeftPoint.y);
        mPath.lineTo(bottomRightPoint.x,bottomRightPoint.y);
        mPath.lineTo(upperRightPoint.x,upperRightPoint.y);
        mPath.lineTo(upperLeftPoint.x,upperLeftPoint.y);
        canvas.drawPath(mPath,mPaint);
    }


    private void makeSegment2(Canvas canvas, PointF middleCenterPoint, float fishAngle) {
        // 注意细节,
        float segmentAngle = (float) (fishAngle + Math.cos(Math.toRadians(currentValue * 1.5)) * 25);// 左右25度摆动
        // 梯形上低圆心
        PointF upperSmallCenterPoint =
                calculatePoint(middleCenterPoint,FIND_SMALL_CIRCLE_LENGTH,segmentAngle-180);
        PointF bottomLeftPoint = calculatePoint(middleCenterPoint, middle_radius, segmentAngle + 90);
        PointF bottomRightPoint = calculatePoint(middleCenterPoint, middle_radius, segmentAngle - 90);
        PointF upperLeftPoint = calculatePoint(upperSmallCenterPoint, small_radius, segmentAngle + 90);
        PointF upperRightPoint = calculatePoint(upperSmallCenterPoint, small_radius, segmentAngle - 90);
        // 画小圆
        canvas.drawCircle(upperSmallCenterPoint.x,upperSmallCenterPoint.y,small_radius,mPaint);
        // 画梯形 reset 就是把 path 之前保存的路径清零
        mPath.reset();
        mPath.moveTo(bottomLeftPoint.x,bottomLeftPoint.y);
        mPath.lineTo(bottomRightPoint.x,bottomRightPoint.y);
        mPath.lineTo(upperRightPoint.x,upperRightPoint.y);
        mPath.lineTo(upperLeftPoint.x,upperLeftPoint.y);
        canvas.drawPath(mPath,mPaint);
    }


    // 画鱼鳍
    private void makeFins(Canvas canvas, PointF startPoint, float fishAngle,boolean isRight) {
        // 鱼鳍的终点,也就是二阶贝塞尔曲线的终点,还差一个控制点
        PointF endPoint = calculatePoint(startPoint,fins_length,fishAngle-180);
        // 控制点
        PointF controlPoint = calculatePoint(startPoint,
                fins_length * 1.8f,
                isRight ? fishAngle - 115 : fishAngle + 115);
        // 绘制
        mPath.reset();
        // 将画笔移动到起始点
        mPath.moveTo(startPoint.x,startPoint.y);
        // 这个就是画二阶贝塞尔曲线的
        mPath.quadTo(controlPoint.x,controlPoint.y,endPoint.x,endPoint.y);
        canvas.drawPath(mPath,mPaint);
    }

    // startPoint 起始点坐标,length 要求的点到起始点的距离---也就是线长,angle 当前的朝向角度,如果是0 度的话那么就是水平朝右
    // 计算每个部位相对于中心点的坐标
    public PointF calculatePoint(PointF startPoint, float length, float angle) {
        // x 坐标
        float x = (float) (Math.cos(Math.toRadians(angle)) * length);
        // y 坐标
        float y = (float) (Math.sin(Math.toRadians(angle - 180)) * length);
        return new PointF(startPoint.x + x, startPoint.y + y);
    }

    // 确定整个Drawable的大小,ImageView 一般设置成Wrap_content,然后又下面来确定
    @Override
    public int getIntrinsicWidth() {
        return (int) (8.38 * HEAD_RADIUS);
    }

    // 确定整个Drawable的大小
    @Override
    public int getIntrinsicHeight() {
        return (int) (8.38 * HEAD_RADIUS);
    }

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

点击实现鱼游动的代码,但是目前还没有搞明白三阶贝塞尔曲线,第二个控制点的计算方案,这块确实有点难度

package com.lgj.xxkt.ui;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PointF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.RelativeLayout;

import androidx.annotation.RequiresApi;

// 实现点击和小于的游动
public class FishRelativeLayout extends RelativeLayout {
    private int OTHER_ALPHA = 110;
    Paint mPaint;
    FishDrawable fishDrawable;
    private float ripple;//水波纹
    ImageView ivFish;

    public FishRelativeLayout(Context context) {
        this(context, null);
    }

    public FishRelativeLayout(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public FishRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        // ViewGroup 默认是不执行onDraw方法的
        setWillNotDraw(false);// 设置成false之后,就执行onDraw方法
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setARGB(OTHER_ALPHA, 244, 92, 71);
        mPaint.setDither(true);
        // 这个 画笔是为了画水波纹
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);
        ivFish = new ImageView(context);
        LayoutParams layoutParams = new LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
        ivFish.setLayoutParams(layoutParams);
        fishDrawable = new FishDrawable();
        ivFish.setImageDrawable(fishDrawable);
        addView(ivFish);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(touchX, touchY, ripple * 150, mPaint);
    }

    float touchX;
    float touchY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        touchX = event.getX();
        touchY = event.getY();
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(this, "ripple", 0, 1f).setDuration(1000);
        objectAnimator.start();
        makeTrail();
        return super.onTouchEvent(event);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void makeTrail() {
        // 这个是鱼重心的相对坐标
        PointF fishRelativeMiddle = fishDrawable.getCenterPoint();
        // 鱼重心的绝对坐标
        PointF fishMiddle = new PointF(ivFish.getX() + fishRelativeMiddle.x, ivFish.getY() + fishRelativeMiddle.y);
        // 鱼头圆心的坐标
        PointF headPoint = fishDrawable.getControlOne();
        // 鱼头绝对坐标,也就是控制点 1
        PointF fishHead = new PointF(headPoint.x + ivFish.getX(), headPoint.y + ivFish.getY());
        // 结束点坐标
        PointF touch = new PointF(touchX, touchY);
        // 控制点2
        float angle = includeAngle(fishMiddle, fishHead, touch) / 2;
        float delta = includeAngle(fishMiddle, new PointF(fishMiddle.x + 1, fishMiddle.y), fishHead);
        // 控制点2 的坐标
        PointF controlPoint = fishDrawable.calculatePoint(fishMiddle,
                fishDrawable.getHeadRadius() * 1.6f, angle + delta);

        Path path = new Path();
        path.moveTo(fishMiddle.x - fishRelativeMiddle.x, fishMiddle.y - fishRelativeMiddle.y);
        path.cubicTo(fishHead.x - fishRelativeMiddle.x, fishHead.y - fishRelativeMiddle.y,
                controlPoint.x - fishRelativeMiddle.x, controlPoint.y - fishRelativeMiddle.y,
                touchX - fishRelativeMiddle.x, touchY - fishRelativeMiddle.y);
        // 用属性动画,改变鱼的 X 、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.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 float getRipple() {
        return ripple;
    }
    public void setRipple(float ripple) {
        this.ripple = ripple;
        // 画笔的透明度从 100 到 0
        mPaint.setAlpha((int) (100 * (1 - ripple)));
        invalidate();
    }
}

主要的收获,对path路径api的使用大致熟练了

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值