前言
如何用自定义View画一条鱼,其中涉及到哪些知识点?我们先上效果图:
涉及的知识点:
整体可以分为三大步骤
- 小鱼的绘制
- 小鱼的摆动
- 点击之后小鱼的游动
小鱼的绘制
想实现小鱼的绘制,我们首先需要分解下这个小鱼都由哪些组成
整体可以分成 头、鱼鳍、身体、节肢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,到这里,我们的这条鱼就画完了,一条完美的锦鲤呈现在我们眼前;
好了,鱼的绘制就到这里吧,我们下一章来让我们的小鱼动起来;