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
110

Pushing RN outside the comfort zone

Raúl Gómez Acuña

March 31, 2018
Tweet

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(); }); } componentWillReceiveProps(nextProps: Props) { if (this.props.isPlaying && !nextProps.isPlaying) { MediaPlayer.pause(); } else if (!this.props.isPlaying && nextProps.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 const scoreLeftMargin = this.calculateLeftMargin();
  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 }

    } }, ])} 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.state.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. /** * 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;
  16. 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); },
  17. /** * 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; }); }
  18. 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> ); }
  19. Slide release handleSlidingComplete = (tStamp: number) => { // Convert

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

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

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

    1000); this.setState({ value: tStamp, }); this._timeout = setTimeout( () => { this.setState({ isSliding: false, }); }, 1500, ); };
  23. <View style={styles.progressBar} onLayout={this.handleLayout}> <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 ?
  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