整体代码鱼的游动还是不太明白,抄的享学老师的代码,先这样吧
先上效果图:
画鱼的代码
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的使用大致熟练了