likes
comments
collection
share

前端动画问题和总结

作者站长头像
站长
· 阅读数 93

在移动端开发带有动画的页面时,必须要考虑动画的兼容性和动画的流畅性,这里对遇到的动画问题做一些总结,同时这里得出一个结论:

复杂的动画还是不能单靠纯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动画性能和兼容性

使用transformtransform3d来实现动画,在动画移动前添加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. 安卓手机动画卡顿

使用transformtransform3d来实现动画,在动画移动前添加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
评论
请登录