background

Alipay membership page card, there is a left and right flip phone, the light moves with gesture effect.

We’re going to do that, but our cards are in RN pages, so can RN do that?

research

I started with the React-native sensors

subscription = attitude.subscribe(({ x, y, z }) =>
    {
        letNewTranslateX = y * screenWidth * 0.5 + screenWidth/2 - imgWidth/2; this.setState({ translateX: newTranslateX }); });Copy the code

This is still the traditional way to refresh the page — setState. In the end, asynchronous communication between JS and Native is carried out through bridge, so the final result will be stuck.

How do I get native to update my view without Using bridge? – Using native Driver for Animated!!

Using Native Driver for Animated

What is Animated

The Animated API makes Animated run smoothly by binding Animated.Value to the styles or props of the View, and then updating the animation by manipulating Animated. More on the Animated API can be found here.

Animated defaults to using the JS driver, which works as shown below:

The page update process is as follows:

[JS] The animation driver uses requestAnimationFrame to update Animated.Value [JS] Interpolate calculation [JS] Update Viewprops [JS→N] Serialized View Update events [N] The UIView or Android.View is updated.

Animated.event

You can use Animated. Event to associate Animated.Value with a View event.

<ScrollView
  scrollEventThrottle={16}
  onScroll={Animated.event(
    [{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
  )}
>
  {content}
</ScrollView>
Copy the code

useNativeDriver

The RN documentation describes useNativeDriver as follows:

The Animated API is designed to be serializable. By using the native driver, we send everything about the animation to native before starting the animation, allowing native code to perform the animation on the UI thread without having to go through the bridge on every frame. Once the animation has started, the JS thread can be blocked without affecting the animation.

The useNativeDriver can be used to achieve rendering in Native UI threads. After using onScroll, it looks like this:

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
  scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
  onScroll={Animated.event(
    [{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
    { useNativeDriver: true } // <-- Add this
  )}
>
  {content}
</Animated.ScrollView>
Copy the code

After using the useNativeDriver, the page update does not involve JS

[N] Native use CADisplayLink or android.view.Choreographer to update Animated.Value [N] Interpolate calculation

[N] Update Animated.View props

[N] The UIView or android.View is updated.

What we want to achieve now is the real-time flip Angle data of the sensor. If there is an Event like onScroll of ScrollView mapped, it is the most appropriate. Now let’s see how to achieve it.

implementation

First on the JS side, the Animated API has a createAnimatedComponent method, which is used to implement all of the apis inside Animated

const Animated = {
  View: AnimatedImplementation.createAnimatedComponent(View),
  Text: AnimatedImplementation.createAnimatedComponent(Text),
  Image: AnimatedImplementation.createAnimatedComponent(Image),
  ...
}
Copy the code

And then how does the onScroll of native RCTScrollView implement

RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName
                                                                 reactTag:self.reactTag
                                                               scrollView:scrollView
                                                                 userData:userData
                                                            coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:scrollEvent];
Copy the code

RCTScrollEvent encapsulates RCTScrollEvent, which is a subclass of RCTEvent. Is this necessary? Why not? So I tried using the original call:

if (self.onMotionChange) {
    self.onMotionChange(data);
}
Copy the code

Found, well, not surprisingly, not work. Let’s debug onScroll’s last call to native:

Therefore, it is necessary to call [RCTEventDispatcher sendEvent:] to trigger the update of the Native UI. Then we implement RCTMotionEvent as RCTScrollEvent. The body function is called:

- (NSDictionary *)body
{
    NSDictionary *body = @{
                           @"attitude": @ {@"pitch":@(_motion.attitude.pitch),
                                   @"roll":@(_motion.attitude.roll),
                                   @"yaw":@(_motion.attitude.yaw),
                                   },
                           @"rotationRate": @ {@"x":@(_motion.rotationRate.x),
                                   @"y":@(_motion.rotationRate.y),
                                   @"z":@(_motion.rotationRate.z)
                                   },
                           @"gravity": @ {@"x":@(_motion.gravity.x),
                                   @"y":@(_motion.gravity.y),
                                   @"z":@(_motion.gravity.z)
                                   },
                           @"userAcceleration": @ {@"x":@(_motion.userAcceleration.x),
                                   @"y":@(_motion.userAcceleration.y),
                                   @"z":@(_motion.userAcceleration.z)
                                   },
                           @"magneticField": @ {@"field": @ {@"x":@(_motion.magneticField.field.x),
                                           @"y":@(_motion.magneticField.field.y),
                                           @"z":@(_motion.magneticField.field.z)
                                           },
                                   @"accuracy":@(_motion.magneticField.accuracy)
                                   }
                           };
    
    return body;
}
Copy the code

Finally, the code used on the JS side is

var interpolatedValue = this.state.roll.interpolate(...)

<AnimatedDeviceMotionView
  onDeviceMotionChange={
    Animated.event([{
      nativeEvent: {
        attitude: {
          roll: this.state.roll,
        }
      },
    }],
    {useNativeDriver: true},
    )
  }
/>

<Animated.Image style={{height: imgHeight, width: imgWidth, transform: [{translateX:interpolatedValue}]}} source={require('./image.png')} / >Copy the code

Final effect:

Continue to optimize

One of the problems with the above implementation is that you need to write a useless AnimatedMotionView in render to connect Animated. Event to Animated.Value. So is there a way to get rid of this useless view and use our component like an RN module?

What Animated. Event does is associate event with Animated.Value, so how does that work?

First of all, we look at the node_modules/react – native/Libraries/Animated/SRC/AnimatedImplementation js createAnimatedComponent implementation, AttachNativeEvent = attachNativeEvent;

NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping);
Copy the code

Let’s see how this function is implemented in native code:

- (void)addAnimatedEventToView:(nonnull NSNumber *)viewTag
                     eventName:(nonnull NSString *)eventName
                  eventMapping:(NSDictionary<NSString *, id> *)eventMapping
{
  NSNumber *nodeTag = [RCTConvert NSNumber:eventMapping[@"animatedValueTag"]]. RCTAnimatedNode *node = _animationNodes[nodeTag]; . NSArray<NSString *> *eventPath = [RCTConvert NSStringArray:eventMapping[@"nativeEventPath"]];

  RCTEventAnimation *driver =
    [[RCTEventAnimation alloc] initWithEventPath:eventPath valueNode:(RCTValueAnimatedNode *)node];

  NSString *key = [NSString stringWithFormat:@"% @ % @", viewTag, eventName];
  if(_eventDrivers[key] ! = nil) { [_eventDrivers[key] addObject:driver]; }else{ NSMutableArray<RCTEventAnimation *> *drivers = [NSMutableArray new]; [drivers addObject:driver]; _eventDrivers[key] = drivers; }}Copy the code

The information in eventMapping eventually constructs an eventDriver, which will be called when our native RCTEvent calls sendEvent:

- (void)handleAnimatedEvent:(id<RCTEvent>)event
{
  if (_eventDrivers.count == 0) {
    return;
  }

  NSString *key = [NSString stringWithFormat:@"% @ % @", event.viewTag, event.eventName];
  NSMutableArray<RCTEventAnimation *> *driversForKey = _eventDrivers[key];
  if (driversForKey) {
    for (RCTEventAnimation *driver indriversForKey) { [driver updateWithEvent:event]; } [self updateAnimations]; }}Copy the code

Wait, so what that viewTag and eventName do, they connect together to make a key? What?

The view tag that identifies a view in RN is just going to be a unique string, so can we just have a unique view tag instead of a view?

With that in mind, let’s look at generating this unique viewTag. Let’s look at the code for loading UIView from JS (RN version 0.45.1)

mountComponent: function( transaction, hostParent, hostContainerInfo, context, ) { var tag = ReactNativeTagHandles.allocateTag(); this._rootNodeID = tag; this._hostParent = hostParent; this._hostContainerInfo = hostContainerInfo; . UIManager.createView( tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload, ); .return tag;
}
Copy the code

We can use the allocateTag method of ReactNativeTagHandles to generate this viewTag.

Update 2019.02.25: In RN0.58.5, since the allocateTag() method is not exposed, only a large number can be assigned to tag as a workaround

At this point, we can connect Animated. Event to Animated.Value using the attachNativeEvent method in the AnimatedImplementation without having to add a useless view at render time.

Github: github.com/rrd-fe/reac… Please give a star if you feel good 🙂

Finally, welcome to star, our renrendai big front-end team blog. All the articles will be updated synchronously to zhihu column and Nuggets account. We will share several high quality big front-end technology articles every week.

Reference

Facebook. Making. IO/react – nativ…

Facebook. Making. IO/react – nativ…

Medium.com/xebia/linki…

www.raizlabs.com/dev/2018/03…

www.jianshu.com/p/7aa301632…