Angular 路由轉(zhuǎn)場(chǎng)動(dòng)畫(huà)

2022-07-11 09:58 更新

前提條件

對(duì)下列概念有基本的理解:

路由能讓用戶在應(yīng)用中的不同路由之間導(dǎo)航。當(dāng)用戶從一個(gè)路由導(dǎo)航到另一個(gè)路由時(shí),Angular 路由器會(huì)把這個(gè) URL 映射到一個(gè)相關(guān)的組件,并顯示其視圖。為這種路由轉(zhuǎn)換添加動(dòng)畫(huà),將極大地提升用戶體驗(yàn)。

Angular 路由器天生帶有高級(jí)動(dòng)畫(huà)功能,它可以讓你為在路由變化時(shí)為視圖之間設(shè)置轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。要想在路由切換時(shí)生成動(dòng)畫(huà)序列,你需要首先定義出嵌套的動(dòng)畫(huà)序列。從宿主視圖的頂層組件開(kāi)始,在這些內(nèi)嵌視圖的宿主組件中嵌套添加其它動(dòng)畫(huà)。

要啟用路由轉(zhuǎn)場(chǎng)動(dòng)畫(huà),需要做如下步驟:

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

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


路由配置

首先,使用 ?RouterModule ?類(lèi)提供的方法來(lái)配置一組路由。該路由配置會(huì)告訴路由器該如何導(dǎo)航。

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

注意:
在根模塊 ?AppModule ?中使用 ?RouterModule.forRoot? 方法來(lái)注冊(cè)一些頂層應(yīng)用路由和提供者。對(duì)于特性模塊,則改用 ?RouterModule.forChild? 方法。

下列配置定義了應(yīng)用程序中可能的路由。

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 ?路徑分別關(guān)聯(lián)著 ?HomeComponent ?和 ?AboutComponent ?視圖。該路由配置告訴 Angular 路由器當(dāng)導(dǎo)航匹配了相應(yīng)的路徑時(shí),就實(shí)例化 ?HomeComponent ?和 ?AboutComponent ?視圖。

除了 ?path?、?component ?之外,每個(gè)路由定義中的 ?data ?屬性也定義了與此路由有關(guān)的動(dòng)畫(huà)配置。當(dāng)路由變化時(shí),?data ?屬性的值就會(huì)傳給 ?AppComponent?。你還可以在路由配置中傳遞其它的值供路由的動(dòng)畫(huà)使用。?data ?屬性的值必須滿足 ?routeAnimation? 中定義的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的要求,稍后我們就會(huì)定義它。

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

路由出口

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

?ChildrenOutletContexts ?包含有關(guān)插座和激活路由的信息。我們可以用每個(gè) ?Route ?的 ?data ?屬性來(lái)為我們的路由轉(zhuǎn)換設(shè)置動(dòng)畫(huà)。

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

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

constructor(private contexts: ChildrenOutletContexts) {}

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

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

動(dòng)畫(huà)定義

動(dòng)畫(huà)可以直接在組件中定義。對(duì)于此范例,我們會(huì)在獨(dú)立的文件中定義動(dòng)畫(huà),這讓我們可以復(fù)用這些動(dòng)畫(huà)。

下面的代碼片段定義了一個(gè)名叫 ?slideInAnimation ?的可復(fù)用動(dòng)畫(huà)。

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())
      ]),
    ])
  ]);

該動(dòng)畫(huà)定義做了如下事情:

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

路由的變化會(huì)激活這個(gè)動(dòng)畫(huà)觸發(fā)器,并應(yīng)用一個(gè)與該狀態(tài)變更相匹配的轉(zhuǎn)場(chǎng)

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

通過(guò)將可復(fù)用動(dòng)畫(huà) ?slideInAnimation ?添加到 ?AppComponent ?的 ?animations ?元數(shù)據(jù)中,可以讓此動(dòng)畫(huà)定義能用在你的應(yīng)用中。

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

那么,我們來(lái)分解一下這個(gè)動(dòng)畫(huà)定義,并仔細(xì)看看它做了什么……

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

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

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

查詢視圖的容器

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

假設(shè)你正在從 Home 轉(zhuǎn)場(chǎng)到 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())
  ]),
])

在設(shè)置了視圖的樣式之后,動(dòng)畫(huà)代碼會(huì)執(zhí)行如下操作:

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

  5. 當(dāng)主動(dòng)畫(huà)完成之后,在這個(gè)新視圖上調(diào)用 ?animateChild()? 方法,以運(yùn)行其子動(dòng)畫(huà)。

你現(xiàn)在有了一個(gè)基本的路由動(dòng)畫(huà),可以在從一個(gè)視圖路由到另一個(gè)視圖時(shí)播放動(dòng)畫(huà)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)