Angular와 RxJS로 카운트업 애니메이션 만들기
27792 단어 rxjsanimationsangular
이 기사에서는 반응형 방식으로 Angular에서 카운트업 애니메이션을 빌드하는 방법을 설명합니다. 우리는 서드파티 라이브러리 없이 처음부터 카운트업 지시문을 만들 것입니다. 최종 결과는 다음과 같습니다.

시작하자!
Angular CLI로 지시문 생성
Angular에서 지시문을 만들려면 다음 명령을 실행합니다.
ng generate directive count-up
Angular CLI는 빈 지시문이 포함된
count-up.directive.ts
파일을 생성합니다.@Directive({
selector: '[countUp]'
})
export class CountUpDirective {
constructor() {}
}
입력 정의
CountUpDirective
에는 count 및 animation duration의 두 가지 입력이 있습니다. 여기서 count 입력의 이름은 지시문 선택기의 이름과 같습니다. 템플릿에서 CountUpDirective
를 사용하면 다음과 같습니다.<p [countUp]="200" [duration]="5000"></p>
이러한 입력은 다음과 같이
CountUpDirective
에 정의되어 있습니다.@Directive({
selector: '[countUp]'
})
export class CountUpDirective {
@Input('countUp') // input name is the same as selector name
set count(count: number) {}
@Input()
set duration(duration: number) {}
}
보시다시피 입력은 설정자로 정의됩니다. 입력 값은
OnChanges
수명 주기 후크를 사용하지 않고 반응적으로 변경 사항을 수신할 수 있도록 RxJS 주제로 내보내집니다.로컬 상태 정의
CountUpDirective
에는 행동 주제에 저장될 두 개의 로컬 상태 슬라이스가 있습니다.@Directive({
selector: '[countUp]'
})
export class CountUpDirective {
// default count value is 0
private readonly count$ = new BehaviorSubject(0);
// default duration value is 2000 ms
private readonly duration$ = new BehaviorSubject(2000);
}
그런 다음 입력이 변경되면 새 입력 값이 이러한 주제에 내보내집니다.
@Directive({
selector: '[countUp]'
})
export class CountUpDirective {
private readonly count$ = new BehaviorSubject(0);
private readonly duration$ = new BehaviorSubject(2000);
@Input('countUp')
set count(count: number) {
// emit a new value to the `count$` subject
this.count$.next(count);
}
@Input()
set duration(duration: number) {
// emit a new value to the `duration$` subject
this.duration$.next(duration);
}
}
다음 단계는 템플릿에 현재 카운트를 표시하는 데 사용할
currentCount$
옵저버블을 빌드하는 것입니다.현재 카운트 계산
현재 개수를 계산하려면
count$
및 duration$
주제의 값이 필요합니다. combineLatest
연산자를 사용하여 count$
또는 duration$
가 변경될 때마다 현재 카운트 계산을 재설정합니다. 다음 단계는 0에서 시작하여 시간이 지남에 따라 현재 카운트를 증가시킨 다음 느려지고 애니메이션 지속 시간이 만료될 때 count
값으로 끝나는 간격으로 외부 관찰 가능 항목을 전환하는 것입니다.private readonly currentCount$ = combineLatest([
this.count$,
this.duration$,
]).pipe(
switchMap(([count, duration]) => {
// get the time when animation is triggered
const startTime = animationFrameScheduler.now();
// use `animationFrameScheduler` for better rendering performance
return interval(0, animationFrameScheduler).pipe(
// calculate elapsed time
map(() => animationFrameScheduler.now() - startTime),
// calculate progress
map((elapsedTime) => elapsedTime / duration),
// complete when progress is greater than 1
takeWhile((progress) => progress <= 1),
// apply quadratic ease-out function
// for faster start and slower end of counting
map((progress) => progress * (2 - progress)),
// calculate current count
map((progress) => Math.round(progress * count)),
// make sure that last emitted value is count
endWith(count),
distinctUntilChanged()
);
}),
);
더 나은 렌더링 성능을 위해 기본값
animationFrameScheduler
대신 asyncScheduler
를 사용합니다. animationFrameScheduler
가 interval
와 함께 사용될 때 첫 번째 인수는 0
여야 합니다. 그렇지 않으면 asyncScheduler
로 대체됩니다. 즉, currentCount$
가 asyncScheduler
함수에 두 번째 인수로 전달되지만 animationFrameScheduler
의 다음 구현은 후드 아래에서 interval
를 사용합니다.private readonly currentCount$ = combineLatest([
this.count$,
this.duration$,
]).pipe(
switchMap(([count, animationDuration]) => {
const frameDuration = 1000 / 60; // 60 frames per second
const totalFrames = Math.round(animationDuration / frameDuration);
// interval falls back to `asyncScheduler`
// because the `frameDuration` is different from 0
return interval(frameDuration, animationFrameScheduler).pipe(
// calculate progress
map((currentFrame) => currentFrame / totalFrames),
// complete when progress is greater than 1
takeWhile((progress) => progress <= 1),
// apply quadratic ease-out function
map((progress) => progress * (2 - progress)),
// calculate current count
map((progress) => Math.round(progress * count)),
// make sure that last emitted value is count
endWith(count),
distinctUntilChanged()
);
})
);
💡 If you're not familiar with the
animationFrameScheduler
and its advantages for updating the DOM over theasyncScheduler
, take a look at the resources section.
현재 카운트 표시
지시문의 호스트 요소 내에서 현재 카운트를 렌더링하려면
Renderer2
의 인스턴스와 호스트 요소에 대한 참조가 필요합니다. 둘 다 생성자를 통해 주입할 수 있습니다. 또한 Destroy
가 파괴될 때 관찰 가능한 currentCount$
를 구독 취소하는 데 도움이 되는 CountUpDirective
공급자를 주입합니다.@Directive({
selector: '[countUp]',
// `Destroy` is provided at the directive level
providers: [Destroy],
})
export class CountUpDirective {
constructor(
private readonly elementRef: ElementRef,
private readonly renderer: Renderer2,
private readonly destroy$: Destroy
) {}
}
💡 Take a look at to learn more about
Destroy
provider.
그런 다음
currentCount$
변경 사항을 수신하고 호스트 요소 내에서 방출된 값을 표시하는 메서드를 만들어야 합니다.private displayCurrentCount(): void {
this.currentCount$
.pipe(takeUntil(this.destroy$))
.subscribe((currentCount) => {
this.renderer.setProperty(
this.elementRef.nativeElement,
'innerHTML',
currentCount
);
});
}
displayCurrentCount
메서드는 ngOnInit
메서드에서 호출됩니다.마무리
CountUpDirective
의 최종 버전은 다음과 같습니다./**
* Quadratic Ease-Out Function: f(x) = x * (2 - x)
*/
const easeOutQuad = (x: number): number => x * (2 - x);
@Directive({
selector: '[countUp]',
providers: [Destroy],
})
export class CountUpDirective implements OnInit {
private readonly count$ = new BehaviorSubject(0);
private readonly duration$ = new BehaviorSubject(2000);
private readonly currentCount$ = combineLatest([
this.count$,
this.duration$,
]).pipe(
switchMap(([count, duration]) => {
// get the time when animation is triggered
const startTime = animationFrameScheduler.now();
return interval(0, animationFrameScheduler).pipe(
// calculate elapsed time
map(() => animationFrameScheduler.now() - startTime),
// calculate progress
map((elapsedTime) => elapsedTime / duration),
// complete when progress is greater than 1
takeWhile((progress) => progress <= 1),
// apply quadratic ease-out function
// for faster start and slower end of counting
map(easeOutQuad),
// calculate current count
map((progress) => Math.round(progress * count)),
// make sure that last emitted value is count
endWith(count),
distinctUntilChanged()
);
}),
);
@Input('countUp')
set count(count: number) {
this.count$.next(count);
}
@Input()
set duration(duration: number) {
this.duration$.next(duration);
}
constructor(
private readonly elementRef: ElementRef,
private readonly renderer: Renderer2,
private readonly destroy$: Destroy
)
ngOnInit(): void {
this.displayCurrentCount();
}
private displayCurrentCount(): void {
this.currentCount$
.pipe(takeUntil(this.destroy$))
.subscribe((currentCount) => {
this.renderer.setProperty(
this.elementRef.nativeElement,
'innerHTML',
currentCount
);
});
}
}
데모
자원
requestAnimationFrame
function animationFrameScheduler
피어 리뷰어
이 기사에 대해 유용한 제안을 해주신 Tim에게 감사드립니다!
Reference
이 문제에 관하여(Angular와 RxJS로 카운트업 애니메이션 만들기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/angular/building-count-up-animation-with-angular-and-rxjs-240k텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)