首先大家可以看一下这个鱼的图案
一.前期准备
1.新建一个项目
2.将MainActivity的XML文件写成
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/iv_fish"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>
相当于我们后面只是对ImageView
进行涂画
3.新建fishDrawable类,继承Drawable
不用多想,就把fishDrawable
类看作图片即可
自定义View
和Drawable
区别不大,主要是Drawable
使用起来更简单一些
public class fishDrawable extends Drawable {
@Override
public void draw(@NonNull Canvas canvas) {
}
@Override
public void setAlpha(int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return 0;
}
}
4.在MainActivity中将ImageView控件与fishDrawable类进行关联
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//建立关联
ImageView fishView = findViewById(R.id.iv_fish);
fishView.setImageDrawable(new fishDrawable());
}
}
二.具体实操
参考比例
分解图
注意:由于代码添加并不是非常的规律,所以有时可能会忽略非常小一部分代码。但是大家不用担心,在这篇文章的最后我会把完整代码展示,并且会将项目上传到github,并给出链接
1.其余三个方法进行套公式
除了draw
,其余三个方法都是比较固定的写法
2.新增两个方法,固定大小
细心的朋友们可能发现了,我们一开始在XML
中设置ImageView
的大小的时候都是Wrap_Content
,那总得固定一个默认大小吧,所以这两个方法就用上了
(HEAD_RADIUS
为鱼头的圆的半径)
3.进行一些初始化
public class fishDrawable extends Drawable {
//创建路径和画笔
private Path mPath;
private Paint mPaint;
//设置画笔的透明度
private int OTHER_ALPHA = 110;
//设置鱼头的圆的大小
private float HEAD_RADIUS = 100;
fishDrawable(){
init();
}
//初始化
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
//抗锯齿
mPaint.setAntiAlias(true);
//防抖动
mPaint.setDither(true);
//设置颜色
mPaint.setARGB(OTHER_ALPHA,244,92,71);
}
@Override
public void draw(@NonNull Canvas canvas) {
}
//下面三个方法一般为固定写法
@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;
}
@Override
public int getIntrinsicWidth() {
//设置宽度
return (int) (8.38 * HEAD_RADIUS);
}
@Override
public int getIntrinsicHeight() {
//设置高度
return (int) (8.38 * HEAD_RADIUS);
}
}
4.确定鱼的重心(PointF)
这里我们中心和重心就不进行区分了,就通俗理解为鱼的中心点那个位置。鱼头的中心点是以鱼中心点为参考进行绘制的。
//保存鱼的重心
private PointF middlePoint;
fishDrawable(){
init();
}
//初始化
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
//抗锯齿
mPaint.setAntiAlias(true);
//防抖动
mPaint.setDither(true);
//设置颜色
mPaint.setARGB(OTHER_ALPHA,244,92,71);
//确定鱼的重心
middlePoint = new PointF(4.19f * HEAD_RADIUS,4.19F * HEAD_RADIUS);
}
5.写出核心求坐标算法
此算法为后面所有算法的基础。利用数学知识,此算法的思想是,
- 根据一个点的坐标,以及另外一个点到这个点的连线与X轴夹角,以及这条连线的长度,求出另外一个点的坐标。(数学知识,三角函数)
可能光说就有些抽象,我们看下面的图
比如我们知道a点,知道ab这条直线的长度,知道角α的大小,就可以求出b点的坐标。这样说大家应该就理解了。
比如这里面求b的坐标
b点的横坐标就是
a.x+ab*cosα
b点的纵坐标就是
a.y+ab*sinα
OK,知道了原理,我们就开始写这个算法
private PointF calculatePoint(PointF startPoint,float length,float angle){
//x坐标的一部分
float deltaX = (float) (Math.cos(Math.toRadians(angle))*length);
//y坐标的一部分
float deltaY = (float) Math.sin(Math.toRadians(angle - 180))*length;
return new PointF(startPoint.x + deltaX,startPoint.y + deltaY);
}
注意:
①Math.toRadians
是把角度换算为弧度,因为Math.sin/cos
的参数是接收弧度的。
②由于安卓坐标系和数学坐标系的y轴正好相反,所以这里的求y坐标时要-180
度,以达到正好相反的效果
6.利用鱼中心坐标计算鱼头圆心坐标
@Override
public void draw(@NonNull Canvas canvas) {
//鱼的朝向
float fishAngle = fishMainAngle;
//计算鱼头的圆心坐标
PointF headPoint = calculatePoint(middlePoint,BODY_RADIUS/2,fishAngle);
//画鱼头的圆
canvas.drawCircle(headPoint.x,headPoint.y,HEAD_RADIUS,mPaint);
}
效果如下
7.画鱼鳍
//设置鱼鳍边界点和鱼头中心点的距离
private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
//鱼鳍长度
private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
@Override
public void draw(@NonNull Canvas canvas) {
//画右鱼鳍
PointF rightFinsPoint = calculatePoint(headPoint,FIND_FINS_LENGTH,fishAngle - 100);
makeFins(canvas,rightFinsPoint,fishAngle,true);
//画左鱼鳍
PointF leftFinsPoint = calculatePoint(headPoint,FIND_FINS_LENGTH,fishAngle + 100);
makeFins(canvas,leftFinsPoint,fishAngle,false);
}
画鱼鳍的算法
private void makeFins(Canvas canvas, PointF startFinsPoint, float fishAngle,boolean isRight) {
//设置二阶贝塞尔曲线控制点的角度
float controlAngle = 110;//通过看示意图得知,它要比上面那个110要大
//求出右鱼鳍终点的坐标点
PointF endFinsPoint = calculatePoint(startFinsPoint,FINS_LENGTH,fishAngle - 180);
//求出控制点的坐标
PointF controlPoint = calculatePoint(startFinsPoint,FINS_LENGTH * 1.8f
,isRight ? fishAngle - controlAngle : fishAngle + controlAngle);
//画线
mPath.reset();//首先别忘了reset
mPath.moveTo(startFinsPoint.x,startFinsPoint.y);
mPath.quadTo(controlPoint.x,controlPoint.y,endFinsPoint.x,endFinsPoint.y);
canvas.drawPath(mPath,mPaint);
}
效果
8.画鱼节肢
//大圆的半径
private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;
//中圆的半径
private float MIDDLE_CIRCLE_RADIUS = 0.6f * BIG_CIRCLE_RADIUS;
//小圆的半径
private float SMALL_CIRCLE_RADIUS = 0.4f * MIDDLE_CIRCLE_RADIUS;
//寻找尾部中圆圆心的线长
private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS * (0.6f + 1);
//寻找尾部小圆圆心的线长
private final float FIND_SMALL_CIRCLE_LENGTH = MIDDLE_CIRCLE_RADIUS * (0.4f + 2.7f);
@Override
public void draw(@NonNull Canvas canvas) {
//画节肢1
//首先找到鱼身体底部的中心点
PointF bodyBottomCenterPoint = calculatePoint(headPoint,BODY_LENGTH,fishAngle-180);
PointF middleCirclePoint = makeSegment(canvas,bodyBottomCenterPoint,MIDDLE_CIRCLE_RADIUS,
BIG_CIRCLE_RADIUS,FIND_MIDDLE_CIRCLE_LENGTH,fishAngle,true);
//画节肢2
makeSegment(canvas,middleCirclePoint,SMALL_CIRCLE_RADIUS,MIDDLE_CIRCLE_RADIUS,
FIND_SMALL_CIRCLE_LENGTH,fishAngle,false);
}
画节肢算法
private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint,float smallRadius
,float bigRadius,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);
//画大圆和中圆
//大圆只有在节肢1的时候才会画
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(bottomRightPoint.x,bottomRightPoint.y);
mPath.lineTo(upperRightPoint.x,upperRightPoint.y);
mPath.lineTo(upperLeftPoint.x,upperLeftPoint.y);
canvas.drawPath(mPath,mPaint);
//将中圆的圆心坐标返回
return upperCenterPoint;
}
效果展示
9.画底边三角形
//寻找大三角形底边中心点的线长
private final float FIND_TRIANGLE_LENGTH = MIDDLE_CIRCLE_RADIUS * 2.7f;
//画底边三角形
@Override
public void draw(@NonNull Canvas canvas) {
//画底边三角形
makeTriangle(canvas,middleCirclePoint,FIND_TRIANGLE_LENGTH,BIG_CIRCLE_RADIUS,fishAngle);
makeTriangle(canvas,middleCirclePoint,FIND_TRIANGLE_LENGTH-20,BIG_CIRCLE_RADIUS - 10,fishAngle);
}
画三角形的算法
private void makeTriangle(Canvas canvas, PointF middleCirclePoint, float findCenterLength,
float halfFdge,float fishAngle) {
//首先得到三角形底边中点坐标
PointF triangleBottomPoint = calculatePoint(middleCirclePoint,findCenterLength,fishAngle + 180);
//然后计算三角形各个顶点的坐标
PointF leftPoint = calculatePoint(triangleBottomPoint,halfFdge,fishAngle + 90);
PointF rightPoint = calculatePoint(triangleBottomPoint,halfFdge,fishAngle - 90);
//画线
mPath.reset();
mPath.moveTo(middleCirclePoint.x,middleCirclePoint.y);
mPath.lineTo(leftPoint.x,leftPoint.y);
mPath.lineTo(rightPoint.x,rightPoint.y);
canvas.drawPath(mPath,mPaint);
}
效果展示
10.画身体
@Override
public void draw(@NonNull Canvas canvas) {
//画身体
makeBody(canvas,headPoint,bodyBottomCenterPoint,fishAngle);
}
画身体的算法
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.lineTo(topRightPoint.x,topRightPoint.y);
mPath.quadTo(controlRight.x,controlRight.y,bottomRightPoint.x,bottomRightPoint.y);
mPath.lineTo(bottomLeftPoint.x,bottomLeftPoint.y);
mPath.quadTo(controlLeft.x,controlLeft.y,topLeftPoint.x,topLeftPoint.y);
canvas.drawPath(mPath,mPaint);
}
效果展示
全部代码
public class fishDrawable extends Drawable {
//创建路径和画笔
private Path mPath;
private Paint mPaint;
//设置画笔的透明度
private int OTHER_ALPHA = 110;
//保存鱼的重心
private PointF middlePoint;
//设置鱼的主要角度,与X轴的夹角
private float fishMainAngle = 0;
/**
* 与鱼的长度有关的所有值
*/
//设置鱼头的圆的大小
private float HEAD_RADIUS = 100f;
//设置鱼身的大小
private float BODY_LENGTH = 3.2f*HEAD_RADIUS;
//设置鱼鳍边界点和鱼头中心点的距离
private float FIND_FINS_LENGTH = 0.9f * HEAD_RADIUS;
//鱼鳍长度
private float FINS_LENGTH = 1.3f * HEAD_RADIUS;
//大圆的半径
private float BIG_CIRCLE_RADIUS = 0.7f * HEAD_RADIUS;
//中圆的半径
private float MIDDLE_CIRCLE_RADIUS = 0.6f * BIG_CIRCLE_RADIUS;
//小圆的半径
private float SMALL_CIRCLE_RADIUS = 0.4f * MIDDLE_CIRCLE_RADIUS;
//寻找尾部中圆圆心的线长
private final float FIND_MIDDLE_CIRCLE_LENGTH = BIG_CIRCLE_RADIUS * (0.6f + 1);
//寻找尾部小圆圆心的线长
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;
fishDrawable(){
init();
}
//初始化
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.FILL);
//抗锯齿
mPaint.setAntiAlias(true);
//防抖动
mPaint.setDither(true);
//设置颜色
mPaint.setARGB(OTHER_ALPHA,244,92,71);
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 - 100);
makeFins(canvas,rightFinsPoint,fishAngle,true);
//画左鱼鳍
PointF leftFinsPoint = calculatePoint(headPoint,FIND_FINS_LENGTH,fishAngle + 100);
makeFins(canvas,leftFinsPoint,fishAngle,false);
//画节肢1
//首先找到鱼身体底部的中心点
PointF bodyBottomCenterPoint = calculatePoint(headPoint,BODY_LENGTH,fishAngle-180);
PointF middleCirclePoint = makeSegment(canvas,bodyBottomCenterPoint,MIDDLE_CIRCLE_RADIUS,
BIG_CIRCLE_RADIUS,FIND_MIDDLE_CIRCLE_LENGTH,fishAngle,true);
//画节肢2
makeSegment(canvas,middleCirclePoint,SMALL_CIRCLE_RADIUS,MIDDLE_CIRCLE_RADIUS,
FIND_SMALL_CIRCLE_LENGTH,fishAngle,false);
//画底边三角形
makeTriangle(canvas,middleCirclePoint,FIND_TRIANGLE_LENGTH,BIG_CIRCLE_RADIUS,fishAngle);
makeTriangle(canvas,middleCirclePoint,FIND_TRIANGLE_LENGTH-20,BIG_CIRCLE_RADIUS - 10,fishAngle);
//画身体
makeBody(canvas,headPoint,bodyBottomCenterPoint,fishAngle);
}
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.lineTo(topRightPoint.x,topRightPoint.y);
mPath.quadTo(controlRight.x,controlRight.y,bottomRightPoint.x,bottomRightPoint.y);
mPath.lineTo(bottomLeftPoint.x,bottomLeftPoint.y);
mPath.quadTo(controlLeft.x,controlLeft.y,topLeftPoint.x,topLeftPoint.y);
canvas.drawPath(mPath,mPaint);
}
private void makeTriangle(Canvas canvas, PointF middleCirclePoint, float findCenterLength,
float halfFdge,float fishAngle) {
//首先得到三角形底边中点坐标
PointF triangleBottomPoint = calculatePoint(middleCirclePoint,findCenterLength,fishAngle + 180);
//然后计算三角形各个顶点的坐标
PointF leftPoint = calculatePoint(triangleBottomPoint,halfFdge,fishAngle + 90);
PointF rightPoint = calculatePoint(triangleBottomPoint,halfFdge,fishAngle - 90);
//画线
mPath.reset();
mPath.moveTo(middleCirclePoint.x,middleCirclePoint.y);
mPath.lineTo(leftPoint.x,leftPoint.y);
mPath.lineTo(rightPoint.x,rightPoint.y);
canvas.drawPath(mPath,mPaint);
}
private PointF makeSegment(Canvas canvas, PointF bottomCenterPoint,float smallRadius
,float bigRadius,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);
//画大圆和中圆
//大圆只有在节肢1的时候才会画
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(bottomRightPoint.x,bottomRightPoint.y);
mPath.lineTo(upperRightPoint.x,upperRightPoint.y);
mPath.lineTo(upperLeftPoint.x,upperLeftPoint.y);
canvas.drawPath(mPath,mPaint);
//将中圆的圆心坐标返回
return upperCenterPoint;
}
private void makeFins(Canvas canvas, PointF startFinsPoint, float fishAngle,boolean isRight) {
//设置二阶贝塞尔曲线控制点的角度
float controlAngle = 110;//通过看示意图得知,它要比上面那个110要大
//求出右鱼鳍终点的坐标点
PointF endFinsPoint = calculatePoint(startFinsPoint,FINS_LENGTH,fishAngle - 180);
//求出控制点的坐标
PointF controlPoint = calculatePoint(startFinsPoint,FINS_LENGTH * 1.8f
,isRight ? fishAngle - controlAngle : fishAngle + controlAngle);
//画线
mPath.reset();//首先别忘了reset
mPath.moveTo(startFinsPoint.x,startFinsPoint.y);
mPath.quadTo(controlPoint.x,controlPoint.y,endFinsPoint.x,endFinsPoint.y);
canvas.drawPath(mPath,mPaint);
}
private PointF calculatePoint(PointF startPoint,float length,float angle){
//x坐标的一部分
float deltaX = (float) (Math.cos(Math.toRadians(angle))*length);
//y坐标的一部分
float deltaY = (float) Math.sin(Math.toRadians(angle - 180))*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;
}
//设置宽度和高度
@Override
public int getIntrinsicWidth() {
return (int) (8.38 * HEAD_RADIUS);
}
@Override
public int getIntrinsicHeight() {
return (int) (8.38 * HEAD_RADIUS);
}
}
三.总结
感觉逻辑上并没有什么难度,难的在于点与点之间长度大小的确定和度数的确定。这个需要不断的去调试来看效果。我这边就直接给出现成的结果了。
再一个是从画各种身体部位的算法中,要学会一些设计算法的思想。比如要提高方法的复用性,可以加一个boolean
型的参数,如果是右肢就传入true
,进行相关操作,如果是左肢就传入false
,然后再进行相关操作。而进行的这个相关操作很多都是一样的,不一样的地方就是靠这个boolean
值来进行区分的。