欧美日韩精品一区二区在线线,一级无码在线收看,精品国产高清91,久久精品欧美电影

# 自定義路由

小程序采用多 WebView 架構(gòu),頁面間跳轉(zhuǎn)形式十分單一,僅能從右到左進行動畫。而原生 App 的動畫形式則多種多樣,如從底部彈起,頁面下沉,半屏等。

Skyline 渲染引擎下,頁面有兩種渲染模式: WebViewSkyline,它們通過頁面配置中的 renderer 字段進行區(qū)分。在連續(xù)的 Skyline 頁面間跳轉(zhuǎn)時,可實現(xiàn)自定義路由效果。

# 效果展示

下方為半屏頁面效果,點擊可查看更多 Skyline 示例。

掃碼打開小程序示例,交互動畫 - 基礎(chǔ)組件 - 自定義路由 即可體驗。

# 使用方法

建議先閱讀完 worklet 動畫手勢系統(tǒng) 兩個章節(jié),它們是自定義路由的基礎(chǔ)內(nèi)容。

# 接口定義

自定義路由相關(guān)的接口

type AddRouteBuilder = (routeType: string, routeBuilder: CustomRouteBuilder) => void

type CustomRouteBuilder = (routeContext: CustomRouteContext, routeOptions: Record<string, any>) => CustomRouteConfig

interface SharedValue<T> {
  value: T;
}

interface CustomRouteContext {
  // 動畫控制器,影響推入頁面的進入和退出過渡效果
  primaryAnimation: SharedValue<number>
  // 動畫控制器狀態(tài)
  primaryAnimationStatus: SharedValue<number>
  // 動畫控制器,影響棧頂頁面的推出過渡效果
  secondaryAnimation: SharedValue<number>
  // 動畫控制器狀態(tài)
  secondaryAnimationStatus: SharedValue<number>
  // 當前路由進度由手勢控制
  userGestureInProgress: SharedValue<number>
  // 手勢開始控制路由
  startUserGesture: () => void
  // 手勢不再控制路由
  stopUserGesture: () => void
  // 返回上一級,效果同 wx.navigateBack
  didPop: () => void
}

interface CustomRouteConfig {
  // 下一個頁面推入后,不顯示前一個頁面
  opaque?: boolean;
  // 是否保持前一個頁面狀態(tài)
  maintainState?: boolean;
  // 頁面推入動畫時長,單位 ms
  transitionDuration?: number;
  // 頁面推出動畫時長,單位 ms
  reverseTransitionDuration?: number;
  // 遮罩層背景色,支持 rgba() 和 #RRGGBBAA 寫法
  barrierColor?: string;
  // 點擊遮罩層返回上一頁
  barrierDismissible?: boolean;
  // 無障礙語義
  barrierLabel?: string;  
  // 是否與下一個頁面聯(lián)動,決定當前頁 secondaryAnimation 是否生效
  canTransitionTo?: boolean;
  // 是否與前一個頁面聯(lián)動,決定前一個頁 secondaryAnimation 是否生效
  canTransitionFrom?: boolean;
  // 處理當前頁的進入/退出動畫,返回 StyleObject
  handlePrimaryAnimation?: RouteAnimationHandler;
  // 處理當前頁的壓入/壓出動畫,返回 StyleObject
  handleSecondaryAnimation?: RouteAnimationHandler;
  // 處理上一級頁面的壓入/壓出動畫,返回 StyleObject 基礎(chǔ)庫 <3.0.0> 起支持
  handlePreviousPageAnimation?: RouteAnimationHandler;
  // 頁面進入時是否采用 snapshot 模式優(yōu)化動畫性能 基礎(chǔ)庫 <3.2.0> 起支持
  allowEnterRouteSnapshotting?: boolean
  // 頁面退出時是否采用 snapshot 模式優(yōu)化動畫性能 基礎(chǔ)庫 <3.2.0> 起支持
  allowExitRouteSnapshotting?: boolean
  // 右滑返回時,可拖動范圍是否撐滿屏幕,基礎(chǔ)庫 <3.2.0> 起支持,常用于半屏彈窗
  fullscreenDrag?: boolean
  // 返回手勢方向 基礎(chǔ)庫 <3.4.0> 起支持
  popGestureDirection?: 'horizontal' | 'vertical' | 'multi'
}

type RouteAnimationHandler = () => { [key: string] : any}

# 默認路由配置

const defaultCustomRouteConfig = {
  opaque: true,
  maintainState: true,
  transitionDuration: 300,
  reverseTransitionDuration: 300,
  barrierColor: '',
  barrierDismissible: false,
  barrierLabel: '',
  canTransitionTo: true,
  canTransitionFrom: true,
  allowEnterRouteSnapshotting: false,
  allowExitRouteSnapshotting: false,
  fullscreenDrag: false,
  popGestureDirection: 'horizontal'
}

# 示例模板

以下是注冊自定義路由的一份示例模板(未添加手勢處理部分),完整實現(xiàn)半屏路由效果見示例代碼。

const customRouteBuiler = (routeContext: CustomRouteContext) : CustomRouteConfig => {
  const {
    primaryAnimation,
    secondaryAnimation,
    userGestureInProgress
  } = routeContext

  const handlePrimaryAnimation: RouteAnimationHandler = () => {
    'worklet'
    let t = primaryAnimation.value
    if (!userGestureInProgress.value) {
      // select another curve, t = xxx
    }
    // StyleObject
    return {}
  }
  
  const handleSecondaryAnimation: RouteAnimationHandler = () => {
    'worklet'
    let t = secondaryAnimation.value
    if (!userGestureInProgress.value) {
      // select another curve, t = xxx
    }
    // StyleObject
    return {}
  }

  return {
    opaque: true,
    handlePrimaryAnimation,
    handleSecondaryAnimation
  }
}

// 在頁面跳轉(zhuǎn)前定義好 routeBuilder
wx.router.addRouteBuilder('customRoute', customRouteBuiler)

// 跳轉(zhuǎn)新頁面時,指定對應的 routeType
wx.navigateTo({
  url: 'xxxx',
  routeType: 'customRoute'
})

# 工作原理

以半屏效果為例,路由前后頁面記為 A 頁、B 頁,一個路由的生命周期中,會經(jīng)歷如下階段:

  1. push 階段 :調(diào)用 wx.navigateTo,B 頁自底向上彈出,A 頁下沉收縮
  2. 手勢拖動:在 B 頁上下滑動時,路由動畫隨之變化
  3. pop 階段 :調(diào)用 wx.navigateBack,B 頁向下關(guān)閉,A 恢復原樣

細分到每個頁面,在上述階段會有以下動畫方式

  1. 進入/退出動畫
  2. 壓入/壓出動畫
  3. 手勢拖動
  • push 階段,B 頁進行的是進入動畫,A 頁進行的是壓入動畫;
  • pop 階段,B 頁進行的是退出動畫,A 頁進行的是壓出動畫;

可以看到在路由過程中,前后兩個頁面動畫進行了聯(lián)動。在自定義路由模式下,我們可以對動畫各個階段的時長、曲線、效果以及是否聯(lián)動進行自定義,以實現(xiàn)靈活多變的頁面專場效果。

# 路由控制器

當打開新頁面時,框架會為其創(chuàng)建兩個 SharedValue 類型的動畫控制器 primaryAnimationsecondaryAnimation,分別控制進入/退出動畫和壓入/壓出動畫。

頁面的進入和退出可指定不同的時長,但進度變化始終在 0~1 之間。仍以半屏效果為例,路由前后頁面記為 A 頁、B 頁。

# push 階段

  1. B 頁對應的 primaryAnimation0 -> 1 變化,做進入動畫
  2. A 頁對應的 secondaryAnimation0 -> 1 變化,做壓入動畫

# pop 階段

  1. B 頁對應的 primaryAnimation1 -> 0 變化,做退出動畫
  2. A 頁對應的 secondaryAnimation1 -> 0 變化,做壓出動畫

其中,AsecondaryAnimation 的值始終與 BprimaryAnimation 的值同步變化。

通常頁面的進入和退出可能采用不同的動畫曲線,可通過對應的狀態(tài)變量 primaryAnimationStatussecondaryAnimationStatus 來區(qū)分當前處于哪一階段,ts 定義如下

enum AnimationStatus {
  // 動畫停在起點
  dismissed = 0,
  // 動畫從起點向終點進行
  forward = 1,
  // 動畫從終點向起點進行
  reverse = 2,
  // 動畫停在終點
  completed = 3,
}

primaryAnimationStatus 為例,頁面進入和退出過程中變化情況如下

  1. push 階段:dismissed -> forward -> completed
  2. pop 階段:completed -> reverse -> dismissed

# 路由手勢

在頁面推入后,除了調(diào)用 wx.navigateBack 接口返回上一級外,還可以通過手勢來處理,例如 iOS 上常見的右滑返回。自定義路由模式下,開發(fā)者可根據(jù)不同的頁面轉(zhuǎn)場效果,來選取所需的退出方式,如半屏效果可采用下滑返回。關(guān)于手勢監(jiān)聽的內(nèi)容,可參考 手勢系統(tǒng) 一章,路由手勢僅是在其基礎(chǔ)上,補充了幾個路由相關(guān)的接口。

startUserGesturestopUserGesture 兩個函數(shù)總是成對調(diào)用的,startUserGesture 調(diào)用后 userGestureInProgress 的值會加 1。

當開發(fā)者自行修改 primaryAnimation 的值來控制路由進度的時候,就需要調(diào)用這兩個接口。由于手勢拖動過程中通常采用不同的動畫曲線,可通過 userGestureInProgress 值進行判斷。

當手勢處理后確定需要返回上一級頁面時,調(diào)用 didPop 接口,作用等同 wx.navigateBack。

# 路由聯(lián)動

路由動畫過程中,默認前后兩個頁面是一起聯(lián)動的,可通過配置項關(guān)閉。

  1. canTransitionTo:是否與下一個頁面聯(lián)動,棧頂頁面該屬性置為 false ,推入下一頁面時,則棧頂頁面始終不動
  2. canTransitionFrom:是否與前一個頁面聯(lián)動,新推入頁面該屬性置為 false,則棧頂頁面始終不動

# 路由上下文對象

由示例模版可見,自定義路由的動畫效果就是根據(jù) CustomRouteContext 上下文對象上的路由控制器,編寫適當?shù)膭赢嫺潞瘮?shù)來實現(xiàn)。

CustomRouteContext 上下文對象還可在頁面/自定義組件中通過 wx.router.getRouteContext(this) 讀取,進而在手勢處理過程中訪問,通過對 primaryAnimation 值的改寫實現(xiàn)頁面手勢返回。

小技巧:可在 CustomRouteContext 對象上添加一些私有屬性,在頁面中進行讀取/修改。

# 多類型路由跳轉(zhuǎn)

考慮這樣的場景,從頁面 A 可能跳轉(zhuǎn)到 B 頁和 C 頁,但具有不同的路由動畫

  1. A -> B 時,希望實現(xiàn)半屏效果,A 需要下沉收縮
  2. A -> C 時,希望采用普通路由,A 需要向左移動

跳轉(zhuǎn)下一級頁面時的動畫由 handleSecondaryAnimation 控制,這樣就需要在定義 ACustomRouteBuilder 時考慮所有的路由類型,實現(xiàn)較為繁瑣。

基礎(chǔ)庫 3.0.0 版本起,自定義路由新增 handlePreviousPageAnimation 接口,用于控制上一級頁面的壓入/壓出動畫。

const customRouteBuiler = (routeContext: CustomRouteContext) : CustomRouteConfig => {
  const { primaryAnimation } = routeContext

  const handlePrimaryAnimation: RouteAnimationHandler = () => {
    'worklet'
    let t = primaryAnimation.value
    // 控制當前頁的進入和退出
  }
  
  const handlePreviousPageAnimation: RouteAnimationHandler = () => {
    'worklet'
    let t = primaryAnimation.value
    // 控制上一級頁面的壓入和退出
  }

  return {
    handlePrimaryAnimation,
    handlePreviousPageAnimation
  }
}

A 跳轉(zhuǎn)到 B 時, AsecondaryAnimation 的值始終與 BprimaryAnimation 的值同步變化。

我們可以在定義 BCustomRouteBulder 時,通過 primaryAnimation 得知當前路由進度,handlePreviousPageAnimation 返回的 StyleObject 會作用于上一級頁面。

同時也不再需要提前聲明 A 為自定義路由,在此之前 A 跳轉(zhuǎn) B 希望實現(xiàn)半屏效果時,A 也必須定義為自定義路由。

完整的示例可參考如下代碼,借助 handlePreviousPageAnimation 可去掉對 secondaryAnimation 的依賴,簡化代碼邏輯。

在開發(fā)者工具中預覽效果

# 實際案例

下面以半屏效果為例,講解自定義路由的具體實現(xiàn)過程,完整代碼見示例代碼。

路由前后頁面分別記為 A 頁和 B 頁,需要分別為其注冊自定義路由。未注冊任何自定義路由效果時,新打開的頁面 B 會立即覆蓋顯示在 A 頁上。

# Step-1 頁面進入動畫

我們先分別簡單實現(xiàn) 首頁 -> A 頁 -> B 頁的進入動畫,再一步步進行完善。

對于 A 頁面,進入方式為自右向左,通過 transform 平移實現(xiàn)。

function ScaleTransitionRouteBuilder(customRouteContext) {
  const {
    primaryAnimation
  } = customRouteContext

  const handlePrimaryAnimation = () => {
    'worklet'
    let t = primaryAnimation.value
    const transX = windowWidth * (1 - t)
    return {
      transform: `translateX(${transX}px)`,
    }
  }
  return {
    handlePrimaryAnimation
  }
}

對于 B 頁面,進入方式為自底向上,也是通過 transform 平移實現(xiàn),但需要對頁面大小、圓角進行修改。

const HalfScreenDialogRouteBuilder = (customRouteContext) => {
  const {
    primaryAnimation,
  } = customRouteContext

  const handlePrimaryAnimation = () => {
    'worklet'
    let t = primaryAnimation.value
    // 距離頂部邊距因子
    const topDistance = 0.12
    // 距離頂部邊距
    const marginTop = topDistance * screenHeight
    // 半屏頁面大小
    const pageHeight = (1 - topDistance) * screenHeight
    // 自底向上顯示頁面
    const transY = pageHeight * (1 - t)
    return {
      overflow: 'hidden',
      borderRadius: '10px',
      marginTop: `${marginTop}px`,
      height: `${pageHeight}px`,
      transform: `translateY(${transY}px)`,
    }
  }

  return {
    handlePrimaryAnimation,
  }
}

頁面跳轉(zhuǎn)效果如下,可以看到由于采用線性曲線(未對 t 做任何變換),動畫有些呆板,同時未區(qū)分進入/退出動畫。在 B 頁完全進入后,A 頁變的不可見。

# Step-2 自定義動畫曲線

B 頁為例,根據(jù) AnimationStatus 值,采用不同的動畫曲線,同時設(shè)置 opaquefalse,使得路由動畫完成后仍顯示 A 頁面。

const { Easing, derived } = wx.workelt

const Curves = {
  linearToEaseOut: Easing.cubicBezier(0.35, 0.91, 0.33, 0.97),
  easeInToLinear: Easing.cubicBezier(0.67, 0.03, 0.65, 0.09),
  fastOutSlowIn: Easing.cubicBezier(0.4, 0.0, 0.2, 1.0),
  fastLinearToSlowEaseIn: Easing.cubicBezier(0.18, 1.0, 0.04, 1.0),
}

function CurveAnimation({ animation, animationStatus, curve,reverseCurve }) {
  return derived(() => {
    'worklet'
    const useForwardCurve = !reverseCurve || animationStatus.value !== AnimationStatus.reverse
    const activeCurve = useForwardCurve ? curve : reverseCurve
    const t = animation.value
    if (!activeCurve) return t
    if (t === 0 || t === 1) return t
    return activeCurve(t)
  })
}
const HalfScreenDialogRouteBuilder = (customRouteContext) => {
  const {
    primaryAnimation,
    primaryAnimationStatus,
  } = customRouteContext

  // 1. 頁面進入時,采用 Curves.linearToEaseOut 曲線
  // 2. 頁面退出時,采用 Curves.easeInToLinear 曲線
  const _curvePrimaryAnimation = CurveAnimation({
    animation: primaryAnimation,
    animationStatus: primaryAnimationStatus,
    curve: Curves.linearToEaseOut,
    reverseCurve: Curves.easeInToLinear,
  })

  const handlePrimaryAnimation = () => {
    'worklet'
    let t = _curvePrimaryAnimation.value
    ... // 其余內(nèi)容等上面的代碼一致
  }

  return {
    opaque: false,
    handlePrimaryAnimation,
  }
}

這里的區(qū)別僅在于,當前的進度不再直接讀取 primaryAnimation 的值。封裝的 CurveAnimation 函數(shù)會根據(jù) AnimationStatus 判斷是處于進入還是退出狀態(tài),從而選擇不同的動畫曲線。框架提供了多種曲線類型,可進一步參考 worklet.Easing。改進后的頁面轉(zhuǎn)場效果如下

# Step-3 頁面聯(lián)動效果

B 頁進入時,A 頁作壓入動畫,由 secondaryAnimation 控制。接下來,我們?yōu)槠涮砑酉鲁列Ч?,實現(xiàn)和 B 頁的聯(lián)動。

function ScaleTransitionRouteBuilder(customRouteContext) {
  const {
    primaryAnimation
  } = customRouteContext

  const handlePrimaryAnimation = () => {
    'worklet'
    ...
  }

  const _curveSecondaryAnimation = CurveAnimation({
    animation: secondaryAnimation,
    animationStatus: secondaryAnimationStatus,
    curve: Curves.fastOutSlowIn,
  })

  const handleSecondaryAnimation = () => {
    'worklet'
    let t = _curveSecondaryAnimation.value
    // 頁面縮放大小
    const scale = 0.08
    // 距離頂部邊距因子
    const topDistance = 0.1
    // 估算的偏移量
    const transY = screenHeight * (topDistance - 0.5 * scale) * t 
    return {
      overflow: 'hidden',
      borderRadius: `${ 12 * t }px`,
      transform: `translateY(${transY}px) scale(${ 1 - scale * t })`,
    }
  }

  return {
    handlePrimaryAnimation,
    handleSecondaryAnimation
  }
}

通過對 A 頁作 scaletranslate 變換實現(xiàn)下沉效果。AsecondaryAnimation 的值始終與 BprimaryAnimation 的值保持同步。

頁面是否聯(lián)動還可通過 canTransitionTocanTransitionFrom 兩個屬性進行配置,可在開發(fā)者工具上修改體驗。

# Step-4 手勢返回

目前動畫效果已經(jīng)基本實現(xiàn),還需要最后一步,手勢返回。對于半屏效果,我們?yōu)?A 頁添加右滑返回手勢,B 頁添加下滑返回手勢。

以最常見的右滑返回為例,這里只截取松手后的手勢處理部分代碼,拖動過程實現(xiàn)較為簡單,可參考示例代碼。

page({
  handleDragEnd(velocity) {
    'worklet';
    const {
        primaryAnimation,
        stopUserGesture,
        didPop
    } = this.customRouteContext;

    let animateForward = false;
    if (Math.abs(velocity) >= 1.0) {
      animateForward = velocity <= 0;
    } else {
      animateForward = primaryAnimation.value > 0.5;
    }
    const t = primaryAnimation.value;
    const animationCurve = Curves.fastLinearToSlowEaseIn;
    if (animateForward) {
        const duration = Math.min(
          Math.floor(lerp(300, 0, t)),
          300,
        );
        primaryAnimation.value = timing(
          1.0, {
            duration,
            easing: animationCurve,
          },
          () => {
            'worklet'
            stopUserGesture();
          },
        );
    } else {
      const duration = Math.floor(lerp(0, 300, t));
      primaryAnimation.value = timing(
        0.0, {
          duration,
          easing: animationCurve,
        },
        () => {
          'worklet'
          stopUserGesture();
          didPop();
        },
      );
    }
  },
})

首先根據(jù)松手時的速度和位置,決定是否要真正返回上一級。

  1. 向右滑動且速度大于 1
  2. 或者速度較小時,已拖動超過屏幕 1/2

滿足以上條件時,確定返回。通過 timing 接口,為 primaryAnimation 添加過渡動畫,使其變化到 0,最后調(diào)用 didPop 。否則使其變化到 1,恢復到拖動前的狀態(tài)。

這里需要注意的是,當需要對 primaryAnimation 值手動修改,自由掌控其過渡方式時,才需要調(diào)用 startUserGesturestopUserGesture 接口。

右滑手勢已經(jīng)在示例代碼中封裝成 swipe-back 組件,開發(fā)者可直接使用。下滑手勢返回邏輯基本一致,僅一些數(shù)值上略有差異。

最后的實現(xiàn)效果如圖

# 設(shè)置頁面透明

一些自定義路由效果下,需要實現(xiàn)頁面透明背景,這里對 Skylinewebview 模式下背景色的層級關(guān)系進行說明。

# 自定義路由下的頁面背景色

Skyline 模式下使用自定義路由方式跳轉(zhuǎn)頁面,頁面背景色有如下幾層

  1. 頁面背景色:可通過 page 選擇器在 wxss 中定義,默認為白色
  2. 頁面容器背景色:可在頁面 json 文件中通過 backgroundColorContent 屬性定義,支持 #RRGGBBAA 寫法,默認白色
  3. 自定義路由容器背景色,由路由配置項中返回的 StyleObject 控制,默認透明
  4. 控制是否顯示前一個頁面,由路由配置項中的 opaque 字段控制,默認不顯示

當需要設(shè)置下一個頁面漸顯進入時,可簡單設(shè)置

  1. 頁面背景色透明: page { background-color: transparent; }
  2. 頁面容器背景色透明: backgroundColorContent: "#ffffff00"

查看自定義路由頁面漸顯示例

# webview 下的頁面背景色

對比看下,webview 模式下的頁面背景色

  1. 頁面背景色:可通過 page 選擇器在 wxss 中定義,默認為透明
  2. 頁面容器背景色:可在頁面 json 文件中通過 backgroundColorContent 屬性定義,支持 #RRGGBB 寫法,默認白色
  3. 窗口背景色:可通過 wx.setBackgroundColor 接口或頁面配置修改,默認為白色

# 示例代碼

在開發(fā)者工具中預覽效果