一、需求背景
某天产品经理孙某在厕所拉屎的时候饿了想吃老乡鸡,发现点餐的有个加入购物车的动画,发现我们公司小程序加入购物车没有动画效果,使用起来生硬,用户体验不好,一拍脑袋就提了加个动画的需求。

这种动画该怎么做呢?如果你也想实现它,看完这篇文章,你一定有所收获。我会先说明抛物线动画的原理,再解释实现它的关键代码,最后给出完整的代码示例。
二、抛物线动画的原理
高中物理告诉我们,平抛运动、斜抛运动可以分解为水平方向的匀速直线运动、竖直方向自由落体运动(匀加速直线运动)。

同理,我们可以把抛物线动画分解为水平的匀速动画、竖直的匀加速动画。
水平匀速动画很容易实现,直接 animation-timing-function 取值 linear 就行。
竖直的匀加速直线运动,严格实现非常困难,我们可以近似实现。因为匀加速直线运动,速度是越来越快的,所以我们可以用一个先慢后快的动画替代,你可能立刻就想到给 animation-timing-function 设置 ease-in。不过 ease-in 先慢后快的效果不是很明显。针对这个问题,产品经理边吃老乡鸡边提供了一个贝塞尔曲线 cubic-bezier(0.5, -0.1, 1, 1) forwards;当然,你也可以用 cubic-bezier 自己调一个先慢后快的贝塞尔曲线。
关键代码实现
我们把代码分为两部分,第一部分是布局代码、第二部分是动画代码。
布局代码
首先是 HTML 代码,代码非常简单。下图中小球代表商品、长方形代表购物车。

一般都是种形式,此图由作者本人亲自操刀。 HTML比较轻松,本人轻轻松松搞定,本人的想法是点击元素的时候拿到元素信息,将小球定位到你选择的商品中心。小球的left是固定的,可以写死,高度通过点击事件可以拿到,后面有具体代码。
<view style="top: {{ ballTop }};--ballLeft--:translateX({{ ballLeft }});"
class="ball ball-animation">
<image src="{{ ballAnimationImg }}"
style="--ballHeight--:translateY({{ ballHeight }});"
class="inner inner-animation"></image>
</view>
CSS动画代码
.ball{
position: fixed;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
left: 260rpx;
z-index: 3;
}
.inner{
width: 100%;
height: 100%;
border-radius: 50%;
z-index: 3;
}
.inner-animation {
animation: moveY 0.6s cubic-bezier(0.5, -0.1, 1, 1) forwards;
}
.ball-animation {
animation: moveX 0.6s linear forwards; /* 动画类 */
}
@keyframes moveX {
to {
transform: var(--ballLeft--);
}
}
@keyframes moveY {
to {
transform: var(--ballHeight--);
}
}
JS代码
startBallAnimation(e) {
return new Promise((resolve, reject) => {
if (this.data.ballAnimation) return reject('动画进行中,请稍后'); // 动画正在进行中,拒绝新动画
this.setData({
ballAnimationImg: e.logo,
});
this.createSelectorQuery()
.select(`#dishItem${e.pid}`)
.boundingClientRect(rect => {
if (rect) {
const translateX = this.calculateTranslateX(rect.top);
this.setData({
ballLeft: translateX,
ballTop: rect.top + 'px',
ballAnimation: true,// 启动动画
});
resolve(); // 动画准备完成,resolve
} else {
reject('未找到元素'); // 未找到元素,reject
}
}).exec();
});
},
calculateTranslateX(y) {
// 定义已知的点
const points = [
{ x: -120, y: 140 },
{ x: -150, y: 400 },
{ x: -200, y: 560 },
{ x: -300, y: 648 }
];
// 判断 y 是否在范围内
if (y <= 140) return '-120px';
if (y >= 648) return '-300px';
// 找到 y 值所在的区间
for (let i = 0; i < points.length - 1; i++) {
const p1 = points[i];
const p2 = points[i + 1];
// 如果 y 在两个点之间
if (y >= p1.y && y <= p2.y) {
// 使用线性插值公式
const slope = (p2.x - p1.x) / (p2.y - p1.y);
const x = p1.x + slope * (y - p1.y);
return `${ x }px`; // 返回 px 格式
}
}
// 如果 y 值超出范围,可以选择返回 null 或者抛出错误
return '-120px'; // 或者 throw new Error('y 值超出范围');
},
主要思路就是点击拿到元素信息,作者主要是拿到商品图片,距离顶部的高度(1.根据高度定位小球出现的高度2.根据高度和距离左边的值用线性插值公式动态计算小球向左偏移的距离),开启动画。然后等动画结束,关闭动画开关。
总结
写完后效果跟老乡鸡差不多,产品经理孙某边啃鸡腿边说上线,其实还有几点不足。
1.小球落入的位置有偏移,因为是px,各个设备之间有落入差异
2.高度写死,不够灵活。其实可以用购物车距离顶部的距离减去点击元素距离顶部的距离来设为高度(qunimalaozibuxiele ),不过效果差距不大,而且出于性能原因写死较为方便。
5976





