Angular 路由轉場動畫

2022-07-11 09:58 更新

前提條件

對下列概念有基本的理解:

路由能讓用戶在應用中的不同路由之間導航。當用戶從一個路由導航到另一個路由時,Angular 路由器會把這個 URL 映射到一個相關的組件,并顯示其視圖。為這種路由轉換添加動畫,將極大地提升用戶體驗。

Angular 路由器天生帶有高級動畫功能,它可以讓你為在路由變化時為視圖之間設置轉場動畫。要想在路由切換時生成動畫序列,你需要首先定義出嵌套的動畫序列。從宿主視圖的頂層組件開始,在這些內嵌視圖的宿主組件中嵌套添加其它動畫。

要啟用路由轉場動畫,需要做如下步驟:

  1. 為應用導入路由模塊,并創(chuàng)建一個路由配置來定義可能的路由。
  2. 添加路由器出口,來告訴 Angular 路由器要把激活的組件放在 DOM 中的什么位置。
  3. 定義動畫。

讓我們以兩個路由之間的導航過程來解釋一下路由轉場動畫,Home 和 About 分別與 ?HomeComponent ?和 ?AboutComponent ?的視圖相關聯(lián)。所有這些組件視圖都是頂層視圖的子節(jié)點,其宿主是 ?AppComponent?。我們將實現(xiàn)路由器過渡動畫,該動畫會在出現(xiàn)新視圖時向右滑動,并當用戶在兩個路由之間導航時把舊視圖滑出。


路由配置

首先,使用 ?RouterModule ?類提供的方法來配置一組路由。該路由配置會告訴路由器該如何導航。

使用 ?RouterModule.forRoot? 方法來定義一組路由。同時,把其返回值添加到主模塊 ?AppModule ?的 ?imports ?數(shù)組中。

注意:
在根模塊 ?AppModule ?中使用 ?RouterModule.forRoot? 方法來注冊一些頂層應用路由和提供者。對于特性模塊,則改用 ?RouterModule.forChild? 方法。

下列配置定義了應用程序中可能的路由。

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { OpenCloseComponent } from './open-close.component';
import { OpenClosePageComponent } from './open-close-page.component';
import { OpenCloseChildComponent } from './open-close.component.4';
import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
import { StatusSliderComponent } from './status-slider.component';
import { StatusSliderPageComponent } from './status-slider-page.component';
import { HeroListPageComponent } from './hero-list-page.component';
import { HeroListGroupPageComponent } from './hero-list-group-page.component';
import { HeroListGroupsComponent } from './hero-list-groups.component';
import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
import { HeroListAutoComponent } from './hero-list-auto.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { InsertRemoveComponent } from './insert-remove.component';
import { QueryingComponent } from './querying.component';


@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    RouterModule.forRoot([
      { path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
      {
        path: 'open-close',
        component: OpenClosePageComponent,
        data: { animation: 'openClosePage' }
      },
      {
        path: 'status',
        component: StatusSliderPageComponent,
        data: { animation: 'statusPage' }
      },
      {
        path: 'toggle',
        component: ToggleAnimationsPageComponent,
        data: { animation: 'togglePage' }
      },
      {
        path: 'heroes',
        component: HeroListPageComponent,
        data: { animation: 'filterPage' }
      },
      {
        path: 'hero-groups',
        component: HeroListGroupPageComponent,
        data: { animation: 'heroGroupPage' }
      },
      {
        path: 'enter-leave',
        component: HeroListEnterLeavePageComponent,
        data: { animation: 'enterLeavePage' }
      },
      {
        path: 'auto',
        component: HeroListAutoCalcPageComponent,
        data: { animation: 'autoPage' }
      },
      {
        path: 'insert-remove',
        component: InsertRemoveComponent,
        data: { animation: 'insertRemovePage' }
      },
      {
        path: 'querying',
        component: QueryingComponent,
        data: { animation: 'queryingPage' }
      },
      {
        path: 'home',
        component: HomeComponent,
        data: { animation: 'HomePage' }
      },
      {
        path: 'about',
        component: AboutComponent,
        data: { animation: 'AboutPage' }
      },
    ])
  ],

?home ?和 ?about ?路徑分別關聯(lián)著 ?HomeComponent ?和 ?AboutComponent ?視圖。該路由配置告訴 Angular 路由器當導航匹配了相應的路徑時,就實例化 ?HomeComponent ?和 ?AboutComponent ?視圖。

除了 ?path?、?component ?之外,每個路由定義中的 ?data ?屬性也定義了與此路由有關的動畫配置。當路由變化時,?data ?屬性的值就會傳給 ?AppComponent?。你還可以在路由配置中傳遞其它的值供路由的動畫使用。?data ?屬性的值必須滿足 ?routeAnimation? 中定義的轉場動畫的要求,稍后我們就會定義它。

注意:
這個 ?data ?中的屬性名可以是任意的。比如,上面例子中使用的名字 animation 就是隨便起的。

路由出口

配置好路由之后,還要告訴 Angular 路由器當路由匹配時,要把視圖渲染到那里。你可以通過在根組件 ?AppComponent ?的模板中插入一個 ?<router-outlet>? 容器來指定路由出口的位置。

?ChildrenOutletContexts ?包含有關插座和激活路由的信息。我們可以用每個 ?Route ?的 ?data ?屬性來為我們的路由轉換設置動畫。

<div [@routeAnimations]="getRouteAnimationData()">
  <router-outlet></router-outlet>
</div>

?AppComponent ?中定義了一個可以檢測視圖何時發(fā)生變化的方法,該方法會基于路由配置的 ?data ?屬性值,將動畫狀態(tài)值賦值給動畫觸發(fā)器(?@routeAnimation?)。下面就是一個 ?AppComponent ?中的范例方法,用于檢測路由在何時發(fā)生了變化。

constructor(private contexts: ChildrenOutletContexts) {}

getRouteAnimationData() {
  return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
}

這里的 ?getRouteAnimationData()? 方法會獲取這個 outlet 指令的值(通過 ?#outlet="outlet"?),并根據當前活動路由的自定義數(shù)據返回一個表示動畫狀態(tài)的字符串值??梢杂眠@個數(shù)據來控制各個路由之間該執(zhí)行哪個轉場。

動畫定義

動畫可以直接在組件中定義。對于此范例,我們會在獨立的文件中定義動畫,這讓我們可以復用這些動畫。

下面的代碼片段定義了一個名叫 ?slideInAnimation ?的可復用動畫。

export const slideInAnimation =
  trigger('routeAnimations', [
    transition('HomePage <=> AboutPage', [
      style({ position: 'relative' }),
      query(':enter, :leave', [
        style({
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%'
        })
      ]),
      query(':enter', [
        style({ left: '-100%' })
      ]),
      query(':leave', animateChild()),
      group([
        query(':leave', [
          animate('300ms ease-out', style({ left: '100%' }))
        ]),
        query(':enter', [
          animate('300ms ease-out', style({ left: '0%' }))
        ]),
      ]),
    ]),
    transition('* <=> *', [
      style({ position: 'relative' }),
      query(':enter, :leave', [
        style({
          position: 'absolute',
          top: 0,
          left: 0,
          width: '100%'
        })
      ]),
      query(':enter', [
        style({ left: '-100%' })
      ]),
      query(':leave', animateChild()),
      group([
        query(':leave', [
          animate('200ms ease-out', style({ left: '100%', opacity: 0 }))
        ]),
        query(':enter', [
          animate('300ms ease-out', style({ left: '0%' }))
        ]),
        query('@*', animateChild())
      ]),
    ])
  ]);

該動畫定義做了如下事情:

  • 定義兩個轉場。每個觸發(fā)器都可以定義多個狀態(tài)和多個轉場
  • 調整宿主視圖和子視圖的樣式,以便在轉場期間,控制它們的相對位置
  • 使用 ?query()? 來確定哪個子視圖正在進入或離開宿主視圖

路由的變化會激活這個動畫觸發(fā)器,并應用一個與該狀態(tài)變更相匹配的轉場

注意:
這些轉場狀態(tài)必須和路由配置中定義的 ?data ?屬性的值相一致。

通過將可復用動畫 ?slideInAnimation ?添加到 ?AppComponent ?的 ?animations ?元數(shù)據中,可以讓此動畫定義能用在你的應用中。

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
    slideInAnimation
  ]
})

那么,我們來分解一下這個動畫定義,并仔細看看它做了什么……

為宿主組件和子組件添加樣式

在轉場期間,新視圖將直接插入在舊視圖后面,并且這兩個元素會同時出現(xiàn)在屏幕上。要防止這種行為,就要修改宿主視圖,改用相對定位。然后,把已移除或已插入的子視圖改用絕對定位。在這些視圖中添加樣式,就可以讓容器就地播放動畫,并防止某個視圖影響頁面中其它視圖的位置。

trigger('routeAnimations', [
  transition('HomePage <=> AboutPage', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ]),

查詢視圖的容器

使用 ?query()? 方法可以找出當前宿主組件中的動畫元素。?query(":enter")? 語句會返回已插入的視圖,?query(":leave")? 語句會返回已移除的視圖。

假設你正在從 Home 轉場到 About,?Home => About?。

query(':enter', [
    style({ left: '-100%' })
  ]),
  query(':leave', animateChild()),
  group([
    query(':leave', [
      animate('300ms ease-out', style({ left: '100%' }))
    ]),
    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ]),
  ]),
]),
transition('* <=> *', [
  style({ position: 'relative' }),
  query(':enter, :leave', [
    style({
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%'
    })
  ]),
  query(':enter', [
    style({ left: '-100%' })
  ]),
  query(':leave', animateChild()),
  group([
    query(':leave', [
      animate('200ms ease-out', style({ left: '100%', opacity: 0 }))
    ]),
    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ]),
    query('@*', animateChild())
  ]),
])

在設置了視圖的樣式之后,動畫代碼會執(zhí)行如下操作:

  1. ?query(':enter style({ left: '-100%'})? 會匹配添加的視圖,并通過將其定位在最左側來隱藏這個新視圖。
  2. 在正在離開的視圖上調用 ?animateChild()?,來運行其子動畫。
  3. 使用?group()?函數(shù)使內部動畫并行運行。
  4. 在 ?group()? 函數(shù)中:
    • 查詢已移除的視圖,并讓它從右側滑出。
    • 使用緩動函數(shù)和持續(xù)時間定義的動畫,讓這個新視圖滑入。
    • 此動畫將導致 ?about ?視圖從左側劃入。

  5. 當主動畫完成之后,在這個新視圖上調用 ?animateChild()? 方法,以運行其子動畫。

你現(xiàn)在有了一個基本的路由動畫,可以在從一個視圖路由到另一個視圖時播放動畫。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號