自定义View-雪花和下雪动画

一、武汉每年都会下一场雪,然而今年却没有,雪花还是很漂亮的,据说雪花的形状有几十种,大自然鬼斧神工啊。

二、前几天看到有人写了一个关于“下雪”的自定义View,很有意思,但是感觉雪花写得不是很满意,是用椭圆代替的,虽说现实中观看的确是一坨坨的,但是我还是想从细腻的角度去描绘一番(下图为代码所写)。

先上个图看效果吧:

三、绘制过程
1.难点在于绘制当前这个雪花,我把它进行了分割,简化处理:
分为四个部分,

六边形部分:

边刺部分:

角刺主干和角刺部分:

2.绘制过程中将坐标原点转换至视图中心位置,以该view中心点为坐标原点,方便坐标的计算。
3.绘制正六边形:
初始化六边形边长length,先绘制最上的一条边,然后旋转画布60°,分别绘制其余5边,具体如下:

public void drawHexagon(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
int x1 = -mLength / 2;
int y1 = -(int) (Math.sqrt(3) * mLength / 2);
int x2 = 0;
int y2 = y1;
canvas.save();
for (int i = 0; i < 6; i++) {
mPaint.setStrokeWidth(6);
canvas.drawLine(x1, y1, x1 + mLength, y1, mPaint);
mPaint.setStrokeWidth(2);
canvas.drawLine(x2, y2, 0, 0, mPaint);
canvas.rotate(60, 0, 0);
}
canvas.restore();
}

4.绘制边刺:
该边刺为不规则图形,只能用path进行绘制,主要在于需确定好坐偏移量和上偏移量的坐标点位置,绘制一个后,同样对画布进行旋转60°,绘制其余5条:

public void drawThorn(Canvas canvas) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(2);
int y = -(int) (Math.sqrt(3) * mLength / 2);
mHexagonPath_1.moveTo(0 - mThornPadding_1, y);
mHexagonPath_1.lineTo(0 - mThornPadding_1 - mThornPadding_2, y - mThornHeight_1);
mHexagonPath_1.lineTo(0, y - mThornHeight_2);
mHexagonPath_1.lineTo(0 + mThornPadding_1 + mThornPadding_2, y - mThornHeight_1);
mHexagonPath_1.lineTo(0 + mThornPadding_1, y);
mHexagonPath_1.close();
canvas.save();
for (int i = 0; i < 6; i++) {
canvas.drawPath(mHexagonPath_1, mPaint);
canvas.rotate(60, 0, 0);
}
canvas.restore();
}

5.绘制主干和角刺的的策略和边刺一样,只是角刺的起始位子位于垂直位置偏移30°,这个不打紧,直接想象当做垂直处理,绘制完后旋转30°,然后同上循环6次绘制其余5条,其中绘制角刺的方式(主干和边刺逻辑相似,不码了):

public void drawCornerThorn(Canvas canvas, int h) {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(2);
int y = -(int) (Math.sqrt(3) * mLength / 2);
mHexagonPath_2.moveTo(0 - mCornerThornPadding_1, y);
mHexagonPath_2.lineTo(0 - mCornerThornPadding_1 - mCornerThornPadding_2, y - mCornerThornHeight_1);
mHexagonPath_2.lineTo(0, y - mCornerThornHeight_2);
mHexagonPath_2.lineTo(0 + mCornerThornPadding_1 + mCornerThornPadding_2, y - mCornerThornHeight_1);
mHexagonPath_2.moveTo(0 + mCornerThornPadding_1, y);
mHexagonPath_2.close();
canvas.save();
canvas.translate(0, -h);
canvas.rotate(-30, 0, y);
canvas.drawPath(mHexagonPath_2, mPaint);
canvas.rotate(60, 0, y);
canvas.drawPath(mHexagonPath_2, mPaint);
canvas.restore();
}

6.雪花的旋转采用值动画处理,动画过程改变view的旋转角度,每次重绘的时候提取到最新的旋转角度:

/**
* 雪花的旋转动画(属性)
*
* @param duration
*/
public void snowRotateAnim(int duration) {
if (null == mValueAnimator) {
return;
}
mValueAnimator.setDuration(duration);
mValueAnimator.setInterpolator(mLinearInterpolator);
mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAngle = (int) mValueAnimator.getAnimatedValue();
}
});
mValueAnimator.start();
}

7.雪花向下移动:当超过边界,则重置该view的坐标位置:

/**
* 更新雪花的X坐标
*
* @param x
*/
public void setOriginX(int x) {
this.mOriginX += x;
if (mOriginX > mW || mOriginX < -mWidth) {
mOriginX = Utils.randomIntPositive(mW);
}
}
/**
* 更新雪花的Y坐标
*
* @param y
*/
public void setOriginY(int y) {
this.mOriginY += y;
if (mOriginY > mH) {
mOriginY = -mHeight;
}
}

8.遗留问题,下雪动画一次性初始化了30个雪花对象,同时在onDraw中进行绘制,略有卡顿,优化策略待定。

四、源码地址:
自定义View-雪花和下雪动画