# Custom Routing

Mini Program uses more WebView Architecture, the form of jump between pages is very single, only from right to left for animation. And native App The forms of animations are varied, such as bouncing from the bottom, sinking the page, half-screen and so on.

Skyline Under the rendering engine, the page has two rendering modes: WebView and Skyline, which are configured by the page in the Renderer Field to make a distinction. inContinuous Skyline pageWhen jumping between, can realize custom route effect.

# Effect display

Below is the half-screen page effect,Click to see more Skyline Example

Scan the code to open the Mini Program example, interactive animation - Basic components - Custom Routing Can experience.

# Methods of Use

It is recommended to read first worklet animation and Gesture system Two chapters, which are the basics of custom routing.

# Interface definition

Custom Route Dependent Interfaces

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

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

Interface SharedValue<T> {
  value: T
}

Interface CustomRouteContext {
  // Animation controller that affects the entry and exit transition effect of the pushed page
  primaryAnimation: SharedValue<number>
  // Animation Controller State
  primaryAnimationStatus:  SharedValue<number>
  // Animation controller that affects the rollout of the top page of the stack
  secondaryAnimation: SharedValue<number>
  // Animation Controller State
  secondaryAnimationStatus: SharedValue<number>
  // Current routing progress is controlled by gesture
  userGestureInProgress: SharedValue<number>
  // Gesture Start Control Route
  startUserGesture: () => void
  // Gestures no longer control routing
  stopUserGesture: () => void
  // Return to the previous level with the same effect wx.navigateBack 
  didPop: () => void
}

Interface CustomRouteConfig {
  // After the next page is pushed, the previous page is not displayed
  opaque?: boolean
  // Whether to maintain the previous page state
  maintainState?: boolean
  // Page Push Animation Length, Unit ms
  transitionDuration?: number
  // Page Launch Animation Length, Unit ms
  reverseTransitionDuration?: number
  // Mask layer background color, support rgba() and #RRGGBBAA Writing method
  barrierColor?: string
  // Click on the mask layer to return to the previous page
  barrierDismissible?: boolean
  // Accessibility semantics
  barrierLabel?: string  
  // Whether to link with the next page to determine the current page secondaryAnimation Entry into force
  canTransitionTo?: boolean
  // Whether it is linked to the previous page, determines the previous secondaryAnimation Entry into force
  canTransitionFrom?: boolean
  // Processing the entry of the current page/Exit Animation, Return StyleObject
  handlePrimaryAnimation?: RouteAnimationHandler 
  // Handles pressing of the current page/Press out the animation, return StyleObject
  handleSecondaryAnimation?: RouteAnimationHandler 
  // Handle the pressing of a page at a previous level/Press out the animation, return StyleObject Base library <3.0.0> Support from
  handlePreviousPageAnimation?: RouteAnimationHandler 
  // Whether the page is entered by snapshot Mode Optimizes Animation Performance Base library <3.2.0> Support from
  allowEnterRouteSnapshotting?: boolean
  // When the page exits snapshot Mode Optimizes Animation Performance Base library <3.2.0> Support from
  allowExitRouteSnapshotting?: boolean
  // When the right slide back, whether the draggable range fills the screen, the base library <3.2.0> Support, often used for half-screen pop-up windows
  Fullscreen Drag:  boolean
  // Return to Gesture Direction Base library <3.4.0> Support from
  popGestureDirection?: 'horizontal'  | 'vertical' | 'multi'
}

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

# Default route configuration

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

# Sample Templates

The following is a sample template for registering a custom route (without adding gesture processing), see the sample code for the full implementation of the half-screen routing effect.

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
  }
}

// Define it before the page jump routeBuilder
wx.router.addRouteBuilder('customRoute', customRouteBuiler)

// When you jump to a new page, specify the corresponding routeType
wx.navigateTo ({
  url: 'xxxx',
  routeType: 'customRoute'
})

# Working principle

Take the half-screen effect as an example, the pages before and after the route are recorded as A Page,B Pages, in the lifetime of a route, go through the following stages:

  1. push stage : Call wx.navigateToB The page pops up from the bottom up,A Page Sink Shrink
  2. Gesture drag: in the B The route animation changes as the page slides up and down
  3. pop stage : Call wx.navigateBackB Page down close,A Back to the original

Subdivided into each page, in the above stage will be the following animation

  1. Get into/Exit animation
  2. Press/Squeeze animation
  3. Gesture drag
  • in push Stage,B Page is performed by entering the animation,A The page is pressed into the animation
  • in pop Stage,B Page is performed by exit animation,A The page is pressed out of the animation

You can see in the routing process, before and after the two page animation linkage. In the custom routing mode, we can customize the length, curve, effect and linkage of each stage of the animation to achieve flexible and changeable page special effects.

# Route controller

When a new page is opened, the framework creates two SharedValue Type of animation controller primaryAnimation and secondaryAnimation, respectively control the entry into the/Exit Animation and Pressing/Press out the animation.

The entry and exit of the page can be specified for different durations, but the progress change is always in the 01 Between. Still taking the half-screen effect as an example, the page before and after the route is recorded as A Page,B Pages.

# push stage

  1. B Page-corresponding primaryAnimation from 0 -> 1 Changes, do enter the animation
  2. A Page-corresponding secondaryAnimation from 0 -> 1 Change, do the press-in animation

# pop stage

  1. B Page-corresponding primaryAnimation from 1 -> 0 Change, do exit animation
  2. A Page-corresponding secondaryAnimation from 1 -> 0 Change, do the press-out animation

Among them,A page secondaryAnimation The value of the B page primaryAnimation The value changes synchronously.

Usually the entry and exit of the page may use different animation curves, which can be used by the corresponding state variables. primaryAnimationStatus and secondaryAnimationStatus To tell which stage you're in,ts Defined as follows

enum AnimationStatus  {
  // Animation stops at the beginning
  dismissed = 0,
  // Animation from start to finish
  forward = 1,
  // Animation from the end to the beginning
  reverse = 2,
  // Animation stops at the end
  completed = 3,
}

with primaryAnimationStatus For example, the page entry and exit process changes as follows

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

# Routing gesture

After the page is pushed, in addition to calling the wx.navigateBack Interface to return the previous level, you can also be handled by gestures, such as iOS On the common right slide back. In custom routing mode, developers can choose the required exit mode according to different page transition effects, such as half-screen effect can be used to slide back. For the content of gesture monitoring, refer to the Gesture system The chapter on routing gestures is only based on it, supplemented by several route-related interfaces.

startUserGesture and stopUserGesture Two functions are always called in pairs,startUserGesture After the call userGestureInProgress The value will be added 1

When developers modify themselves primaryAnimation To control the routing progress, you need to call these two interfacesBecause different animation curves are often used during gesture dragging, you can use the userGestureInProgress Value to judge.

When the gesture processing determines that it is necessary to return to the previous level page, call the didPop Interface, the role of the equivalent wx.navigateBack

# Routing linkage

Routing animation process, the default before and after the two pages are linked together, can be turned off through the configuration item.

  1. canTransitionTo: whether to link with the next page, the top of the stack This property is set to false , the top page of the stack remains untouched when the next page is pushed
  2. canTransitionFrom: whether to link with the previous page, the new push page This property is set to false, the top page of the stack remains unmoved

# Route context object

As you can see from the example template, the animation of the custom route is based on the CustomRouteContext Context object on the route controller, write the appropriate animation update function to implement.

CustomRouteContext The context object is also available on the page/In a custom component by wx.router.getRouteContext(this) Read, and in turn accessed during gesture processing, through the primaryAnimation Value to achieve page gesture return.

Tip: Available at CustomRouteContext Object to read from the page/Modification.

# Multiple Route Jump

Consider such a scenario, from the page A May jump to B Pages and C Page, but with a different route animation

  1. A -> B When you want to achieve the half-screen effect,A Need to sink and contract
  2. A -> C When it is desirable to employ ordinary routing,A Need to move to the left

Animation when you jump to the next level of the page by handleSecondaryAnimation Control, so that it is necessary to define the A of CustomRouteBuilder When considering all routing types, the implementation is more cumbersome.

Base library 3.0.0 Version, Custom Routing added handlePreviousPageAnimation Interface for controlling the compression of the page at the previous level/Press out the animation.

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

  const handlePrimaryAnimation: RouteAnimationHandler  = () => {
    'worklet'
    let t = primaryAnimation.value
    // Control the entry and exit of the current page
  }
  
  const handlePreviousPageAnimation: RouteAnimationHandler  = () => {
    'worklet'
    let t = primaryAnimation.value
    // Control the push in and out of the previous page
  }

  return {
    handlePrimaryAnimation,
    handlePreviousPageAnimation
  }
}

A Jump to B When, A page secondaryAnimation The value of the B page primaryAnimation The value changes synchronously.

We can define B of CustomRouteBulder When, through primaryAnimation Knowing the current routing progress,handlePreviousPageAnimation Returned StyleObject It will work on the next page.

And there's no need to declare A For custom routes, and before that A Jump B When you want to achieve a half-screen effect,A Must also be defined as a custom route.

The complete example can be found in the following code, with the help of handlePreviousPageAnimation Can remove the right secondaryAnimation To simplify the code logic.

Preview the effect in developer tools

# Practical cases

The following half-screen effect as an example, to explain the specific implementation of custom routing process, the complete code see sample code.

The pages before and after the route are recorded as A Pages and B Page for which you need to register custom routes, respectively.Newly opened pages when no custom routing effects are registered B Will immediately override the display in the A On the page.

# Step-1 Page Entry Animation

Let's start with simple implementation. home page -> A page -> B Page into the animation, and then step by step to improve.

for A Page, enter from right to left, through the transform Translation implementation.

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
  }
}

for B Page, the entry is bottom-up, but also through the transform Translation, but need to change the page size, rounded corners.

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

  const handlePrimaryAnimation = () => {
    'worklet'
    let t = primaryAnimation.value
    // Distance Top Margin Factor
    const topDistance = 0.12
    // Distance Top Margin
    const marginTop = topDistance * screenHeight
    // Half Screen Page Size
    const pageHeight = (1 - topDistance) * screenHeight
    // Bottom-up display page
    const transY = pageHeight * (1 - t)
    return {
      overflow: 'hidden',
      borderRadius: '10px',
      marginTop: `${marginTop}px`,
      height: `${pageHeight}px`,
      transform: `translateY(${transY}px )`,
    }
  }

  return {
    handlePrimaryAnimation,
  }
}

The page jump effect is as follows, and you can see that due to the use of linear curves (not t Do any transformation), the animation is somewhat rigid, while not differentiating into/Exit animation. in B After the page is fully entered,A The page becomes invisible.

# Step-2 Custom Animation Curves

with B Page, for example, according to the AnimationStatus Values, employing different animation curves, while setting the opaque for falseSo that the route animation is still displayed when it is finished A Page.

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. When the page is entered, use the Curves.linearToEaseOut curve
  // 2. When the page exits, take the Curves.easeInToLinear curve
  const _curvePrimaryAnimation = CurveAnimation({
    animation: primaryAnimation,
    animationStatus:  primaryAnimationStatus, 
    curve: Curves.linearToEaseOut,
    reverseCurve: Curves.easeInToLinear,
  })

  const handlePrimaryAnimation = () => {
    'worklet'
    let t = _curvePrimaryAnimation.value
    ... // The rest of the content, etc. The above code is consistent
  }

  return {
    opaque: false,
    handlePrimaryAnimation,
  }
}

The difference here is only that the current progress is no longer read directly primaryAnimation The value of. Encapsulated CurveAnimation The function will be based on the AnimationStatus Determines whether it is in or out state to select a different animation curve. Frame provides a variety of curve types for further reference worklet.EasingThe improved page transitions are as follows

# Step-3 Page linkage effect

B When the page enters,A The page is pressed into the animation by secondaryAnimation Control. Next, we add a sinking effect to it, implement and B Linkage of the page.

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
    // Page Zoom Size
    const scale = 0.08
    // Distance Top Margin Factor
    const topDistance = 0.1
    // Estimated offset
    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
  }
}

By means of A Page Work scale and translate Transform to achieve the sinking effect.A page secondaryAnimation The value of the B page primaryAnimation The values of the.

The page can also be linked by canTransitionTo and canTransitionFrom Two properties are configured to modify the experience on the developer tools.

# Step-4 Gesture Return

At present, the animation effect has been basically achieved, and the final step, gesture return, is still needed. For the half-screen effect, we create A Page to add the right slide back gesture,B Page to add slide back gesture.

To the most common right slide back as an example, here only intercepts the hand gesture after the hand handling part of the code, the drag process is relatively simple to achieve, can refer to the sample code.

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()
        },
      )
    }
  },
})

First, depending on the speed and position of the release, decide whether to actually return to the previous level.

  1. Slide to the right and the speed is greater than 1
  2. Or the speed is small, has been dragged more than the screen 1/2

When the above conditions are met, the return is determined. adopt Timing Interface, for primaryAnimation Add the transition animation so that it changes to the 0, Last Call didPop Otherwise make it change to 1, return to the state before the drag.

It is important to note here that when there is a need for primaryAnimation The value is manually modified and is free to control its transition mode startUserGesture and stopUserGesture Interface.

The right slide gesture has been encapsulated in the sample code as swipe-back Components that developers can use directly. The return logic of glide gestures is basically the same, with only slight differences in some values.

The final implementation effect is shown in the figure

# Setting Page Transparency

Some custom routing effects, you need to achieve a transparent background page, here on the Skyline and webview The hierarchical relationship of background color is described.

# Page Background Color Under Custom Route

Skyline Mode to use a custom route jump page, the page background color has the following layers

  1. Page Background Color: Available by Page Selector in the wxss Defined in, the default is white
  2. Page Container Background Color: Available on Page json In the document through backgroundColorContent Property Definition, Support #RRGGBBAA Writing, default white
  3. Custom route container background color, as returned in the route configuration entry StyleObject Control, Transparent by Default
  4. Controls whether the previous page is displayed by the opaque Field control, does not show by default

When you need to set the next page to gradually enter, you can simply set

  1. Page background color transparent: Page { background-color: transparent }
  2. Page Container Background Color Transparent: backgroundColorContent: "#ffffff00"

View Custom Route Page Fade Sample

# webview The page background color under

By comparison,webview Page background color in mode

  1. Page Background Color: Available by Page Selector in the wxss Defined in, the default is transparent
  2. Page Container Background Color: Available on Page json In the document through backgroundColorContent Property Definition, Support #RRGGBB Writing, default white
  3. Window Background Color: Available by wx.setBackgroundColor Interface or page configuration modification, defaults to white

# sample code

Preview the effect in developer tools