一、实现效果:
当点击屏幕的时候,屏幕中的锦鲤会身体摆动并且游到屏幕点击处,如下图:
效果分析:
1、小鱼的身体各个部件都是简单的半透明几何图形。
2、各个部件都可以活动。
3、从头到尾方向的部件摆动幅度越来越大,频率越来越高。
二、实现思路:
1、实现小鱼的绘制,创建一个自定义Drawable,把小鱼的形状画出来,然后放到ImageView上面显示。
2、实现小鱼的原地摆动,通过属性动画ValueAnimator改变鱼的角度来实现让小鱼可以在原地摆动。
3、实现小鱼的点击游动。
三、自定义Drawable
1、drawable是什么?
(1)、一种可以在Canvas上进行绘制的抽象的概念。
(2)、颜色、图片等都可以是一个Drawable。
(3)、Drawable可以通过XML定义,或者通过代码创建。
(4)、Android中Drawable是一个抽象类,每个具体的Drawable都是其子类。
2、drawable的优点有哪些?
(1)、使用简单,比自定义View成本低。
(2)、非图片类的Drawable所占空间小,能减小apk的大小。
四、鱼鳍和鱼身的弧线如何实现?
要实现弧线,我们需要使用贝塞尔曲线,而对于贝塞尔曲线,最重要的点是数据点和控制 点。
数据点: 指一条路径的起始点和终止点。
控制点:控制点决定了一条路径的弯曲轨迹
根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲 线 (1 个控制点)、三阶贝塞尔曲线(2个控制点)等等。
我们可以使用二阶贝赛尔曲线来实现:
五、代码实现
1、activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--实现鱼的原地摆尾-->
<ImageView
android:id="@+id/iv_fish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
2、MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView ivFish = findViewById(R.id.iv_fish);
ivFish.setImageDrawable(new FishDrawable());
}
}
3、FishDrawable.java
public class FishDrawable extends Drawable {
private Paint mPaint;
private Path mPath;
// 身体之外的部分的透明度
private final static int OTHER_ALPHA = 110;
// 身体的透明度
private final static int BODY_ALPHA = 160;
// 鱼的重心(鱼身的中心)
private PointF middlePoint;
private float fishMainAngle = 90;
public final static float HEAD_RADIUS = 150;
// 鱼身的长度
private final static float BODY_LENGTH = 3.2f * HEAD_RADIUS;
private final static float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
private final static float FINS_LENGTH = 1.3f * HEAD_RADIUS;
// -------------鱼尾---------------
// 尾部大圆的半径(圆心就是身体底部的中点)
private final float BIG_CIRCLE_RADIUS = HEAD_RADIUS * 0.7f;
// 尾部中圆的半径
private final float MIDDLE_CIRCLE_RADIUS = BIG_CIRCLE_RADIUS * 0.6f;
// 尾部小圆的半径
private final float SMALL_CIRCLE_RADIUS = MIDDLE_CIRCLE_RADIUS * 0.4f;
// --寻找尾部中圆圆心的线长
private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS + MIDDLE_CIRCLE_RADIUS;
// --寻找尾部小圆圆心的线长
private final float FIND_SMALL_CIRCLE_LENGTH = MIDDLE_CIRCLE_RADIUS * (0.4f + 2.7f);
// --寻找大三角形底边中心点的线长
private final float FIND_TRIANGLE_LENGTH = MIDDLE_CIRCLE_RADIUS * 2.7f;
public FishDrawable() {
init();
}
private void init() {
mPath = new Path();// 路径
mPaint = new Paint();// 画笔
mPaint.setStyle(Paint.Style.FILL);// 画笔类型,填充
mPaint.setARGB(OTHER_ALPHA, 244, 92, 71);// 设置颜色
mPaint.setAntiAlias(true);// 抗锯齿
mPaint.setDither(true);// 防抖
middlePoint = new PointF(4.19f * HEAD_RADIUS, 4.19f * HEAD_RADIUS);
}
@Override
public void draw(@NonNull Canvas canvas) {
float fishAngle = fishMainAngle;
// 绘制鱼头
PointF 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,BIG_CIRCLE_RADIUS, MIDDLE_CIRCLE_RADIUS,
FIND_MIDDLE_CIRCLE_LENGTH, fishAngle, true);
// 绘制节肢2
PointF middleCircleCenterPoint = calculatePoint(bodyBottomCenterPoint,
FIND_MIDDLE_CIRCLE_LENGTH, fishAngle - 180);
makeSegment(canvas,middleCircleCenterPoint,MIDDLE_CIRCLE_RADIUS, SMALL_CIRCLE_RADIUS,
FIND_SMALL_CIRCLE_LENGTH, fishAngle, false);
// 绘制大三角形
makeTriangle(canvas, middleCircleCenterPoint, FIND_TRIANGLE_LENGTH,
BIG_CIRCLE_RADIUS, fishAngle);
// 绘制小三角形
makeTriangle(canvas, middleCircleCenterPoint, FIND_TRIANGLE_LENGTH - 10,
BIG_CIRCLE_RADIUS - 20, fishAngle);
// 画身体
makeBody(canvas, headPoint, bodyBottomCenterPoint, fishAngle);
}
/**
* 画鱼身
* @param headPoint
* @param bodyBottomCenterPoint
*/
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);
}
/**
* 画三角形
* @param findCenterLength 顶点到底部的垂直线长
* @param findEdgeLength 底部一半
*/
private void makeTriangle(Canvas canvas, PointF startPoint,
float findCenterLength, float findEdgeLength, float fishAngle) {
// 底部中心点的坐标
PointF centerPoint = calculatePoint(startPoint, findCenterLength, fishAngle - 180);
// 三角形底部两个点
PointF leftPoint = calculatePoint(centerPoint, findEdgeLength, fishAngle + 90);
PointF rightPoint = calculatePoint(centerPoint, findEdgeLength, 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);
}
/**
* 画节肢
* @param bottomCenterPoint 梯形底部的中心点坐标(长边)
* @param bigRadius 大圆的半径
* @param smallRadius 小圆的半径
* @param findSmallCircleLength 寻找梯形小圆的线长
* @param hasBigCircle 是否有大圆
*/
private void makeSegment(Canvas canvas, PointF bottomCenterPoint, float bigRadius,
float smallRadius, float findSmallCircleLength, float fishAngle,
boolean 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){
// 绘制大圆
canvas.drawCircle(bottomCenterPoint.x, bottomCenterPoint.y, bigRadius, mPaint);
}
// 绘制小圆
canvas.drawCircle(upperCenterPoint.x, upperCenterPoint.y, smallRadius, mPaint);
// 绘制梯形
mPath.reset();
mPath.moveTo(bottomLeftPoint.x, bottomLeftPoint.y);
mPath.lineTo(upperLeftPoint.x, upperLeftPoint.y);
mPath.lineTo(upperRightPoint.x, upperRightPoint.y);
mPath.lineTo(bottomRightPoint.x, bottomRightPoint.y);
canvas.drawPath(mPath, mPaint);
}
/**
* 绘制鱼鳍
*
* @param startPoint 起始点的坐标
* @param fishAngle 鱼头相对于x坐标的角度
* @param isRightFins
*/
private void makeFins(Canvas canvas, PointF startPoint, float fishAngle, boolean isRightFins) {
float controlAngle = 115;
// 结束点
PointF endPoint = calculatePoint(startPoint, FINS_LENGTH, fishAngle - 180);
// 控制点
PointF controlPoint = calculatePoint(startPoint, 1.8f * FINS_LENGTH,
isRightFins ? 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);
}
/**
* 求对应点的坐标 -- 知道起始点,知道鱼头的角度,知道两点间的距离,就可以算出想要的点的坐标
*
* @param startPoint 起始点的坐标
* @param length 两点间的长度
* @param angle 鱼头相对于x坐标的角度
* @return
*/
public static PointF calculatePoint(PointF startPoint, float length, float angle) {
// angle 角度(0度~360度) 三角函数 -- 弧度
float deltaX = (float) (Math.cos(Math.toRadians(angle)) * length);
float deltaY = (float) (-Math.sin(Math.toRadians(angle)) * length);
return new PointF(startPoint.x + deltaX, startPoint.y + deltaY);
}
// 设置透明度
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
// 设置颜色过滤器
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
// 只有绘制的地方才覆盖底下的内容
return PixelFormat.TRANSLUCENT;
}
// 如果ImageView的宽高为wrap_content,则获取这个值
@Override
public int getIntrinsicHeight() {
return (int) (8.38f * HEAD_RADIUS);
}
@Override
public int getIntrinsicWidth() {
return (int) (8.38f * HEAD_RADIUS);
}
}
到此,鱼的形状已经描绘成功,想要实现游泳效果,请下载完整demo: