之前在dribbble看到一个很好看的动画效果,很想要,遂仿之。也为了练一下自定义控件,有段时间了,现在整理出来
dribbble地址:https://dribbble.com/shots/4761564
拆解一下,还是比较简单,需要绘制的有:
需要进行的动画:
不必绘制圆角外框,因为各个手机厂商的应用icon的圆角不一样,我们可以在Android Studio里生成应用图标。如果有必要也可以自己使用shape画出来。
其中难处是进行太阳的动画和绘制云朵,因为太阳的旋转动画需要计算旋转的圆上点的坐标,而云朵的形状是不规则的。
这里的白色圆角外框是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切成圆形会出现的情况为:
如果是单纯画太阳的话,确定好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 dst The 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()
:
已知的条件:圆心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 = (float) animation.getAnimatedValue();
invalidate();
}
});
mSunAnimator.start();
}
画了上面的太阳旋转动画后,这个就相对比较简单了,因为只涉及到纵坐标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 = (float) animation.getAnimatedValue();
invalidate();
}
});
mMidMouAnimator.start();
}
这次的动画和山的动画非常相似,只是由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增加一些属性,用来自定义颜色:
<declare-styleable name="SceneryView">
<!--The color of sun-->
<attr name="sun_color" format="color" />
<!--The color of the cloud-->
<attr name="cloud_color" format="color" />
<!--The color of the left mountain-->
<attr name="left_mountain_color" format="color" />
<!--The color of the right mountain-->
<attr name="right_mountain_color" format="color" />
<!--The color of the middle mountain-->
<attr name="mid_mountain_color" format="color" />
<!--The color of the background-->
<attr name="background_color" format="color" />
</declare-styleable>
这里的主要难点是动画的理解和使用:
matrix.postTranslate(dx, dy);
path.transform(matrix, momputePath);
canvas.drawPath(momputePath, mPaint);
我们通过动态改变dx和dy的值来达到动的效果,然后就是绘制三角形、圆形、圆角矩形以及它们坐标位置的动态处理。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/SM94lGglCGbu3QU3qwtBnQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。