آنلاین

توالی‌های پیچیده animation

تا اینجا animationهای ساده یک element در HTML را آموختیم. Angular امکان animate کردن توالی‌های هماهنگ را نیز فراهم می‌کند؛ مانند یک grid یا فهرست کامل از elementها هنگام ورود به صفحه و خروج از آن. می‌توانید چند animation را به‌صورت موازی اجرا کنید یا animationهای مجزا را به‌ترتیب و یکی پس از دیگری اجرا کنید.

توابع کنترل‌کننده توالی‌های پیچیده animation عبارت‌اند از:

توابعجزئیات
query()یک یا چند element داخلی HTML را پیدا می‌کند.
stagger()تأخیری آبشاری بر animation چندین element اعمال می‌کند.
group()چند مرحله animation را به‌صورت موازی اجرا می‌کند.
sequence()مراحل animation را یکی پس از دیگری اجرا می‌کند.

تابع query()

بیشتر animationهای پیچیده برای یافتن elementهای فرزند و اعمال animation بر آن‌ها به تابع query() متکی هستند. نمونه‌های پایه عبارت‌اند از:

نمونه‌هاجزئیات
query() و سپس animate()برای جست‌وجوی elementهای ساده HTML و اعمال مستقیم animation بر آن‌ها استفاده می‌شود.
query() و سپس animateChild()برای جست‌وجوی elementهای فرزندی استفاده می‌شود که خود metadata مربوط به animation دارند و آن animationها را فعال می‌کند؛ animationهایی که در غیر این صورت توسط animation مربوط به element فعلی یا والد مسدود می‌شوند.

آرگومان اول query() یک رشته selector در CSS است که می‌تواند tokenهای مخصوص Angular زیر را نیز داشته باشد:

tokenهاجزئیات
:enter
:leave
برای elementهای در حال ورود یا خروج.
:animatingبرای elementهایی که در حال animate شدن هستند.
@*
@triggerName
برای elementهای دارای هر trigger یا یک trigger خاص.
:selfخود element در حال animate شدن.

animate کردن چند element با توابع query() و stagger()

پس از جست‌وجوی elementهای فرزند با query()، تابع stagger() به شما امکان می‌دهد فاصله زمانی میان هر آیتم را مشخص کنید تا elementها با تأخیر نسبت به یکدیگر animate شوند.

مثال زیر نحوه استفاده از توابع query() و stagger() را برای animate کردن یک فهرست از heroها نشان می‌دهد؛ هر مورد با اندکی تأخیر و به‌ترتیب از بالا به پایین اضافه می‌شود.

element را شفاف کنید و با transform آن را از موقعیت اصلی بیرون ببرید تا بتواند به محل خود slide شود.

  • با query() elementای را پیدا کنید که وارد صفحه می‌شود و معیارهای مشخصی دارد.
  • برای هر یک از این elementها، با style() یک style اولیه یکسان تنظیم کنید.
  • با stagger() هر animation را ۳۰ میلی‌ثانیه به تأخیر بیندازید.
  • هر element را با یک منحنی easing سفارشی طی ۰٫۵ ثانیه روی صفحه animate کنید؛ هم‌زمان opacity آن را افزایش دهید و transform را برگردانید.
hero-list-page.ts
// #docplaster
// #docregion
import {Component, HostBinding, OnInit} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';

// #docregion filter-animations
@Component({
  // #enddocregion filter-animations
  selector: 'app-hero-list-page',
  templateUrl: 'hero-list-page.html',
  styleUrls: ['hero-list-page.css'],
  // #docregion page-animations, filter-animations
  animations: [
    // #enddocregion filter-animations
    trigger('pageAnimations', [
      transition(':enter', [
        query('.hero', [
          style({opacity: 0, transform: 'translateY(-100px)'}),
          stagger(30, [
            animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})),
          ]),
        ]),
      ]),
    ]),
    // #enddocregion page-animations
    // #docregion increment
    // #docregion filter-animations
    trigger('filterAnimation', [
      transition(':enter, * => 0, * => -1', []),
      transition(':increment', [
        query(
          ':enter',
          [
            style({opacity: 0, width: 0}),
            stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]),
          ],
          {optional: true},
        ),
      ]),
      transition(':decrement', [
        query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]),
      ]),
    ]),
    // #enddocregion  increment
  ],
})
export class HeroListPage implements OnInit {
  // #enddocregion filter-animations
  @HostBinding('@pageAnimations')
  public animatePage = true;

  // #docregion filter-animations
  heroesTotal = -1;

  get heroes() {
    return this._heroes;
  }
  private _heroes: Hero[] = [];

  ngOnInit() {
    this._heroes = HEROES;
  }

  updateCriteria(criteria: string) {
    criteria = criteria ? criteria.trim() : '';

    this._heroes = HEROES.filter((hero) =>
      hero.name.toLowerCase().includes(criteria.toLowerCase()),
    );
    const newTotal = this.heroes.length;

    if (this.heroesTotal !== newTotal) {
      this.heroesTotal = newTotal;
    } else if (!criteria) {
      this.heroesTotal = -1;
    }
  }
}
// #enddocregion filter-animations

animation موازی با تابع group()

دیدید که چگونه میان animationهای متوالی تأخیر ایجاد کنید. اما ممکن است بخواهید animationهایی را پیکربندی کنید که به‌صورت موازی رخ می‌دهند. برای مثال، شاید بخواهید دو property در CSS مربوط به یک element را animate کنید، اما برای هرکدام تابع easing متفاوتی به‌کار ببرید. برای این کار می‌توانید از تابع animation یعنی group() استفاده کنید.

مثال زیر از group() برای هر دو حالت :enter و :leave با دو پیکربندی زمان‌بندی متفاوت استفاده می‌کند؛ بنابراین دو animation مستقل را به‌صورت موازی بر یک element اعمال می‌کند.

hero-list-groups.ts (excerpt)
import {Component, output, input} from '@angular/core';
import {trigger, state, style, animate, transition, group} from '@angular/animations';

import {Hero} from './hero';

@Component({
  selector: 'app-hero-list-groups',
  template: `
    <ul class="heroes">
      @for (hero of heroes(); track hero) {
        <li [@flyInOut]="'in'">
          <button class="inner" type="button" (click)="removeHero(hero.id)">
            <span class="badge">{{ hero.id }}</span>
            <span class="name">{{ hero.name }}</span>
          </button>
        </li>
      }
    </ul>
  `,
  styleUrls: ['./hero-list-page.css'],
  // #docregion animationdef
  animations: [
    trigger('flyInOut', [
      state(
        'in',
        style({
          width: '*',
          transform: 'translateX(0)',
          opacity: 1,
        }),
      ),
      transition(':enter', [
        style({width: 10, transform: 'translateX(50px)', opacity: 0}),
        group([
          animate(
            '0.3s 0.1s ease',
            style({
              transform: 'translateX(0)',
              width: '*',
            }),
          ),
          animate(
            '0.3s ease',
            style({
              opacity: 1,
            }),
          ),
        ]),
      ]),
      transition(':leave', [
        group([
          animate(
            '0.3s ease',
            style({
              transform: 'translateX(50px)',
              width: 10,
            }),
          ),
          animate(
            '0.3s 0.2s ease',
            style({
              opacity: 0,
            }),
          ),
        ]),
      ]),
    ]),
  ],
  // #enddocregion animationdef
})
export class HeroListGroups {
  readonly heroes = input<Hero[]>([]);

  readonly remove = output<number>();

  removeHero(id: number) {
    this.remove.emit(id);
  }
}

animationهای متوالی در برابر موازی

در animationهای پیچیده ممکن است چند اتفاق هم‌زمان رخ دهند. اما اگر بخواهید چند animation را یکی پس از دیگری اجرا کنید چه؟ پیش‌تر از group() برای اجرای موازی و هم‌زمان چند animation استفاده کردید.

تابع دیگری با نام sequence() به شما امکان می‌دهد همان animationها را یکی پس از دیگری اجرا کنید. درون sequence()، مراحل animation از فراخوانی تابع style() یا animate() تشکیل می‌شوند.

  • با style() داده‌های style ارائه‌شده را بی‌درنگ اعمال کنید.
  • با animate() داده‌های style را طی یک بازه زمانی مشخص اعمال کنید.

مثال animation مربوط به filter

animation دیگری را در صفحه نمونه بررسی کنید. در تب Filter/Stagger، متنی مانند Magnet یا tornado را در کادر Search Heroes وارد کنید.

filter هم‌زمان با تایپ شما و به‌صورت بلادرنگ کار می‌کند. با تایپ هر حرف جدید، elementها از صفحه خارج و filter به‌تدریج سخت‌گیرانه‌تر می‌شود. با حذف هر حرف از کادر filter، فهرست heroها به‌تدریج دوباره وارد صفحه می‌شود.

template مربوط به HTML دارای triggerای با نام filterAnimation است.

hero-list-page.html
<!-- #docplaster -->
<h2>Filter/Stagger</h2>

<!-- #docregion filter-animations -->
<label for="search">Search heroes: </label>
<input
  type="text"
  id="search"
  #criteria
  (input)="updateCriteria(criteria.value)"
  placeholder="Search heroes"
/>

<ul class="heroes" [@filterAnimation]="heroesTotal">
  @for (hero of heroes; track hero) {
    <li class="hero">
      <div class="inner">
        <span class="badge">{{ hero.id }}</span>
        <span class="name">{{ hero.name }}</span>
      </div>
    </li>
  }
</ul>
<!-- #enddocregion filter-animations -->

filterAnimation در decorator مربوط به component شامل سه transition است.

hero-list-page.ts
// #docplaster
// #docregion
import {Component, HostBinding, OnInit} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';

// #docregion filter-animations
@Component({
  // #enddocregion filter-animations
  selector: 'app-hero-list-page',
  templateUrl: 'hero-list-page.html',
  styleUrls: ['hero-list-page.css'],
  // #docregion page-animations, filter-animations
  animations: [
    // #enddocregion filter-animations
    trigger('pageAnimations', [
      transition(':enter', [
        query('.hero', [
          style({opacity: 0, transform: 'translateY(-100px)'}),
          stagger(30, [
            animate('500ms cubic-bezier(0.35, 0, 0.25, 1)', style({opacity: 1, transform: 'none'})),
          ]),
        ]),
      ]),
    ]),
    // #enddocregion page-animations
    // #docregion increment
    // #docregion filter-animations
    trigger('filterAnimation', [
      transition(':enter, * => 0, * => -1', []),
      transition(':increment', [
        query(
          ':enter',
          [
            style({opacity: 0, width: 0}),
            stagger(50, [animate('300ms ease-out', style({opacity: 1, width: '*'}))]),
          ],
          {optional: true},
        ),
      ]),
      transition(':decrement', [
        query(':leave', [stagger(50, [animate('300ms ease-out', style({opacity: 0, width: 0}))])]),
      ]),
    ]),
    // #enddocregion  increment
  ],
})
export class HeroListPage implements OnInit {
  // #enddocregion filter-animations
  @HostBinding('@pageAnimations')
  public animatePage = true;

  // #docregion filter-animations
  heroesTotal = -1;

  get heroes() {
    return this._heroes;
  }
  private _heroes: Hero[] = [];

  ngOnInit() {
    this._heroes = HEROES;
  }

  updateCriteria(criteria: string) {
    criteria = criteria ? criteria.trim() : '';

    this._heroes = HEROES.filter((hero) =>
      hero.name.toLowerCase().includes(criteria.toLowerCase()),
    );
    const newTotal = this.heroes.length;

    if (this.heroesTotal !== newTotal) {
      this.heroesTotal = newTotal;
    } else if (!criteria) {
      this.heroesTotal = -1;
    }
  }
}
// #enddocregion filter-animations

کد این مثال کارهای زیر را انجام می‌دهد:

  • وقتی کاربر برای نخستین بار این صفحه را باز می‌کند یا به آن می‌رود، animationها را نادیده می‌گیرد (animation مربوط به filter نتایج موجود را محدود می‌کند؛ بنابراین فقط روی elementهایی کار می‌کند که از قبل در DOM وجود دارند).
  • heroها را بر اساس مقدار ورودی جست‌وجو filter می‌کند.

برای هر تغییر:

در طول animation،‏ element دارای width و opacity پیش‌فرض خود می‌شود.

  • با تنظیم opacity و width روی ۰، element در حال خروج از DOM را پنهان می‌کند.
  • element در حال ورود به DOM را طی ۳۰۰ میلی‌ثانیه animate می‌کند.
  • اگر چند element وارد DOM یا از آن خارج شوند، animation هر element را از بالای صفحه و با تأخیر ۵۰ میلی‌ثانیه نسبت به element بعدی به‌صورت stagger اجرا می‌کند.

animate کردن آیتم‌های یک فهرست مرتب‌شونده

با اینکه Angular آیتم‌های فهرست *ngFor را به‌صورت پیش‌فرض به‌درستی animate می‌کند، در صورت تغییر ترتیب آن‌ها قادر به انجام این کار نخواهد بود. دلیل این است که Angular دیگر نمی‌تواند تشخیص دهد هر element کدام است و در نتیجه animationها به‌درستی اجرا نمی‌شوند. تنها راه کمک به Angular برای ردیابی چنین elementهایی، اختصاص یک TrackByFunction به directive مربوط به NgForOf است. این کار تضمین می‌کند Angular همواره هر element را تشخیص دهد و در نتیجه animation درست را همیشه بر element صحیح اعمال کند.

animationها و View Encapsulation در component

animationهای Angular بر ساختار DOM مربوط به componentها استوارند و View Encapsulation را مستقیماً در نظر نمی‌گیرند. یعنی componentهای دارای ViewEncapsulation.Emulated دقیقاً مانند زمانی رفتار می‌کنند که از ViewEncapsulation.None استفاده می‌کنند (ViewEncapsulation.ShadowDom و ViewEncapsulation.ExperimentalIsolatedShadowDom رفتار متفاوتی دارند که کمی بعد بررسی می‌کنیم).

برای مثال، اگر تابع query() ــ که در ادامه راهنمای Animations بیشتر با آن آشنا می‌شوید ــ در بالای درختی از componentهای دارای view encapsulation شبیه‌سازی‌شده اعمال شود، این query می‌تواند elementهای DOM را در هر عمقی از درخت شناسایی و در نتیجه animate کند.

از سوی دیگر، ViewEncapsulation.ShadowDom و ViewEncapsulation.ExperimentalIsolatedShadowDom با «پنهان‌کردن» elementهای DOM درون elementهای ShadowRoot، ساختار DOM مربوط به component را تغییر می‌دهند. چنین دست‌کاری‌هایی در DOM مانع عملکرد درست برخی پیاده‌سازی‌های animation می‌شوند، زیرا این پیاده‌سازی‌ها به ساختار ساده DOM متکی‌اند و elementهای ShadowRoot را در نظر نمی‌گیرند. بنابراین توصیه می‌شود animation را روی viewهایی که شامل componentهای دارای view encapsulation از نوع ShadowDom هستند اعمال نکنید.

خلاصه توالی animation

توابع Angular برای animate کردن چند element، با query() برای یافتن elementهای داخلی آغاز می‌شوند؛ برای مثال، جمع‌آوری تمام تصویرهای درون یک <div>. توابع باقی‌مانده یعنی stagger()،‏ group() و sequence() تأخیرهای آبشاری اعمال می‌کنند یا امکان کنترل شیوه اعمال چند مرحله animation را فراهم می‌سازند.

مطالب بیشتر درباره animationهای Angular

ممکن است مطالب زیر نیز برایتان مفید باشند: