Player.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. import React, { useState, useEffect, useRef, useMemo, useContext } from 'react';
  2. import {
  3. View,
  4. Text,
  5. StyleSheet,
  6. Dimensions,
  7. PanResponder,
  8. ScrollView,
  9. Image,
  10. Platform,
  11. StatusBar,
  12. TouchableOpacity,
  13. Share
  14. } from 'react-native';
  15. import Modal from 'react-native-modal';
  16. import { Slider, Icon } from 'react-native-elements';
  17. import Animated, { Easing } from 'react-native-reanimated';
  18. import { State, PanGestureHandler } from 'react-native-gesture-handler';
  19. import { getStatusBarHeight } from '../utils/StatusBarHeight';
  20. import C from 'rn-class';
  21. import PlayerControls from './PlayerControls';
  22. import PlayerContents from './PlayerContents';
  23. import PlayerHeader from './PlayerHeader';
  24. import { getInset } from 'react-native-safe-area-view';
  25. import Video from 'react-native-video';
  26. import { PlayerDispatchContext, PlayerStateContext } from './PlayerReducer';
  27. const { width, height } = Dimensions.get('window');
  28. const statusBarHeight = getStatusBarHeight(true);
  29. const minHeight = height * 0.1;
  30. const midBound = height - height / 2.5;
  31. const upperBound = midBound + minHeight;
  32. //const destination = height - height * 0.2
  33. //const destination = height - height*0.16 -2 - getStatusBarHeight(true);
  34. //getInset('bottom')
  35. // height > 560
  36. // ?height * 0.75 - getInset('bottom')
  37. // :height * 0.70;
  38. const {
  39. Value,
  40. event,
  41. Extrapolate,
  42. cond,
  43. Clock,
  44. eq,
  45. set,
  46. add,
  47. sub,
  48. multiply,
  49. lessThan,
  50. clockRunning,
  51. startClock,
  52. spring,
  53. stopClock,
  54. interpolate,
  55. timing,
  56. neq,
  57. or,
  58. and,
  59. greaterThan,
  60. call
  61. } = Animated;
  62. const shadow = {
  63. alignItems: 'center',
  64. shadowColor: 'black',
  65. shadowOffset: { width: 0, height: 0 },
  66. shadowOpacity: 0.18,
  67. shadowRadius: 2
  68. };
  69. const shadow2 = {
  70. alignItems: 'center',
  71. shadowColor: '#000',
  72. shadowOffset: {
  73. width: 0,
  74. height: 7
  75. },
  76. shadowOpacity: 0.41,
  77. shadowRadius: 9.11
  78. //elevation: 14
  79. };
  80. const shadowShare = {
  81. shadowColor: '#000',
  82. shadowOffset: {
  83. width: 0,
  84. height: 7
  85. },
  86. shadowOpacity: 0.41,
  87. shadowRadius: 9.11,
  88. elevation: 14
  89. };
  90. const shadowImage = {
  91. shadowColor: '#000',
  92. shadowOffset: {
  93. width: 0,
  94. height: 2
  95. },
  96. shadowOpacity: 0.25,
  97. shadowRadius: 3.84,
  98. elevation: 5
  99. };
  100. function runSpring(clock, value, dest) {
  101. const state = {
  102. finished: new Value(0),
  103. velocity: new Value(0),
  104. position: new Value(0),
  105. time: new Value(0)
  106. };
  107. const config = {
  108. damping: 20,
  109. mass: 1,
  110. stiffness: 100,
  111. overshootClamping: false,
  112. restSpeedThreshold: 1,
  113. restDisplacementThreshold: 0.5,
  114. toValue: new Value(0)
  115. };
  116. return [
  117. cond(clockRunning(clock), 0, [
  118. set(state.finished, 0),
  119. set(state.velocity, 0),
  120. set(state.position, value),
  121. set(config.toValue, dest),
  122. startClock(clock)
  123. ]),
  124. spring(clock, state, config),
  125. cond(state.finished, stopClock(clock)),
  126. state.position
  127. ];
  128. }
  129. const onShare = async () => {
  130. try {
  131. const result = await Share.share({
  132. message: '페이스북 | https://facebook.com'
  133. });
  134. if (result.action === Share.sharedAction) {
  135. if (result.activityType) {
  136. // shared with activity type of result.activityType
  137. } else {
  138. // shared
  139. }
  140. } else if (result.action === Share.dismissedAction) {
  141. // dismissed
  142. }
  143. } catch (error) {
  144. alert(error.message);
  145. }
  146. };
  147. export default props => {
  148. const [modalVisible, setModalVisible] = useState(null);
  149. const { y: tabY } = props.tabPosition;
  150. const destination = tabY - minHeight - statusBarHeight;
  151. const [currentPlayerValue, setCurrentPlayerValue] = useState(0);
  152. const [isPlay, setPlay] = useState(false);
  153. const { playerData } = props;
  154. const {
  155. onGestureEvent,
  156. translateY,
  157. opacity,
  158. playContainerWidth,
  159. playerContainerHeight,
  160. videoWidth,
  161. videoHeight,
  162. playerControlOpaciy,
  163. playerBoderRadius,
  164. mainPlayerContainerHeight,
  165. PlayerHeadWidth,
  166. PlayerHeadHeight,
  167. PlayerHeadOpacity
  168. } = useMemo(() => {
  169. const translationY = new Value(0);
  170. const velocityY = new Value(0);
  171. const offsetY = new Value(0);
  172. const offsetY2 = new Value(0);
  173. const gestureState = new Value(State.UNDETERMINED);
  174. const clockY = new Clock();
  175. const finalTranslateY = add(translationY, multiply(0.2, velocityY));
  176. /**
  177. * snapPoint 제스처가 끝날 때 위치 지정을 해주는 값
  178. * TabBar 의 높이가 해상도와 스크린에서 그려지는 값이 틀려 TabBar 에서
  179. * onLayout 이벤트를 이용하여 TabBar의 y 값을 가져와 처리
  180. * destination 변수가 플레이어 축소 시 y 값 높이
  181. */
  182. const snapPoint = cond(
  183. lessThan(finalTranslateY, sub(offsetY, height / 4)),
  184. 0,
  185. destination
  186. );
  187. const ty = cond(
  188. and(eq(gestureState, State.END), neq(translationY, 0)),
  189. [
  190. set(
  191. translationY,
  192. runSpring(clockY, add(offsetY, translationY), snapPoint)
  193. ),
  194. set(offsetY, translationY),
  195. translationY
  196. ],
  197. [
  198. cond(eq(gestureState, State.BEGAN), stopClock(clockY)),
  199. add(offsetY, translationY)
  200. ]
  201. );
  202. const translateY = add(ty, offsetY2);
  203. // const onGestureEvent = event(
  204. // [
  205. // {
  206. // nativeEvent: {
  207. // translationY,
  208. // velocityY,
  209. // state: gestureState
  210. // }
  211. // }
  212. // ],
  213. // { useNativeDriver: true }
  214. // );
  215. return {
  216. translateY,
  217. onGestureEvent: event(
  218. [
  219. {
  220. nativeEvent: {
  221. translationY,
  222. velocityY,
  223. state: gestureState
  224. }
  225. }
  226. ],
  227. { useNativeDriver: true }
  228. ),
  229. opacity: interpolate(translateY, {
  230. inputRange: [0, midBound],
  231. outputRange: [1, 0],
  232. extrapolate: Extrapolate.CLAMP
  233. }),
  234. playContainerWidth: interpolate(translateY, {
  235. inputRange: [0, midBound],
  236. outputRange: [width, width - 16],
  237. extrapolate: Extrapolate.CLAMP
  238. }),
  239. playerContainerHeight: interpolate(translateY, {
  240. inputRange: [0, midBound],
  241. outputRange: [height, 0],
  242. extrapolate: Extrapolate.CLAMP
  243. }),
  244. videoWidth: interpolate(translateY, {
  245. inputRange: [0, midBound, upperBound],
  246. outputRange: [width, width - 16, width / 4],
  247. extrapolate: Extrapolate.CLAMP
  248. }),
  249. videoHeight: interpolate(translateY, {
  250. inputRange: [0, midBound, upperBound],
  251. outputRange: [width / 1.78, minHeight * 1.3, minHeight],
  252. extrapolate: Extrapolate.CLAMP
  253. }),
  254. playerControlOpaciy: interpolate(translateY, {
  255. inputRange: [midBound, upperBound],
  256. outputRange: [0, 1],
  257. extrapolate: Extrapolate.CLAMP
  258. }),
  259. playerBoderRadius: interpolate(translateY, {
  260. inputRange: [midBound, upperBound],
  261. outputRange: [0, 15],
  262. extrapolate: Extrapolate.CLAMP
  263. }),
  264. mainPlayerContainerHeight: interpolate(translateY, {
  265. inputRange: [0, midBound, upperBound],
  266. outputRange: [height, minHeight * 1.3, minHeight],
  267. extrapolate: Extrapolate.CLAMP
  268. }),
  269. PlayerHeadWidth: interpolate(translateY, {
  270. inputRange: [0, width],
  271. outputRange: [width, 0],
  272. extrapolate: Extrapolate.CLAMP
  273. }),
  274. PlayerHeadHeight: interpolate(translateY, {
  275. inputRange: [0, 50],
  276. outputRange: [50, 0],
  277. extrapolate: Extrapolate.CLAMP
  278. }),
  279. PlayerHeadOpacity: interpolate(translateY, {
  280. inputRange: [0, minHeight],
  281. outputRange: [1, 0],
  282. extrapolate: Extrapolate.CLAMP
  283. })
  284. };
  285. }, []);
  286. useEffect(() => {
  287. playerDispatch({type:'currentTime', setCurrentTime: 0});
  288. return () => {
  289. playerDispatch({type:'currentTime', setCurrentTime: 0});
  290. };
  291. },[playerData.soundURI]);
  292. const slideUp = () => {};
  293. const player = useRef(null);
  294. const playerDispatch = useContext(PlayerDispatchContext);
  295. const { uri, paused, muted } = useContext(PlayerStateContext);
  296. const playerOnBuffer = (data) => {
  297. // console.log('bufffer::: ' + data)
  298. }
  299. const playerOnError = (e) => {
  300. console.log(e);
  301. }
  302. const playerOnLoad = (data) => {
  303. // console.log(data.duration)
  304. let duration=0;
  305. data.duration < 0 ? duration=1 : duration=data.duration
  306. playerDispatch({type:'paused', setPaused:false});
  307. playerDispatch({type:'duration', setDuraton: Math.floor(duration)});
  308. }
  309. const playerOnProgress = (data) => {
  310. // console.log(data.currentTime)
  311. playerDispatch({type:'currentTime', setCurrentTime: data.currentTime});
  312. }
  313. const onLoadStart = () => {
  314. // console.log('onload start');
  315. playerDispatch({type:'player', player: player});
  316. }
  317. const playerOnEnd = () => {
  318. //console.log('end');
  319. }
  320. const onSeek = (data) => {
  321. //console.log(data)
  322. if(data.seekTime===0 && data.currentTime===0) {
  323. playerDispatch({type:'paused', setPaused: true});
  324. playerDispatch({type:'currentTime', setCurrentTime: 0});
  325. }
  326. }
  327. return (
  328. <>
  329. {/* <C.View
  330. style={{ ...StyleSheet.absoluteFillObject, zIndex: 1000, marginTop:getStatusBarHeight(true) }}
  331. cls="h20 bgc-rgba-white-0_5"
  332. >
  333. <C.Text cls="f5 ta-c bgc-rgba-red-0_5">테스트용 Component</C.Text>
  334. <C.View cls="h10 ai-c bgc-rgba-blue-0_5">
  335. <Slider
  336. style={{ width: '95%' }}
  337. trackStyle={{ height: 2 }}
  338. thumbTintColor="#333"
  339. thumbStyle={{ width: 10, height: 5 }}
  340. value={currentPlayerValue}
  341. onValueChange={value => setCurrentPlayerValue(value)}
  342. />
  343. <C.View cls="w95 jc-sb flx-row">
  344. <C.Text>Time : {currentPlayerValue}</C.Text>
  345. </C.View>
  346. </C.View>
  347. <C.TouchableOpacity
  348. cls="h5 bgc-rgba-black-0_5"
  349. onPress={() => {
  350. setPlay(!isPlay);
  351. }}
  352. >
  353. <C.Text cls="f5 ta-c rgba-white-0_5">Click Test</C.Text>
  354. </C.TouchableOpacity>
  355. </C.View> */}
  356. <PanGestureHandler
  357. onHandlerStateChange={onGestureEvent}
  358. activeOffsetY={[-10, 10]}
  359. onGestureEvent={onGestureEvent}
  360. >
  361. <Animated.View
  362. style={{
  363. ...StyleSheet.absoluteFillObject,
  364. zIndex: 10,
  365. marginTop: statusBarHeight,
  366. transform: [{ translateY }],
  367. alignItems: 'center',
  368. height: mainPlayerContainerHeight
  369. }}
  370. >
  371. <Animated.View
  372. style={{
  373. padding: 0,
  374. backgroundColor: 'white',
  375. width: playContainerWidth,
  376. borderRadius: playerBoderRadius
  377. }}
  378. >
  379. <Animated.View
  380. style={[
  381. Platform.OS === 'android'
  382. ? { borderColor: '#eee' }
  383. : { borderColor: 'white' },
  384. {
  385. ...StyleSheet.absoluteFillObject,
  386. borderWidth: 1,
  387. borderRadius: 15,
  388. opacity: playerControlOpaciy,
  389. backgroundColor: 'white'
  390. },
  391. shadow2
  392. ]}
  393. >
  394. <PlayerControls
  395. {...props}
  396. />
  397. </Animated.View>
  398. <Animated.View
  399. style={{
  400. opacity: PlayerHeadOpacity,
  401. backgroundColor: 'rgba(255,255,255,0)',
  402. width: PlayerHeadWidth,
  403. height: PlayerHeadHeight
  404. }}
  405. >
  406. <PlayerHeader {...props} visibleMenu={setModalVisible} />
  407. </Animated.View>
  408. <Animated.View style={{ width: videoWidth, height: videoHeight }}>
  409. <Animated.Image
  410. resizeMode="contain"
  411. style={{ flex: 1, margin: 10, width: null, height: null }}
  412. source={{uri: playerData.img}}
  413. />
  414. <Video
  415. source={{uri:playerData.soundURI}}
  416. style={StyleSheet.absoluteFill}
  417. ref={player}
  418. //rate={this.state.rate}
  419. paused={paused}
  420. //volume={this.state.volume}
  421. muted={muted}
  422. //ignoreSilentSwitch={this.state.ignoreSilentSwitch}
  423. resizeMode='contain'
  424. onLoad={playerOnLoad}
  425. onLoadStart={onLoadStart}
  426. onBuffer={playerOnBuffer}
  427. onProgress={playerOnProgress}
  428. onSeek={onSeek}
  429. onError={playerOnError}
  430. onEnd={playerOnEnd}
  431. //poster='https://baconmockup.com/300/200/'
  432. repeat={true}
  433. //filter={this.state.filter}
  434. //filterEnabled={this.state.filterEnabled}
  435. audioOnly={true}
  436. />
  437. </Animated.View>
  438. </Animated.View>
  439. <Animated.View
  440. style={{
  441. backgroundColor: 'white',
  442. width: playContainerWidth,
  443. height: playerContainerHeight
  444. }}
  445. >
  446. <Animated.View style={{ opacity }}>
  447. <PlayerContents
  448. {...props}
  449. />
  450. </Animated.View>
  451. </Animated.View>
  452. </Animated.View>
  453. </PanGestureHandler>
  454. <Modal
  455. isVisible={modalVisible === 'detail'}
  456. onSwipeComplete={() => {
  457. setModalVisible(null);
  458. }}
  459. swipeDirection="left"
  460. //deviceHeight={height}
  461. deviceWidth={width}
  462. style={{ margin: 0 }}
  463. backdropColor="white"
  464. backdropOpacity={1}
  465. animationIn="slideInLeft"
  466. animationOut="slideOutLeft"
  467. >
  468. <C.View cls="w90 h100 bgc-rgba-black-0_5">
  469. <ScrollView style={{ flex: 1, marginTop: '10%' }}>
  470. <C.Text cls="flx1 fw-b ta-c">{playerData.title}</C.Text>
  471. <C.View cls="flx2 ph5 mt5">
  472. <C.Text cls="fw-b">{playerData.title}</C.Text>
  473. <C.Text cls="mt2">{playerData.content}</C.Text>
  474. <C.Text cls="mt3" numberOfLines={1}>
  475. {/* by {playerData.author.replace(/\r\n|\n|\r/gm, ' ')} */}
  476. </C.Text>
  477. <C.Text cls="mt5 as-e" numberOfLines={1}>
  478. {playerData.date}
  479. </C.Text>
  480. </C.View>
  481. </ScrollView>
  482. </C.View>
  483. <TouchableOpacity
  484. style={[
  485. StyleSheet.absoluteFillObject,
  486. {
  487. width: '90%',
  488. justifyContent: 'center',
  489. paddingRight: '5%',
  490. alignItems: 'flex-end'
  491. }
  492. ]}
  493. onPress={() => {
  494. setModalVisible(null);
  495. }}
  496. >
  497. <C.EL.Icon
  498. color="rgb(10,132,255)"
  499. {...C.n2cls('size10')}
  500. type="ionicon"
  501. name="md-arrow-dropleft"
  502. />
  503. </TouchableOpacity>
  504. </Modal>
  505. <Modal isVisible={modalVisible === 'share'} style={{ margin: 0 }}>
  506. <View
  507. style={{
  508. flex: 1,
  509. justifyContent: 'center',
  510. alignItems: 'center',
  511. backgroundColor: 'rgb(242,242,247)'
  512. }}
  513. >
  514. <ScrollView>
  515. <View style={{ height: statusBarHeight }} />
  516. <View style={{ marginHorizontal: '5%', alignItems: 'flex-end' }}>
  517. <C.EL.Icon
  518. color="black"
  519. {...C.n2cls('size10')}
  520. type="ionicon"
  521. name="md-close"
  522. onPress={() => {
  523. setModalVisible(null);
  524. }}
  525. />
  526. </View>
  527. <View
  528. style={{
  529. //...shadowImage,
  530. //backgroundColor: 'white',
  531. marginHorizontal: '5%',
  532. width: width * 0.8,
  533. height: (width * 0.8) / 1.78
  534. }}
  535. >
  536. <Image
  537. resizeMode="contain"
  538. style={{
  539. flex: 1,
  540. marginVertical: 10,
  541. width: null,
  542. height: null
  543. }}
  544. source={{uri:playerData.img}}
  545. />
  546. </View>
  547. <C.View cls="flx-row jc-sa ai-t mt2">
  548. <C.Text cls="fw-b">{playerData.title}</C.Text>
  549. <C.Text cls="fw-b">{playerData.duration}</C.Text>
  550. </C.View>
  551. <View style={{ height: minHeight }} />
  552. <TouchableOpacity
  553. onPress={onShare}
  554. style={{ marginHorizontal: '5%', alignItems: 'center' }}
  555. >
  556. <View style={[s.sharContainer]}>
  557. <C.EL.Icon
  558. color="black"
  559. {...C.n2cls('size10')}
  560. type="ionicon"
  561. name="md-share"
  562. />
  563. <C.Text cls="f2.5">공유하기, 메세지 보내기</C.Text>
  564. </View>
  565. </TouchableOpacity>
  566. <View style={{ height: minHeight }} />
  567. </ScrollView>
  568. </View>
  569. </Modal>
  570. </>
  571. );
  572. };
  573. const s = StyleSheet.create({
  574. sharContainer: {
  575. backgroundColor: 'white',
  576. flexDirection: 'row',
  577. justifyContent: 'space-around',
  578. alignItems: 'center',
  579. borderColor: '#ddd',
  580. borderRadius: 15,
  581. flex: 1,
  582. width: width * 0.8,
  583. height: minHeight,
  584. ...shadowShare
  585. }
  586. });