此为前文章写的仿Win10加载动画的优化版
源代码
已更新到github
优化分析
原生 | 自定义高仿(v1版) |
---|---|
一直觉得自己写的与原生的有差别,经过仔细对比观察,发现:
- 原生的圆点出发位置不是都在底部,而是第一个在底部,后面的紧接着前面一个,像球在管子里一样
- 圆点结束的位置就是该圆点开始的位置
- 经过比对,发现一个周期的时间是7500ms,非7000ms
经过优化后的对比:
原生 | 自定义高仿(v1.1版) |
---|---|
优化后的时间校正图:
核心代码
不可否认,原生更加自然。为了自然,尝试了过去掉顶部的两段匀速运动,直接用四段三阶贝塞尔曲线,原理上是行的通的。但测试过N多参数,后来还是觉得不够自然,放弃了。最终还是选择原来的方法。
起始角度的计算
第一个圆点在最底部,第二个与第一个相差一个圆点对旋转中心所占的角度,后面也是圆点也是一样,与前一个圆点相差此角度。此角度可通过圆点半径与轨迹半径计算:
// 计算圆点对旋转中心所占的角度
float trackR = halfSize - dotR;
dotDegree = (float) Math.toDegrees(2 * Math.asin(dotR / trackR));
参数与之前相比,有了很大调整(最主要的就是参数,调了无数遍才调出来……):
/**
* 创建动画
*
* @param view 需执行的控件
* @param index 该控件执行的顺序
* @return 该控件的动画
*/
private Animator createViewAnim(final View view, final int index) {
long duration = 7500; // 一个周期(2圈)一共运行7500ms,固定值
// 最小执行单位时间
final float minRunUnit = duration / 100f;
// 最小执行单位时间所占总时间的比例
double minRunPer = minRunUnit / duration;
// 在插值器中实际值(Y坐标值),共8组
final double[] trueRunInOne = new double[]{
0,
0,
160 / 720d,
190 / 720d,
360 / 720d,
520 / 720d,
550 / 720d,
1
};
// 动画开始的时间比偏移量。剩下的时间均摊到每个圆点上
final float offset = (float) (index * (100 - 86) * minRunPer / (mDotViews.length - 1));
// 在差值器中理论值(X坐标值),与realRunInOne对应
final double[] rawRunInOne = new double[]{
0,
offset + 0,
offset + 11 * minRunPer,
offset + 32 * minRunPer,
offset + 43 * minRunPer,
offset + 54 * minRunPer,
offset + 75 * minRunPer,
offset + 86 * minRunPer
};
logI("minRunUnit=%f, minRunPer=%f, offset=%f", minRunUnit, minRunPer, offset);
// 各贝塞尔曲线控制点的Y坐标
final float p1_2 = calculateLineY(rawRunInOne[2], trueRunInOne[2], rawRunInOne[3], trueRunInOne[3], rawRunInOne[1]);
final float p1_4 = calculateLineY(rawRunInOne[2], trueRunInOne[2], rawRunInOne[3], trueRunInOne[3], rawRunInOne[4]);
final float p1_5 = calculateLineY(rawRunInOne[5], trueRunInOne[5], rawRunInOne[6], trueRunInOne[6], rawRunInOne[4]);
final float p1_7 = calculateLineY(rawRunInOne[5], trueRunInOne[5], rawRunInOne[6], trueRunInOne[6], rawRunInOne[7]);
// A 创建属性动画:绕着中心点旋转2圈
ObjectAnimator objAnim = ObjectAnimator.ofFloat(view, "rotation", -dotDegree * index, 720 - dotDegree * index);
// B 设置一个周期执行的时间
objAnim.setDuration(duration);
// C 设置重复执行的次数:无限次重复执行下去
objAnim.setRepeatCount(ValueAnimator.INFINITE);
// D 设置差值器
objAnim.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
if (input < rawRunInOne[1]) {
// 1 等待开始
if (view.getVisibility() != INVISIBLE) {
view.setVisibility(INVISIBLE);
}
return 0;
} else if (input < rawRunInOne[2]) {
if (view.getVisibility() != VISIBLE) {
view.setVisibility(VISIBLE);
}
// 2 底部 → 左上角:贝赛尔曲线1
// 先转换成[0, 1]范围
input = calculateNewPercent(rawRunInOne[1], rawRunInOne[2], 0, 1, input);
return calculateBezierQuadratic(trueRunInOne[1], p1_2, trueRunInOne[2], input);
} else if (input < rawRunInOne[3]) {
// 3 左上角 → 顶部:直线
return calculateLineY(rawRunInOne[2], trueRunInOne[2], rawRunInOne[3], trueRunInOne[3], input);
} else if (input < rawRunInOne[4]) {
// 4 顶部 → 底部:贝赛尔曲线2
input = calculateNewPercent(rawRunInOne[3], rawRunInOne[4], 0, 1, input);
return calculateBezierQuadratic(trueRunInOne[3], p1_4, trueRunInOne[4], input);
} else if (input < rawRunInOne[5]) {
// 5 底部 → 左上角:贝赛尔曲线3
input = calculateNewPercent(rawRunInOne[4], rawRunInOne[5], 0, 1, input);
return calculateBezierQuadratic(trueRunInOne[4], p1_5, trueRunInOne[5], input);
} else if (input < rawRunInOne[6]) {
// 6 左上角 → 顶部:直线
return calculateLineY(rawRunInOne[5], trueRunInOne[5], rawRunInOne[6], trueRunInOne[6], input);
} else if (input < rawRunInOne[7]) {
// 7 顶部 → 底部:贝赛尔曲线4
input = calculateNewPercent(rawRunInOne[6], rawRunInOne[7], 0, 1, input);
return calculateBezierQuadratic(trueRunInOne[6], p1_7, trueRunInOne[7], input);
} else {
// 8 消失
if (view.getVisibility() != INVISIBLE) {
view.setVisibility(INVISIBLE);
}
return 1;
}
}
});
return objAnim;
}