Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Pushing RN outside the comfort zone

Raúl Gómez Acuña
March 31, 2018
78

Pushing RN outside the comfort zone

Raúl Gómez Acuña

March 31, 2018
Tweet

Transcript

  1. View Slide

  2. Frontend Software engineer at
    @rgommezz

    View Slide

  3. Origin

    View Slide

  4. • Score rendering and motion
    • Support for mixing different audio sources
    • Seeking by interacting with the screen
    • Seeking by interacting with the progress bar (Spotify)
    • TImer component
    • Replay button

    View Slide

  5. Can we build this with React
    Native?

    View Slide

  6. View Slide

  7. View Slide

  8. The backbone

    View Slide

  9. View Slide

  10. Approaches

    View Slide

  11. NetInfo
    BackHandler
    AppState
    COMPONENTS APIs
    Native UI
    Component
    Native Module

    View Slide

  12. Native Module

    View Slide

  13. class Playback extends React.Component {
    componentDidMount() {
    MediaPlayer.init({ src: this.props.src }).then(() => {
    if (this.props.isPlaying) {
    MediaPlayer.play();
    }
    });
    }
    import * as React from 'react';
    import { NativeModules } from 'react-native';
    const MediaPlayer = NativeModules.MediaPlayer;
    type Props = {
    isPlaying: boolean,
    src: string,
    };

    View Slide

  14. class Playback extends React.Component {
    componentDidMount() {
    MediaPlayer.init({ src: this.props.src })
    .then(() => {
    // Song loaded into memory
    if (this.props.isPlaying) MediaPlayer.play();
    });
    }
    componentWillReceiveProps(nextProps: Props) {
    if (this.props.isPlaying && !nextProps.isPlaying) {
    MediaPlayer.pause();
    } else if (!this.props.isPlaying && nextProps.isPlaying) {
    MediaPlayer.play();
    }
    }
    render() {
    return null;
    }
    }
    };

    View Slide

  15. Sending progress to JS

    View Slide

  16. import {
    View,
    Button,
    NativeModules,
    NativeEventEmitter,
    Platform,
    DeviceEventEmitter,
    Score,
    } from 'react-native';
    import Playback from ‘./PlayerNative';
    const { MediaPlayer } = NativeModules;
    const mediaPlayerTimeProgressEmitter = Platform.select({
    ios: new NativeEventEmitter(MediaPlayer),
    android: DeviceEventEmitter,
    });
    type State = {
    isPlaying: boolean,
    progress: number,

    View Slide

  17. type State = {
    isPlaying: boolean,
    progress: number,
    };
    class Player extends React.Component {
    subscription: *;
    state: State = {
    isPlaying: false,
    progress: 0, // progress of the song in ms
    };
    componentDidMount() {
    this.subscription = mediaPlayerTimeProgressEmitter.addListener(
    'TimeProgress',
    progress =>
    this.setState({
    progress,
    const mediaPlayerTimeProgressEmitter = Platform.select({
    ios: new NativeEventEmitter(MediaPlayer),
    android: DeviceEventEmitter,
    });

    View Slide

  18. isPlaying: false,
    progress: 0,
    };
    componentDidMount() {
    // We are re-rendering 60 times per second with the new progress
    this.subscription = mediaPlayerTimeProgressEmitter.addListener(
    'TimeProgress',
    (progress: number) =>
    this.setState({
    progress,
    }),
    );
    }
    componentWillUnmount() {
    this.subscription.remove();
    }
    render() {
    const { isPlaying } = this.state;
    // Calculating the left margin based on the score width,
    // and total duration of the song
    const scoreLeftMargin = this.calculateLeftMargin();

    View Slide

  19. Score renderer

    View Slide

  20. View Slide

  21. View Slide

  22. source={{
    uri: svgUri,
    }}
    />

    View Slide

  23. source={{
    uri: `https://svgwrapper.net/?url=${src}`,
    }}
    />

    View Slide

  24. render() {
    const { isPlaying } = this.state;
    // Calculates the margin left based on the time progress,
    // width of the score and duration of the song
    const scoreLeftMargin = this.calculateLeftMargin();
    return (



    title={isPlaying ? 'PAUSE' : 'PLAY'}
    onPress={() =>
    this.setState({
    isPlaying: !isPlaying,
    })}
    />

    );
    }
    componentWillUnmount() {
    this.subscription.remove();
    }

    View Slide

  25. View Slide

  26. Drawbacks
    • No way to follow the tempo
    • 60 bridge passes per second: frames skipped
    • Styles not set optimally
    • Animations run on JS thread
    • Not easy to scale

    View Slide

  27. Native UI Component

    View Slide

  28. ScrollView

    View Slide

  29. onScroll={Animated.event([
    {
    nativeEvent:
    {
    contentOffset: { y: this.scrollY }
    }
    },
    ])}
    scrollEventThrottle={16}
    >
    const bigTitleOpacity =
    this.scrollY.interpolate({
    inputRange: [0, 50],
    outputRange: [1, 0],
    extrapolate: 'clamp',
    });

    View Slide

  30. The mental model

    View Slide


  31. View Slide

  32. ref={(ref: any) => {
    this.playback = ref;
    }}
    playing={this.state.isPlaying}
    tracks={this.state.tracksComposition}
    onProgress={Animated.event(
    [
    {
    nativeEvent: {
    progress: this.state.progress,
    },
    },
    ])}
    />

    View Slide

  33. return (

    style={[
    styles.container,
    { transform: [{ translateX: marginLeft }] },
    ]}
    >


    style={[
    styles.tempoBar,
    { transform: [{ translateX: barOffset }] },
    ]}
    />

    );

    View Slide

  34. Benefits
    Can we go even further?
    (x1, t1) (x2, t2)
    • Easy to follow the tempo
    • Optimally setting styles with Animated Views

    View Slide

  35. Using native driver

    View Slide

  36. ref={(ref: any) => {
    this.playback = ref;
    }}
    playing={this.state.isPlaying}
    tracks={this.state.tracksComposition}
    onProgress={Animated.event(
    [
    {
    nativeEvent: {
    progress: this.state.progress,
    },
    },
    ],
    {
    useNativeDriver: true,
    },
    )}
    />

    View Slide

  37. Player demo

    View Slide

  38. Diving deeper into
    interpolations
    The power of interpolation

    View Slide

  39. Seeking

    View Slide

  40. View Slide

  41. Pan Responder

    View Slide

  42. Playback
    Progress
    Animated.Value
    Score Position Red bar position
    Animated.event
    Animated.interpolate Animated.interpolate

    View Slide

  43. Playback
    Progress
    Animated.Value
    Pan Responder
    Score Position Red bar position
    Animated.interpolate Animated.interpolate
    onPanResponderRelease
    this.playback.seekTo
    onPanResponderMove
    this.progress.setValue

    View Slide

  44. Pan Responder lifecycle

    View Slide

  45. Negotiation
    onStartShouldSetPanResponder: () => !this.props.isPlaying,
    onStartShouldSetPanResponderCapture: () => !this.props.isPlaying,
    onMoveShouldSetResponderCapture: () => !this.props.isPlaying,
    onMoveShouldSetPanResponderCapture: () => !this.props.isPlaying,

    View Slide

  46. Touch start
    onPanResponderGrant: () => {
    this.progressAccValue = this.props.progress.__getValue();
    this.props.progress.setValue(this.progressAccValue);
    },

    View Slide

  47. /**
    * Copyright (c) 2013-present, Facebook, Inc.
    *
    * This source code is licensed under the MIT license found in the
    * LICENSE file in the root directory of this source tree.
    *
    * @providesModule ReactGlobalSharedState
    */
    'use strict';
    const {
    __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
    } = require('ReactNative');
    module.exports =
    __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactGlobalSharedState;

    View Slide

  48. Touch move
    onPanResponderMove: (evt, gestureState) => {
    // Setting the new animated value based on the accumulated
    // distance of the gesture since the touch started, dx.
    // Duration and score width are constants
    const nextValue = -(gestureState.dx /
    (this.state.width / this.props.duration));
    this.props.progress.setValue(nextValue + this.progressAccValue);
    },

    View Slide

  49. Touch release
    onPanResponderRelease: (evt, gestureState) => {
    this.props.progress.stopAnimation(lastTimeStamp => {
    this.playback.seekTo(lastTimeStamp);
    });
    },

    View Slide

  50. Seeking demo

    View Slide

  51. Time Progress Component

    View Slide





  52. View Slide

  53. View Slide

  54. Android first

    View Slide





  55. View Slide

  56. Performance demands

    View Slide

  57. style={styles.absoluteBottom}
    progress={this.state.progress}
    duration={song.duration}
    onSlidingStart={this.handleSlidingStart}
    onSlidingComplete={this.handleSlidingComplete}
    />

    View Slide

  58. Animated ad-hoc listeners

    View Slide

  59. /**
    * Controlling the progress bar from the player.
    * We'll update the local state 1 time per second
    * when the value is controlled by the playback
    */
    componentDidMount() {
    let prevValue = 0;
    this._listenerId = this.props.progress.addListener((e: *) => {
    // Math.round() will always round down to the lesser integer
    const nextTimeInSeconds = Math.floor(e.value / 1000);
    if (!this.state.isSliding && prevValue !== nextTimeInSeconds) {
    this.setState({ value: nextTimeInSeconds });
    }
    prevValue = nextTimeInSeconds;
    });
    }

    View Slide

  60. render() {
    const { duration, style } = this.props;
    const { value } = this.state;
    const maxValue = Math.round(duration / 1000);
    return (

    minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    />
    duration={duration}
    value={value}
    />

    );
    }

    View Slide

  61. Time progress demo

    View Slide

  62. Spotify example

    View Slide

  63. View Slide

  64. Seeking by sliding

    View Slide


  65. minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    onSlidingStart={this.handleSlidingStart}
    onValueChange={this.handleValueChange}
    onSlidingComplete={this.handleSlidingComplete}
    />

    View Slide

  66. Slide start
    handleSlidingStart = (value: number) => {
    this.setState(
    {
    value,
    isSliding: true,
    },
    );
    };

    View Slide

  67. Slide move
    handleValueChange = (value: number) => {
    this.setState({
    value,
    });
    };

    View Slide

  68. Slide release
    handleSlidingComplete = (tStamp: number) => {
    // Convert to ms
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    isSliding: false,
    });
    };

    View Slide

  69. View Slide

  70. Slide release
    handleSlidingComplete = (tStamp: number) => {
    // Convert to ms
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    isSliding: false,
    });
    };

    View Slide

  71. JS Thread Native Thread
    this.playback.seekTo()
    seekTo() nSources times
    postFrameCallback()
    onSeekComplete()
    UIManagerModule
    .dispatchEvent(‘onProgress’, ts);

    this.props.progress.addListener(…)

    View Slide

  72. Slide release
    handleSlidingComplete = (tStamp: number) => {
    this.playback.seekTo(tStamp * 1000);
    this.setState({
    value: tStamp,
    });
    this._timeout = setTimeout(
    () => {
    this.setState({
    isSliding: false,
    });
    },
    1500,
    );
    };

    View Slide

  73. Seeking by tapping

    View Slide


  74. onPressIn={this.tapSliderHandler}
    >
    minimumValue={0}
    maximumValue={maxValue}
    step={1}
    value={value}
    onValueChange={this.handleValueChange}
    onSlidingStart={this.handleSlidingStart}
    onSlidingComplete={this.handleSlidingComplete}
    />


    width
    xTap
    =
    duration
    ?

    View Slide

  75. Benefits
    • Updates isolated on leaf component
    • Avoiding unnecessary re-renders on other parts of the
    screens

    View Slide

  76. Progress bar seeking demo

    View Slide

  77. Replay

    View Slide

  78. this.playback.seekTo(0)

    View Slide

  79. Takeaways

    View Slide

  80. Takeaways
    • Is React Native ready for the challenge? YES!
    • Native Modules and Native UI Components are the game
    changers
    • Keep the ScrollView in mind as a reference model:
    Practical hacks for delightful interactions
    • Think. Research. Plan. Write. Validate. Fail. Modify

    View Slide

  81. Thank you :)

    View Slide