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

前言


如何用自定义View画一条鱼,其中涉及到哪些知识点?我们先上效果图:

涉及的知识点:

整体可以分为三大步骤

  1. 小鱼的绘制
  2. 小鱼的摆动
  3. 点击之后小鱼的游动

小鱼的绘制


想实现小鱼的绘制,我们首先需要分解下这个小鱼都由哪些组成

整体可以分成 头、鱼鳍、身体、节肢1、节肢2、尾巴 六大部分组成,我们接下来分别进行绘制;

绘制整条小鱼,我们今天使用一个自定义 Drawable 来完成,继承 Drawable 需要实现下面四个方法;

less 复制代码
public class Fish extends Drawable {

    @Override
    public void draw(@NonNull Canvas canvas) {

    }

    /**
     * 设置透明度
     * @param canvas The canvas to draw into 
     */
    @Override
    public void setAlpha(int alpha) {

    }

    /**
     * 设置颜色过滤器,在绘制出来之前,被绘制的内容的每一个像素都会被颜色过滤器改变
     * @param colorFilter The color filter to apply, or {@code null} to remove the 
     *                    existing color filter
     */
    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {

    }
    
    /**
     * 这个值,可以根据 setAlpha 中设置的值进行调整,比如,alpha == 0 的时候设置为 PixelFormat.TRANSPARENT。
     * 在alpha == 255 时这是为 PixelFormat.OPAQUE。在其他时候设置为 PixelFormat.TRANSLUCENT。
     * PixelFormat.OPAQUE 完全不透明,遮盖在它下面的所有内容上
     * PixelFormat.TRANSPARENT 透明,完全不显示任何东西
     * PixelFormat.TRANSLUCENT 只有绘制的地方才覆盖底下的内容
     * @return PixelFormat.TRANSLUCENT
     */
    @Override
    public int getOpacity() {
        return PixelFormat.TRANSLUCENT;
    }
}

自定义View 自然少不了 Paint 和 Path,我们来初始化这两个对象;Path 和 Paint 这两个类不做过多解释了,不了解的同学可以看下扔物线对这两个的解释,比较详细;

scss 复制代码
public Fish() {
    init();
}

/**
 * 初始化
 */
private void init() {
    mPath = new Path();
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Paint.Style.FILL);
    mPaint.setDither(true);
    mPaint.setARGB(110, 244, 92, 71);
}

接下来,我们开始绘制鱼

计算小鱼的宽高

计算小鱼的宽高,我们需要以小鱼的中心点为起点,小鱼的尾部为终点,计算个距离,然后 x2,因为我们的小鱼是可以旋转的,以中心为圆点的话,那么最长半径的2倍,才能完整的放下我们这条小鱼;

小鱼的鱼头是圆的,那么半径就可以我们自己定义,然后根据鱼头的半径我们来计算各个位置的大小,总体的一个计算结果如下:

可以看到,鱼的中心点位置到鱼尾的距离是 4.19R;所以整个鱼的宽高就是 4.192R = 8.38R;然后 Drawable 也提供了一个设置宽高的方法;

当然了 这些值都是自己可以定义的,只要你画出的鱼符合设计的要求即可;

java 复制代码
private static final float HEAD_RADIUS = 150f;

@Override
public int getIntrinsicHeight() {
    return (int) (8.38 * HEAD_RADIUS);
}

@Override
public int getIntrinsicWidth() {
    return (int) (8.38 * HEAD_RADIUS);
}

确定小鱼的中心

前面说到了,整个鱼的宽高就是 4.192R = 8.38R,所以小鱼的中心就是 4.19R;

csharp 复制代码
private PointF pointF;

private void init() {
    pointF = new PointF((4.19f * HEAD_RADIUS), (4.19f * HEAD_RADIUS));
}

绘制鱼头

我们可以找到小鱼的中心点位置,那么我们就可以根据中心位置来计算鱼头的中心点坐标,然后以这个坐标画一个圆,那么鱼头的中心点坐标怎么计算呢?

假设我们以小鱼的中心为(0, 0)点,那么当鱼旋转到X正轴方向上的时候,鱼头圆心位置就是(2.6R, 0)当鱼旋转到蓝色线的位置的时候,那么我们只需要计算出鱼头圆心的位置点坐标,那么不管这条鱼怎么旋转,我们都能获取到这个鱼头的圆心坐标,我们来看下怎么计算?

这里就用到了我们初中学习的三角函数

我们如果想要求 B 的坐标,也就 『对边a』和『邻边b』的长度,根据勾股定理可以知道

a = sinA * 斜边c

b = cosA * 斜边c

这样我们就能获取 B 的坐标(b,a)另外,在 Android 坐标系中『下正上负』,和数学中的坐标不一样(上正下负),所以我们需要一个取反操作,一种是直接加一个负号,一种是角度 - 180,最终的计算逻辑如下:

arduino 复制代码
/**
 * 计算点位函数
 *
 * @param startPoint 起始点
 * @param length 起始点到终点的距离
 * @param angle 起始点和终点的角度
 */
private PointF calculatePoint(PointF startPoint, int length, int angle) {
    // X 轴坐标 也就是临边 b 的长度 b = cosA * 斜边 c
    float deltaX = (float) (Math.cosh(Math.toRadians(angle)) * length);
    // Y 轴坐标 也就是对边 a 的长度 a = sinA * 斜边 c
    float deltaY = (float) (Math.sin(Math.toRadians(angle - 180)) * length);
    return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
}

然后我们来画鱼头

java 复制代码
// 初始角度
private float fishMainAngle = 0f;
// 鱼头半径
private static final float HEAD_RADIUS = 150f;
// 鱼身体长度
private final float FISH_BODY_LENGTH = HEAD_RADIUS * 3.2f;

@Override
public void draw(@NonNull Canvas canvas) {
    float fishAngle = fishMainAngle;
    // 鱼头的圆心坐标
    PointF fishHeadPoint = calculatePoint(middlePointF, FISH_BODY_LENGTH / 2f, fishAngle);
    // 画鱼头
    canvas.drawCircle(fishHeadPoint.x, fishHeadPoint.y, HEAD_RADIUS, mPaint);
}

我们运行看下效果:

鱼头已经画了出来;

绘制鱼鳍

鱼鳍的绘制,这里应用到了二阶贝塞尔曲线,起点,终点,以及控制点

所以鱼鳍的绘制,我们只需要找出这三个点就可以了;鱼鳍的位置可以相对鱼头来画,这样的话我们就依赖鱼头的圆心点坐标来计算,因为鱼鳍是两个,我们分为左鱼鳍和右鱼鳍;

鱼鳍的坐标计算方式如下:

less 复制代码
@Override
public void draw(@NonNull Canvas canvas) {
    // 画鱼鳍
    // 鱼鳍的起始点坐标
    PointF rightFinsPoint = calculatePoint(fishHeadPoint, FISH_FINS_LENGTH, fishAngle - 110);
    makeFins(canvas, rightFinsPoint, fishAngle);
}

makeFins 画鱼鳍的逻辑

scss 复制代码
/**
 * 画鱼鳍
 *
 * @param canvas 画布
 * @param startPoint 鱼鳍坐标
 * @param fishAngle 鱼鳍的角度
 */
private void makeFins(Canvas canvas, PointF startPoint, float fishAngle) {
    // 控制点的弧度,用来计算控制点的坐标
    float controlAngle = 115;
    // 鱼鳍的结束点坐标
    PointF endPoint = calculatePoint(startPoint, FINS_LENGTH, fishAngle - 180);
    // 鱼鳍的控制点坐标
    PointF controlPoint = calculatePoint(startPoint, FINS_CONTROL_LENGTH, fishAngle - controlAngle);
    // 绘制
    mPath.reset();
    // 画笔移动到起始点
    mPath.moveTo(startPoint.x, startPoint.y);
    // 绘制二阶贝塞尔曲线,需要传入的是 控制点坐标和结束点坐标
    mPath.quadTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
    canvas.drawPath(mPath, mPaint);
}

我们运行看下效果:

可以看到,我们的鱼鳍绘制了出来,接下来我们来绘制左鱼鳍,左鱼鳍其实和右的绘制逻辑是一样的,只是坐标抽取反即可;

less 复制代码
@Override
public void draw(@NonNull Canvas canvas) {
    // 画鱼鳍
    // 鱼鳍的起始点坐标
    PointF rightFinsPoint = calculatePoint(fishHeadPoint, FISH_FINS_LENGTH, fishAngle - 110);
    makeFins(canvas, rightFinsPoint, fishAngle, true);
    
    PointF leftFinsPoint = calculatePoint(fishHeadPoint, FISH_FINS_LENGTH, fishAngle + 110);
    makeFins(canvas, leftFinsPoint, fishAngle, false);
}

然后我们需要修改下控制点的坐标,也是对称取反,我们这里使用一个标志位,是画左还是右;

scss 复制代码
/**
 * 画鱼鳍
 *
 * @param canvas 画布
 * @param startPoint 鱼鳍坐标
 * @param fishAngle 鱼鳍的角度
 */
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_CONTROL_LENGTH, 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);
}

我们运行看下效果:

左边的鱼鳍我们也绘制了出来;

画节肢

画节肢,分为三部分,两个圆和一个梯形,我们先求身体底部中心点坐标,还是以鱼头圆心为参照点

ini 复制代码
PointF bodyBottomCenterPoint = calculatePoint(headPoint, FISH_BODY_LENGTH, fishAngle - 180);

然后我们来绘制梯形,梯形的绘制,我们也是画线,我们需要获取梯形四个点的坐标,以及两个圆的中心点坐标

scss 复制代码
/**
 * 画节肢
 * @param canvas 画布
 * @param bottomCenterPoint 大圆中心点坐标
 * @param fishAngle 角度
 */
private void makeSegment(Canvas canvas, PointF bottomCenterPoint, float fishAngle) {

    // 计算中圆坐标(梯形上底圆的圆心)
    PointF upperCenterPoint = calculatePoint(bottomCenterPoint, FIND_MIDDLE_CIRCLE_LENGTH, fishAngle - 180);
    // 计算梯形的四个点
    PointF bottomLeftPoint = calculatePoint(bottomCenterPoint, BIG_CIRCLE_RADIUS, fishAngle + 90);
    PointF bottomRightPoint = calculatePoint(bottomCenterPoint, BIG_CIRCLE_RADIUS, fishAngle - 90);
    PointF upperLeftPoint = calculatePoint(upperCenterPoint, MIDDLE_CIRCLE_RADIUS, fishAngle + 90);
    PointF upperRightPoint = calculatePoint(upperCenterPoint, MIDDLE_CIRCLE_RADIUS, fishAngle - 90);
    // 画大圆
    canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, BIG_CIRCLE_RADIUS, mPaint);
    // 画小圆
    canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, MIDDLE_CIRCLE_RADIUS, 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);
}

我们运行看下效果:

我们画出了两个圆和一个梯形;我们接下来画第二个节肢,第二个节肢和第一个比较类似,它是一个圆形,一个梯形;

我们可以利用我们前面写的 makeSegment 方法来画节肢2,我们将相关常量值提取出来

scss 复制代码
private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint, float bigRadius, float smallRadius,
                           float findSmallCircleLength, float fishAngle, boolean hasBigCircle) {
    // 相关常量值进行替换,以及节肢1的大圆只在画节肢1的时候才执行,增加一个 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;   
}

// 以节肢的底圆中心为参照点
PointF middlePointF = makeSegment(canvas, bodyBottomCenterPoint, FIND_MIDDLE_CIRCLE_LENGTH, BIG_CIRCLE_RADIUS, MIDDLE_CIRCLE_RADIUS, fishAngle, true);
//
makeSegment(canvas, middlePointF, MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
        FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);

// 画节肢2
makeSegment(canvas, middlePointF, MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
        FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);

我们运行看下效果:

可以看到 节肢2 也绘制了出来了;

画尾巴

我们接下来画鱼的尾巴,尾巴就是两个三角形叠在一起,一个大的,一个小的,以中圆的圆心(middlePointF)为参照点,绘制一个三角形,中圆的圆心我们提前也已经拿到了;三角形也是使用 Path 来画线;

scss 复制代码
private void makeTriangle(Canvas canvas, PointF startPoint, float fishAngle) {
    // 三角形底边中心点坐标
    PointF centerPoint = calculatePoint(startPoint, FIND_TRIANGLE_LENGTH, fishAngle);
    // 三角形底边两点
    PointF leftPoint = calculatePoint(centerPoint, BIG_CIRCLE_RADIUS, fishAngle + 90);
    PointF rightPoint = calculatePoint(centerPoint, BIG_CIRCLE_RADIUS, 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);
}

我们运行看下效果

大三角画出来了,我们来画小三角,小三角其实就是半径小点和中轴线短一些,中心点一样,所以我们把这个方法抽取下;

scss 复制代码
private void makeTriangle(Canvas canvas, PointF startPoint, float findTriangleLength,
                          float bigCircleRadius, float fishAngle) {
    // 三角形底边中心点坐标
    PointF centerPoint = calculatePoint(startPoint, findTriangleLength, fishAngle);
    // 三角形底边两点
    PointF leftPoint = calculatePoint(centerPoint, bigCircleRadius, fishAngle + 90);
    PointF rightPoint = calculatePoint(centerPoint, bigCircleRadius, 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);
}
scss 复制代码
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH, BIG_CIRCLE_RADIUS, fishAngle);
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH - 10,
        BIG_CIRCLE_RADIUS - 20, fishAngle);

运行看下效果:

第二个三角形我们也绘制了出来,我们接下来绘制鱼的身体;

绘制鱼身体

鱼的身体绘制,其实左右两边各是一条二阶贝塞尔曲线,所以我们来绘制两条贝塞尔曲线,大圆和中圆的的圆心点我们也已经获取到了,我们可以求出身体的四个点,topLeft,topRight,bottomLeft,bottomRight;

ini 复制代码
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);
}

我们运行看小效果:

OK,到这里,我们的这条鱼就画完了,一条完美的锦鲤呈现在我们眼前;

好了,鱼的绘制就到这里吧,我们下一章来让我们的小鱼动起来;

下一章预告

小鱼游动

欢迎三连

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

相关推荐
wkj0018 分钟前
接口实现类向上转型和向上转型解析
java·开发语言·c#
qqxhb9 分钟前
零基础设计模式——行为型模式 - 观察者模式
java·观察者模式·设计模式·go
寒士obj36 分钟前
类加载的过程
java·开发语言
无名之逆39 分钟前
大三自学笔记:探索Hyperlane框架的心路历程
java·开发语言·前端·spring boot·后端·rust·编程
Chuck1sn41 分钟前
我把 Cursor AI 整合到 Ruoyi 中,从此让 Java 脚手架脱离人工!
java·vue.js·后端
敲代码的剑缘一心43 分钟前
手把手教你学会写 Gradle 插件
android·gradle
水木石画室44 分钟前
Spring Boot 常用注解面试题深度解析
java·spring boot·后端
hweiyu001 小时前
tomcat指定使用的jdk版本
java·开发语言·tomcat
百锦再1 小时前
.NET 类库开发详细指南c
java·log4j·.net·net·dot
青蛙娃娃1 小时前
漫画Android:动画是如何实现的?
android·android studio