前端动画问题和总结
在移动端开发带有动画的页面时,必须要考虑动画的兼容性和动画的流畅性,这里对遇到的动画问题做一些总结,同时这里得出一个结论:
复杂的动画还是不能单靠纯css解决,还是需要使用javascript,在某些transfrom与动画物体样式不兼容的时候,可以在动画期间使用transform,动画结束的时候再恢复原来的样子。比如:平时用margin-top撑开高度,在动画开始时用transformY替代,动画结束后再换回margin-top。
遇到的已知问题
- 安卓手机动画卡顿
- transform包裹fixed元素、sticky元素造成位置错乱
- 拖动(上下)动画:与滚动冲突,与浏览器左滑后退手势冲突,与浏览器下拉手势冲突
- 安卓低端机:canvas动画卡顿
- 双击造成动画执行多遍,双击元素移走后点击到链接跳走,返回后因为动画未执行完成造成动画效果错乱
css动画实现
使用transition
实现:
.anim-block{
transition: transform 0.3s ease-in-out;
will-change: transform;
transform: translate3d(0px, 0px, 0px);
&.clear {
transition: unset;
transform: unset;
will-change: unset;
}
&.loading {
transform: translate3d(0px, 80px, 0px);
}
}
使用animation
实现:
.anim-block{
animation: rotate 0.8s infinite;
}
@keyframes rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
常见的动画函数
参考:缓动函数
- linear: 匀速
- ease: 渐入缓出
- ease-in: 缓入
- ease-out: 缓出
- ease-in-out: 缓入缓出
拖动(上下)动画
<div
onTouchStart={handleTouchStart}
>
...
</div>
拖动:
const handleTouchStart = (_e: React.TouchEvent | TouchEvent) => {
const passiveFlag = {
passive: false,
capture: false,
};
const top =
ref.current.getBoundingClientRect().top || ref.current.offsetTop;
let startY = 0;
let tmpY = _e.touches[0].clientY;
const handleTouchMove = (e: TouchEvent) => {
const v = e.touches[0].clientY - tmpY;
tmpY = e.touches[0].clientY;
startY += avgV;
const curTop = top + startY;
this.ref.current?.style.setProperty(
'transform',
`translate3d(0,${curTop}px,0)`,
);
};
const handleTouchEnd = (e: TouchEvent) => {
document.removeEventListener('touchmove', handleTouchMove, passiveFlag);
document.removeEventListener('touchend', handleTouchEnd, false);
document.removeEventListener('touchcancel', handleTouchEnd, false);
};
document.addEventListener('touchmove', handleTouchMove, passiveFlag);
document.addEventListener('touchend', handleTouchEnd, false);
document.addEventListener('touchcancel', handleTouchEnd, false);
}
蒙层实现遮罩动画
主要原理:利用box-shadow
属性实现遮罩效果,通过改定box-shadow层的样式而改变蒙层,而内容区域并不会改变,从而达到遮罩动画的效果。
<div class="card">
<div class="card__mask"></div>
<div class="card__inner">demo</div>
</div>
样式:
.card {
height: 500px;
box-shadow: 1px 1px 1px #ccc;
position: relative;
}
.card__mask {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 178px;
box-shadow: 0 0 0 2000px #ffff;
border-radius: 12px;
margin-top: 15px;
margin-right: 8px;
margin-left: 8px;
transition: all 0.5s ease-in-out;
}
.card.active .card__mask {
margin-top: 0;
height: 100%;
margin-left: 0;
margin-right: 0;
border-radius: 0;
}
.card__inner {
display: flex;
justify-content: center;
height: 100%;
color: white;
font-size: 20px;
font-weight: bold;
background-color: blue;
padding-top: 40px;
}
css动画性能和兼容性
使用transform
和transform3d
来实现动画,在动画移动前添加will-change
属性。触发GPU渲染能够使整体动画在安卓上不卡顿,ios则无此问题。
canavs动画
const _run = () => {
if (typeof window === 'undefined') {
return;
}
if (!isStop) {
run(); // 绘制动画
tIndex = window.requestAnimationFrame(_run);
} else {// 暂停
tIndex && window.cancelAnimationFrame(tIndex);
tIndex = 0;
}
};
if (
typeof window !== 'undefined' &&
window.requestAnimationFrame &&
!duration
) {
tIndex = window.requestAnimationFrame(_run);
} else {
tIndex = setInterval(() => {
if (isStop) {
tIndex && clearInterval(tIndex);
tIndex = 0;
} else {
run(); // 绘制动画
}
}, duration);
}
canvas颜色渐变
css渐变颜色转canvas渐变:
参考:canvas线性渐变实现:根据渐变线角度计算坐标x0,y0,x1,y1
颜色渐变动画
通过透明度实现一种颜色到另一种颜色的变化
function draw(color: string, alpha: number){
context.save();
context.globalAlpha = alpha; // 设置透明度
context.fillStyle = color;
// 绘制图像
context.restore();
}
draw(color1, alpha); // color1: 原来的颜色
draw(color2, 1 - alpha); // color2: 新的颜色
canvas动画性能优化
1. save-restore必须要成对出现
context.save
用于存储当前状态,需要在绘制完成后释放当前状态,否则会造成内存泄露,页面越来越卡顿。
2. 减少绘制次数
设置时间锁,当属性发生变动时再重新绘制,比如位移缩放。
int changeFlag = Date.now();
function zoom(){
// todo: 缩放
changeFlag = Date.now();
}
function run(){
if(Date.now() - changeFlag < 500){
// todo 调用draw方法
}
}
3.采用脏区绘制的方式
canvas脏区重绘:【Fanvas技术解密】HTML5 canvas实现脏区重绘
对于脏区绘制目前并没有做深入研究,但不可否认采用脏区绘制对canvas动画整体性能会有一个较高的提升。
解决问题
1. 安卓手机动画卡顿
使用transform
和transform3d
来实现动画,在动画移动前添加will-change
属性。
this.ref.current.style.setProperty('will-change','transform');
this.ref.current.style.setProperty( 'transition', 'transform 3s ease-in-out');
this.ref.current?.style.setProperty('transform', 'translate3d(0px,10px,0px)');
2. transform包裹fixed元素、sticky元素造成位置错乱
在动画结束后移除transform
样式。
const handleClearAnim = ()=>{
this.ref.current?.style.removeProperty('transition');
this.ref.current?.style.removeProperty('will-change');
this.ref.current?.style.removeProperty('transform');
};
if (this.ref.current) {
let flag = false;
const handleTransitionEnd = () => {
if (!flag) {
handleClearAnim();
}
this.ref.current?.removeEventListener(
'transitionend',
handleTransitionEnd,
);
this.ref.current?.removeEventListener(
'webkitTransitionEnd',
handleTransitionEnd,
);
flag = true;
};
this.ref.current.addEventListener('transitionend', handleTransitionEnd);
this.ref.current.addEventListener(
'webkitTransitionEnd',
handleTransitionEnd,
);
this.ref.current.style.setProperty('will-change', 'transform');
this.ref.current.style.setProperty(
'transition',
`transform 3s ease-in-out`,
);
this.ref.current?.style.setProperty(
'transform',
`translate3d(0px,10px,0px)`,
);
}
这里用transitionend
来监听动画结束而不是setTimeout
,因为后者不太准确,主要是是在低端机上面,动画执行过程可能会有延迟,造成动画整体时长大于动画时长。
3. 拖动(上下)动画:与滚动冲突,与浏览器左滑后退手势冲突,与浏览器下拉手势冲突
与下拉刷新冲突解决方案:参考使用 CSS overscroll-behavior 控制滚动行为:自定义下拉刷新和溢出效果
在body设置样式:
body{
overscroll-behavior-y: contain;
}
但是部分浏览器中不生效,需要阻止默认事件和冒泡,同理,阻止手势冲突也是这样:
useEffect(()=>{
const handleTouchMove = (e: TouchEvent) => {
e.stopPropagation();
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
};
const passiveFlag = {
passive: false,
capture: false,
};
this.ref.current?.addEventListener('touchmove', handleTouchMove,
passiveFlag);
return ()=>{
this.ref.current?.removeEventListener('touchmove', handleTouchMove, passiveFlag);
};
},[]);
4. 安卓低端机:canvas动画卡顿
canvas动画一般是比较顺畅的,但是如果与长页面渲染一起执行,则可能在低端机上造成动画卡顿。因此一般需要动画执行完成再进行数据加载和页面渲染,简而言之:canvas动画和页面渲染最好不要同时就行。
5. 双击造成动画执行多遍,双击元素移走后点击到链接跳走,返回后因为动画未执行完成造成动画效果错乱
屏蔽双击事件:
实现时间锁,800毫秒以内不可以二次点击。
let doubleClickLock: number = 0;
const handleClick = ()=>{
const now = Date.now();
if(now - doubleClickLock < 800){
return;
}
doubleClickLock = now;
// todo
};
另外在动画执行过程中,为了防止点击造成页面跳转或者执行其他动画,也需要屏蔽点击事件。
const disabledClick = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
e.cancelBubble = true;
};
window.addEventListener('click', disabledClick, true);
// 执行动画后移除点击屏蔽
doAnim(...,()=>{
// 回调
// todo: 清除transform属性
// 移除点击屏蔽
window.removeEventListener('click', disabledClick, true);
});
转载自:https://juejin.cn/post/7161693001087975432