本文属于Android技术论述文章,阅读完大致需要五分钟
原创文章,转载请注明出处。
没时间的小伙伴可以直接跳过文章,点击项目地址,如果喜欢的话,顺手给个star那是极好的【娇羞……】
好吧。先说一下为什么要做这个项目。
前几天使用薄荷的时候,凑巧看到了这个Loading动画,觉得效果还不错。就想尝试着实现一下。
先看一下原版的效果,GIF录制的比较快,但应该还可以看清楚。
先上本次最终实现的效果图吧,颜色当然选择今年最流行的原谅色:
思路分析
整个图形的形状分析
- 好了,首先我们来分析一下这个图案,如果是静态的,那么如何绘制?
很简单,拆分。我们将图形拆开分解,然后再看。分析细节和步骤,这是要点。
我这里将这个图分成了三份。
- 第一个,也就是叶柄。也就是下面那一条小小的竖线。原Loading图中不甚明显,但还是有的。叶柄没什么说的,直线就可以了。
- 第二个,叶子的左轮廓边缘和右轮廓边缘。这是一段下肥上窄的弧线,椭圆截取感觉不妥,我这里采用的是贝塞尔二阶曲线。有关Android贝塞尔相关的知识大家可以看看这篇文章。
- 第三个,也就是叶片的脉络,线和线交叉连接,没什么可说的。
- 那么重点其实就是叶子左右轮廓的绘制了,我画了一张草图。大家可以看看:
其中黑色的框作为View的边界。A点是左轮廓曲线的起点,B点事贝塞尔曲线的控制点,我把它定义到了View的左边框那里。C点事整个贝塞尔曲线的终点,D点则是实际上曲线的最高点。
右轮廓则和左轮廓是镜像存在。
图有点潦草,不过应该还看得懂。
如何让线条动起来
整个项目中,如何让线条真正的动起来才是要点。刚开始在这里的思路,是想使用canvas.drawCircle
绘制在一张Bitmap上,以点汇面。后面实现起来发现,这种方式特别不靠谱。
为什么不靠谱呢?因为点连接成线,每次移动的速率和距离都得计算,很麻烦。很容易出现断点的情况。
最后,我采用的是让canvas
去绘制一段Path
路径,然后Path
路径不停的刷新改变。这样做的好处,是Path
更加直观易于控制。而且还不用多绘制一张Bitmap
。
整个项目中,自定义的View,LeafAnimView
做的工作很少,只是在onDraw
方法内,调起了绘制而已。具体的绘制都交给LeafAtom
了。面向对象嘛。
具体的思路,是我把总时间按比例分成四部分。生成四个属性动画,在属性动画的监听里作Path
的x和y的变化。在绘制的时候,只需要将这四个动画依次播放,即可得到每个时间段的具体运动值。而且还是均匀变化的。
LeafAnimView
内部作为动画引擎的是一个ValueAnimator
,使用它来触发View的onDraw。同时也使用它来控制整个动画的时间。
1 2 3 4 5 6 7 8 9
| mValueAnimator = ValueAnimator.ofFloat(0, 1); mValueAnimator.setDuration(5000); mValueAnimator.setRepeatCount(ValueAnimator.INFINITE); mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { invalidate(); } });
|
LeafAtom
类内部接受到这个总时长,然后将运动总时间分割,根据比例计算出绘制叶柄、左右轮廓、脉络的动画时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| -------在LeafAnimView类内部--------- @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (null == mLeafAtom) { //传入总时长 mLeafAtom = new LeafAtom(getWidth(), getHeight(), mValueAnimator.getDuration()); } if (!mValueAnimator.isStarted()) { mValueAnimator.start(); } //开始绘制 mLeafAtom.drawGraph(canvas, mPaint); } -------在LeafAtome类--------------- public static final float PETIOLE_RATIO = 0.1f;//叶柄所占比例 public LeafAtom(int width, int height, long duration) {
mWidth = width; mHeight = height;
mPetioleTime = (long) (duration * PETIOLE_RATIO);//绘制叶柄的时间 mArcTime = (long) (duration * (1 - PETIOLE_RATIO) * 0.4f);//左右轮廓弧线的时间 mLastLineTime = duration - mPetioleTime - mArcTime * 2;//最后一段叶脉的时间
mBezierBottom = new PointF(mWidth * 0.5f, mHeight * (1 - PETIOLE_RATIO));//左侧轮廓底部点 mBezierControl = new PointF(0, mHeight * (1 - 3 * PETIOLE_RATIO));//左侧轮廓控制点 mBezierTop = new PointF(mWidth * 0.5f, 0);//左侧轮廓顶部结束点
mVeinBottomY = mHeight * (1 - PETIOLE_RATIO) - 10;//右侧轮廓底部点Y轴坐标,稍稍低一点 mOneNodeY = mVeinBottomY * 4 / 5;//第一个节点的Y轴坐标 mTwoNodeY = mVeinBottomY * 2 / 5;//第二个节点Y轴坐标 initEngine(); setOrginalStatus(); }
|
- 在
LeafAtom
的构造函数中,得到每一个阶段动画的时间,然后生成四个属性动画,在这个属性动画的监听里去做Path的x和y坐标的值变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| /** * 初始化path引擎 */ private void initEngine() { //叶柄动画,Y轴变化由底部运动到叶柄高度的地方 mPetioleAnim = ValueAnimator.ofFloat(mHeight, mHeight * (1 - PETIOLE_RATIO)).setDuration(mPetioleTime); //左右轮廓贝塞尔曲线,只需要只奥时间变化是从0~1的。起点、控制点、结束点都知道了 mArcAnim = ValueAnimator.ofFloat(0, 1.0f).setDuration(mArcTime); //绘制叶脉的动画 mLastAnim = ValueAnimator.ofFloat(mVeinBottomY, 0).setDuration(mLastLineTime);
mPetioleAnim.setInterpolator(new LinearInterpolator()); mArcAnim.setInterpolator(new LinearInterpolator()); mLastAnim.setInterpolator(new LinearInterpolator()); mArcRightAnim = mArcAnim.clone();
mPetioleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mY = (float) animation.getAnimatedValue(); } }); mArcAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { computeArcPointF(animation, true); } }); mArcRightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { computeArcPointF(animation, false); } }); mLastAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mY = (float) animation.getAnimatedValue(); float tan = (float) Math.tan(Math.toRadians(30)); if (mY <= mOneNodeY && mY > mTwoNodeY) { mOneLpath.moveTo(mX, mOneNodeY); mOneRpath.moveTo(mX, mOneNodeY); //这里的参数x和y代表相对当前位置偏移量,y轴不加偏移量会空一截出来,这里的15是经验值 mMainPath.addPath(mOneLpath, 0, EXPRIENCE_OFFSET); mMainPath.addPath(mOneRpath, 0, EXPRIENCE_OFFSET); //第一个节点和第二个节点之间 float gapY = mOneNodeY - mY; mOneLpath.rLineTo(-gapY * tan, -gapY); mOneRpath.lineTo(mX + gapY * tan, mY); } else if (mY <= mTwoNodeY) { mTwoLpath.moveTo(mX, mTwoNodeY); mTwoRpath.moveTo(mX, mTwoNodeY);
//第二个节点,为避免线超出叶子,取此时差值的一半作计算 float gapY = (mTwoNodeY - mY) * 0.5f; mMainPath.addPath(mTwoLpath, 0, EXPRIENCE_OFFSET); mMainPath.addPath(mTwoRpath, 0, EXPRIENCE_OFFSET);
mTwoLpath.rLineTo(-gapY * tan, -gapY); mTwoRpath.rLineTo(gapY * tan, -gapY); } } });
mEngine = new AnimatorSet(); mEngine.playSequentially(mPetioleAnim, mArcAnim, mArcRightAnim, mLastAnim); mEngine.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); setOrginalStatus(); } }); }
|
- 计算贝塞尔曲线运动过程中的方法。贝塞尔曲线是有一个函数的,我们知道起点、控制点、终点的话,就可以根据时间计算出此时此刻的x和y的坐标。而这个时间变化是从0~1变化的。谨记。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| private void computeArcPointF(ValueAnimator animation, boolean isLeft) { float ratio = (float) animation.getAnimatedValue(); //ratio从0~1变化,左右轮廓三个点不一样 PointF bezierStart = isLeft ? mBezierBottom : mBezierTop; PointF bezierControl = isLeft ? mBezierControl : new PointF(mWidth, mHeight * (1 - 3 * PETIOLE_RATIO)); PointF bezierEnd = isLeft ? mBezierTop : new PointF(mWidth * 0.5f, mVeinBottomY); PointF pointF = calculateCurPoint(ratio, bezierStart, bezierControl, bezierEnd); mX = pointF.x; mY = pointF.y; } private PointF calculateCurPoint(float t, PointF p0, PointF p1, PointF p2) { PointF point = new PointF(); float temp = 1 - t; point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x; point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y; return point; }
|
1 2 3 4 5 6 7 8
| public void drawGraph(Canvas canvas, Paint paint) { if (mEngine.isStarted()) { canvas.drawPath(mMainPath, paint); mMainPath.lineTo(mX, mY); } else { mEngine.start(); } }
|
以上,就是本次项目的主要思路了。相关注释代码里都写的很清楚了,项目地址在这里。仿薄荷Loading动画,大家走过路过千万别忘了给个Star啊。