相关推荐:
1. 简述
这次来实现一个简单的动画,算是动画方面的实践了。要实现的效果如下:
这是一个指示牌,每切换一次,就会从底部重新弹出来,并且文字也改变。动画简单,适合入门。
2. 动画分解
动画在一个Rect:240px*290px
的方形区域内进行(这里以1280*720的尺寸为例),其有三个主体:指示牌,指示牌杆,指示牌文字。动画时间是350毫秒,速度是先加速后减速。
指示牌杆:
一开始位于底部,露出一点,并且有顺时针角度的倾斜,偏离X轴中央的右边,然后从底部弹出来,升到快到最高处,然后下落,最后到达终点,终点是在X轴的中央,杆的底部贴紧动画区域的底部,并且杆的倾斜度变成0°。
- 1)从偏离X轴中央的右边移动到终点,这是X轴方向的位移
- 2)弹出到落下,这是Y轴方向的位移变化
- 3)倾斜角度X到倾斜角度为0,这是旋转角度的变化
指示牌:
指示牌也是跟指示牌杆同样的动画,一开始在动画区域的底下,不可见,然后弹出来,具体来看,指示牌是没有X轴方向的位移,整个过程都是在动画区域X轴中央,只有Y轴方向的位移,和旋转角度变化
指示牌文字:
文字随着指示牌,同样的动画,不过要注意文字绘制的起点。
3. 简单版实现
我们设置一个动画完成度因子:changeFactor
,用ObjectAnimator
去改变因子的值,从而带动每一个主体的运动。
private float changeFactor = 0;
private final float FACTOR_END = 100;
private final float FACTOR_START = 0;
objectAnimator = ObjectAnimator.ofFloat(this, "changeFactor", FACTOR_START, FACTOR_END);
objectAnimator.setDuration(350);
objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
objectAnimator.start();
changeFactor
代表着整个动画的进度,进度是从0-100,别忘了要设置对应的getter/setter方法,在setter方法里面,通知view重绘。
public void setChangeFactor(float changeFactor) {
this.changeFactor = changeFactor;
invalidate();
}
public float getChangeFactor() {
return changeFactor;
}
指示牌杆
指示牌杆有三种变化,组合起来就是最终的效果,我们先看第一种:
- 这里我们加载指示牌杆:
bitmapTrunk = BitmapFactory.decodeResource(getResources(), R.drawable.car_sign_trunk);
- 从偏离X轴中央的右边移动到终点,这是X轴方向的位移,从UI提供的具体数据,变化是从距离X轴中心位置的右边移动到中央,移动了
bitmapTrunk.getWidth() / 2
,而我们的changeFactor
也就是动画完成度是从0->100
,需要映射到0->bitmapTrunk.getWidth() / 2
,所以需要一个映射函数:
/**
* 因子映射
* @param currentFactor 原因子的值
* @param origStartFactor 原因子的上限
* @param origEndFactor 原因子的下限
* @param startFactor 新因子的上限
* @param endFactor 新因子的下限
* @return 新因子的值
*/
public static float factorMapping(float currentFactor, float origStartFactor, float origEndFactor, float startFactor, float endFactor) {
return (currentFactor - origStartFactor) * (endFactor - startFactor) / (origEndFactor - origStartFactor) + startFactor;
}
- 应用到
onDraw
里面,为了更好地说明,绘制了一个动画区域:RectF rectFSign = new RectF(0,0,240,290)
,因为考虑到后面是需要旋转,位移等动画,所以,这里用Matrix
去绘制Bitmap
//绘制动画区域
paint.setColor(Color.GRAY);
canvas.translate(100,20);
canvas.drawRect(rectFSign,paint);
// bitmapTrunk
//横向位移,bitmapTrunk width/2
matrix.reset();
float xOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, 0, bitmapTrunk.getWidth() / 2);
matrix.postTranslate(rectFSign.right/2 - xOffset, rectFSign.bottom);
canvas.drawBitmap(bitmapTrunk, matrix, paint);
- 出来的效果如下:
接着看第二种,是Y轴的位移
- 我们以绘制区域左上角为原点,杆的Y轴运动就是:从开始(杆顶距离绘制区域bottom的35px处)
y=255
,向上移动到y=5
的位置,然后再向下移动到y=120
的位置,所以呢,这里我们又得对changeFactor
Mapping到y=255 -> y=5 -> 120
,这里Mapping变化的中间有一个插值,所以不能直接使用上面的函数,要改造一下, 这算是一个数学问题了:给出两个变化区域A:(origStartFactor,origEndFactor),B:(startFactor,interpolatorFactor,endFactor)和区域A当前的值currentFactor,B区域有一个插值,即其变化是:startFacotr -> interpolatorFactor -> endFactor,求出,区域A当前的值currentFactor,所对应的B区域的当前值 currentFactor ? 这里给出我写的一个答案:
/**
* 因子映射
* @param currentFactor 原因子的值
* @param origStartFactor 原因子的上限
* @param origEndFactor 原因子的下限
* @param startFactor 新因子的上限
* @param interpolatorFactor 新因子的插值
* @param endFactor 新因子的下限
* @return 新因子的值
*/
public static float factorMapping(float currentFactor, float origStartFactor, float origEndFactor,
float startFactor, float interpolatorFactor, float endFactor) {
//现在的百分比
float temp = (currentFactor - origStartFactor) / (origEndFactor - origStartFactor);
//新的总大小
float newFactorMax = Math.abs(startFactor-interpolatorFactor) + Math.abs(endFactor-interpolatorFactor);
//上限所占的百分比
float startTemp = Math.abs(startFactor-interpolatorFactor) / newFactorMax;
if (startTemp<temp) {
//当前限所占百分比
float tempX = Math.abs( (temp - startTemp) * newFactorMax / Math.abs(endFactor-interpolatorFactor));
return tempX * (endFactor-interpolatorFactor) + interpolatorFactor;
}
else {
//当前限所占百分比
float tempX = Math.abs(temp * newFactorMax / Math.abs(startFactor-interpolatorFactor));
return tempX * (interpolatorFactor-startFactor) + startFactor;
}
}
- 应用到
onDraw
里面
// bitmapTrunk
//横向位移,bitmapTrunk width/2
matrix.reset();
float xOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, 0, bitmapTrunk.getWidth() / 2);
//纵向位移,( rectFSign.bottom - 35 , 5 , 120 )
float yOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, rectFSign.bottom - 35, 5, 120);
matrix.postTranslate(rectFSign.right/2 - xOffset, yOffset);
canvas.drawBitmap(bitmapTrunk, matrix, paint);
第三种也是一样
- 旋转从
21°
到0°
,我们要基于bitmap进行中心旋转
,封装一下matrix
的旋转操作:
/**
* 将旋转应用到矩阵
* 此旋转是基于Bitmap的中心旋转
*
* @param bitmap 位图对象
* @param rotation 旋转度数
* @param posX canvas绘制Bitmap的起点
* @param posY canvas绘制Bitmap的起点
* @param matrix matrix
*/
public static void applySelfRotationToMatrix(Bitmap bitmap, float rotation, float posX, float posY, Matrix matrix) {
float offsetX = bitmap.getWidth() / 2;
float offsetY = bitmap.getHeight() / 2;
matrix.postTranslate(-offsetX, -offsetY);
matrix.postRotate(rotation);
matrix.postTranslate(posX + offsetX, posY + offsetY);
}
- 应用到
onDraw
// bitmapTrunk
//横向位移,bitmapTrunk width/2
float xOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, 0, bitmapTrunk.getWidth() / 2);
//纵向位移,( rectFSign.bottom - 35 , 5 , 120 )
float yOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, rectFSign.bottom - 35, 5, 120);
//旋转,21° - 0°,逆时针
float rotationOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, 21, 0);
matrix.reset();
ViewUtils.applySelfRotationToMatrix(bitmapTrunk, rotationOffset,rectFSign.right / 2 - xOffset, yOffset, matrix);
canvas.drawBitmap(bitmapTrunk, matrix, paint);
指示牌
有了上面的分析,指示牌也是一样的:
// bitmapCover
//旋转,-10° - 0°,逆时针
rotationOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, -10, 0);
//纵向位移,( rectFSign.bottom , 80 , 108 )
yOffset = ViewUtils.factorMapping(changeFactor, FACTOR_START, FACTOR_END, rectFSign.bottom, 80, 108);
matrix.reset();
ViewUtils.applySelfRotationToMatrix(bitmapCover, rotationOffset,rectFSign.right / 2 - bitmapCover.getWidth() / 2, yOffset, matrix);
canvas.drawBitmap(bitmapCover, matrix, paint);
指示牌文字
- 文字需要跟着指示牌一起动,也就是旋转,位移,所以普通的
drawText
就不行了,我们使用drawTextOnPath
,这方法是在一条path上写文本,只要在指示牌的相应位置画一条path,path上就写文本,然后让这path随着指示牌一起动,也就是文本也随着动了。为了说明,放出一张灵魂画图:
- 如果,指示牌没有旋转,那么其文本
path
就是蓝色的AB线(大致位置,后面可以微调),A,B点的坐标也非常容易得到,而旋转我们是使用了基于指示牌的中心旋转,也就是基于M点的旋转,那么蓝色的AB线也应该是基于M点旋转相同的角度得到对应的红色A'B'线,那么我们怎么样获取到A',B'点的坐标呢?这又是一个数学问题了:在坐标平面上,给出一点A(ax,ay),和M(mx,my),求A点基于M点旋转任意角度θ后的A'坐标。
/**
* 在平面中,一个点绕任意点旋转 radian 弧度
* 后的点的坐标
*
* @param rX0 旋转基准点x
* @param rY0 旋转基准点y
* @param radian 旋转radian
* @param x 旋转点x
* @param y 旋转点y
* @return 旋转的结果,x,y
*/
public static float pointRotateGetX(float rX0, float rY0, float radian, float x, float y) {
return (float) ((x - rX0) * Math.cos(radian) - (y - rY0) * Math.sin(radian) + rX0);
}
public static float pointRotateGetY(float rX0, float rY0, float radian, float x, float y) {
return (float) ((x - rX0) * Math.sin(radian) + (y - rY0) * Math.cos(radian) + rY0);
}
- 来先画一条Path,旋转一下看看:
//左下点 A
float x0 = rectFSign.right / 2 - bitmapCover.getWidth() / 2;
float y0 = yOffset + bitmapCover.getHeight();
//旋转点 M
float rX0 = x0 + bitmapCover.getWidth() / 2;
float rY0 = y0 - bitmapCover.getHeight() / 2;
//右下点 B
float x1 = x0 + bitmapCover.getWidth();
float y1 = y0;
//旋转弧度
float radians = (float) Math.toRadians(rotationOffset);
//A'
path.moveTo(ViewUtils.pointRotateGetX(rX0, rY0, radians, x0, y0), ViewUtils.pointRotateGetY(rX0, rY0, radians, x0, y0));
//B'
path.lineTo(ViewUtils.pointRotateGetX(rX0, rY0, radians, x1, y1), ViewUtils.pointRotateGetY(rX0, rY0, radians, x1, y1));
//A'B'线
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
canvas.drawPath(path,paint);
- 嗯,差不多了,微调一下,把文字写上去就可以了:
// str 的绘制 path
paint.setColor(Color.RED);
path.reset();
//左下点
float x0 = rectFSign.right / 2 - bitmapCover.getWidth() / 2;
float y0 = yOffset + bitmapCover.getHeight();
//旋转点
float rX0 = x0 + bitmapCover.getWidth() / 2;
float rY0 = y0 - bitmapCover.getHeight() / 2;
//右下点
float x1 = x0 + bitmapCover.getWidth();
float y1 = y0;
float radians = (float) Math.toRadians(rotationOffset);
path.moveTo(ViewUtils.pointRotateGetX(rX0, rY0, radians, x0, y0), ViewUtils.pointRotateGetY(rX0, rY0, radians, x0, y0));
path.lineTo(ViewUtils.pointRotateGetX(rX0, rY0, radians, x1, y1), ViewUtils.pointRotateGetY(rX0, rY0, radians, x1, y1));
paint.setColor(Color.WHITE);
paint.setTextSize(signStrSize);
if (typeface != null)
paint.setTypeface(typeface);
float hOffset = (bitmapCover.getWidth() - paint.measureText(signStr)) / 2;
canvas.drawTextOnPath(signStr, path, hOffset, -20, paint);
详情可以看Demo
4. 优雅版实现
- 上面的写完后,总感觉不太优雅,好像很多函数很多API都自己写,难道原生的没有吗?查了一下,自己还是太年轻了,原生早就有这样的一套动画API。
- 我们的动画涉及到诸多属性值的变化,而且是共享动画时间和插值器,所以这里应该使用
AnimatorSet
属性动画集合去做。PropertyValuesHolder
是动画集合的元素,代表一个属性值。 这里直接贴出代码,主要是要确定好,属性值的变化区域即可:
//指示牌杆X轴的位移
PropertyValuesHolder trunkXPropertyValuesHolder =
PropertyValuesHolder.ofFloat("trunkX",trunkX, trunkX-bitmapTrunk.getWidth()/2);
//指示牌杆Y轴的位移
PropertyValuesHolder trunkYPropertyValuesHolder =
PropertyValuesHolder.ofFloat("trunkY",trunkY,5,120);
//指示牌杆的旋转变化
PropertyValuesHolder trunkRotationDegreePropertyValuesHolder =
PropertyValuesHolder.ofFloat("trunkRotationDegree",trunkRotationDegree,0);
//指示牌Y轴的位移
PropertyValuesHolder coverYPropertyValuesHolder =
PropertyValuesHolder.ofFloat("coverY",coverY,80,108);
//指示牌Y轴的旋转变化
PropertyValuesHolder coverRotationDegreePropertyValuesHolder =
PropertyValuesHolder.ofFloat("coverRotationDegree",coverRotationDegree,0);
//添加到动画集合
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(this,
trunkXPropertyValuesHolder,trunkYPropertyValuesHolder,trunkRotationDegreePropertyValuesHolder,
coverYPropertyValuesHolder,coverRotationDegreePropertyValuesHolder);
animator.setDuration(350);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(valueAnimator -> CarAdvancedSignView.this.invalidate());
animator.start();
- 这里,你还要声明对应的一系列的属性值:
trunkX,trunkY,trunkRotationDegree ...
和对应的getter/setter
方法。注意到,动画属性值更新的时候,会逐一调用每一个属性值的setter
方法,当所有的属性值都更新完一遍后,会调用:
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
}
});
-
所以,你需要在
onAnimationUpdate
里面去主动刷新。 -
文字的话,跟上面一样的思路。
-
最后的效果:
效果是差不多的,CPU的表现也是差不多的,代码量,啧啧啧...
详情可以看Demo
5. 总结
- 通过这个动画,熟悉了
drawTextOnPath
的使用,当文字有动画效果的时候(主要是位移,旋转),就可以考虑用这个思路 - 更加熟悉了
AnimatorSet
的使用
码字不易,方便的话素质三连,或者关注我的公众号 技术酱,专注 Android 技术,不定时推送新鲜文章,如果你有好的文章想和大家分享,欢迎关注投稿!