Android自定义View——风车

最近在Pure天气上看到一个风车的效果感觉不错,决定来模仿一个类似的。

Pure天气的效果图:

Pure天气

自己做的效果图:

风车

太大了感觉蛮丑的 (┬_┬)

问题分析

风车可以由一下几个部分组成:

  1. 2条线架在一起是风车架子
  2. 三个风车叶子组成,每个叶子可以看成2个等腰三角形组成(便于计算,小等腰三角形是一个等腰直角三角形),每个叶子对应边的夹角为120度
  3. 让每条边绘制的时候角度递增形成动画
  4. 为了避免叶子过长,在旋转的时候超出视图之外,叶子长度取视图宽度和高度最小值的一半再减去20像素(WIND_LENGTH = Math.min(width, height) / 2 - 20
  5. 叶子的旋转中心坐标为(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️⃣ . 绘制风车叶子

分析图

绘制风车叶子有两种思路:

  1. 分别找出每个叶子四个顶点的坐标,然后写三次代码绘制三次
  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);
    }
}

总结

1. 找出对应点的坐标关系,用canvas.drawPath() 绘制路径,path.moveTo()将画笔移动到第一个点,path.lineTo() 将上一个点与当前点连线,path.close() 将连线闭合。

2. 设定一个角度偏移量,每次绘制的时候改变偏移量达到动画的效果,postInvalidateDelayed() 延时绘制。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值