在动画进行期间暂停调度NgRx操作

Tar*_*alo 16 javascript rxjs css-transitions ngrx angular

我有一个微型应用程序,在屏幕上显示一个点。 应用程序截图

这是绑定到NgRx存储中的状态的简单div。

<div class="dot"
   [style.width.px]="size$ | async"
   [style.height.px]="size$ | async"
   [style.backgroundColor]="color$ | async"
   [style.left.px]="x$ | async"
   [style.top.px]="y$ | async"
   (transitionstart)="transitionStart()"
   (transitionend)="transitionEnd()"></div>
Run Code Online (Sandbox Code Playgroud)

点状态更改由CSS过渡设置动画。

.dot {
  border-radius: 50%;
  position: absolute;

  $moveTime: 500ms;
  $sizeChangeTime: 400ms;
  $colorChangeTime: 900ms;
  transition:
    top $moveTime, left $moveTime,
    background-color $colorChangeTime,
    width $sizeChangeTime, height $sizeChangeTime;
}
Run Code Online (Sandbox Code Playgroud)

我有一个后端,它推送点的更新(位置,颜色和大小)。我将这些更新映射到NgRx动作上。

export class AppComponent implements OnInit {
  ...

  constructor(private store: Store<AppState>, private backend: BackendService) {}

  ngOnInit(): void {
    ...

     this.backend.update$.subscribe(({ type, value }) => {
       // TODO: trigger new NgRx action when all animations ended
       if (type === 'position') {
         const { x, y } = value;
         this.store.dispatch(move({ x, y }));
       } else if (type === 'color') {
         this.store.dispatch(changeColor({ color: value }));
       } else if (type === 'size') {
         this.store.dispatch(changeSize({ size: value }));
       }
     });
   }
 }
Run Code Online (Sandbox Code Playgroud)

问题是来自后端的新更改有时会比动画结束早。我的目标是延迟所有存储状态的更新(暂停触发新的NgRx操作),直到所有转换结束。我们可以轻松地处理此刻,因为chrome已经支持该transitionstart事件。

我也可以用这样的图来解释 间距

间隔取决于过渡持续时间。

这是可运行的应用程序https://stackblitz.com/edit/angular-qlpr2g和仓库https://github.com/cwayfinder/pausable-ngrx

Val*_*kov 5

您可以使用concatMapdelayWhen来执行此操作。另请注意,如果更改了多个属性,则transitionEnd事件可以多次触发,因此我使用debounceTime来过滤此类double事件。我们不能使用它distinctUntilChanged,因为第一个transitionEnd将触发下一个更新,该更新立即将transitionInProgress $状态更改为true。我不使用transitionStart回调,因为在触发transitionStart之前可以进行多个更新。这是工作示例。

export class AppComponent implements OnInit {
  ...

  private readonly  transitionInProgress$ = new BehaviorSubject(false);

  ngOnInit(): void {
    ...

    this.backend.update$.pipe(
      concatMap(update => of(update).pipe(
        delayWhen(() => this.transitionInProgress$.pipe(
          // debounce the transition state, because transitionEnd event fires multiple
          // times for a single transiation, if multiple properties were changed
          debounceTime(1),
          filter(inProgress => !inProgress)
        ))
      ))
    ).subscribe(update => {
        this.transitionInProgress$.next(true)

        if (update.type === 'position') {
          this.store.dispatch(move(update.value));
        } else if (update.type === 'color') {
          this.store.dispatch(changeColor({ color: update.value }));
        } else if (update.type === 'size') {
          this.store.dispatch(changeSize({ size: update.value }));
        }
    });
  }

  transitionEnd(event: TransitionEvent) {
    this.transitionInProgress$.next(false)
  }
}
Run Code Online (Sandbox Code Playgroud)


Tar*_*alo 2

我想我有一个或多或少好的解决方案。检查https://stackblitz.com/edit/angular-xh7ndi

我已经重写了 NgRx 类ActionSubject

import { Injectable } from '@angular/core';
import { Action, ActionsSubject } from '@ngrx/store';
import { BehaviorSubject, defer, from, merge, Observable, Subject } from 'rxjs';
import { bufferToggle, distinctUntilChanged, filter, map, mergeMap, share, tap, windowToggle } from 'rxjs/operators';

@Injectable()
export class PausableActionsSubject extends ActionsSubject {

  queue$ = new Subject<Action>();
  active$ = new BehaviorSubject<boolean>(true);

  constructor() {
    super();

    const active$ = this.active$.pipe(distinctUntilChanged());
    active$.subscribe(active => {
      if (!active) {
        console.time('pauseTime');
      } else {
        console.timeEnd('pauseTime');
      }
    });

    const on$ = active$.pipe(filter(v => v));
    const off$ = active$.pipe(filter(v => !v));

    this.queue$.pipe(
      share(),
      pause(on$, off$, v => this.active$.value)
    ).subscribe(action => {
      console.log('action', action);
      super.next(action);
    });
  }

  next(action: Action): void {
    this.queue$.next(action);
  }

  pause(): void {
    this.active$.next(false);
  }

  resume(): void {
    this.active$.next(true);
  }
}

export function pause<T>(on$: Observable<any>, off$: Observable<any>, haltCondition: (value: T) => boolean) {
  return (source: Observable<T>) => defer(() => { // defer is used so that each subscription gets its own buffer
    let buffer: T[] = [];
    return merge(
      source.pipe(
        bufferToggle(off$, () => on$),
        // append values to your custom buffer
        tap(values => buffer = buffer.concat(values)),
        // find the index of the first element that matches the halt condition
        map(() => buffer.findIndex(haltCondition)),
        // get all values from your custom buffer until a haltCondition is met
        map(haltIndex => buffer.splice(0, haltIndex === -1 ? buffer.length : haltIndex + 1)),
        // spread the buffer
        mergeMap(toEmit => from(toEmit)),
      ),
      source.pipe(
        windowToggle(on$, () => off$),
        mergeMap(x => x),
      ),
    );
  });
}
Run Code Online (Sandbox Code Playgroud)

AppModule我指定的提供者中

providers: [
    PausableActionsSubject,
    { provide: ActionsSubject, useExisting: PausableActionsSubject }
]
Run Code Online (Sandbox Code Playgroud)

为了调试目的,我增加了 CSS 转换时间

.dot {
  border-radius: 50%;
  position: absolute;

  $moveTime: 3000ms;
  $sizeChangeTime: 2000ms;
  $colorChangeTime: 1000ms;
  transition:
    top $moveTime, left $moveTime,
    background-color $colorChangeTime,
    width $sizeChangeTime, height $sizeChangeTime;
}
Run Code Online (Sandbox Code Playgroud)

在浏览器控制台中我看到了这个

在此输入图像描述