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

Pushing React Native Outside the Comfort Zone

Pushing React Native Outside the Comfort Zone

Raúl Gómez Acuña

September 14, 2018
Tweet

More Decks by Raúl Gómez Acuña

Other Decks in Technology

Transcript

  1. • 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
  2. class Playback extends React.Component<Props, void> { 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, };
  3. class Playback extends React.Component<Props, void> { componentDidMount() { MediaPlayer.init({ src:

    this.props.src }) .then(() => { // Song loaded into memory if (this.props.isPlaying) MediaPlayer.play(); }); } componentDidUpdate(prevProps: Props) { if (prevProps.isPlaying && !this.props.isPlaying) { MediaPlayer.pause(); } else if (!prevProps.isPlaying && this.props.isPlaying) { MediaPlayer.play(); } } render() { return null; } } };
  4. 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,
  5. type State = { isPlaying: boolean, progress: number, }; class

    Player extends React.Component<void, State> { 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, });
  6. 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
  7. 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 ( <View style={{ flex: 1 }}> <Score style={{ marginLeft: scoreLeftMargin }} /> <Playback src={'https://player/song.mp4'} isPlaying /> <Button title={isPlaying ? ‘PAUSE' : 'PLAY'} onPress={() => this.setState({ isPlaying: !isPlaying, })} /> </View> ); } } componentWillUnmount() { this.subscription.remove(); }
  8. 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
  9. <Animated.ScrollView onScroll={Animated.event([ { nativeEvent: { contentOffset: { y: this.scrollY }

    } }, ], { useNativeDriver: true })} scrollEventThrottle={16} > const bigTitleOpacity = this.scrollY.interpolate({ inputRange: [0, 50], outputRange: [1, 0], extrapolate: 'clamp', });
  10. <Playback ref={(ref: any) => { this.playback = ref; }} playing={this.state.isPlaying}

    tracks={this.state.tracksComposition} onProgress={Animated.event( [ { nativeEvent: { progress: this.progress, }, }, ])} />
  11. return ( <View style={styles.container}> <Animated.View style={[ styles.container, { transform: [{

    translateX: marginLeft }] }, ]} > <WebViewComponent source={{uri}} /> </Animated.View> <Animated.View style={[ styles.tempoBar, { transform: [{ translateX: barOffset }] }, ]} /> </View> );
  12. Benefits Can we go even further? (x1, t1) (x2, t2)

    • Easy to follow the tempo • Optimally setting styles with Animated Views
  13. <Playback ref={(ref: any) => { this.playback = ref; }} playing={this.state.isPlaying}

    tracks={this.state.tracksComposition} onProgress={Animated.event( [ { nativeEvent: { progress: this.state.progress, }, }, ], { useNativeDriver: true, }, )} />
  14. Playback Progress Animated.Value Pan Responder Score Position Red bar position

    Animated.interpolate Animated.interpolate onPanResponderRelease this.playback.seekTo onPanResponderMove this.progress.setValue
  15. 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); },
  16. /** * 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; }); }
  17. render() { const { duration, style } = this.props; const

    { value } = this.state; const maxValue = Math.round(duration / 1000); return ( <View style={[styles.progressContainer, style]}> <Slider minimumValue={0} maximumValue={maxValue} step={1} value={value} /> <Timer duration={duration} value={value} /> </View> ); }
  18. Slide release handleSlidingComplete = (tStamp: number) => { // Convert

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

    to ms this.playback.seekTo(tStamp * 1000); this.setState({ value: tStamp, isSliding: false, }); };
  20. JS Thread Native Thread this.playback.seekTo() seekTo() nSources times postFrameCallback() onSeekComplete()

    UIManagerModule .dispatchEvent(‘onProgress’, ts); <Playback onProgress={Animated.event(…)} /> this.props.progress.addListener(…)
  21. Slide release handleSlidingComplete = (tStamp: number) => { this.playback.seekTo(tStamp *

    1000); this.setState({ value: tStamp, }); this._timeout = setTimeout( () => { this.setState({ isSliding: false, }); }, 1500, ); };
  22. <View style={styles.progressBar}> <TouchableWithoutFeedback onPressIn={this.tapSliderHandler} > <Slider minimumValue={0} maximumValue={maxValue} step={1} value={value}

    onValueChange={this.handleValueChange} onSlidingStart={this.handleSlidingStart} onSlidingComplete={this.handleSlidingComplete} /> </TouchableWithoutFeedback> </View> width xTap = duration ?
  23. Benefits • Updates isolated on leaf component • Updates via

    setState are happening 1 time per second. • Avoiding unnecessary re-renders on other parts of the screens
  24. 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