完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!

完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

原文链接: https://juejin.cn/post/702488...
之前在dribbble看到一个很好看的动画效果,很想要,遂仿之。也为了练一下自定义控件,有段时间了,现在整理出来
完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

dribbble地址:https://dribbble.com/shots/47...
思路 拆解一下,还是比较简单,需要绘制的有:
  • 圆形背景
  • 太阳(圆形)
  • 山(三角形)
  • 云朵(圆角矩形 + 三个圆)
需要进行的动画:
  • 太阳 - 旋转动画
  • 山 - 上下平移动画
  • 云朵 - 左右平移动画
不必绘制圆角外框,因为各个手机厂商的应用icon的圆角不一样,我们可以在Android Studio里生成应用图标。如果有必要也可以自己使用shape画出来。
其中难处是进行太阳的动画和绘制云朵,因为太阳的旋转动画需要计算旋转的圆上点的坐标,而云朵的形状是不规则的。
绘制 1.圆形背景 完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

这里的白色圆角外框是shape画的,蓝色的圆形背景绘制也比较简单,主要是在onDraw()方法里使用canvas.drawCircle()
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 将View切成圆形,否则绘制的山和云朵会出现在圆形背景之外 mRoundPath.reset(); mRoundPath.addCircle(mViewCircle, mViewCircle, mViewCircle, Path.Direction.CW); canvas.clipPath(mRoundPath); // 绘制圆形背景 canvas.drawCircle(mViewCircle, mViewCircle, mViewCircle, mBackgroundPaint); }

这里的mViewCircle是指view的半径;mBackgroundPaint是用来画背景色的Paint。
mViewCircle获取:
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 取宽高的最小值 mParentWidth = mParentHeight = Math.min(getWidth(), getHeight()); // View的半径 mViewCircle = mParentWidth >> 1; }

mBackgroundPaint背景色设置一个颜色就好:
mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBackgroundPaint.setColor(mBackgroundColor);

其中如果不将View切成圆形会出现的情况为:
完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

2.绘制太阳和进行旋转动画 完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

如果是单纯画太阳的话,确定好x,y坐标和半径,然后加个颜色paint就好了:
canvas.drawCircle((mParentWidth / 2) - getValue(90), (mParentHeight / 2) - getValue(80), sunWidth / 2, mSunPaint);

但是我们要加上动画,这时候我们需要了解到:
/** * Transform the points in this path by matrix, and write the answer * into dst. If dst is null, then the the original path is modified. * * @param matrix The matrix to apply to the path * @param dstThe transformed path is written here. If dst is null, *then the the original path is modified */ public void transform(Matrix matrix, Path dst) { long dstNative = 0; if (dst != null) { dst.isSimplePath = false; dstNative = dst.mNativePath; } nTransform(mNativePath, matrix.native_instance, dstNative); }

该方法可以将一个path进行matrix转换,即矩阵转换,因此我们可以通过方法matrix.postTranslate来实现平移动画,即创建一个循环动画,通过postTranslate来设置动画值就可以了。
/** * Postconcats the matrix with the specified translation. M' = T(dx, dy) * M * dx,dy,是x,y坐标移动的差值。 */ public boolean postTranslate(float dx, float dy) { nPostTranslate(native_instance, dx, dy); return true; } 复制代码 我们先在onSizeChanged()里得到起始点太阳圆心的x,y坐标,然后再在onDraw()里实时获取要旋转时的x,y坐标,最后得到对应的差值。 onSizeChanged()里绘制太阳和得到旋转时起始点的x,y坐标: private void drawSun() { // sun图形的直径 int sunWidth = getValue(70); // sun图形的半径 int sunCircle = sunWidth / 2; // sun动画半径 = (sun半径 + 80(sun距离中心点的高度) + 整个View的半径 + sun半径 + 20(sun距离整个View的最下沿的间距)) / 2 mSunAnimCircle = (sunWidth + getValue(100) + mViewCircle) / 2; // sun动画的圆心x坐标 mSunAnimX = mViewCircle; // sun动画的圆心y坐标 = sun动画半径 + (整个View的半径 - 80(sun距离中心点的高度) - sun半径) mSunAnimY = mSunAnimCircle + (mViewCircle - getValue(80) - sunCircle); // 得到圆形旋转动画起始点的x,y坐标,初始角度为-120 mSunAnimXY = getCircleXY(mSunAnimX, mSunAnimY, mSunAnimCircle, -120); // 绘制sun mSunPath.addCircle(mSunAnimXY[0], mSunAnimXY[1], sunCircle, Path.Direction.CW); }

其中稍微困难点的是得到圆上的x,y坐标 getCircleXY()
完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

已知的条件:圆心O的坐标(mSunAnimX,mSunAnimY)、半径为sunCircle、角度angle = -120
(角度是相对于图中横线,顺时针为正,逆时针为负),要计算p点的坐标(x1,y1)有如下公式:
x1 = x0 + r * cos(angle * PI / 180) y1 = y0 + r * sin(angle * PI /180)

其中angle* PI/180是将角度转换为弧度。
/** * 求sun旋转时,圆上的点。起点为最右边的点,顺时针。 * x1=x0+r*cos(a*PI/180) * y1=y0+r*sin(a*PI/180) * * @param angle角度 * @param circleCenterX 圆心x坐标 * @param circleCenterY 圆心y坐标 * @param circleR半径 */ private int[] getCircleXY(int circleCenterX, int circleCenterY, int circleR, float angle) { int x = (int) (circleCenterX + circleR * Math.cos(angle * Math.PI / 180)); int y = (int) (circleCenterY + circleR * Math.sin(angle * Math.PI / 180)); return new int[]{x, y}; }

然后我们在onDraw()里可动态得到圆上的其他点的x,y坐标达到旋转的效果:
// x y 坐标 int[] circleXY = getCircleXY(mSunAnimX, mSunAnimY, mSunAnimCircle, mSunAnimatorValue); mSunComputeMatrix.postTranslate(circleXY[0] - mSunAnimXY[0], circleXY[1] - mSunAnimXY[1]); mSunPath.transform(mSunComputeMatrix, mSunComputePath); canvas.drawPath(mSunComputePath, mSunPaint); 复制代码 mSunAnimatorValue为变化的角度[-120,240]。这样就可以执行太阳的旋转动画: /** * sun的动画 */ private void setSunAnimator() { ValueAnimator mSunAnimator = ValueAnimator.ofFloat(-120, 240); mSunAnimator.setDuration(2700); mSunAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); mSunAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mSunAnimatorValue = https://www.it610.com/article/(float) animation.getAnimatedValue(); invalidate(); } }); mSunAnimator.start(); }

3.山和上下平移动画 完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

画了上面的太阳旋转动画后,这个就相对比较简单了,因为只涉及到纵坐标y的变化,x不会变,仔细观察会发现,y坐标会先向上移动然后再向下快速移动。
onSizeChanged()里绘制三座山和得到要平移的y坐标:drawMou(mViewCircle, mViewCircle - getValue(10), getValue(10));
/** * 画中间的三座山 * * @param x 中心点左坐标 * @param y 中心点右坐标 */ private void drawMou(int x, int y, int down) { // 左右山 Y坐标相对于中心点下移多少 int lrmYpoint = down + getValue(30); // 左右山 X坐标相对于中心点左移或右移多少 int lrdPoint = getValue(120); // 左右山 山的一半的X间距是多少 int lrBanDis = getValue(140); // 中间山 山的一半的X间距是多少 int lrBanGao = getValue(150); // 左山 mLeftMountainPath.reset(); // 起点 mLeftMountainPath.moveTo(x - lrdPoint, y + lrmYpoint); mLeftMountainPath.lineTo(x - lrdPoint + lrBanDis, y + lrmYpoint + lrBanGao); mLeftMountainPath.lineTo(x - lrdPoint - lrBanDis, y + lrmYpoint + lrBanGao); // 使这些点构成封闭的多边形 mLeftMountainPath.close(); // 右山 mRightMountainPath.reset(); mRightMountainPath.moveTo(x + lrdPoint + getValue(10), y + lrmYpoint); mRightMountainPath.lineTo(x + lrdPoint + getValue(10) + lrBanDis, y + lrmYpoint + lrBanGao); mRightMountainPath.lineTo(x + lrdPoint + getValue(10) - lrBanDis, y + lrmYpoint + lrBanGao); mRightMountainPath.close(); // 中山 mMidMountainPath.reset(); mMidMountainPath.moveTo(x, y + down); mMidMountainPath.lineTo(x + getValue(220), y + down + mParentHeight / 2 + mParentHeight / 14); mMidMountainPath.lineTo(x - getValue(220), y + down + mParentHeight / 2 + mParentHeight / 14); mMidMountainPath.close(); // 左右山移动的距离 mMaxMouTranslationY = (y + down + mViewCircle) / 14; }

然后我们在onDraw()里根据动态的y坐标去移动,以中间的山为例:
// 中间的山 mMidComputeMatrix.reset(); mMidComputePath.reset(); mMidComputeMatrix.postTranslate(0, mMaxMouTranslationY * mMidMouAnimatorValue); mMidMountainPath.transform(mMidComputeMatrix, mMidComputePath); canvas.drawPath(mMidComputePath, mMidMountainPaint);

mMidMouAnimatorValue变化,注意y坐标会先上升一点再下降:
/** * 中间山的动画 */ private void setMidMouAnimator(final boolean isFirst) { ValueAnimator mMidMouAnimator; if (isFirst) { mMidMouAnimator = ValueAnimator.ofFloat(0, -1, 10); mMidMouAnimator.setStartDelay(200); mMidMouAnimator.setDuration(1000); } else { mMidMouAnimator = ValueAnimator.ofFloat(10, 0); mMidMouAnimator.setStartDelay(0); mMidMouAnimator.setDuration(600); } mMidMouAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); mMidMouAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMidMouAnimatorValue = https://www.it610.com/article/(float) animation.getAnimatedValue(); invalidate(); } }); mMidMouAnimator.start(); }

4.云朵和左右平移动画 完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!
文章图片

这次的动画和山的动画非常相似,只是由y坐标的变化改成x坐标的变化,但是绘制云朵稍微有点麻烦:
想要深入了解的可看这里:Android 自定义View之下雨动画 - 画云。总的来说是由四块view组成,底部的矩形(因为整体下移了所以这里基本没有看到矩形),还有矩形上面的三个圆形。
// 绘制圆角矩形 path.addRoundRect(RectF rect, float rx, float ry, Direction dir) // 绘制圆形 path.addCircle(float x, float y, float radius, Direction dir)

然后得到x坐标后根据增量值mCloudAnimatorValue进行动态移动:
mCloudComputeMatrix.postTranslate(mMaxCloudTranslationX * mCloudAnimatorValue, 0); mCloudPath.transform(mCloudComputeMatrix, mCloudComputePath); canvas.drawPath(mCloudComputePath, mCloudPaint);

然后我们将太阳的旋转动画、三座山的上下平移动画、云朵的左右平移动画,这五个动画组合起来就得到了一个完整的连贯动画。
最后 为了扩展性,我们给View增加一些属性,用来自定义颜色:

这里的主要难点是动画的理解和使用:
matrix.postTranslate(dx, dy); path.transform(matrix, momputePath); canvas.drawPath(momputePath, mPaint);

我们通过动态改变dx和dy的值来达到动的效果,然后就是绘制三角形、圆形、圆角矩形以及它们坐标位置的动态处理。
以上源代码在这里可以拿到:https://github.com/youlookwha...
文末 【完美!自定义View实现Dribbble上动感的Gallery App Icon 动画!】您的点赞收藏就是对我最大的鼓励!
欢迎关注我的简书,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,欢迎在评论区一起留言讨论!

    推荐阅读