最近在Pure天气上看到一个风车的效果感觉不错,决定来模仿一个类似的。
Pure天气的效果图:
自己做的效果图:
太大了感觉蛮丑的 (┬_┬)
问题分析
风车可以由一下几个部分组成:
- 2条线架在一起是风车架子
- 三个风车叶子组成,每个叶子可以看成2个等腰三角形组成(便于计算,小等腰三角形是一个等腰直角三角形),每个叶子对应边的夹角为120度
- 让每条边绘制的时候角度递增形成动画
- 为了避免叶子过长,在旋转的时候超出视图之外,叶子长度取视图宽度和高度最小值的一半再减去20像素(
WIND_LENGTH = Math.min(width, height) / 2 - 20
) - 叶子的旋转中心坐标为(width/2,叶子长度
WIND_LENGTH
)
具体实现
1️⃣ . 绘制风车架子
架子线用 canvas.drawLine()
,起点坐标一样(width/2,风车叶子长度WIND_LENGTH
),终点坐标分别为(width/3,height)和(2*width/3,height)
// 绘制风车支架
private void drawLines(Canvas canvas) {
canvas.drawLine(width / 3, height, width / 2, WIND_LENGTH, mPaint);
canvas.drawLine(width / 2, WIND_LENGTH, 2 * width / 3, height, mPaint);
canvas.drawPath(mPath, mPaint);
}
2️⃣ . 绘制风车叶子
绘制风车叶子有两种思路:
- 分别找出每个叶子四个顶点的坐标,然后写三次代码绘制三次
- 找出每个叶子对应顶点的坐标关系公式,for循环绘制(本篇使用)
设定上图中绿色三角形的直角边长(LENGTH_OF_SIDE
)为叶子长度AC(WIND_LENGTH
)的十分之一。
LENGTH_OF_SIDE = (float) (WIND_LENGTH / 10 / Math.cos(45));
以2条红线分别作为X,Y轴,以与Y轴正方向的夹角为0度角,即AB与AC的夹角为-45度。
三个叶子的对应边分别是 AB-AE-AG , AD-AF-AH
因此得出:
AB与Y轴正方向的夹角为 -45度
AE与Y轴正方向的夹角为 -45度+120度=75度
AG与Y轴正方向的夹角为 -45度+120度+120度=195度
AD与Y轴正方向的夹角为 45度
AF与Y轴正方向的夹角为 45度+120度=165度
AH与Y轴正方向的夹角为 45度+120度+120度=285度
每个叶子的中垂线(AC/AI/AJ)与Y轴正方向的夹角分别为0度、120度、240度。
每个叶子用canva.drawPath()
绘制,首先定位出点 A B C D四个点的坐标以及对应点的坐标。
第一个点 A(共用点) :
A点坐标 (width/2,WIND_LENGTH
)
第二个点(B E G)
B点坐标 (width/2 + LENGTH_OF_SIDE * sin(-45), WIND_LENGTH - LENGTH_OF_SIDE * cos(-45)
)
E点坐标 (width/2 + LENGTH_OF_SIDE * sin(75), WIND_LENGTH - LENGTH_OF_SIDE * cos(75)
)
G点坐标 (width/2 + LENGTH_OF_SIDE * sin(195), WIND_LENGTH - LENGTH_OF_SIDE * cos(195)
)
因此可以得出BEG三点的对应坐标公式为
width/2 + LENGTH_OF_SIDE * sin(-45 + 120 * i), WIND_LENGTH - LENGTH_OF_SIDE * cos(-45 + 120 * i)
第三个点(C I J)
C点坐标 (width/2 + WIND_LENGTH * sin(0), WIND_LENGTH - WIND_LENGTH * cos(0)
)
I点坐标 (width/2 + WIND_LENGTH * sin(120), WIND_LENGTH - WIND_LENGTH * cos(120)
)
J点坐标 (width/2 + WIND_LENGTH * sin(240), WIND_LENGTH - WIND_LENGTH * cos(240)
)
因此可以得出CIJ三点的对应坐标公式为
width/2 + WIND_LENGTH * sin(120 * i), WIND_LENGTH - WIND_LENGTH * cos(120 * i)
第四个点(D F H)
D点坐标 (width/2 + LENGTH_OF_SIDE * sin(45), WIND_LENGTH - LENGTH_OF_SIDE * cos(45)
)
F点坐标 (width/2 + LENGTH_OF_SIDE * sin(165), WIND_LENGTH - LENGTH_OF_SIDE * cos(165)
)
H点坐标 (width/2 + LENGTH_OF_SIDE * sin(285), WIND_LENGTH - LENGTH_OF_SIDE * cos(285)
)
因此可以得出DFH三点的对应坐标公式为
width/2 + LENGTH_OF_SIDE * sin(45 + 120 * i), WIND_LENGTH - WIND_LENGTH * cos(45 + 120 * i)
即:
需要注意的是 弧度和角度的区别,三角函数传入的参数为弧度而不是角度
角度与弧度转换:
角度转弧度: π/180×角度
弧度变角度:180/π×弧度
上面的几行代码就可以绘制出一个静态的风车,那怎么让它转动起来呢?我的思路是每绘制一次
3️⃣ . 让风车动起来
每绘制一次风车的角度旋转一定的角度,初始化一个变量 ANGLE
为每次转动的角度,让 ANGLE
不断的递增达到动画的效果,postInvalidateDelayed()
方法每隔设定的时间重绘一次。
完整代码
public class WindView extends View {
private Paint mPaint;
private Path mPath;
private static int LINE_COLOR = Color.WHITE;
private static int LINE_WIDTH = 5;
private static int WIND_LENGTH; // 扇叶长度
private static float LENGTH_OF_SIDE; // 扇叶小三角形的斜边长
private static float ANGLE = 0F;
private int width, height;
public WindView(Context context) {
this(context, null);
}
public WindView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public WindView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
mPaint = new Paint();
mPath = new Path();
mPaint.setColor(LINE_COLOR);
mPaint.setStrokeWidth(LINE_WIDTH);
mPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 450);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 450);
}
width = getWidth() - getPaddingLeft() - getPaddingRight();
height = getHeight() - getPaddingTop() - getPaddingBottom();
WIND_LENGTH = Math.min(width, height) / 2 - 20;
LENGTH_OF_SIDE = (float) (WIND_LENGTH / 10 / Math.cos(45));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawLines(canvas);
drawWindMills(canvas);
}
// 每个扇叶由2个三角形组成,小三角形是由2个等边直角三角形组成,变成是扇叶长度的十分之一
private void drawWindMills(Canvas canvas) {
if (ANGLE >= 360) ANGLE = 0;
ANGLE += 5;
for (int i = 0; i < 3; i++) {
Log.i("drawWindMills", height + " / " + WIND_LENGTH);
mPath.moveTo(width / 2, WIND_LENGTH);
mPath.lineTo(width / 2 + LENGTH_OF_SIDE * (float) Math.sin(Math.PI / 180 * (-45 + ANGLE + 120 * i)),
WIND_LENGTH - LENGTH_OF_SIDE * (float) Math.cos(Math.PI / 180 * (-45 + ANGLE + 120 * i)));
mPath.lineTo(width / 2 + WIND_LENGTH * (float) Math.sin(Math.PI / 180 * (120 * i + ANGLE)),
WIND_LENGTH - WIND_LENGTH * (float) Math.cos(Math.PI / 180 * (120 * i + ANGLE)));
mPath.lineTo(width / 2 + LENGTH_OF_SIDE * (float) Math.sin(Math.PI / 180 * (45 + ANGLE + 120 * i)),
WIND_LENGTH - LENGTH_OF_SIDE * (float) Math.cos(Math.PI / 180 * (45 + ANGLE + 120 * i)));
mPath.lineTo(width / 2, WIND_LENGTH);
mPath.close();
canvas.drawPath(mPath, mPaint);
}
postInvalidateDelayed(10);
mPath.reset();
}
// 绘制风车支架
private void drawLines(Canvas canvas) {
canvas.drawLine(width / 3, height, width / 2, WIND_LENGTH, mPaint);
canvas.drawLine(width / 2, WIND_LENGTH, 2 * width / 3, height, mPaint);
canvas.drawPath(mPath, mPaint);
}
}