Browse Source

Player 상단 메뉴 추가 및 상세보기, 공유하기 화면 추가

goodboy 6 years ago
parent
commit
07bcb18687

+ 515 - 0
src/components/Player.js

@@ -0,0 +1,515 @@
+import React, { useState, useEffect, useRef, useMemo } from 'react';
+import {
+  View,
+  Text,
+  StyleSheet,
+  Dimensions,
+  PanResponder,
+  ScrollView,
+  Image,
+  Platform,
+  StatusBar,
+  TouchableOpacity,
+  Share
+} from 'react-native';
+import Modal from 'react-native-modal';
+
+import { Slider, Icon } from 'react-native-elements';
+import Animated, { Easing } from 'react-native-reanimated';
+import { State, PanGestureHandler } from 'react-native-gesture-handler';
+import { getStatusBarHeight } from '../utils/StatusBarHeight';
+import C from 'rn-class';
+import PlayerControls from './PlayerControls';
+import PlayerContents from './PlayerContents';
+import PlayerHeader from './PlayerHeader';
+import { getInset } from 'react-native-safe-area-view';
+
+const { width, height } = Dimensions.get('window');
+const statusBarHeight = getStatusBarHeight(true);
+const minHeight = height * 0.1;
+const midBound = height - height / 2.5;
+const upperBound = height * 0.75;
+//const destination = height - height * 0.2
+//const destination = height - height*0.16 -2 - getStatusBarHeight(true);
+//getInset('bottom')
+// height > 560
+// ?height * 0.75 - getInset('bottom')
+// :height * 0.70;
+
+const {
+  Value,
+  event,
+  Extrapolate,
+  cond,
+  Clock,
+  eq,
+  set,
+  add,
+  sub,
+  multiply,
+  lessThan,
+  clockRunning,
+  startClock,
+  spring,
+  stopClock,
+  interpolate,
+  timing,
+  neq,
+  or,
+  and,
+  greaterThan
+} = Animated;
+
+const shadow = {
+  alignItems: 'center',
+  shadowColor: 'black',
+  shadowOffset: { width: 0, height: 0 },
+  shadowOpacity: 0.18,
+  shadowRadius: 2
+};
+const shadow2 = {
+  alignItems: 'center',
+  shadowColor: '#000',
+  shadowOffset: {
+    width: 0,
+    height: 7
+  },
+  shadowOpacity: 0.41,
+  shadowRadius: 9.11
+
+  //elevation: 14
+};
+
+const shadowShare = {
+  shadowColor: '#000',
+  shadowOffset: {
+    width: 0,
+    height: 7
+  },
+  shadowOpacity: 0.41,
+  shadowRadius: 9.11,
+  elevation: 14
+};
+
+const shadowImage = {
+  shadowColor: '#000',
+  shadowOffset: {
+    width: 0,
+    height: 2
+  },
+  shadowOpacity: 0.25,
+  shadowRadius: 3.84,
+
+  elevation: 5
+};
+
+function runSpring(clock, value, dest) {
+  const state = {
+    finished: new Value(0),
+    velocity: new Value(0),
+    position: new Value(0),
+    time: new Value(0)
+  };
+
+  const config = {
+    damping: 20,
+    mass: 1,
+    stiffness: 100,
+    overshootClamping: false,
+    restSpeedThreshold: 1,
+    restDisplacementThreshold: 0.5,
+    toValue: new Value(0)
+  };
+
+  return [
+    cond(clockRunning(clock), 0, [
+      set(state.finished, 0),
+      set(state.velocity, 0),
+      set(state.position, value),
+      set(config.toValue, dest),
+      startClock(clock)
+    ]),
+    spring(clock, state, config),
+    cond(state.finished, stopClock(clock)),
+    state.position
+  ];
+}
+
+const onShare = async () => {
+  try {
+    const result = await Share.share({
+      message: '페이스북 | https://facebook.com'
+    });
+
+    if (result.action === Share.sharedAction) {
+      if (result.activityType) {
+        // shared with activity type of result.activityType
+      } else {
+        // shared
+      }
+    } else if (result.action === Share.dismissedAction) {
+      // dismissed
+    }
+  } catch (error) {
+    alert(error.message);
+  }
+};
+
+export default props => {
+  const [modalVisible, setModalVisible] = useState(null);
+  const { y: tabY } = props.Pages.tabPosition;
+  const destination = tabY - minHeight - getStatusBarHeight(true);
+  const [currentPlayerValue, setCurrentPlayerValue] = useState(0);
+  const [isPlay, setPlay] = useState(false);
+  const { onGestureEvent, translateY } = useMemo(() => {
+    const translationY = new Value(0);
+    const velocityY = new Value(0);
+    const offsetY = new Value(0);
+    const offsetY2 = new Value(0);
+    const gestureState = new Value(State.UNDETERMINED);
+
+    const clockY = new Clock();
+    const finalTranslateY = add(translationY, multiply(0.2, velocityY));
+    /**
+     *  snapPoint 제스처가 끝날 때 위치 지정을 해주는 값
+     *  TabBar 의 높이가 해상도와 스크린에서 그려지는 값이 틀려 TabBar 에서
+     *  onLayout 이벤트를 이용하여 TabBar의 y 값을 가져와 처리
+     *  destination 변수가 플레이어 축소 시 y 값 높이
+     */
+    const snapPoint = cond(
+      lessThan(finalTranslateY, sub(offsetY, height / 4)),
+      0,
+      destination
+    );
+    const ty = cond(
+      and(eq(gestureState, State.END), neq(translationY, 0)),
+      [
+        set(
+          translationY,
+          runSpring(clockY, add(offsetY, translationY), snapPoint)
+        ),
+        set(offsetY, translationY),
+        translationY
+      ],
+      [
+        cond(eq(gestureState, State.BEGAN), stopClock(clockY)),
+        add(offsetY, translationY)
+      ]
+    );
+
+    // const translateY = add(ty, offsetY2);
+    // const onGestureEvent = event(
+    //   [
+    //     {
+    //       nativeEvent: {
+    //         translationY,
+    //         velocityY,
+    //         state: gestureState
+    //       }
+    //     }
+    //   ],
+    //   { useNativeDriver: true }
+    // );
+
+    return {
+      translateY: add(ty, offsetY2),
+      onGestureEvent: event(
+        [
+          {
+            nativeEvent: {
+              translationY,
+              velocityY,
+              state: gestureState
+            }
+          }
+        ],
+        { useNativeDriver: true }
+      )
+    };
+  }, []);
+
+  const opacity = interpolate(translateY, {
+    inputRange: [0, midBound - 100],
+    outputRange: [1, 0],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const statusBarOpacity = interpolate(translateY, {
+    inputRange: [0, statusBarHeight],
+    outputRange: [1, 0],
+    extrapolateLeft: Extrapolate.CLAMP
+  });
+  const playContainerWidth = interpolate(translateY, {
+    inputRange: [0, midBound],
+    outputRange: [width, width - 16],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const playerContainerHeight = interpolate(translateY, {
+    inputRange: [0, midBound],
+    outputRange: [height, 0],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const videoWidth = interpolate(translateY, {
+    inputRange: [0, midBound, upperBound],
+    outputRange: [width, width - 16, width / 4],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const videoHeight = interpolate(translateY, {
+    inputRange: [0, midBound, upperBound],
+    outputRange: [width / 1.78, minHeight * 1.3, minHeight],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const playerControlOpaciy = interpolate(translateY, {
+    inputRange: [midBound, upperBound],
+    outputRange: [0, 1],
+    extrapolate: Extrapolate.CLAMP
+  });
+
+  const playerBoderRadius = interpolate(translateY, {
+    inputRange: [midBound, upperBound],
+    outputRange: [0, 15],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const mainPlayerContainerHeight = interpolate(translateY, {
+    inputRange: [0, midBound, upperBound],
+    outputRange: [height, minHeight * 1.3, minHeight],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const PlayerHeadWidth = interpolate(translateY, {
+    inputRange: [0, width],
+    outputRange: [width, 0],
+    extrapolate: Extrapolate.CLAMP
+  });
+  const PlayerHeadHeight = interpolate(translateY, {
+    inputRange: [0, 50],
+    outputRange: [50, 0],
+    extrapolate: Extrapolate.CLAMP
+  });
+  useEffect(() => {
+    return () => {};
+  });
+
+  const slideUp = () => {};
+
+  return (
+    <>
+      <PanGestureHandler
+        onHandlerStateChange={onGestureEvent}
+        activeOffsetY={[-10, 10]}
+        onGestureEvent={onGestureEvent}
+      >
+        <Animated.View
+          style={{
+            ...StyleSheet.absoluteFillObject,
+            zIndex: 10,
+            marginTop: statusBarHeight,
+            transform: [{ translateY }],
+            alignItems: 'center',
+            height: mainPlayerContainerHeight
+          }}
+        >
+          <Animated.View
+            style={{
+              padding: 0,
+              backgroundColor: 'white',
+              width: playContainerWidth,
+              borderRadius: playerBoderRadius
+            }}
+          >
+            <Animated.View
+              style={[
+                Platform.OS === 'android'
+                  ? { borderColor: '#eee' }
+                  : { borderColor: 'white' },
+                {
+                  ...StyleSheet.absoluteFillObject,
+                  borderWidth: 1,
+                  borderRadius: 15,
+                  opacity: playerControlOpaciy,
+                  backgroundColor: 'white'
+                },
+                shadow2
+              ]}
+            >
+              <PlayerControls
+                {...props}
+                currentPlayerValue={currentPlayerValue}
+                setCurrentPlayerValue={setCurrentPlayerValue}
+              />
+            </Animated.View>
+            <Animated.View
+              style={{
+                opacity,
+                backgroundColor: 'rgba(255,255,255,0)',
+                width: PlayerHeadWidth,
+                height: PlayerHeadHeight
+              }}
+            >
+              <PlayerHeader
+                {...props}
+                visibleMenu={setModalVisible}
+              />
+            </Animated.View>
+            <Animated.View style={{ width: videoWidth, height: videoHeight }}>
+              <Animated.Image
+                resizeMode="contain"
+                style={{ flex: 1, margin: 10, width: null, height: null }}
+                source={props.Pages.img}
+              />
+            </Animated.View>
+          </Animated.View>
+          <Animated.View
+            style={{
+              backgroundColor: 'white',
+              width: playContainerWidth,
+              height: playerContainerHeight
+            }}
+          >
+            <Animated.View style={{ opacity }}>
+              <PlayerContents 
+                {...props}
+                currentPlayerValue={currentPlayerValue}
+                setCurrentPlayerValue={setCurrentPlayerValue}
+              />
+            </Animated.View>
+          </Animated.View>
+        </Animated.View>
+      </PanGestureHandler>
+      <Modal
+        isVisible={modalVisible === 'detail'}
+        onSwipeComplete={() => {
+          setModalVisible(null);
+        }}
+        swipeDirection="left"
+        //deviceHeight={height}
+        deviceWidth={width}
+        style={{ margin: 0 }}
+        backdropColor="white"
+        backdropOpacity={1}
+        animationIn="slideInLeft"
+        animationOut="slideOutLeft"
+      >
+        <C.View cls="w90 h100 bgc-rgba-black-0_5">
+          <ScrollView style={{ flex: 1, marginTop: '10%' }}>
+            <C.Text cls="flx1 ta-c">{props.Pages.subtitle}</C.Text>
+            <C.View cls="flx2 ph5 mt5">
+              <C.Text cls="fw-b">{props.Pages.subtitle}</C.Text>
+              <C.Text cls="mt2">{props.Pages.contens}</C.Text>
+              <C.Text cls="mt3" numberOfLines={1}>
+                by {props.Pages.subtitle.replace(/\r\n|\n|\r/gm, ' ')}
+              </C.Text>
+              <C.Text cls="mt5 as-e" numberOfLines={1}>
+                March 19
+              </C.Text>
+            </C.View>
+          </ScrollView>
+        </C.View>
+        <TouchableOpacity
+          style={[
+            StyleSheet.absoluteFillObject,
+            {
+              width: '90%',
+              justifyContent: 'center',
+              paddingRight: '5%',
+              alignItems: 'flex-end'
+            }
+          ]}
+          onPress={() => {
+            setModalVisible(!modalVisible);
+          }}
+        >
+          <C.EL.Icon
+            color="rgb(10,132,255)"
+            {...C.n2cls('size10')}
+            type="ionicon"
+            name="md-arrow-dropleft"
+          />
+        </TouchableOpacity>
+      </Modal>
+
+      <Modal isVisible={modalVisible === 'share'} style={{ margin: 0 }}>
+        <View
+          style={{
+            flex: 1,
+            justifyContent: 'center',
+            alignItems: 'center',
+            backgroundColor: 'rgb(242,242,247)'
+          }}
+        >
+          <ScrollView>
+            <View style={{ height: getStatusBarHeight(true) }} />
+            <View style={{ marginHorizontal: '5%', alignItems: 'flex-end' }}>
+              <C.EL.Icon
+                color="black"
+                {...C.n2cls('size10')}
+                type="ionicon"
+                name="md-close"
+                onPress={() => {
+                  setModalVisible(null);
+                }}
+              />
+            </View>
+
+            <View
+              style={{
+                //...shadowImage,
+                //backgroundColor: 'white',
+                marginHorizontal: '5%',
+                width: width * 0.8,
+                height: (width * 0.8) / 1.78
+              }}
+            >
+              <Image
+                resizeMode="contain"
+                style={{
+                  flex: 1,
+                  marginVertical: 10,
+                  width: null,
+                  height: null
+                }}
+                source={props.Pages.img}
+              />
+            </View>
+            <C.View cls="flx-row jc-sa ai-t mt2">
+              <C.Text cls="fw-b">{'Spring of Music'}</C.Text>
+              <C.Text cls="fw-b">{`18min`}</C.Text>
+            </C.View>
+
+            <View style={{ height: minHeight }} />
+
+            <TouchableOpacity
+              onPress={onShare}
+              style={{ marginHorizontal: '5%', alignItems: 'center' }}
+            >
+              <View style={[s.sharContainer]}>
+                <C.EL.Icon
+                  color="black"
+                  {...C.n2cls('size10')}
+                  type="ionicon"
+                  name="md-share"
+                />
+                <C.Text cls="f2.5">공유하기, 메세지 보내기</C.Text>
+              </View>
+            </TouchableOpacity>
+
+            <View style={{ height: minHeight }} />
+          </ScrollView>
+        </View>
+      </Modal>
+    </>
+  );
+};
+
+const s = StyleSheet.create({
+  sharContainer: {
+    backgroundColor: 'white',
+    flexDirection: 'row',
+    justifyContent: 'space-around',
+    alignItems: 'center',
+    borderColor: '#ddd',
+    borderRadius: 15,
+    flex: 1,
+    width: width * 0.8,
+    height: minHeight,
+    ...shadowShare
+  }
+});

+ 30 - 12
src/components/PlayerContents.js

@@ -1,12 +1,15 @@
 import React, { useState } from 'react';
-import { ScrollView } from 'react-native';
+import { ScrollView, Text } from 'react-native';
 import { Icon, Slider } from 'react-native-elements';
 import C from 'rn-class';
 
-const PlayerContents = props => {
+export default props => {
   const { Pages } = props;
-  const [curPlayIdx, setCurPlayIdx] = useState(0);
-
+  const [curPlayerValue, setCurPlayerValue] = useState(0);
+  const [isPlay, setPlay] = useState(false);
+  const onSetPlay = () => {
+    setPlay(!isPlay);
+  };
   return (
     <ScrollView>
       <C.Text cls="flx1 ta-c">{Pages.subtitle}</C.Text>
@@ -15,22 +18,37 @@ const PlayerContents = props => {
         <C.Text cls="mt2" numberOfLines={10}>
           {Pages.contens}
         </C.Text>
+        <C.Text cls="mt3" numberOfLines={1}>
+          by {Pages.subtitle.replace(/\r\n|\n|\r/gm, ' ')}
+        </C.Text>
+        <C.Text cls="mt5 as-e" numberOfLines={1}>
+          March 19
+        </C.Text>
       </C.View>
       <C.View cls="flx1 flx-row jc-sa ai-c mt5">
-        <C.EL.Icon type="ionicon" name="md-rewind" />
-        <C.EL.Icon type="ionicon" name="md-pause" />
-        <C.EL.Icon type="ionicon" name="md-fastforward" />
+        <C.EL.Icon {...C.n2cls('size8')} type="ionicon" name="md-rewind" />
+        { isPlay
+          ?<C.EL.Icon {...C.n2cls('size10')} type="ionicon" name="md-pause" onPress={onSetPlay} />
+          :<C.EL.Icon {...C.n2cls('size10')} type="ionicon" name="md-play" onPress={onSetPlay} />
+        }
+        <C.EL.Icon {...C.n2cls('size8')} type="ionicon" name="md-fastforward" />
+        <C.EL.Icon {...C.n2cls('size10')} type="ionicon" name="md-square" />
       </C.View>
       <C.View cls="flx1 ai-c mt2">
         <Slider
-          style={{ width: '80%' }}
+          style={{ width: '95%' }}
+          trackStyle={{height:2}}
           thumbTintColor="#333"
-          value={curPlayIdx}
-          onValueChange={value => setCurPlayIdx(value)}
+          thumbStyle={{ width: 10, height: 5 }}
+          value={curPlayerValue}
+          onValueChange={value => setCurPlayerValue(value)}
         />
+        <C.View cls="w95 jc-sb flx-row">
+          <C.Text>Time : {curPlayerValue}</C.Text>
+          <C.Text>{`2:50`}</C.Text>
+        </C.View>
       </C.View>
+      <C.View cls="h50" />
     </ScrollView>
   );
 };
-
-export default PlayerContents;

+ 36 - 27
src/components/PlayerControls.js

@@ -4,7 +4,8 @@ import {
   StyleSheet,
   Text,
   Dimensions,
-  TouchableWithoutFeedback
+  TouchableWithoutFeedback,
+  Platform
 } from 'react-native';
 import { Icon, Slider } from 'react-native-elements';
 import C from 'rn-class';
@@ -13,30 +14,50 @@ const { width } = Dimensions.get('window');
 
 export const PLACEHOLDER_WIDTH = width / 4;
 
-const setPlayer = () => {};
-
-const PlayerControls = props => {
-  const { title, closePlayer, slideup } = props;
-  const [curPlayIdx, setCurPlayIdx] = useState(0);
-
+export default PlayerControls = props => {
+  const { Pages, closePlayer } = props;
+  const [curPlayerValue, setCurPlayerValue] = useState(0);
+  const [isPlay, setPlay] = useState(false);
+  const onSetPlay = () => {
+    setPlay(!isPlay);
+  }
+  const getThumbTopFromStyleHeight = (styleHeight=40, trackHeight=4) => {
+    const thumbTop = (styleHeight / 2) + (trackHeight / 2) + 
+    (Platform.OS==='android'&&(styleHeight<40||trackHeight<4)?0.5:0);
+    return {
+      sh: {height:styleHeight},
+      th: {height:trackHeight},
+      tt: {top:thumbTop}
+    }
+  }
+  const sliderLayout = getThumbTopFromStyleHeight(20,2);
   return (
-    <TouchableWithoutFeedback onPress={slideup}>
+    <>
       <View style={styles.container}>
         <View style={styles.placeholder} />
         <C.View cls="flx2">
-          <Text numerOfLine={1}>{title}</Text>
+          <C.View cls="flx-row jc-sb">
+            <Text numerOfLine={1}>{Pages.subtitle.replace(/\r\n|\n|\r/gm,' ')}</Text>
+            <Text>{`2:50`}</Text>
+          </C.View>
           <Slider
+            style={sliderLayout.sh}
+            trackStyle={sliderLayout.th}
             thumbTintColor="#333"
-            value={curPlayIdx}
-            onValueChange={value => setCurPlayIdx(value)}
+            thumbStyle={{width:10, height:5, ...sliderLayout.tt}}
+            value={curPlayerValue}
+            onValueChange={value => setCurPlayerValue(value)}
           />
         </C.View>
-        <C.View cls="flx1 jc-sa ai-sa flx-row">
-          <C.EL.Icon type="entypo" name="controller-play" />
-          <C.EL.Icon type="entypo" name="cross" onPress={closePlayer} />
+        <C.View cls="flx0.5 jc-sa ai-sa flx-row">
+          { isPlay
+            ?<C.EL.Icon {...C.n2cls("size8")} type="ionicon" name="md-pause" onPress={onSetPlay} />
+            :<C.EL.Icon {...C.n2cls("size8")} type="ionicon" name="md-play" onPress={onSetPlay} />
+          }
         </C.View>
+        <C.EL.Icon ccls="mr2" type="ionicon" name="md-close" onPress={closePlayer} />
       </View>
-    </TouchableWithoutFeedback>
+    </>
   );
 };
 
@@ -47,19 +68,7 @@ const styles = StyleSheet.create({
     justifyContent: 'space-around',
     alignItems: 'center'
   },
-  title: {
-    flex: 2,
-    flexWrap: 'wrap',
-    paddingLeft: 8
-  },
   placeholder: {
     width: PLACEHOLDER_WIDTH
-  },
-  icon: {
-    fontSize: 24,
-    color: 'gray',
-    padding: 8
   }
 });
-
-export default PlayerControls;

+ 46 - 0
src/components/PlayerHeader.js

@@ -0,0 +1,46 @@
+import React, { useState } from 'react';
+import {
+  View,
+  StyleSheet
+} from 'react-native';
+import { Icon, Slider } from 'react-native-elements';
+import C from 'rn-class';
+
+export default PlayerHead = props => {
+  const { closePlayer, visibleMenu} = props;
+  const [isMute, setIsMute] = useState(false);
+
+  const setMute = () => {
+    setIsMute(!isMute);
+  }
+  const iconsize = {...C.n2cls('size7')};
+
+  return (
+    <View style={styles.container}>
+      <C.View cls="flx-row flx0.3 ai-s jc-sa">
+        <C.EL.Icon {...iconsize} type="ionicon" name="md-book" onPress={()=>{visibleMenu('detail')}} />
+        <C.EL.Icon {...iconsize} type="ionicon" name="md-share" onPress={()=>{visibleMenu('share')}} />
+        <C.EL.Icon {...iconsize} color="rgb(255,45,85)" type="ionicon" name="md-heart" />
+      </C.View>
+      <C.View cls="flx-row flx0.3 ai-e jc-sa">
+        { isMute
+          ?<C.EL.Icon {...iconsize} type="ionicon" name="md-volume-off" onPress={setMute} />
+          :<C.EL.Icon {...iconsize} type="ionicon" name="md-volume-high" onPress={setMute} />
+        }
+        <C.EL.Icon {...iconsize} type="ionicon" name="md-arrow-down" />
+        <C.EL.Icon {...iconsize} type="ionicon" name="md-close" onPress={closePlayer} />
+      </C.View>
+    </View>
+  );
+}
+
+const styles = StyleSheet.create({
+    container: {
+      ...StyleSheet.absoluteFillObject,
+      flexDirection: 'row',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      zIndex: 20
+    },
+  });
+