前言


怎样用自定义View画一条鱼,其间涉及到哪些知识点?咱们先上作用图:

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

涉及的知识点:

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

整体能够分为三大步骤

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

小鱼的制作


想完成小鱼的制作,咱们首先需求分解下这个小鱼都由哪些组成

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

整体能够分红 头、鱼鳍、身体、节肢1、节肢2、尾巴 六大部分组成,咱们接下来分别进行制作;

制作整条小鱼,咱们今日运用一个自定义 Drawable 来完成,承继 Drawable 需求完成下面四个办法;

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 这两个类不做过多解说了,不了解的同学能够看下扔物线对这两个的解说,比较详细;

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倍,才能完好的放下咱们这条小鱼;

小鱼的鱼头是圆的,那么半径就能够咱们自己定义,然后依据鱼头的半径咱们来核算各个方位的大小,总体的一个核算结果如下:

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

能够看到,鱼的中心点方位到鱼尾的间隔是 4.19R;所以整个鱼的宽高便是 4.192R = 8.38R;然后 Drawable 也提供了一个设置宽高的办法;

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

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;

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

制作鱼头

咱们能够找到小鱼的中心点方位,那么咱们就能够依据中心方位来核算鱼头的中心点坐标,然后以这个坐标画一个圆,那么鱼头的中心点坐标怎样核算呢?

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

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

这儿就用到了咱们初中学习的三角函数

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

咱们假如想要求 B 的坐标,也就 『对边a』和『邻边b』的长度,依据勾股定理能够知道

a = sinA * 斜边c

b = cosA * 斜边c

这样咱们就能获取 B 的坐标(b,a)另外,在 Android 坐标系中『下正上负』,和数学中的坐标不一样(上正下负),所以咱们需求一个取反操作,一种是直接加一个负号,一种是视点 – 180,终究的核算逻辑如下:

/**
 * 核算点位函数
 *
 * @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);
}

然后咱们来画鱼头

// 初始视点
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);
}

咱们运转看下作用:

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

鱼头现已画了出来;

制作鱼鳍

鱼鳍的制作,这儿应用到了二阶贝塞尔曲线,起点,结尾,以及控制点

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

所以鱼鳍的制作,咱们只需求找出这三个点就能够了;鱼鳍的方位能够相对鱼头来画,这样的话咱们就依赖鱼头的圆心点坐标来核算,因为鱼鳍是两个,咱们分为左鱼鳍和右鱼鳍;

鱼鳍的坐标核算方法如下:

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

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

makeFins 画鱼鳍的逻辑

/**
 * 画鱼鳍
 *
 * @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);
}

咱们运转看下作用:

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

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

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

然后咱们需求修改下控制点的坐标,也是对称取反,咱们这儿运用一个标志位,是画左还是右;

/**
 * 画鱼鳍
 *
 * @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);
}

咱们运转看下作用:

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

左边的鱼鳍咱们也制作了出来;

画节肢

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

PointF bodyBottomCenterPoint = calculatePoint(headPoint, FISH_BODY_LENGTH, fishAngle - 180);

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

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

/**
 * 画节肢
 * @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);
}

咱们运转看下作用:

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

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

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

咱们能够利用咱们前面写的 makeSegment 办法来画节肢2,咱们将相关常量值提取出来

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

咱们运转看下作用:

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

能够看到 节肢2 也制作了出来了;

画尾巴

咱们接下来画鱼的尾巴,尾巴便是两个三角形叠在一同,一个大的,一个小的,以中圆的圆心(middlePointF)为参照点,制作一个三角形,中圆的圆心咱们提前也现已拿到了;三角形也是运用 Path 来画线;

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

咱们运转看下作用

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

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

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);
}
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH, BIG_CIRCLE_RADIUS, fishAngle);
makeTriangle(canvas, middlePointF, FIND_TRIANGLE_LENGTH - 10,
        BIG_CIRCLE_RADIUS - 20, fishAngle);

运转看下作用:

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

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

制作鱼身体

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

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

咱们运转看小作用:

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

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

好了,鱼的制作就到这儿吧,咱们下一章来让咱们的小鱼动起来;

下一章预告

小鱼游动

欢迎三连

来都来了,点个重视,点个赞吧,你的支撑是我最大的动力;