这是绑定到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>
点状态更改通过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;
}
我有一个后端,它推送点的更新(位置,颜色和大小)。我将这些更新映射到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 }));
}
});
}
}
问题是来自后端的新更改有时会比动画结束更早出现。
我的目标是延迟所有存储状态的更新(暂停触发新的NgRx操作),直到所有转换结束。因为chrome已经支持transitionstart
事件,所以我们可以轻松应对这一时刻。
间隔取决于过渡持续时间。
这是可运行的应用程序https://stackblitz.com/edit/angular-qlpr2g和仓库https://github.com/cwayfinder/pausable-ngrx。
答案 0 :(得分:2)
我修改了您的StackBlitz演示,为您提供了工作示例,请看here。
作为解释,我从StackBlitz复制了重要的代码来解释重要的细节:
const delaySub = new BehaviorSubject<number>(0);
const delay$ = delaySub.asObservable().pipe(
concatMap(time => timer(time + 50)),
share(),
)
const src$ = this.backend.update$
.pipe(
tap(item => item['type'] === 'position' && delaySub.next(3000)),
tap(item => item['type'] === 'size' && delaySub.next(2000)),
tap(item => item['type'] === 'color' && delaySub.next(1000)),
)
zip(src$, delay$).pipe(
map(([item, delay]) => item)
).subscribe(({ type, value }) => {
// TODO: trigger new NgRx action when all animations ended
if (type === 'position') {
this.store.dispatch(move(value));
} else if (type === 'color') {
this.store.dispatch(changeColor({ color: value }));
} else if (type === 'size') {
this.store.dispatch(changeSize({ size: value }));
}
})
当事件从this.backend.update$
到达时,我们将根据事件类型更新延迟主题。我们将发出持续时间(以毫秒为单位),这将有助于我们延迟其他事件,这些事件需要花费时间+ 50进行额外的护理。
zip(src$, delay$)
将立即从src $发出第一事件,但是从src$
发出将根据项目类型为delay$
产生新值。例如,如果第一个偶数为position
,则delaySub将获得3000的值,而当下一个事件到达src$
时,zip将在concatMap(time => timer(time + 50)),
的帮助下将此新值和最新延迟3000配对。 。最终,我们将获得预期的行为,第一个项目将毫无延迟地到达,随后的事件必须在上一个事件的基础上借助zip
,concatMap
和其他事件来等待特定的时间量运算符。
如果您对我的代码有任何疑问,请让我更新我的答案。
答案 1 :(得分:2)
您可以使用concatMap和delayWhen来执行此操作。还要注意,transitionEnd
事件can be fired multiple times如果更改了多个属性,那么我使用debounceTime来过滤此类双重事件。我们不能使用distinctUntilChanged
,因为第一个transitionEnd
将触发下一个更新,该更新立即将transitionInProgress $状态更改为true。我不使用transitionStart
回调,因为在触发transitionStart之前可以进行多个更新。 Here is工作示例。
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)
}
}
答案 2 :(得分:1)
我认为我有一个或多或少的好的解决方案。选中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),
),
);
});
}
在AppModule
中,我指定了提供商
providers: [
PausableActionsSubject,
{ provide: ActionsSubject, useExisting: PausableActionsSubject }
]
出于调试目的,我增加了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;
}
在浏览器控制台中,我看到了
答案 3 :(得分:0)
实际上,我认为zip
有一个非常简单的解决方案,类似于@ goga-koreli所做的事情。
基本上zip
仅在其所有源发出第n个项目时才发出第n个项目。因此,您可以推送尽可能多的后端更新,然后保留另一个仅在动画结束事件时才发出第n个值的Observable(在本例中为Subject)。换句话说,即使后端发送更新的速度过快,zip
也会仅在动画完成后尽快调度动作。
private animationEnd$ = new Subject();
...
zip(
this.backend.update$,
this.animationEnd$.pipe(startWith(null)), // `startWith` to trigger the first animation.
)
.pipe(
map(([action]) => action),
)
.subscribe(({ type, value }) => {
...
});
...
transitionEnd() {
this.animationEnd$.next();
}
您的更新演示:https://stackblitz.com/edit/angular-42alkp?file=src/app/app.component.ts