前言
基础环境描述
效果展示


源码video.tsx
import React, { Component } from 'react';
import { View, Text, StyleSheet, Dimensions, TouchableOpacity, StatusBar, ActivityIndicator } from 'react-native';
import Slider from '@react-native-community/slider';
import Video from 'react-native-video';
import * as _ from 'lodash';
import { s_to_hs } from '@src/utils/common';
import Icon from 'react-native-vector-icons/FontAwesome5';
import Orientation from 'react-native-orientation-locker';
import ThreeSlider from './threeSlider';
import { Avatar } from 'react-native-elements';
import { ScrollView } from 'react-native-gesture-handler';
import { pxToDp } from '@src/utils/stylesKits';
import { Image } from 'react-native-elements/dist/image/Image';
const w = Dimensions.get('window').width;
const h = Dimensions.get('window').height;
const closeMenuDelay = 5000;
export interface AppProps {
route?: any;
navigation?: any;
}
class video extends Component<AppProps, any> {
timerisShowMenu: any;
timer: any;
player: any;
clickTimes: any;
douClickTimeOut: any;
constructor(props: AppProps) {
super(props);
this.state = {
isFullScreen: true,
rate: 1,
videoQuality: 6,
duration: 0,
enableSetTime: true,
slideValue: 0.0,
step: 0.01,
isPaused: true,
isHdMenu: false,
hdMenuType: 1,
isShowMenu: true,
isLoading: true,
currentTime: 0,
isShowAB: false,
start: 0,
end: 0,
isShowSliderText: false,
sliderText: 0,
multipleList: [
{
value: 2,
text: '2.0X'
},
{
value: 1.5,
text: '1.5X'
},
{
value: 1.25,
text: '1.25X'
},
{
value: 1,
text: '1.0X'
},
{
value: 0.75,
text: '0.75X'
},
{
value: 0.5,
text: '0.5X'
}
],
videoQualityList: [
{
value: 5,
text: '1080P 高码率'
},
{
value: 4,
text: '1080P 高清'
},
{
value: 3,
text: '720P 高清'
},
{
value: 2,
text: '480P 清晰'
},
{
value: 1,
text: '360 流畅'
},
{
value: 6,
text: '自动'
}
]
};
}
componentDidMount() {
console.log(this.props.route.params.videoUri);
Orientation.lockToPortrait();
console.log(Orientation.getInitialOrientation());
this.setState({ isFullScreen: true });
if (this.state.isPaused) {
return;
}
this.timer = setTimeout(() => {
this.setState({ isShowMenu: false });
}, closeMenuDelay);
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
this.timerisShowMenu && clearTimeout(this.timerisShowMenu);
this.douClickTimeOut && clearTimeout(this.timerisShowMenu);
}
switchScreen = () => {
if (this.state.isFullScreen) {
console.log(' //屏幕方向锁定为X轴');
this.setState({ isFullScreen: false });
Orientation.lockToLandscapeLeft();
} else {
this.setState({ isFullScreen: true });
console.log(' //屏幕方向锁定为Y轴');
Orientation.lockToPortrait();
}
};
setTime = (data: any) => {
if (this.state.isShowAB) {
if (data.currentTime >= this.state.end) {
this.player.seek(this.state.start);
}
}
if (this.state.currentTime == data.currentTime && this.state.currentTime > 2) {
this.setState({ isLoading: true });
return;
}
this.setState({ isLoading: false });
this.setState({ currentTime: data.currentTime });
this.setState({
slideValue: data.currentTime
});
};
onEnd = () => {
console.log('播放结束');
if (this.state.isShowAB) {
this.player.seek(this.state.start);
return;
}
this.setState({ isPaused: true });
this.setState({ slideValue: this.state.duration });
this.player.seek(this.state.duration - 2);
clearTimeout(this.timerisShowMenu);
this.setState({ isShowMenu: false });
};
paused = () => {
this.setState({ isPaused: !this.state.isPaused });
if (!this.state.isPaused) {
clearTimeout(this.timerisShowMenu);
return;
}
clearTimeout(this.timerisShowMenu);
this.timerisShowMenu = setTimeout(() => {
this.setState({ isShowMenu: false });
clearTimeout(this.timerisShowMenu);
}, closeMenuDelay);
};
repetPlay = () => {
this.setState({ slideValue: 0 });
this.setState({ isPaused: !this.state.isPaused });
};
navigationBack = () => {
this.setState({ isShowAB: false });
if (this.state.isFullScreen) {
this.props.navigation.goBack();
} else {
this.switchScreen();
this.setState({ isHdMenu: false });
}
};
bottomMenu = () => {
if (this.state.isFullScreen) {
if (this.state.isShowMenu) {
return this.portraitBottomMenu();
}
} else {
if (this.state.isShowMenu) {
return this.fullBottomMenu();
}
}
};
portraitBottomMenu = () => {
return (
<View
style={{
flexDirection: 'row',
height: 40,
alignItems: 'center',
backgroundColor: 'black',
position: 'absolute',
width: '100%',
bottom: 0,
zIndex: 3,
opacity: 0.7
}}
>
{/* 播放 */}
<TouchableOpacity onPress={this.paused} style={{ width: 30, height: 40, alignItems: 'center', justifyContent: 'center' }}>
{this.state.isPaused ? <Icon name="play" size={20} color="#FFFFFF" /> : <Icon name="pause" size={20} color="#FFFFFF" />}
</TouchableOpacity>
<Slider
style={{ flex: 1, height: 40 }}
value={this.state.slideValue}
minimumValue={0}
maximumValue={this.state.duration}
minimumTrackTintColor={'#FF6B9F'}
maximumTrackTintColor={'#FFFFFF'}
// thumbImage={require('@/res/images/home2.png')}
step={this.state.step}
onValueChange={(value) => this.setState({ slideValue: value, sliderText: value })}
onSlidingComplete={(value) => {
console.log('用户释放滑块时调用的回调,无论值是否已更改。当前值作为参数传递给回调处理程序。');
this.player.seek(value);
//隐藏
this.setState({ isShowSliderText: false });
}}
onSlidingStart={(value) => {
console.log('当用户拿起滑块时调用的回调。初始值作为参数传递给回调处理程序。');
//显示
this.setState({ isShowSliderText: true });
}}
/>
<Text style={{ color: '#FFFFFF', fontSize: 13 }}>
{this.state.slideValue === 0 && this.state.duration === 0
? '00:00/00:00'
: s_to_hs(this.state.slideValue) + '/' + s_to_hs(this.state.duration)}
</Text>
{/* 全屏按钮 */}
<TouchableOpacity onPress={this.switchScreen} style={{ height: 40, width: 40, alignItems: 'center', justifyContent: 'center' }}>
<Icon name="expand" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View>
);
};
abInit = () => {
this.setState({ isPaused: true });
this.setState({ start: 0, end: this.state.duration });
this.setState({ isShowAB: !this.state.isShowAB });
};
onGrant() {
this.setState({ isShowSliderText: true });
}
onRelease() {
this.setState({ isShowSliderText: false });
}
onStartMove(start: any) {
console.log(start);
this.setState({ start, sliderText: start });
}
onStartSliderMove(start: any, slideValue: any) {
this.setState({ start, slideValue, sliderText: start });
this.player.seek(slideValue);
}
onEndMove(end: any) {
this.setState({ end, sliderText: end });
}
onEndSliderMove(end: any, slideValue: any) {
this.setState({ end, slideValue, sliderText: end });
this.player.seek(slideValue);
}
onSlideMove(slideValue: any) {
this.setState({ slideValue, sliderText: slideValue });
this.player.seek(slideValue);
}
threeSliderView = () => {
return (
<View style={{ flex: 1, height: 70 }}>
<ThreeSlider
range={this.state.duration}
startA={this.state.start}
endB={this.state.end}
slideValue={this.state.slideValue}
onGrant={this.onGrant.bind(this)} //开始移动的回调
onRelease={this.onRelease.bind(this)} // 移动结束时的回调
onStartMove={this.onStartMove.bind(this)} //A点正在移动时的回调
onStartSliderMove={this.onStartSliderMove.bind(this)} //A点正在移动时Slider不在区域内
onEndMove={this.onEndMove.bind(this)} //B点正在移动时的回调
onEndSliderMove={this.onEndSliderMove.bind(this)} //B点正在移动时Slider不在区域内的回调
onSlideMove={this.onSlideMove.bind(this)} //播放正在移动时的回调
/>
</View>
);
};
sildeFullNormalView = () => {
return (
<Slider
style={{ flex: 1, height: 40 }}
value={this.state.slideValue}
minimumValue={0}
maximumValue={this.state.duration}
minimumTrackTintColor={'#FF6B9F'}
maximumTrackTintColor={'#FFFFFF'}
// thumbImage={require('@/res/images/home2.png')}
step={this.state.step}
onValueChange={(value) => this.setState({ slideValue: value, sliderText: value })}
onSlidingComplete={(value) => {
this.player.seek(value);
//隐藏
this.setState({ isShowSliderText: false });
}}
onSlidingStart={(value) => {
console.log('当用户拿起滑块时调用的回调。初始值作为参数传递给回调处理程序。');
//显示
this.setState({ isShowSliderText: true });
}}
/>
);
};
fullBottomMenu = () => {
return (
<View
style={{
backgroundColor: 'black',
position: 'absolute',
bottom: 0,
zIndex: 3,
width: '100%',
opacity: 0.7
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 1 }}>
{/* A-B点 和 默认进度条 */}
{this.state.isShowAB ? this.threeSliderView() : this.sildeFullNormalView()}
</View>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', backgroundColor: 'black' }}>
<TouchableOpacity onPress={this.paused} style={{ width: 30, height: 40, alignItems: 'center', justifyContent: 'center' }}>
{this.state.isPaused ? <Icon name="play" size={20} color="#FFFFFF" /> : <Icon name="pause" size={20} color="#FFFFFF" />}
</TouchableOpacity>
<TouchableOpacity style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: '#FFFFFF' }}>弹</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
flex: 1,
borderRadius: 5,
backgroundColor: '#8c8c8c',
padding: 5,
opacity: 0.5,
marginRight: 8
}}
>
<Text style={{ color: '#FFFFFF' }}>发个友善的弹幕见证当下</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.abInit}>
<Text style={{ color: '#FFFFFF', minWidth: 38 }}>A-B</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.mutiple}>
<Text style={{ color: '#FFFFFF', minWidth: 38 }}>倍速</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.videoQuality}>
<Text style={{ color: '#FFFFFF', minWidth: 38 }}>自动</Text>
</TouchableOpacity>
</View>
</View>
);
};
mutiple = () => {
this.setState({ isHdMenu: !this.state.isHdMenu, hdMenuType: 1 });
this.setState({ isShowMenu: false });
};
videoQuality = () => {
this.setState({ isHdMenu: !this.state.isHdMenu, hdMenuType: 2 });
this.setState({ isShowMenu: false });
};
hdMenu = () => {
return (
<View
style={{
backgroundColor: 'black',
zIndex: 3,
position: 'absolute',
right: 0,
height: '100%',
width: 150,
opacity: 0.6,
justifyContent: 'space-around'
}}
>
{this.hdMap()}
</View>
);
};
hdMap = () => {
let data = [];
if (this.state.hdMenuType === 1) {
data = this.state.multipleList;
} else {
data = this.state.videoQualityList;
}
return data.map((item: any, index: number) => {
return (
<TouchableOpacity
onPress={() => {
this.setVideo(item.value);
}}
key={index}
style={{ height: 40, alignItems: 'center', flexDirection: 'row', marginLeft: 20 }}
>
<Text
style={{
color: item.value == this.state.rate || item.value == this.state.videoQuality ? '#EC7DA0' : '#FFFFFF'
}}
>
{item.text}
</Text>
{/* 视频vip图标显示 */}
{item.value == 'vip' ? this.showVIPIcon() : null}
</TouchableOpacity>
);
});
};
showVIPIcon = () => {
if (this.state.hdMenuType === 2) {
return <Text style={{ color: 'red', marginLeft: 10 }}>VIP</Text>;
}
};
setVideo = (value: any) => {
if (this.state.hdMenuType === 1) {
this.setState({ rate: value });
} else {
console.log('切换视频质量为:' + value);
}
this.setState({ isHdMenu: !this.state.isHdMenu });
};
headView = () => {
return (
<View
style={{
backgroundColor: 'black',
opacity: 0.7,
position: 'absolute',
zIndex: 3,
flexDirection: 'row',
alignItems: 'center',
width: '100%'
}}
>
{/* 返回小窗口 */}
<TouchableOpacity onPress={this.navigationBack} style={{ height: 40, width: 30, alignItems: 'center', justifyContent: 'center' }}>
<Icon name="angle-left" size={30} color="#FFFFFF" />
</TouchableOpacity>
<Text style={{ color: '#FFFFFF', marginLeft: 12 }}> 标题</Text>
</View>
);
};
middleView = () => {
return (
<TouchableOpacity
onPress={() => {
console.log('middleView');
this.clickTimes = this.clickTimes || 0;
this.douClickTimeOut && clearTimeout(this.douClickTimeOut);
++this.clickTimes;
this.douClickTimeOut = setTimeout(() => {
// 1-单击事件 2-双击事件
//如果是单击
if (this.clickTimes === 1) {
this.menuFun();
}
//如果是双击
if (this.clickTimes === 2) {
console.log('双击');
//暂停或开始播放视频
this.setState({ isPaused: !this.state.isPaused });
if (this.state.isPaused) {
this.setState({ isShowMenu: true });
clearTimeout(this.timerisShowMenu);
return;
}
//菜单栏隐藏的定时器
clearTimeout(this.timerisShowMenu);
this.timerisShowMenu = setTimeout(() => {
//隐藏头部菜单栏
this.setState({ isShowMenu: false });
clearTimeout(this.timerisShowMenu);
}, closeMenuDelay);
}
this.clickTimes = 0;
}, 250);
}}
style={{
position: 'absolute',
zIndex: 2,
width: '100%',
height: '100%'
}}
></TouchableOpacity>
);
};
menuFun = () => {
if (this.state.isHdMenu) {
this.setState({ isHdMenu: false });
return;
}
clearTimeout(this.timerisShowMenu);
this.setState({ isShowMenu: true });
if (this.state.isShowMenu) {
this.timerisShowMenu = setTimeout(() => {
this.setState({ isShowMenu: false });
clearTimeout(this.timerisShowMenu);
}, closeMenuDelay);
}
};
loadingView = () => {
return (
<View
style={{
backgroundColor: '#888888',
// flex: this.state.isFullScreen ? null : 1,
position: 'absolute',
zIndex: 4,
width: '100%',
opacity: 0.5
}}
>
<ActivityIndicator size="large" color="#FA7198" style={{ height: h * 0.3 }} />
</View>
);
};
showSliderText = () => {
return (
<Text
style={{
color: '#FFFFFF',
fontSize: 13,
position: 'absolute',
backgroundColor: 'black',
padding: 5,
borderRadius: 7,
zIndex: 10,
left: '45%',
top: '45%'
}}
>
{this.state.sliderText === 0 && this.state.duration === 0
? '00:00/00:00'
: s_to_hs(this.state.sliderText) + '/' + s_to_hs(this.state.duration)}
</Text>
);
};
render() {
return (
<View style={{ flex: 1 }}>
{this.videoView()}
{/* 竖屏情况下下半部 */}
{this.state.isFullScreen ? this.bottomView() : null}
</View>
);
}
bottomView = () => {
return (
<View style={{ flex: 7 }}>
<View style={{ padding: 15 }}>
<Text style={{ fontSize: 16, lineHeight: 22, color: '#333333', fontWeight: 'bold' }}>跟上步伐,今日打卡学习舞蹈</Text>
<Text style={{ fontSize: 14, color: '#333333', lineHeight: 23, fontWeight: '400' }}>
每天都准时打卡练习芭蕾,小仙女儿们,一起来啊!每天一练,做个美美的小仙女儿!
</Text>
</View>
{/* 分割线 */}
<View style={{ backgroundColor: '#F5F5F5', width: '100%', height: 10 }} />
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 15 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Avatar
rounded
size={60}
source={{
uri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201710%2F24%2F20171024071632_hzJ8n.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=0d8c0d856224efafbd6a56e9c212be8d'
}}
/>
<View style={{ justifyContent: 'space-around', marginLeft: 3 }}>
<Text style={{ color: '#222222', lineHeight: 22, fontWeight: 'bold', fontSize: 16 }}>天赋舞蹈室</Text>
<Text style={{ color: '#666666', lineHeight: 17, fontWeight: '400', fontSize: 12, marginTop: 3 }}>用心做舞蹈,开心做自己</Text>
</View>
</View>
<TouchableOpacity
onPress={() => {
console.log('回关');
}}
style={{
borderStyle: 'solid',
borderColor: '#FF5E15',
borderWidth: 1,
borderRadius: 15,
width: 63,
height: 26,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
}}
>
<Icon name="plus" size={10} color="#FF5E15" />
<Text style={{ fontSize: 14, color: '#FF5E15', fontWeight: '400', lineHeight: 20, marginLeft: 2 }}>关注</Text>
</TouchableOpacity>
</View>
{/* 分割线 */}
<View style={{ backgroundColor: '#F5F5F5', width: '100%', height: 10 }} />
<ScrollView style={{ padding: 15 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ lineHeight: 25, fontWeight: 'bold', color: '#222222', fontSize: 18 }}>留言</Text>
<Text style={{ fontSize: 14, lineHeight: 20, color: '#666666', fontWeight: '400', marginLeft: 5 }}>(共2条)</Text>
</View>
{this.commentView()}
</ScrollView>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 15 }}>
<TouchableOpacity
style={{
backgroundColor: '#F0F0F0',
borderRadius: 15,
width: pxToDp(208),
height: pxToDp(30),
flexDirection: 'row',
alignItems: 'center'
}}
>
<Image style={{ width: pxToDp(10), height: pxToDp(16), marginLeft: 10 }} source={require('@/res/images/write.png')} />
<Text style={{ color: '#999999', lineHeight: 17, fontWeight: '400', fontSize: 12, marginLeft: 2 }}>请输入留言</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row' }}>
<Icon name="thumbs-up" style={{ marginRight: 10 }} size={16} color="#FF5E15" />
<Icon style={{ marginRight: 5 }} name="share" size={16} color="#FF5E15" />
<Text>分享</Text>
</View>
</View>
</View>
);
};
commentView = () => {
const data = [
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
},
{
id: 1,
headUri:
'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201709%2F23%2F20170923141607_dFJXz.thumb.700_0.jpeg&refer=http%3A%2F%2Fb-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1629947681&t=1f988a002374605f767b3d8297b911e0',
name: '冥想',
time: '2021-03-11 13:21:31',
value: '我也想一起打卡'
}
];
return data.map((item, index) => {
return this.commentItemView(item, index);
});
};
private commentItemView(item: any, index: number) {
return (
<View style={{ marginTop: 10 }} key={index}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Avatar
rounded
source={{
uri: item.headUri
}}
/>
<Text style={{ lineHeight: 21, color: '#666666', fontWeight: '400', fontSize: 15 }}>{item.name}</Text>
</View>
<Text style={{ lineHeight: 18, color: '#666666', fontWeight: '400', fontSize: 13 }}>{item.time}</Text>
</View>
<Text style={{ lineHeight: 21, color: '#333333', fontWeight: '400', fontSize: 15, marginTop: 5 }}>{item.value}</Text>
</View>
);
}
private videoView() {
return (
<View style={{ flex: this.state.isFullScreen ? 3 : 1 }}>
{/* Loading */}
{this.state.isLoading ? this.loadingView() : null}
{/* 播放区域触控 */}
{this.middleView()}
{/* 头部菜单 */}
{this.state.isShowMenu ? this.headView() : this.middleView()}
{/* 拖动进度条显示播放精度 */}
{this.state.isShowSliderText ? this.showSliderText() : null}
{/* 播放器本体 */}
<Video
// https://vjs.zencdn.net/v/oceans.mp4
// http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8
// http://dg1sy-vodcdn.migucloud.com/mgc_transfiles/4947/2021/3/27/0f8Cmn55zakUGv45Btz0/83300486/custom_origin_4M/0f8Cmn55zakUGv45Btz0custom_origin_4Mhls.mp4.m3u8
// ../../res/oceans.mp4
// source={route.params.path}
// source={require('../../../res/oceans.mp4')}
source={{
uri: this.props.route.params.videoUri //在模拟器时无法播放m3u8的视频
}}
ref={(ref) => {
this.player = ref;
}}
// bufferConfig={{
// minBufferMs: 15000, //播放器将始终确保默认的最小媒体持续时间(以毫秒为单位)
// maxBufferMs: 50000, //播放器将尝试缓冲的默认最大媒体持续时间(以毫秒为单位)
// bufferForPlaybackMs: 2500, //在用户操作(例如,搜索)之后必须缓冲以供回放以开始或恢复的媒体的默认持续时间(以毫秒为单位)。
// bufferForPlaybackAfterRebufferMs: 5000 //必须重新缓冲后才能重新播放的必须缓冲的默认媒体持续时间(以毫秒为单位)。重新缓冲区被定义为缓冲区耗尽而不是用户操作引起的。
// }}
paused={this.state.isPaused} //控制播放或者暂停
style={this.state.isFullScreen ? styles.videoMin : styles.videoMax}
poster="https://baconmockup.com/300/200/" //视频加载时显示的图片
posterResizeMode="cover" //图片显示方式
// controls={true} //显示原生播放控件
rate={this.state.rate} //播放速度
resizeMode="cover" //确定当帧与原始视频尺寸不匹配时如何调整视频大小。
onLoad={(data) => {
//获取视频长度毫秒
this.setState({ duration: data.duration });
this.setState({ isLoading: false });
}} //加载媒体并准备播放时调用的回调函数。
progressUpdateInterval={1000} // 两个onProgress事件之间的延迟时间(以毫秒为单位)
onProgress={(data) => this.setTime(data)} //更新进度条
onEnd={this.onEnd} //视频播放结束
onAudioBecomingNoisy={() => {
this.setState({ isPaused: false });
}} //由于音频输出变化而使音频即将变得“嘈杂”时调用的回调函数。通常,当音频输出从外部源(如耳机)切换回内部扬声器时,将调用此方法。最好在发生这种情况时暂停媒体播放,以免扬声器开始发出声音。
/>
{/* 底部菜单 */}
{this.bottomMenu()}
{/* 全屏下 底部菜单栏 倍速、视频质量 */}
{this.state.isHdMenu ? this.hdMenu() : null}
</View>
);
}
}
var styles = StyleSheet.create({
videoMin: {
flex: 1,
backgroundColor: 'black'
},
videoMax: {
flex: 1,
backgroundColor: 'black'
}
});
export default video;
源码threeSlider.tsx
import React, { Component } from 'react';
import { StyleSheet, View, PanResponder, Text, Dimensions } from 'react-native';
const roundSize = 30;
const w222 = Dimensions.get('window').width - roundSize * 2;
const h222 = Dimensions.get('window').height - roundSize * 2;
const width = w222 > h222 ? w222 : h222;
export interface AppProps {
range: number;
startA: number;
endB: number;
slideValue: number;
onGrant: any;
onStartSliderMove: any;
onStartMove: any;
onRelease: any;
onEndSliderMove: any;
onEndMove: any;
onSlideMove: any;
}
class Index extends Component<AppProps, any> {
panResponderStart: any;
panResponderEnd: any;
panResponderPlay: any;
static defaultProps: {
range: number;
startA: number;
endB: number;
};
constructor(props: AppProps) {
super(props);
let scale = width / this.props.range;
let { range, startA, endB, slideValue } = this.props;
let start = Math.round(startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA);
let end = Math.round(endB === 0 ? width : scale * endB);
let slide = Math.round(slideValue === 0 ? roundSize / 2 : slideValue === range ? width : scale * slideValue);
this.state = {
range,
startA,
endB,
slideValue,
start,
end,
slide
};
}
UNSAFE_componentWillReceiveProps(nextProps: any) {
let scale = width / this.props.range;
let { range, startA, endB, slideValue } = nextProps;
let start = Math.round(startA === 0 ? roundSize / 2 : startA === range ? width - roundSize : scale * startA);
let slide = Math.round(slideValue === 0 ? roundSize / 2 : slideValue === range ? width : scale * slideValue);
let end = Math.round(endB === 0 ? width : scale * endB);
this.setState({
range,
startA,
endB,
slideValue,
start,
end,
slide
});
}
UNSAFE_componentWillMount() {
let scale = width / this.props.range;
this.panResponderStart = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.forceUpdate();
this.props.onGrant();
},
onPanResponderMove: (evt, gestureState) => {
let start = gestureState.moveX;
let threshold = this.state.end - roundSize;
if (start >= threshold) {
start = threshold;
}
let startA = Math.round(start / scale);
if (start <= roundSize) {
start = roundSize / 2;
startA = 0;
}
if (start > this.state.slide) {
let slideValue = Math.ceil(start / scale);
this.setState({ slide: start, slideValue, start, startA }, () => {
this.props.onStartSliderMove(this.state.startA, slideValue);
});
} else {
this.setState(
{
start,
startA
},
() => {
this.props.onStartMove(this.state.startA);
}
);
}
},
onPanResponderRelease: (evt, gestureState) => {
this.props.onRelease();
return true;
},
onPanResponderTerminate: (evt, gestureState) => true
});
this.panResponderEnd = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.forceUpdate();
this.props.onGrant();
},
onPanResponderMove: (evt, gestureState) => {
let end = gestureState.moveX;
let threshold = this.state.start + roundSize;
if (end <= threshold) {
end = threshold;
}
let endB = Math.round(end / scale);
if (end >= width) {
end = width;
endB = this.state.range;
}
if (end < this.state.slide) {
let slideValue = Math.floor(end / scale);
this.setState({ slide: end, slideValue });
}
if (end < this.state.slide) {
let slideValue = Math.ceil(end / scale);
this.setState({ slide: end, slideValue, end, endB }, () => {
this.props.onEndSliderMove(this.state.endB, slideValue);
});
} else {
this.setState(
{
end,
endB
},
() => {
this.props.onEndMove(this.state.endB);
}
);
}
},
onPanResponderRelease: (evt, gestureState) => {
this.props.onRelease();
return true;
},
onPanResponderTerminate: (evt, gestureState) => true
});
this.panResponderPlay = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
this.forceUpdate();
this.props.onGrant();
},
onPanResponderMove: (evt, gestureState) => {
let slide = gestureState.moveX;
let slideValue = Math.round(slide / scale);
if (slide <= this.state.start) {
slide = this.state.start;
slideValue = this.state.startA;
}
if (slide >= this.state.end) {
slide = this.state.end;
slideValue = this.state.endB;
}
this.setState(
{
slide,
slideValue
},
() => {
this.props.onSlideMove(this.state.slideValue);
}
);
},
onPanResponderRelease: (evt, gestureState) => {
this.props.onRelease();
return true;
},
onPanResponderTerminate: (evt, gestureState) => true
});
}
render() {
let { start, end, slide } = this.state;
return (
<View style={styles.container}>
<View style={{ flexDirection: 'row' }}>
<View style={[styles.progressContainer, { backgroundColor: '#D6D7E6' }, { width: start == roundSize / 2 ? 0 : start }]} />
<View style={[styles.progressContainer, { width: width - start - (width - end) }]} />
<View style={[styles.progressContainer, { backgroundColor: '#D6D7E6' }, { width: width - end }]} />
</View>
<View style={[styles.startA, { left: start }]} {...this.panResponderStart.panHandlers}>
<Text>A</Text>
</View>
<View style={[styles.endB, { left: end }]} {...this.panResponderEnd.panHandlers}>
<Text>B</Text>
</View>
<View style={[styles.slide, { left: slide }]} {...this.panResponderPlay.panHandlers}></View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
height: 70,
justifyContent: 'center',
alignItems: 'center'
},
progressContainer: {
backgroundColor: '#ffa710',
height: 4
},
startA: {
position: 'absolute',
width: roundSize,
height: roundSize,
borderRadius: roundSize / 2,
borderColor: '#D6D7E6',
borderWidth: 1,
shadowColor: 'rgba(0,0,0,0.6)',
shadowRadius: 5,
shadowOpacity: 0.9,
backgroundColor: 'green',
top: 1,
justifyContent: 'center',
alignItems: 'center'
},
endB: {
position: 'absolute',
width: roundSize,
height: roundSize,
borderRadius: roundSize / 2,
borderColor: '#D6D7E6',
borderWidth: 1,
shadowColor: 'rgba(0,0,0,0.6)',
shadowRadius: 5,
shadowOpacity: 0.9,
backgroundColor: 'yellow',
top: 1,
justifyContent: 'center',
alignItems: 'center'
},
slide: {
position: 'absolute',
width: roundSize,
height: roundSize,
borderRadius: roundSize / 2,
borderColor: '#D6D7E6',
borderWidth: 1,
shadowColor: 'rgba(0,0,0,0.6)',
shadowRadius: 5,
shadowOpacity: 0.9,
backgroundColor: '#094D45',
bottom: 1
}
});
Index.defaultProps = {
range: 1000,
startA: 0,
endB: width
};
export default Index;
后期计划
- 如果有时间的话,希望可以把这个播放器做的跟哔哩哔哩一样。
已知bug
- 在小米手机全面屏情况下,全屏播放时,底部会有白边,暂时不知道怎么解决。
github仓库地址
参考