React Native视频播放方案

做过RN的童鞋都知道,RN上官方的视频组件是react-native-video。然而,官方的文档的demo并不是那么详尽,踩了一身的坑,仍然和理想中的视频播放器相去甚远。本文会完成一个基本的视频播放器,包含:

  • 全屏切换
  • 播放/暂停
  • 进度拖动
  • 滑动手势控制音量、亮度、进度

完整例子见文末。

全屏方案

一般而言,思路有两种。一种是用户点击全屏按钮时,另外打开一个页面,该页面全屏展示一个视频组件,并且应用转为横屏;二是将当前页面上的小视频组件通过一系列CSS变换,铺满屏幕,并且将应用转为横屏。本人两种思路都实现过。
第一种方案需要考虑小视频组件和全屏视频组件两个组件的进度同步性,并且如果页面上存在多个视频组件,各组件之间互不干扰,如下:

小视频组件1 <----> 全屏组件1
小视频组件2 <----> 全屏组件2
小视频组件3 <----> 全屏组件3

看起来我们将小窗口和全屏分为了两个组件,只需要关心同步问题,实际实现上,还是有若干碰壁的。其最致命的问题是,无论如何同步,切换全屏和切回时,都会有一定的不连贯性,甚至有倒帧的现象。

第二种思路,通过CSS变换,将小视频的窗口全屏铺满,这首先避免了同步问题,切换全屏的时候也如丝般顺滑。然而,第二种思路有个大问题是,小视频在布局中的位置是不确定的。在复杂布局中,有可能嵌套的很深,并且不在页面头部的位置。加上RN里没有fix布局,通过CSS铺满全屏异常困难。因此,第二种思路要求,无论视频组件嵌套多深,该组件必须位于页面顶部,这样方便CSS计算。

本文采用第二种思路,视频组件所有用到的三方应用如下:

import React from 'react';
import {
  AppState,
  AsyncStorage,
  BackHandler,
  Dimensions,
  Image,
  Platform,
  Slider,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import SystemSetting from 'react-native-system-setting'  // 一个系统亮度、音量控制相关的库,npm安装
import LinearGradient from 'react-native-linear-gradient';  // 一个渐变色库,用于美化,npm安装
import {rpx} from "../util/AdapterUtil";  // 一个将设计尺寸转换为dp单位的工具方法,具体实现见下
import Orientation from 'react-native-orientation'  // 一个控制应用方向的库,npm安装
import Video from "react-native-video";  // RN视频组件,npm安装。注意不同版本的库,其bug和兼容性问题相差巨大,本文用的是4.4.2版本
import {log} from "../util/artUtils";  // 一个log工具类,可无视

rpx方法实现如下,我相信很多应用都有大同小异的实现,该方法将设计尺寸宽设定为750,也就是rpx(750)为屏幕宽度:

const width = Dimensions.get('window').width;
const designWidth = 750
export function rpx (num) {
  return num * width / designWidth
}

注意,嵌套在视频组件外面的任何父组件不能指定宽度:

<View style={{width: rpx(750)}}>
	<MyVideo></MyVideo>
</View>

如上是有问题的,因为应用转为横屏后,组件宽度没变,导致父组件横向没有铺满屏幕,而视频组件铺满了,超出了父组件。又因为TouchableOpacity在Android上,存在“超出父组件部分无法点击”的bug,超出父组件的按钮就无法响应了。

现在,如果是小视频,可以通过props指定style控制窗口大小;而如果是全屏,则简单的铺满全屏:

  formatFullScreenContainer() {
    const w = Dimensions.get('window').width;
    const h = Dimensions.get('window').height;
    return {
      position: 'absolute',
      left: 0,
      top: 0,
      width: this.isL ? w : h,
      height: this.isL ? h : w,
      zIndex: 99999,
      backgroundColor: '#000'
    }
  }

this.isL单纯是一个记录横竖屏状态的变量。现在写一个方法切换全屏:

  toggleFullScreen() {
    if (this.player) {
      let {fullScreen} = this.state;
      if (this.props.onFullScreenChange) {
        this.props.onFullScreenChange(!fullScreen)
      }
      if (fullScreen) {
        // if (Platform.OS === 'android') {
        //   this.player.dismissFullscreenPlayer();
        // }
        this.isL = false;
        Orientation.lockToPortrait()
      } else {
        if (Platform.OS === 'android') {
          this.player.presentFullscreenPlayer();
        }
        this.isL = true;
        Orientation.lockToLandscape()
      }
      clearTimeout(this.touchTimer);
      this.setState({fullScreen: !fullScreen, isTouchedScreen: false})
    }
  }

render部分的核心代码如下:

  render() {
    let {url, videoStyle, title} = this.props;
    let {paused, fullScreen, isTouchedScreen, duration, showVolumeSlider, showBrightnessSlider, showTimeSlider} = this.state;
    let {volume, brightness, tempCurrentTime} = this.state;
    const w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
    let containerStyle = this.formatVideoStyle(videoStyle);
    return (
      <View style={fullScreen ? this.formatFullScreenContainer() : containerStyle}
            activeOpacity={1}>
        <Video source={{uri: url}} //require('../img/English_sing.mp3')
               ref={ref => this.player = ref}
               rate={1}
               volume={1.0}
               muted={false}
               paused={paused}
               resizeMode="contain"
               repeat={false}
               playInBackground={false}
               playWhenInactive={false}
               ignoreSilentSwitch={"ignore"}
               progressUpdateInterval={250.0}
               style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer]}
               onSeek={() => {
               }}
               onLoad={data => this.onLoad(data)}
               onError={(data) => this.onError(data)}
               onProgress={(data) => this.setTime(data)}
               onEnd={(data) => this.onEnd(data)}
        />
		...

进度拖动

可以用Slider组件,用于控制视频进度。render中核心代码如下:

<Slider
   style={styles.slider}
   value={this.state.slideValue}
   maximumValue={this.state.duration}
   minimumTrackTintColor={'#ca3839'}
   maximumTrackTintColor={'#989898'}
   thumbImage={require('../img/slider.png')}
   step={1}
   onValueChange={value => this.onSeek(value)}
   onSlidingComplete={value => {
     this.player.seek(value);
     this.setState({enableSetTime: true})
   }}
 />

拖动控制音量、亮度、进度

这种需求也是很多的。很多视频app在视频左边上下滑动控制音量,右边上下滑动控制亮度,而左右滑动控制进度。我们可以在View上注册onResponderStart、onResponderMove、onResponderEnd等事件处理滑动,判断滑动方向和距离来做出相应控制。这里一个细节是滑动和其他点击组件的冲突处理;二是“过滑动折返”问题。“过滑动折返”指,你设定了用户向上滑动400px时,能将音量从0加到100%。而用户手快划了600px,这种情况下,其实划了400px音量已经是最大了,剩下200px保持最大音量不变;到最高点时向下折返,用户不需要额外滑动200px,只需要下滑400px就可以将音量调整为0,而不是下滑600px。也就是变向的时候,我们需要做处理。

核心处理代码由若干监听组成:

this.gestureHandlers = {

      onStartShouldSetResponder: (evt) => {
        log('onStartShouldSetResponder');
        return true;
      },

      onMoveShouldSetResponder: (evt) => {
        log('onMoveShouldSetResponder');
        return !this.state.paused;
      },

      onResponderGrant: (evt) => {
        log('onResponderGrant');
      },

      onResponderReject: (evt) => {
        log('onResponderReject');
      },

      onResponderStart: (evt) => {
        log('onResponderStart', evt.nativeEvent.locationX, evt.nativeEvent.locationY);
        this.touchAction.x = evt.nativeEvent.locationX;
        this.touchAction.y = evt.nativeEvent.locationY;
        SystemSetting.getVolume().then((volume) => {
          SystemSetting.getAppBrightness().then((brightness) => {
            this.setState({
              initVolume: volume,
              initBrightness: brightness,
              initTime: this.state.currentTime / this.state.duration,
              tempCurrentTime: this.state.currentTime / this.state.duration,
              showVolumeSlider: false,
              showBrightnessSlider: false,
              showTimeSlider: false
            });
            if (this.sliderTimer) {
              clearTimeout(this.sliderTimer);
              this.sliderTimer = null;
            }
          });
        });
      },

      onResponderMove: (evt) => {
        let formatValue = (v) => {
          if (v > 1.0) return {v: 1.0, outOfRange: true};
          if (v < 0) return {v: 0, outOfRange: true};
          return {v, outOfRange: false}
        };

        let resolveOutOfRange = (outOfRange, props, onSlideBack) => {
          if (outOfRange) {
            if (this.touchAction[props.limit] !== null
              && Math.abs(this.touchAction[props.v] - props.current) < Math.abs(this.touchAction[props.v] - this.touchAction[props.limit])) {
              log('outOfRange', this.touchAction[props.v], props.current, this.touchAction[props.limit]);
              // 用户把亮度、音量调到极限(0或1.0)后,继续滑动,并且反向滑动,此时重新定义基准落点
              this.touchAction[props.v] = this.touchAction[props.limit];
              this.touchAction[props.limit] = null;
              if (onSlideBack) onSlideBack()
            } else {
              this.touchAction[props.limit] = [props.current];
            }
          }
        };

        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen} = this.state;
        let {videoStyle} = this.props;
        let w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
        let h = fullScreen ? Dimensions.get('window').height : videoStyle.height;

        if (!this.touchAction.slideDirection) {
          if (Math.abs(currentY - this.touchAction.y) > rpx(50)) {
            this.touchAction.slideDirection = 'v'
          } else if (Math.abs(currentX - this.touchAction.x) > rpx(50)) {
            this.touchAction.slideDirection = 'h'
          }
        }

        if (this.touchAction.slideDirection === 'v') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dy = this.touchAction.y - currentY;
          let dv = dy / (h / 1.5);
          if (this.touchAction.x > w / 2) {
            let {v, outOfRange} = formatValue(this.state.initVolume + dv);
            log('volume', v, outOfRange);
            this.setState({volume: v, showVolumeSlider: true});
            SystemSetting.setVolume(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initVolume: v})
            })
          } else {
            let {v, outOfRange} = formatValue(this.state.initBrightness + dv);
            log('brightness', v, outOfRange);
            this.setState({brightness: v, showBrightnessSlider: true});
            SystemSetting.setAppBrightness(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initBrightness: v})
            })
          }
        } else if (this.touchAction.slideDirection === 'h') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dx = currentX - this.touchAction.x;
          let dv = dx / (w / 2);
          let {v, outOfRange} = formatValue(this.state.initTime + dv);
          log('time', v, outOfRange);
          this.setState({tempCurrentTime: v, showTimeSlider: true});
          resolveOutOfRange(outOfRange, {current: currentX, v: 'x', limit: 'limitX'}, () => {
            this.setState({initTime: v})
          })
        }
      },

      onResponderRelease: (evt) => {
        log('onResponderRelease');
      },

      onResponderEnd: (evt) => {
        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen, tempCurrentTime, duration} = this.state;
        if (Math.abs(currentX - this.touchAction.x) <= rpx(50) && this.touchAction.isClick) {
          // 触发点击
          if (this.state.paused) {
            this.play()
          } else {
            this.onTouchedScreen()
          }
        }
        if (!this.touchAction.isClick) {
          this.setState({initVolume: this.state.volume, initBrightness: this.state.brightness});
        }
        this.sliderTimer = setTimeout(() => {
          this.sliderTimer = null;
          this.setState({showVolumeSlider: false, showBrightnessSlider: false, tempCurrentTime: null})
        }, 1000);
        if (tempCurrentTime != null) {
          let time = Math.ceil(tempCurrentTime * duration);
          this.onSeek(time);
          this.player.seek(time);
          this.setState({enableSetTime: true, showTimeSlider: false})
        } else {
          this.setState({showTimeSlider: false})
        }
        this.touchAction = {
          x: null,
          y: null,
          slideDirection: null,
          limitX: null,
          limitY: null,
          isClick: true
        };
      },

      onResponderTerminationRequest: (evt) => {
        log('onResponderTerminationRequest');
        return true;
      },

      onResponderTerminate: (evt) => {
        log('onResponderTerminate');
      }
    }

完整代码

import React from 'react';
import {
  AppState,
  AsyncStorage,
  BackHandler,
  Dimensions,
  Image,
  Platform,
  Slider,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';
import SystemSetting from 'react-native-system-setting'
import LinearGradient from 'react-native-linear-gradient';
import {rpx} from "../util/AdapterUtil";
import Orientation from 'react-native-orientation'
import Video from "react-native-video";
import {log} from "../util/artUtils";

export default class PlayerV2 extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      fullScreen: false,
      initVolume: 1.0,
      volume: 1.0,
      initBrightness: 1.0,
      brightness: 1.0,
      initTime: 0,
      tempCurrentTime: null,
      slideValue: 0.00,
      currentTime: 0.00,
      duration: 0.00,
      enableSetTime: true,
      paused: true,
      playerLock: false,
      canShowCover: false,
      isTouchedScreen: false,
      showVolumeSlider: false,
      showBrightnessSlider: false,
      showTimeSlider: false,
    };
    this.touchTimer = null;
    this.sliderTimer = null;
    this.isL = false;

    SystemSetting.getVolume().then((volume) => {
      SystemSetting.getBrightness().then((brightness) => {
        this.setState({volume, brightness, initVolume: volume, initBrightness: brightness})
      });
    });

    this.backListener = () => {
      if (this.state.fullScreen) {
        this.toggleFullScreen();
        return true
      }
      return false
    };
    this.appStateListener = (next) => {
      if (next === 'background' || next === 'inactive') {
        this.dismissFullScreen()
      }
    };

    this.touchAction = {
      x: null,
      y: null,
      limitX: null,
      limitY: null,
      slideDirection: null,
      isClick: true
    };

    this.gestureHandlers = {

      onStartShouldSetResponder: (evt) => {
        log('onStartShouldSetResponder');
        return true;
      },

      onMoveShouldSetResponder: (evt) => {
        log('onMoveShouldSetResponder');
        return !this.state.paused;
      },

      onResponderGrant: (evt) => {
        log('onResponderGrant');
      },

      onResponderReject: (evt) => {
        log('onResponderReject');
      },

      onResponderStart: (evt) => {
        log('onResponderStart', evt.nativeEvent.locationX, evt.nativeEvent.locationY);
        this.touchAction.x = evt.nativeEvent.locationX;
        this.touchAction.y = evt.nativeEvent.locationY;
        SystemSetting.getVolume().then((volume) => {
          SystemSetting.getAppBrightness().then((brightness) => {
            this.setState({
              initVolume: volume,
              initBrightness: brightness,
              initTime: this.state.currentTime / this.state.duration,
              tempCurrentTime: this.state.currentTime / this.state.duration,
              showVolumeSlider: false,
              showBrightnessSlider: false,
              showTimeSlider: false
            });
            if (this.sliderTimer) {
              clearTimeout(this.sliderTimer);
              this.sliderTimer = null;
            }
          });
        });
      },

      onResponderMove: (evt) => {
        let formatValue = (v) => {
          if (v > 1.0) return {v: 1.0, outOfRange: true};
          if (v < 0) return {v: 0, outOfRange: true};
          return {v, outOfRange: false}
        };

        let resolveOutOfRange = (outOfRange, props, onSlideBack) => {
          if (outOfRange) {
            if (this.touchAction[props.limit] !== null
              && Math.abs(this.touchAction[props.v] - props.current) < Math.abs(this.touchAction[props.v] - this.touchAction[props.limit])) {
              log('outOfRange', this.touchAction[props.v], props.current, this.touchAction[props.limit]);
              // 用户把亮度、音量调到极限(0或1.0)后,继续滑动,并且反向滑动,此时重新定义基准落点
              this.touchAction[props.v] = this.touchAction[props.limit];
              this.touchAction[props.limit] = null;
              if (onSlideBack) onSlideBack()
            } else {
              this.touchAction[props.limit] = [props.current];
            }
          }
        };

        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen} = this.state;
        let {videoStyle} = this.props;
        let w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
        let h = fullScreen ? Dimensions.get('window').height : videoStyle.height;

        if (!this.touchAction.slideDirection) {
          if (Math.abs(currentY - this.touchAction.y) > rpx(50)) {
            this.touchAction.slideDirection = 'v'
          } else if (Math.abs(currentX - this.touchAction.x) > rpx(50)) {
            this.touchAction.slideDirection = 'h'
          }
        }

        if (this.touchAction.slideDirection === 'v') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dy = this.touchAction.y - currentY;
          let dv = dy / (h / 1.5);
          if (this.touchAction.x > w / 2) {
            let {v, outOfRange} = formatValue(this.state.initVolume + dv);
            log('volume', v, outOfRange);
            this.setState({volume: v, showVolumeSlider: true});
            SystemSetting.setVolume(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initVolume: v})
            })
          } else {
            let {v, outOfRange} = formatValue(this.state.initBrightness + dv);
            log('brightness', v, outOfRange);
            this.setState({brightness: v, showBrightnessSlider: true});
            SystemSetting.setAppBrightness(v);
            resolveOutOfRange(outOfRange, {current: currentY, v: 'y', limit: 'limitY'}, () => {
              this.setState({initBrightness: v})
            })
          }
        } else if (this.touchAction.slideDirection === 'h') {
          log('onResponderMove');
          this.touchAction.isClick = false;
          let dx = currentX - this.touchAction.x;
          let dv = dx / (w / 2);
          let {v, outOfRange} = formatValue(this.state.initTime + dv);
          log('time', v, outOfRange);
          this.setState({tempCurrentTime: v, showTimeSlider: true});
          resolveOutOfRange(outOfRange, {current: currentX, v: 'x', limit: 'limitX'}, () => {
            this.setState({initTime: v})
          })
        }
      },

      onResponderRelease: (evt) => {
        log('onResponderRelease');
      },

      onResponderEnd: (evt) => {
        let currentX = evt.nativeEvent.locationX;
        let currentY = evt.nativeEvent.locationY;
        let {fullScreen, tempCurrentTime, duration} = this.state;
        if (Math.abs(currentX - this.touchAction.x) <= rpx(50) && this.touchAction.isClick) {
          // 触发点击
          if (this.state.paused) {
            this.play()
          } else {
            this.onTouchedScreen()
          }
        }
        if (!this.touchAction.isClick) {
          this.setState({initVolume: this.state.volume, initBrightness: this.state.brightness});
        }
        this.sliderTimer = setTimeout(() => {
          this.sliderTimer = null;
          this.setState({showVolumeSlider: false, showBrightnessSlider: false, tempCurrentTime: null})
        }, 1000);
        if (tempCurrentTime != null) {
          let time = Math.ceil(tempCurrentTime * duration);
          this.onSeek(time);
          this.player.seek(time);
          this.setState({enableSetTime: true, showTimeSlider: false})
        } else {
          this.setState({showTimeSlider: false})
        }
        this.touchAction = {
          x: null,
          y: null,
          slideDirection: null,
          limitX: null,
          limitY: null,
          isClick: true
        };
      },

      onResponderTerminationRequest: (evt) => {
        log('onResponderTerminationRequest');
        return true;
      },

      onResponderTerminate: (evt) => {
        log('onResponderTerminate');
      }
    }
  }

  componentDidMount() {
    // Orientation.addOrientationListener(this._orientationDidChange);
    BackHandler.addEventListener('hardwareBackPress', this.backListener);
    AppState.addEventListener('change', this.appStateListener);
    if (this.props.cover) {
      this.setState({canShowCover: true})
    }
  }

  _orientationDidChange = (orientation) => {
    if (orientation === 'PORTRAIT') {
      return
    }
    this.isL = orientation === 'LANDSCAPE'
  };

  componentWillUnmount() {
    BackHandler.removeEventListener('hardwareBackPress', this.backListener);
    AppState.removeEventListener('change', this.appStateListener);
    // Orientation.removeOrientationListener(this._orientationDidChange);
  }

  formatVideoStyle(videoStyle) {
    let r = Object.assign({}, videoStyle);
    r.position = 'relative';
    r.zIndex = 99999;
    return r;
  }

  formatFullScreenContainer() {
    const w = Dimensions.get('window').width;
    const h = Dimensions.get('window').height;
    return {
      position: 'absolute',
      left: 0,
      top: 0,
      width: this.isL ? w : h,
      height: this.isL ? h : w,
      zIndex: 99999,
      backgroundColor: '#000'
    }
  }

  onPreNavigate = () => {
    if (!this.state.paused) {
      this.pause()
    }
  };

  onTouchedScreen = () => {
    if (this.state.isTouchedScreen) {
      this.setState({isTouchedScreen: false});
      clearTimeout(this.touchTimer);
      return
    }
    this.setState({isTouchedScreen: !this.state.isTouchedScreen}, () => {
      if (this.state.isTouchedScreen) {
        this.touchTimer = setTimeout(() => {
          this.touchTimer = null;
          this.setState({isTouchedScreen: false})
        }, 10000)
      }
    })
  };

  play() {
    this.setState({
      canShowCover: false,
      paused: !this.state.paused,
    })
  }

  pause() {
    this.setState({
      // canShowCover: false,
      paused: true,
    })
  }

  setDuration(duration) {
    this.setState({duration: duration.duration})
  }

  onLoad(duration) {
    this.setDuration(duration);
  }

  onError(err) {
    log('onError', err)
  }

  onSeek(value) {
    //this.setState({currentTime: value});

    if (this.props.maxPlayTime && this.props.maxPlayTime <= value) {
      this.setState({paused: true, currentTime: 0});
      this.player.seek(0);
      if (this.props.onReachMaxPlayTime) this.props.onReachMaxPlayTime()
    } else {
      this.setState({currentTime: value, enableSetTime: false});
    }
  }

  onEnd(data) {
    //this.player.seek(0);
    this.setState({paused: true, currentTime: 0}, () => {
      this.player.seek(0);
    })
  }

  setTime(data) {
    let sliderValue = parseInt(this.state.currentTime);
    if (this.state.enableSetTime) {
      this.setState({
        slideValue: sliderValue,
        currentTime: data.currentTime,
      });
    }
    if (this.props.maxPlayTime && this.props.maxPlayTime <= data.currentTime) {
      this.setState({paused: true, currentTime: 0});
      this.player.seek(0);
      if (this.props.onReachMaxPlayTime) this.props.onReachMaxPlayTime()
    }
  }

  formatMediaTime(duration) {
    let min = Math.floor(duration / 60);
    let second = duration - min * 60;
    min = min >= 10 ? min : '0' + min;
    second = second >= 10 ? second : '0' + second;
    return min + ':' + second
  }

  dismissFullScreen() {
    // if (Platform.OS === 'android') {
    //   this.player.dismissFullscreenPlayer();
    // }
    if (this.props.onFullScreenChange) {
      this.props.onFullScreenChange(false)
    }
    Orientation.lockToPortrait();
    clearTimeout(this.touchTimer);
    this.setState({fullScreen: false, isTouchedScreen: false})
  }

  toggleFullScreen() {
    if (this.player) {
      let {fullScreen} = this.state;
      if (this.props.onFullScreenChange) {
        this.props.onFullScreenChange(!fullScreen)
      }
      if (fullScreen) {
        // if (Platform.OS === 'android') {
        //   this.player.dismissFullscreenPlayer();
        // }
        this.isL = false;
        Orientation.lockToPortrait()
      } else {
        if (Platform.OS === 'android') {
          this.player.presentFullscreenPlayer();
        }
        this.isL = true;
        Orientation.lockToLandscape()
      }
      clearTimeout(this.touchTimer);
      this.setState({fullScreen: !fullScreen, isTouchedScreen: false})
    }
  }

  render() {
    let {url, videoStyle, title} = this.props;
    let {paused, fullScreen, isTouchedScreen, duration, showVolumeSlider, showBrightnessSlider, showTimeSlider} = this.state;
    let {volume, brightness, tempCurrentTime} = this.state;
    const w = fullScreen ? Dimensions.get('window').width : videoStyle.width;
    let containerStyle = this.formatVideoStyle(videoStyle);
    return (
      <View style={fullScreen ? this.formatFullScreenContainer() : containerStyle}
            activeOpacity={1}>
        <Video source={{uri: url}} //require('../img/English_sing.mp3')
               ref={ref => this.player = ref}
               rate={1}
               volume={1.0}
               muted={false}
               paused={paused}
               resizeMode="contain"
               repeat={false}
               playInBackground={false}
               playWhenInactive={false}
               ignoreSilentSwitch={"ignore"}
               progressUpdateInterval={250.0}
               style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer]}
               onSeek={() => {
               }}
               onLoad={data => this.onLoad(data)}
               onError={(data) => this.onError(data)}
               onProgress={(data) => this.setTime(data)}
               onEnd={(data) => this.onEnd(data)}
        />

        {
          paused ?
            <TouchableOpacity activeOpacity={0.8}
                              style={[fullScreen ? this.formatFullScreenContainer() : styles.videoPlayer, {
                                backgroundColor: 'rgba(0, 0, 0, 0.5)',
                                display: 'flex',
                                alignItems: 'center',
                                justifyContent: 'center'
                              }]}
                              onPress={() => {
                                this.play()
                              }}>
              {this.state.canShowCover ? <Image style={styles.videoPlayer} source={{uri: this.props.cover}}/> : null}
              <Image style={{width: rpx(75), height: rpx(75)}} source={require("./../img/play_video_icon.png")}/>
            </TouchableOpacity> : null
        }

        <View style={[fullScreen ? this.formatFullScreenContainer() : containerStyle, {backgroundColor: 'transparent'}]}
              pointerEvents={paused ? 'box-none' : 'auto'} {...this.gestureHandlers} />

        {
          showVolumeSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.verticalSlider}>
              <Image style={{width: rpx(32), height: rpx(32)}}
                     source={volume <= 0 ? require('../img/no_volume.png') : require('../img/volume.png')}/>
              <View style={{height: rpx(4), width: rpx(192), flexDirection: 'row'}}>
                <View style={{height: rpx(4), width: rpx(192 * volume), backgroundColor: '#ca3839'}}/>
                <View style={{height: rpx(4), flex: 1, backgroundColor: '#787878'}}/>
              </View>
            </View>
          </View>
        }

        {
          showBrightnessSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.verticalSlider}>
              <Image style={{width: rpx(32), height: rpx(32)}}
                     source={require('../img/brightness.png')}/>
              <View style={{height: rpx(4), width: rpx(192), flexDirection: 'row'}}>
                <View style={{height: rpx(4), width: rpx(192 * brightness), backgroundColor: '#ca3839'}}/>
                <View style={{height: rpx(4), flex: 1, backgroundColor: '#787878'}}/>
              </View>
            </View>
          </View>
        }

        {
          showTimeSlider &&
          <View style={[styles.verticalSliderContainer, {position: 'absolute', top: rpx(120), left: 0, right: 0}]}>
            <View style={styles.smallSlider}>
              <Text style={{
                color: 'white',
                fontSize: rpx(26)
              }}>{this.formatMediaTime(Math.floor(tempCurrentTime * duration))}/{this.formatMediaTime(Math.floor(duration))}</Text>
            </View>
          </View>
        }
        {
          isTouchedScreen ?
            <View style={[styles.navContentStyle, {width: w, height: fullScreen ? rpx(160) : rpx(90)}]}>
              <LinearGradient colors={['#666666', 'transparent']}
                              style={{flex: 1, height: fullScreen ? rpx(160) : rpx(90), opacity: 0.5}}/>
              <View style={[styles.navContentStyleInner]}>
                {
                  fullScreen ?
                    <TouchableOpacity
                      style={{width: rpx(22), height: rpx(40), marginLeft: rpx(30)}}
                      onPress={() => {
                        this.toggleFullScreen()
                      }}>
                      <Image style={{width: rpx(22), height: rpx(40)}}
                             source={require('../img/back_arrow_icon_white.png')}/>
                    </TouchableOpacity> : null
                }
                {
                  fullScreen ?
                    <TouchableOpacity onPress={() => {
                      this.toggleFullScreen()
                    }}>
                      <Text
                        style={{
                          backgroundColor: 'transparent',
                          color: 'white',
                          marginLeft: rpx(50)
                        }}>{title}</Text>
                    </TouchableOpacity>
                    : null
                }
              </View>

            </View> : <View style={{height: Platform.OS === 'ios' ? 44 : 56, backgroundColor: 'transparent'}}/>
        }


        {
          isTouchedScreen &&
          <View style={[styles.toolBarStyle, {width: w, height: fullScreen ? rpx(110) : rpx(90)}]}>
            <LinearGradient colors={['transparent', '#666666']}
                            style={{flex: 1, height: fullScreen ? rpx(110) : rpx(90), opacity: 0.5}}/>
            <View style={[styles.toolBarStyleInner, {width: w}]}>
              <TouchableOpacity activeOpacity={0.8} onPress={() => this.play()}>
                <Image style={{width: rpx(50), height: rpx(50)}}
                       source={this.state.paused ? require('./../img/play.png') : require('./../img/pause.png')}/>
              </TouchableOpacity>
              <View style={styles.progressStyle}>
                <Text style={styles.timeStyle}>{this.formatMediaTime(Math.floor(this.state.currentTime))}</Text>
                <Slider
                  style={styles.slider}
                  value={this.state.slideValue}
                  maximumValue={this.state.duration}
                  minimumTrackTintColor={'#ca3839'}
                  maximumTrackTintColor={'#989898'}
                  thumbImage={require('../img/slider.png')}
                  step={1}
                  onValueChange={value => this.onSeek(value)}
                  onSlidingComplete={value => {
                    this.player.seek(value);
                    this.setState({enableSetTime: true})
                  }}
                />
                <View style={{flexDirection: 'row', justifyContent: 'flex-end', width: rpx(70)}}>
                  <Text style={{
                    color: 'white',
                    fontSize: rpx(24)
                  }}>{this.formatMediaTime(Math.floor(duration))}</Text>
                </View>
              </View>
              {
                fullScreen ?
                  <TouchableOpacity activeOpacity={0.8} onPress={() => {
                    this.toggleFullScreen()
                  }}>
                    <Image style={{width: rpx(50), height: rpx(50)}} source={require("./../img/not_full_screen.png")}/>
                  </TouchableOpacity> :
                  <TouchableOpacity activeOpacity={0.8} onPress={() => {
                    this.toggleFullScreen()
                  }}>
                    <Image style={{width: rpx(50), height: rpx(50)}} source={require("./../img/full_screen.png")}/>
                  </TouchableOpacity>
              }
            </View>
          </View>
        }
      </View>
    )
  }
}

const styles = StyleSheet.create({

  fullScreenContainer: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0
  },
  videoPlayer: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0
  },
  toolBarStyle: {
    position: 'absolute',
    left: 0,
    bottom: 0,
    flexDirection: 'row',
    alignItems: 'center',
    height: rpx(110),
    zIndex: 100000
  },
  toolBarStyleInner: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: rpx(30),
    justifyContent: 'space-around',
    flex: 1,
    height: rpx(90),
    position: 'absolute',
    top: 0
  },
  slider: {
    flex: 1,
    marginHorizontal: 5,
    height: rpx(90)
  },
  progressStyle: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-around',
    marginHorizontal: rpx(20)
  },
  timeStyle: {
    width: rpx(70),
    color: 'white',
    fontSize: rpx(24)
  },
  navContentStyle: {
    height: rpx(90),
    flexDirection: 'row',
    alignItems: 'center',
    position: 'absolute',
    top: 0,
    zIndex: 100001,
  },
  navContentStyleInner: {
    height: rpx(90),
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: rpx(20),
    position: 'absolute'
  },
  verticalSlider: {
    width: rpx(320),
    height: rpx(84),
    borderRadius: rpx(42),
    backgroundColor: 'rgba(0,0,0,0.4)',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: rpx(40)
  },
  smallSlider: {
    width: rpx(220),
    height: rpx(84),
    borderRadius: rpx(42),
    backgroundColor: 'rgba(0,0,0,0.4)',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  verticalSliderContainer: {
    height: rpx(84),
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 100001
  }
});

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值