CSS 宽度转换速度不够快,与 translateX 结合时会导致抖动 UI

CSS width transition not fast enough and causes jittery UI when joined with translateX

代码

Code at CodeSandBox -- Demo

上面的codesandbox里面有很多代码,所以我会尽量把相关的部分贴在这里:

基于拖动/平移手势的总 X 方向增量系列,我正在做:

// before setting transform and width, I set the transition
// this way, there will be some tweening that occurs when I do
// change the properties -- relevant to the specific open / 
// close situation
if (this.isOpening) {
  this.content.style.transition = `width 0.1s linear, transform 0.1s linear`;
} else {
  this.content.style.transition = `width 0.0s linear, transform 0.1s linear`;
}


this.content.style.setProperty("--dx", `${nextX}`);
this.content.style.transform = `translateX(${nextX}px)`;
this.resize();


// and then in that resize function
let nextX = parseInt(this.content.style.getPropertyValue("--dx"), 10);
this.content.style.setProperty("width", `${window.innerWidth - nextX}px`);

我想要的行为

当我左右拖动内容(使用鼠标或触摸事件)时,我希望内容的右边缘会粘在 window 的右边缘。我知道单独使用 translateX 可以实现超级流畅的动画,但是动画宽度很棘手,因为它会导致回流,影响性能。

我看到的行为

当我左右拖动内容时,内容的右边缘不会粘在window的右边缘。这部分是因为我根据场景使用动态 css-transitions。我有一个用于打开的过渡,一个用于关闭(显示/隐藏侧边栏)。这样做的原因是,如果我根本没有过渡,动画会感觉不稳定,而且非常不流畅。现在的方式更好,但仍然不理想。

我试过的

无 CSS 转换

向转换添加过渡 属性

向变换和宽度属性添加过渡

其他注意事项

我目前正在使用 hammerjs 来获取触摸事件/手势——我不必使用这个库,所以如果有更好的方法来实现我想要的,我会洗耳恭听。

我想我最后没试过的是 requestAnimationFrame?根据我读过的内容,不确定这是否对这里有帮助——但在这一点上,我愿意尝试任何事情。

在继续寻找解决方案后想通了,最终绊倒在这个页面上:MDN's "Using the Web Animations API"

要点是 css 动画在 css 文件中使用 @keyframes 声明,Web 动画 API 使我们能够调用 .animate 在一个元素上,并允许许多与 css 动画方式相同的选项。

我很高兴我发现了这个 API,因为它具有播放、触发和取消功能,我没有将其用于此边栏动画,但这可能意味着在评估一个动画库,例如 ember-animated,我可以检查它是否使用动画 API 来实现取消,从而减少负载大小。

有效的方法

Code on CodeSandBox -- Demo

主要区别是:

  • 我摆脱了任何设置 transition 属性 的尝试。 (回到 我试过的方法 下的第一件事)
  • 快速打开和快速关闭行为太快了,因此,使用 Web 动画 API,我能够平滑地设置 translateX 和 width 的动画——即使在我的非高端老款 phone,Sony XPeria XZ1(紧凑型)

     // e is a hammer.js Pan event 
     handleDrag(e) {
      // setup and various math omitted
    
      if (e.isFinal) {
        // code determining if a "snap" open or shut should happen
        // and what that x-axis value should be 
        // are omitted
    
        // reset state omitted 
    
        // call the Web Animations API 
        return this.finish(nextX);
      }
    
      // fallback code for when the gesture is still active
      // directly updates the transform property
      // (though, I should probably look in to using requestAnimationFrame
      // to improve performance on lower-end devices)
      this.content.style.transform = `translateX(${nextX}px)`;
      this.resize();
    }
    
    finish(nextX: number) {
      let prevX = this.currentLeft;
    
      let keyFrames = [
        { transform: `translateX(${prevX}px)` },
        { transform: `translateX(${nextX}px)` },
      ];
    
    
      // this covers the main scenario from the question here
      // adding a width animation to the translate X
      // (for medium sized screens and larger, it's important to have both)
      // NOTE: isPushing is a boolean that checks for a certain 
      //       screen size -- if the screen is small enough,
      //       no width animating occurs
      if (!this.isPushing) {
        keyFrames[0].width = `${window.innerWidth - prevX}px`;
        keyFrames[1].width = `${window.innerWidth - nextX}px`;
      }
    
      let easing = 'cubic-bezier(0.215, 0.610, 0.355, 1.000)';
      let animation = this.content.animate(keyFrames as any /* :( */, { duration: 200, easing });
    
      // without the onfinish, the content element will reappear
      // back in its pre-animation position / state.
      animation.onfinish = () => {
        this.content.style.transform = `translateX(${nextX}px)`;
    
        if (!this.isPushing) {
          this.content.style.setProperty('width', `${window.innerWidth - nextX}px`);
        }
      };
    }