Animation

There are three separate ways to animate something here, and which one to reach for depends on where the work needs to happen.

Import
import { AnimatedStyle, WorkletStyle, WorkletScroll } from '@ng-native/components';
Template
[animatedStyle][workletStyle][workletScroll] once they are in the component's imports

Plain CSS

This needs nothing from this package at all: a component's own styles can use transition and @keyframes exactly as they read on the web, compiled into the native stylesheet at build time. <touchable-opacity>'s own press fade is built this way. This is the right choice for anything that only needs to ease between states a class change already describes.

import { Component } from '@angular/core';
import { View } from '@ng-native/components';

@Component({
  selector: 'app-pulsing-dot',
  imports: [View],
  template: `<view class="dot"></view>`,
  styles: `
    @keyframes pulse {
      0%,
      100% {
        opacity: 0.25;
      }
      50% {
        opacity: 1;
      }
    }
    .dot {
      width: 12px;
      height: 12px;
      background-color: #3b6ef5;
      animation: pulse 900ms ease-in-out infinite;
    }
  `,
})
export class PulsingDot {}

The CSS compiler accepts only the default animation direction - alternate, reverse and alternate-reverse are dropped with a build warning - so a pulse that needs to ease back down goes in the keyframes themselves, as the 0%, 100% and 50% steps above do.

AnimatedStyle

AnimatedStyle, from @ng-native/components/animations, drives React Native's own Animated graph directly - Animated.Value, Animated.timing, Animated.spring, interpolation, all of it, since none of that machinery is React to begin with. Import Animated and Easing from the same file, and bind [animatedStyle] to a style object built from Animated nodes:

import { Component } from '@angular/core';
import { View } from '@ng-native/components';
import { Animated, AnimatedStyle } from '@ng-native/components/animations';

@Component({
  selector: 'app-fade-in',
  imports: [View, AnimatedStyle],
  template: `<view [animatedStyle]="{ opacity: this.opacity }"></view>`,
})
export class FadeIn {
  private readonly opacity = new Animated.Value(0);
  constructor() {
    Animated.timing(this.opacity, { toValue: 1, duration: 300, useNativeDriver: true }).start();
  }
}

With useNativeDriver: true the whole animation runs on the UI thread and this directive hands over the element's native tag once, doing nothing per frame; without it, every frame runs in JavaScript and gets written to the node from there.

The same component animates in a browser. A web build resolves that import to a React-free Animated with the same names and React Native's own timing, spring, easing and interpolation formulas, stepped by requestAnimationFrame. There every value is driven from JavaScript, so useNativeDriver changes nothing; Value, timing, spring, sequence, parallel, delay, interpolate and Easing are there, and decay, loops, ValueXY and Animated.event are not.

[animatedStyle]directive@ng-native/components
import { AnimatedStyle } from '@ng-native/components';and add AnimatedStyle to the component's imports

No public members.

Reanimated worklets

WorkletStyle and WorkletScroll, from @ng-native/components/reanimated, are for anything that needs to compute a style on every frame without ever touching the JavaScript thread

  • a value that follows a gesture, or a header that shrinks as a list scrolls. sharedValue creates a value both the JavaScript and UI runtimes can see; workletStyle describes a style computed from one or more shared values, bound to an element with [workletStyle]; workletScroll describes a worklet that runs on every scroll frame, bound with [workletScroll]. A shared value that [workletScroll] writes and [workletStyle] elsewhere reads means the two never involve the JavaScript thread at all:
import { Component } from '@angular/core';
import { ScrollView, View } from '@ng-native/components';
import {
  sharedValue,
  workletScroll,
  workletStyle,
  WorkletScroll,
  WorkletStyle,
} from '@ng-native/components/reanimated';

@Component({
  selector: 'app-parallax-header',
  imports: [ScrollView, View, WorkletScroll, WorkletStyle],
  template: `
    <scroll-view [workletScroll]="onScroll">
      <view [workletStyle]="header"></view>
      <ng-content />
    </scroll-view>
  `,
})
export class ParallaxHeader {
  private readonly offset = sharedValue(0);
  protected readonly onScroll = workletScroll([this.offset], (event, offset) => {
    'worklet';
    offset.value = event.contentOffset.y;
  });
  protected readonly header = workletStyle([this.offset], (offset) => {
    'worklet';
    return { height: Math.max(80, 200 - offset.value) };
  });
}

Every value a worklet reads has to be passed to workletStyle or workletScroll as one of the values in its first argument, and read from the function's own parameters - not closed over from this. A worklet's closure is captured by value when it is created and sent to the other runtime, so a function that read this.offset instead of a passed-in offset parameter would try to send the entire component along with it.

[workletStyle]directive@ng-native/components
import { WorkletStyle } from '@ng-native/components';and add WorkletStyle to the component's imports

No public members.

[workletScroll]directive@ng-native/components
import { WorkletScroll } from '@ng-native/components';and add WorkletScroll to the component's imports

No public members.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.