如何应对Android面试官->实战高级UI,用自定义View画一条锦鲤(下)

Qt之使用QGraphicsEffect实现阴影、模糊、着色和透明效果 QGraphicsEffect是所有图像特效的子类,其四个子类分别提供了四种标准特效 ☆QGraphicsBlurEffect - blurs the item by a given radius ☆QGraphicsDropShadowEffect - renders a dropshadow behind the item ☆QGraphicsColorizeEffect - renders the item in shades of any given color ☆QGraphicsOpacityE 阅读详情

前言


上一章我们用自定义View绘制了一条小鱼,本章我们让这条小鱼游动起来;

涉及的知识点

infoflow 2024-02-19 10-05-04.png

小鱼的原地摆动


实现小鱼的摆动,我们可以通过属性动画 ValueAnimator 来实现,这里先简单介绍下属性动画

属性动画(ValueAnimator)

  1. ValueAnimator 没有重绘,所以需要自己调用 addUpdateListener 方法,结合 AnimatorUpdateListener 使用;
  2. 操作的对象的属性不一定要有 get set 方法;
  3. 默认插值器为 AccelerateDecelerateInterpolator;

基础用法

public void init() {
    //
    ...
    // 
    
    // 动画周期
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1f);
    // 动画时长
    valueAnimator.setDuration(1000);
    // 重复模式,重新开始
    valueAnimator.setRepeatMode(ValueAnimator.RESTART);
    // 重复次数 无线循环
    valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
            animatedValue = animator.getAnimatedValue();
            invalidateSelef();
        }
    });
    valueAnimator.start();
}

鱼头的摆动

上一章节我们有讲到,使用

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

来控制鱼的朝向,那么我们就可以通过它来让我们的鱼头摆动起来,假设我们让鱼头摆动 10 度左右,只需要让 fishMainAngle * animatedValue * 10 就可以了,我们来运行看下效果

@Override
public void draw(@NonNull Canvas canvas) {
    float fishAngle = fishMainAngle + animatedValue * 10;
}

SVID_20240219_103258_1.gif

可以看到,鱼头已经摆动了起来,但是整体还不符合我们的预期,我们接着来调整鱼尾的摆动;

鱼尾的摆动

实现鱼尾的摆动,思路和鱼头一样,只需要让鱼尾的角度改变就行;

/**
 * 画节肢
 *
 * @param canvas               画布
 * @param bottomCenterPoint    大圆中心点坐标
 * @param findMiddleCircleLength 中圆长度
 * @param bigCircleRadius 大圆半径
 * @param middleCircleRadius 中圆半径
 * @param fishAngle 角度
 */
private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float findMiddleCircleLength,
                           float bigCircleRadius, float middleCircleRadius, float fishAngle, boolean hasBigCircle) {

    float segmentAngle = fishAngle + animatedValue * 20;
    // 计算中圆坐标(梯形上底圆的圆心)
    PointF upperCenterPoint = calculatePoint(bottomCenterPoint, findMiddleCircleLength, fishAngle - 180);
    // 计算梯形的四个点
    PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, bigCircleRadius, segmentAngle + 90);
    PointF bottomRightPoint = calculatePoint(bottomCenterPoint, bigCircleRadius, segmentAngle - 90);
    PointF upperLeftPoint = calculatePoint(upperCenterPoint, middleCircleRadius, segmentAngle + 90);
    PointF upperRightPoint = calculatePoint(upperCenterPoint, middleCircleRadius, segmentAngle - 90);
    if (hasBigCircle) {
        // 画大圆
        canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigCircleRadius, mPaint);
    }
    // 画小圆
    canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, middleCircleRadius, 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;
}

我们运行看下效果:

SVID_20240219_104208_1.gif

可以看到,鱼尾也能摆动了起来,但是如果我们想让鱼头和鱼尾不同的频率,显然现有的一个属性动画还是不够的,那么我们如何实现让鱼头和鱼尾不同的频率呢?

我们可以使用两个 ValueAnimator 来分别实现鱼头和鱼尾的摆动,但是我们能使用一个 ValueAnimator 来实现鱼头和鱼尾不同频率的摆动吗?让我们来一探究竟;

鱼头鱼尾不同频率的摆动

上一章我们讲了正弦函数,正弦函数的一个周期是360,如果我们的属性动画的周期是 ValueAnimator.ofFloat(0,3600) 那么就相当于是1s 10个周期,如果我们给 animatedValue 取一个正弦函数,并且让它的周期发生改变,那么我们的鱼头和鱼尾的摆动频率就不一样了;

sin(animatedValue) ==> animatedValue 取值是(0-360) 1s执行1次

sin(animatedValue * 2) ==> animatedValue 取值是(0-360)1s执行2次

也就是说 sin(0-360) 的取值是 -1 ~ 1 这样的一个范围,那么我们的鱼头摆动的角度就是 -10 ~ 10 这样的一个范围,鱼头就实现了从 -10 到 10 的一个左右的摆动了;

也就是说 sin((0-360)X2) 的取值是 -1 ~ 1 & -1 ~ 1 这样的一个范围,那么我们的鱼头摆动的角度就是 -10 ~ 10 这样的一个范围,鱼头就实现了从 -10 到 10 的一个左右的摆动了,并且1s内摆动了2次;

同理,鱼尾也是这样的一个范围,我们只需要调整这个sin(x)的值,让它的结果值不一样,那么摆动的频率也就不一样了;

节肢是分为节肢1和节肢2,节肢1和节肢2的摆动频率也不一样,并且节肢1的摆动,带动了节肢2的摆动,根据正余弦函数 sin 和 cos 正好相差 90,节肢1和节肢2可以采用 sin 和 cos 来实现方向和频率上的变化,具体实现如下:

// 动画周期
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 360f);
@Override
public void draw(@NonNull Canvas canvas) {
    float fishAngle = (float) (fishMainAngle + Math.sin(Math.toRadians(animatedValue)) * 10);
}

节肢1和节肢2的变化

/**
 * 画节肢
 *
 * @param canvas               画布
 * @param bottomCenterPoint    大圆中心点坐标
 * @param findMiddleCircleLength 中圆长度
 * @param bigCircleRadius 大圆半径
 * @param middleCircleRadius 中圆半径
 * @param fishAngle 角度
 */
private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float findMiddleCircleLength,
                           float bigCircleRadius, float middleCircleRadius, float fishAngle, boolean hasBigCircle) {

    float segmentAngle;
    if (hasBigCircle) {
        segmentAngle = (float) (fishAngle + Math.cos(Math.toRadians(animatedValue * 1.5)) * 15);
    } else {
        segmentAngle = (float) (fishAngle + Math.sin(Math.toRadians(animatedValue * 1.5)) * 20);
    }
    // 计算中圆坐标(梯形上底圆的圆心)
    PointF upperCenterPoint = calculatePoint(bottomCenterPoint, findMiddleCircleLength, segmentAngle - 180);
    // 计算梯形的四个点
    PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, bigCircleRadius, segmentAngle + 90);
    PointF bottomRightPoint = calculatePoint(bottomCenterPoint, bigCircleRadius, segmentAngle - 90);
    PointF upperLeftPoint = calculatePoint(upperCenterPoint, middleCircleRadius, segmentAngle + 90);
    PointF upperRightPoint = calculatePoint(upperCenterPoint, middleCircleRadius, segmentAngle - 90);
    if (hasBigCircle) {
        // 画大圆
        canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigCircleRadius, mPaint);
    }
    // 画小圆
    canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, middleCircleRadius, 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;
}

我们运行看下效果:

SVID_20240219_111823_1.gif

可以看到 鱼头、鱼尾不同的摆动频率;

三角形鱼尾的摆动

接下来我们来让三角形也摆动起来,三角形的摆动频率是和节肢2的摆动频率一样的,所以我们可以让这两个值保持一致来看下效果

private void makeTriangle(Canvas canvas, PointF startPoint, float findTriangleLength,
                          float bigCircleRadius, float fishAngle) {
    float triangleAngle = (float) (fishAngle + Math.sin(Math.toRadians(animatedValue * 1.5)) * 20);
    // 三角形底边中心点坐标
    PointF centerPoint = calculatePoint(startPoint, findTriangleLength, triangleAngle);
    // 三角形底边两点
    PointF leftPoint = calculatePoint(centerPoint, bigCircleRadius, triangleAngle + 90);
    PointF rightPoint = calculatePoint(centerPoint, bigCircleRadius, 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);
}

SVID_20240219_124315_1.gif

可以看到,三角形的尾巴也摆动了起来;但是效果一卡一卡的,我们来优化一下;首先我们来看下为什么会一卡一卡的,这个是因为动画结束之后并没有回到起点开始,这个是因为我们在频率变化的地方乘以了1.5
也就是 360 * 1.5 / 360 = 1.5,这个结果不是整数,也就是说不是 360 的整数倍,我们需要把它设置成 360 的整数倍,修改如下

ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 720f);

720 * 1.5 / 360 = 3 是360的整数倍,我们来看下效果:

SVID_20240219_125055_1.gif

卡顿效果没有了;

小鱼的三角形尾巴旋转

这个就比较简单了,周期性的改变这个值即可

// 画三角形
float findTriangleAngle = (float) Math.abs(Math.sin(Math.toRadians(animatedValue * 1.5)) * BIG_CIRCLE_RADIUS);
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH, findTriangleAngle, fishAngle);
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH - 10,
        findTriangleAngle - 20, fishAngle);

运行看下效果:

SVID_20240219_132806_1.gif

三角形尾巴旋转了起来;

小鱼的游动


我们接下来实现小鱼的游动,想要实现小鱼的游动,我们需要把这个 Drawable 放到 ViewGroup 中,并且在这个 ViewGroup 中实现点击画一个水波纹,同时让小鱼游动过去,我们先来搭建一个简单的架子;

public class FishLayout extends RelativeLayout {

    private Paint mPaint;
    private ImageView ivFish;
    private Fish fishDrawable;


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

    public FishLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FishLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        init(context);
    }

    private void init(Context context) {
        // ViewGroup 默认不执行 onDraw 方法
        setWillNotDraw(false);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(8);

        ivFish = new ImageView(context);
        LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        fishDrawable = new Fish();
        ivFish.setLayoutParams(layoutParams);
        ivFish.setImageDrawable(fishDrawable);
        addView(ivFish);
    }
}

水波纹的绘制

水波纹的绘制,我们同样采用属性动画(ObjectAnimator)来实现,区别于 ValueAnimator,ObjectAnimator 在使用的过程中要求属性必须实现 get 和 set 方法;然后我们接入 canvas 的 drawCircle 方法来实现水波纹的绘制

private float touchX;
private float touchY;
private float ripple;
private int alpha;

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

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

public float getRipple() {
    return ripple;
}

public void setRipple(float ripple) {
    alpha = (int) (100 * (1 - ripple));
    this.ripple = ripple;
}

我们运行看下效果:

SVID_20240219_135332_1.gif

我们实现了点击水波纹的效果;

小鱼的移动

小鱼的游动,其实是一个三阶贝塞尔曲线,三阶贝塞尔曲线,需要四个控制点,我们这里采用鱼头圆心点,鱼的身体中心点,点击点,以及这三个点击组成的夹角的一半

infoflow 2024-02-19 14-02-33.png

也就是这四个点,我们来分别计算这四个点的坐标,然后绘制三阶贝塞尔曲线;这里我们就用到了向量夹角的计算公式

向量的夹角公式计算夹角cosAOB = (OA x OB)/(|OA|*|OB|)其中 OA x OB 是向量的数量积,计算过程如下:

OA = (Ax-Ox,Ay-Oy)

OB = (Bx-Ox,By-Oy)

OA x OB = (Ax-Ox)(Bx-Ox)+(Ay-Oy)x(By-Oy)

|OA|表示线段OA的模即OA的长度

所以实现如下:

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

我们还需要决定鱼的转向,也就是说点击点在鱼的右侧,鱼头转向右边游动,点击在鱼的左侧,鱼头转向左边游动;

鱼朝哪边转向.png

也就是说:我们以AO中线为界限,分为四个区域来看

infoflow 2024-02-19 16-19-41.png

也就是 鱼的右边小于0,鱼的左边大于0,所以计算如下:

// 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 0f;
    } else {
        return 180f;
    }
} else {
    if (direction > 0) {
        return -angleAOB;
    } else {
        return angleAOB;
    }
}

所以我们的起始点坐标为:

// 鱼的重心,相当于ImageView的坐标
PointF fishRelativeMiddle = fishDrawable.getMiddlePoint();
// 鱼的重心,绝对坐标 -- 起始点坐标
PointF fishMiddle = new PointF(ivFish.getX() + fishRelativeMiddle.x, ivFish.getY() + fishRelativeMiddle.y);

控制点1的坐标为:

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

点击点坐标,结束点坐标:

// 点击坐标 -- 结束点坐标
PointF touch = new PointF(touchX, touchY);
float angle = includeAngle(fishMiddle, fishHead, touch) / 2;
float delta = includeAngle(fishMiddle, new PointF(fishMiddle.x + 1, fishMiddle.y), fishHead);

控制点2的坐标为:

// 控制点2的坐标
PointF controlPoint = fishDrawable.calculatePoint(fishMiddle, fishDrawable.getHEAD_RADIUS() * 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);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
objectAnimator.setDuration(2000);
objectAnimator.start();

小鱼移动的完整实现如下:

private void move() {
    // 鱼的重心,相当于ImageView的坐标
    PointF fishRelativeMiddle = fishDrawable.getMiddlePoint();
    // 鱼的重心,绝对坐标 -- 起始点坐标
    PointF fishMiddle = new PointF(ivFish.getX() + fishRelativeMiddle.x, ivFish.getY() + fishRelativeMiddle.y);
    // 鱼头圆心坐标 -- 控制点1坐标
    PointF headPoint = fishDrawable.getHeadPoint();
    PointF fishHead = new PointF(ivFish.getX() + headPoint.x, ivFish.getY() + headPoint.y);
    // 点击坐标 -- 结束点坐标
    PointF touch = new PointF(touchX, touchY);
    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.getHEAD_RADIUS() * 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);
    ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ivFish, "x", "y", path);
    objectAnimator.setDuration(2000);
    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));
    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 0f;
        } else {
            return 180f;
        }
    } else {
        if (direction > 0) {
            return -angleAOB;
        } else {
            return angleAOB;
        }
    }
}

在 onTouchEvent 中调用这个 move 方法

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

    move();
    return super.onTouchEvent(event);
}

之前设置的鱼的半径比较大,我们调小一些,方便看一屏下看运行效果

/**
 * 鱼的长度值
 */
// 绘制鱼头的半径
private float HEAD_RADIUS = 50;

我们来运行看下效果:

SVID_20240219_182339_1.gif

可以看到,我们实现了小鱼的点击移动

小鱼游动的时候,鱼尾摆动频率增加

接下来我们来实现,点击移动的时候,鱼尾的摆动频率增加,这里依然借助于属性动画

objectAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
    }

    @Override
    public void onAnimationStart(Animator animation) {
        super.onAnimationStart(animation);
    }
});

我们在游动动画开始的时候,给鱼尾的值增加一个倍数,在所有使用 animatedValue 的地方使用这个倍数即可

float fishAngle = (float) (fishMainAngle + Math.sin(Math.toRadians(animatedValue * frequence)) * 10);
float findTriangleAngle = (float) Math.abs(Math.sin(Math.toRadians(animatedValue * 1.5 * frequence)) * BIG_CIRCLE_RADIUS);
float triangleAngle = (float) (fishAngle + Math.sin(Math.toRadians(animatedValue * 1.5 * frequence)) * 20);
if (hasBigCircle) {
    segmentAngle = (float) (fishAngle + Math.cos(Math.toRadians(animatedValue * 1.5 * frequence)) * 15);
} else {
    segmentAngle = (float) (fishAngle + Math.sin(Math.toRadians(animatedValue * 1.5 * frequence)) * 20);
}

然后在我们动画开始的时候,调大这个值,动画结束的时候恢复这个值

objectAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        fishDrawable.setFrequence(1f);
    }

    @Override
    public void onAnimationStart(Animator animation) {
        super.onAnimationStart(animation);
        fishDrawable.setFrequence(3f);
    }
});

我们运行看下效果:

SVID_20240219_183151_1.gif

可以看到在游动的时候,鱼尾的摆动频率加快了;

鱼头转向

接下来我们来实现鱼的转向,在游动的时候先转向,然后游到点击的位置;鱼头的转向其实也是一个贝塞尔曲线

图片 1.png

转向的实现其实就是鱼头的切线轨迹和游动路线轨迹重合,鱼头切线变化的角度和路径的角度保持一致,我们就能实现鱼头的转向了;

而 切线其实就是我们的 tan 值;切线值的获取需要用到系统提供的 PathMeasure;

final PathMeasure pathMeasure = new PathMeasure(path, false);
final float[] tan = new float[2];
objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        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);
    }
});

我们运行看下效果:

12e0e065-7315-43f7-8aec-6e81a39a9397.gif

我们实现了点击的时候鱼的转向逻辑;

好了,到这里我们就实现了开篇的时候的完整逻辑;

下一章预告


Handler 原理解析;

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~~

android自定义炫酷UI-灵动的锦鲤 通过自定义android高级UI实现了一种类似小鱼游泳的炫酷效果,自定义drawable、自定义layout、属性动画的使用、点击事件的处理、水波纹效果、贝塞尔曲线的使用等等,实现的小鱼不仅会动,还能根据鼠标点击的位置进行游动,十分灵动! 立即下载

相关推荐

Android 动画 游动的鱼

Android 动画 游动的鱼 精品动画Android 动画 游动的鱼 精品动画

如何应对Android面试官->实战高级UI,用自定义View画一条锦鲤(上)

如何用自定义View画一条鱼,其中涉及到哪些知识点?我们先上效果图:涉及的知识点:整体可以分为三大步骤。

BASK2311的博客 1057

自定义View_灵动的锦鲤

自定义View_灵动的锦鲤

Android UI实践 —— 游动的锦鲤

看文章时无意间发现了一个很有趣的动画效果,于是自己动手实现了一下。

tmacfrank 1061

自定义Drawable实现灵动的红鲤鱼动画

自定义Drawable实现灵动的红鲤鱼动画,附:算法计算。

深入AOSP构建系统:从BUILD_FINGERPRINT的生成逻辑,看如何定制你的Android设备标识

本文深入解析AOSP构建系统中的设备指纹生成机制,详细介绍了BUILD_FINGERPRINT的生成逻辑及其在Android设备中的关键作用。通过分析多个指纹属性的区别与联系,以及如何定制设备标识,帮助开发者更好地理解和掌握Android系统构建过程。

weixin_27017211的博客 217

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.

weixin_39413778的博客 562

Dell Precision 7920 Tower开机点不亮?别慌,看电源灯闪烁密码快速定位故障(附BIOS恢复指南)

本文详细解析了Dell Precision 7920 Tower工作站开机点不亮时的电源灯闪烁密码,帮助用户快速定位故障。从内存问题到BIOS恢复,再到CPU配置错误,提供了全面的诊断和修复指南,并附有实用的维护建议和工具推荐。

weixin_30892037的博客 482

android自定义Drawable实现炫酷UI-锦鲤游泳效果

一、实现效果: 当点击屏幕的时候,屏幕中的锦鲤会身体摆动并且游到屏幕点击处,如下图: 效果分析: 1、小鱼的身体各个部件都是简单的半透明几何图形。 2、各个部件都可以活动。 3、从头到尾方向的部件摆动幅度越来越大,频率越来越高。 二、实现思路: 1、实现小鱼的绘制,创建一个自定义Drawable,把小鱼的形状画出来,然后放到ImageView上面显示。 2、实现小鱼的原地摆动,让小鱼可以在原地摆动。 3、实现小鱼的点击游动。 三、自定义Drawable 1、drawable

Jesiah的博客 3326

语义通信理论必看论文:Towards a Theory of Semantic Communication

了解语义通信发展必看的一篇文章

一去不复返的通信er 3924

25、量子计算:中性原子与约瑟夫森结量子比特

本文详细探讨了中性原子和约瑟夫森结两种量子比特在量子计算中的应用。中性原子量子比特利用光晶格实现量子态的操控,具有长相干时间的优势,但面临可扩展性和多量子比特操作的挑战;而约瑟夫森结量子比特基于超导技术,结合半导体制造工艺,具有低耗散和潜在的高可扩展性,但在参数控制和环境干扰方面存在难题。文章还比较了这两种方案的性能、应用场景及未来发展趋势,展望了它们在量子计算发展中的潜力与突破方向。

ol789012的博客 211

自定义View——游动锦鲤实践

目录自定义View——游动锦鲤实践布局文件使用布局——MainAvtivity主要实现——FishDrawable 不积跬步,无以至千里;不积小流,无以成江海。要沉下心来,诗和远方的路费真的很贵! 自定义View——游动锦鲤实践 一个对于自定义View的尝试,主要用于尝试熟悉自定义View的实现过程,有些小瑕疵。 布局文件 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.Constrain

小叮当不懒的博客 806

21、空间数据处理中的克里金方法及应用

本文介绍了空间数据处理中的克里金方法,特别是带外部漂移的克里金法(KED)及其在遥感数据融合和支持变化问题中的应用。讨论了克里金方法的基本原理、优缺点以及在不同空间分辨率遥感图像融合中的具体操作流程。同时,还涉及了地统计数据融合和支持变化问题的解决方案,包括块克里金法、面到点克里金法和多边形克里金法等。

gold的博客 86

实现锦鲤动画

锦鲤动画 分析效果: 小鱼的身体各个部件都是简单的半透明几何图形 各个部件都可以活动 从头到尾方向的部件摆动幅度越来越大、频率越来越高 实现步骤: 实现小鱼的绘制 实现小鱼的原地摆动 实现小鱼点击游动 分解图 Drawable是什么? 一种可以在Canvas上进行绘制的抽象的概念 颜色、图片等都可以是一个Drawable Drawable可以通过XML定义,或者通过代码创建 Android中Drawable是一个抽象类,每个具体的Drawable都是其子类 Drawable的优点

qq_30621333的博客 458

R语言中的Gamma分布函数

在R语言中,我们可以使用最大似然估计(Maximum Likelihood Estimation,MLE)或最小二乘估计(Least Squares Estimation,LSE)等方法来估计Gamma分布的参数。本文介绍了在R语言中使用Gamma分布函数的基本操作,包括生成随机变量、计算概率密度函数和累积分布函数,以及参数估计和拟合。函数来生成Gamma分布的随机变量,计算概率密度函数(PDF)和累积分布函数(CDF),以及进行参数估计和拟合。要计算Gamma分布的累积分布函数(CDF),我们可以使用。

ByteWhiz的博客 1629

mojoPortal 路径穿越漏洞复现(CVE-2025-28367)(附脚本)

 在 mojoPortal CMS 版本 <=2.9.0.1 中发现了一个导致 ViewState 反序列化的目录遍历漏洞。通过利用此问题,未经身份验证的攻击者可以泄露 Web 根目录中的敏感文件,包括应用程序的文件。此配置文件包含用于验证和解密 ViewState 数据的文件。知道此密钥后,网络攻击者可以构建恶意 ViewState 有效负载,从而导致在底层服务器上执行远程代码。(CVE-2025-28367)

iSee857的博客 500

从0开始搞一个锦鲤游动——动起来

上一篇文章主要讲了如何绘制一条锦鲤,而今天这篇文章,就讲如何让我们的锦鲤动起来。这次需要写一些比较复杂的算法,与数学关系很大,个别地方可能难以理解。不过大家可以多看两遍仔细斟酌,如果还是不理解的话可以来评论区交流。 效果展示 一.属性动画(ValueAnimator) 1.属性动画ValueAnimator介绍 ①ValueAnimator没有重绘,所以需要自己调用addUpdateListener方法,需要结合AnimatorUpdateListener使用 ②操作的对象的属性不一定要有getter和s

Android小白 1268

GhidraMCP:AI驱动的逆向工程革命——让LLM自主操作Ghidra进行自动化分析

(Ghidra Machine Control Protocol)应运而生,它通过将LLM与Ghidra(美国NSA开源逆向工具)结合,实现了AI驱动的自动化逆向工程流程。逆向工程是软件安全、漏洞分析和代码审计的核心环节,但传统方法依赖人工分析,耗时且易出错。未来,随着LLM技术的演进和逆向工程工具的优化,GhidraMCP有望成为安全研究人员和开发者的核心工具,推动逆向工程进入“AI辅助时代”。GhidraMCP通过LLM与Ghidra的深度整合,重新定义了逆向工程的自动化边界。

这里汇聚了前沿的技术分享与实用的开发技巧,带你探索从创意到企业的技术创业之路。 2114

可拖动的圆形进度条

效果图如下: 1,通过继承View,重写onDraw(Canvas canvas)方法,实现界面进度显示 2,重写onTouchEvent(MotionEvent event)方法,监听拖动事件 3,在监听位置改变时,计算角度,重新调用onDraw()方法进行绘制 关键代码如下: public class MusicProgressBar extends View

技术笔记 4923

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Qt优秀开源项目之十二:shotcut

官网:https://www.shotcut.org/ github地址:https://github.com/mltframework/shotcut shotcut基于FFmpeg开发,是一款免费、开源、跨平台的视频编辑软件。因为基于FFmpeg,所以最新的音视频格式都是支持的。视频编辑只是其本质工作,居然还支持网络流媒体回放—HTTP, HLS, RTMP, RTSP, MMS, UDP 比较贴心的是官网还提供了视频教程:https://www.shotcut.org/tutorials/ 功能强大没

草上爬的博客 5314

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密

本文记录了在 Android 14+ 环境下,通过将 Burp CA 安装为系统信任锚,成功抓取 targetSdk=35 App 的 HTTPS 明文流量的完整实操过程。针对“浏览器可用、App 不可用”的典型现象,精准识别出根本原因是系统级证书信任策略变更(用户安装的 CA 被屏蔽),而非 SSL Pinning。仅通过 adb 命令将 CA 推送至系统证书存储并重启生效,无需 Frida、Objection 或修改 APK,实现零侵入解密。全文命令均基于 Windows + MuMu 模拟器环境实测通

JIANqiao19931029的博客 341

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

具身智能 2026:启元人形机器人开发者生态的“安卓时刻”来了吗

一个贯穿全文的问题:人形机器人开发者生态,是否正从「封闭垂直」走向「开源平台化」?本文以开源底座的公开数据为标尺,盘点 SDK、URDF、仿真与技能分发的开放程度,并落回启元机器人的开源路径。所有关键数据均标注来源,文末附社区与学堂链接。

在北风吹起的时候加入我们的队伍 在秋雨到来的时候成为彼此的温暖 743

android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送

橘子,计算机视觉方向工程师,深耕 AIGC 与多媒体处理领域,专注技术实用化落地。本文为原创内容,转载请注明来源与作者。优快云 首发 | 版权所有 © 2026。

下载资源或查看教程前,请务必阅读置顶的《免责声明》 498

Android 14 SELinux策略深度解析

init 的全局公开约束(如 neverallow init execute_no_trans,放 public 才能约束所有分区的规则)这就是"哪些可见"的强制机制。alps_s0(MTK 工程)的 device/mediatek/sepolicy/basic/ 提供。所有分区(system/system_ext/product/vendor)都可引用。init的公开约束、跨分区共用的 type/attribute/neverallow。——vendor 的 .cil 只能引用 plat 的。

新时代农民工 102

Android 开发入门实践(二):Kotlin 空安全,从可空类型到安全访问

延续 NewsApp 项目中的新闻标题练习,记录 Kotlin 安全调用。

Easyhood的博客 276
上一篇: 如何应对Android面试官->实战高级UI,用自定义View画一条锦鲤(上)
下一篇: Jetpack Compose -> 重组作用域和remember()
老A说
博客等级 码龄14年 1181粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值