开头
CocosCreator中有个Tween类,控制缓动动画。既然是动画,就是一个时间过程,可以看成一个异步过程。而rxjs在管理异步管理方面有非常强大的功能。因此,习惯将任何异步过程都封装为rxjs的observable,就能使用rxjs强大的管理能力进行操作。
封装
// 可随时中断的Tween
export function TweenToStartWithSkip<T>(target: T, duration: number, props?: __private._cocos_tween_tween__ConstructorType<unknown>, opts?: ITweenOption)
: { observable: Observable<{ target: T, ratio?: number, status: string }>, skip: () => void } {
return _TweenToStart(target, duration, props, opts);
}
function _TweenToStart<T>(target: T, duration: number, props?: __private._cocos_tween_tween__ConstructorType<unknown>, opts?: ITweenOption): { observable: Observable<any>, skip: () => void } {
let needSkip = ReactiveProperty.Create(false);
let skip = () => needSkip.value = true;
const observable = new Observable((observer: Observer<{ target: T, ratio?: number, status: string }>) => {
if (opts == null) opts = {};
opts.onStart = (target: unknown) => observer.next({ target: <T>target, status: 'start' });
opts.onUpdate = (target: unknown, ratio: number) => observer.next({ target: <T>target, ratio, status: 'update' });
opts.onComplete = (target: unknown) => {
observer.next({ target: <T>target, status: 'complete' });
observer.complete();
}
let _t = needSkip.value ? null : tween(target).to(duration, props, opts).start();
needSkip.subscribeWhenTrue(() => {
observer.next({ target: <T>target, status: 'skip' });
observer.complete();
});
return () => {
_t && _t.stop();
}
});
return { observable, skip };
}
如何使用/如何中断一个过程?
方式1:直接unsubscribe即可
let subscription = TweenToStartWithSkip(this.pi0, 2, { progress: 1 }).observable.subscribe({
complete: () => log('正常结束才会调用')
})
await lastValueFrom(Timer(1));
subscription.unsubscribe();
不用管里面的skip,复制的代码,这个方式里面没用,也没有影响。
此时的unsubscribe会调用到_TweenToStart里面的new Observable 的 return () ⇒ {…}的函数,因此相当于上面的tween被stop了。
这种情况,observer.complete()没有机会调用,外部subscribe里面complete也不会被调用。
方式2:利用withSkip模式的写法
如同上面代码,提供TweenToStartWithSkip的方法,封装好后,不但得到observable,而且同时得到一个skip函数,只要调用这个函数,内部就会触发一个标记skip的next,紧接着complete也会执行。
let { observable, skip } = TweenToStartWithSkip(this.pi0, 2, { progress: 1 });
// 一般在此处进行持有,不过此处示例没有这么做
observable.subscribe({
complete: () => log('正常结束与手动skip都会执行')
});
await lastValueFrom(Timer(1));
skip(); // 一般会将其持有,在需要关闭时调用
方式3:利用takeUntil
let skipCommand = ReactiveCommand.Create();
TweenToStartWithSkip(this.pi0, 2, { progress: 1 }).observable.pipe(takeUntil(skipCommand._subject)).subscribe({
next: _ => { this.log('next', _); },
complete: () => { this.log('正常结束与手动skip都会执行'); }
});
await lastValueFrom(Timer(1));
skipCommand.execute();
takeUntil里面加入另一个observable,当这个observable有next后,就结束原observable被subscribe的过程,并且,这种用法会调用到其complete方法。虽然能加入defaultIfEmpty或者endWith来确保至少有个最后的next值,但是原subscribe很难分辨出来是手动skip还是本身完成的。
另外,takeUntil参数为另一个observable。也就是说,如果业务逻辑正好有rx输出来中断时,应当可以直接使用。
总结
几种方式都能够合理的调用到observable中的return的函数。因此都可以中断动画的播放。
其中第一种方式无skip的next,无complete,是最基础的逻辑,很简单;
第二种方式有skip的next,有complete,但是封装过程时代码多,需要处理skip函数等功能
第三种方式无skip的next,有complete,利用到rx本身的操作符takeUnitl,外部subscribe时代码多。
另外,几种方式均能正确中断concat连接的异步过程。
如果是await形式的异步过程,则await后需要额外进行中断。