-
Notifications
You must be signed in to change notification settings - Fork 0
Rewind feature #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import React from 'react' | ||
| import { StyleSheet } from 'react-native' | ||
| import Animated, { | ||
| Extrapolate, | ||
| interpolate, | ||
| SharedValue, | ||
| useAnimatedStyle, | ||
| useSharedValue, | ||
| } from 'react-native-reanimated' | ||
|
|
||
| interface ITriangle { | ||
| triangleRotation: 'left' | 'right' | ||
| animationProgress: SharedValue<number> | ||
| index: number | ||
| delta: number | ||
| numOfTriangles: number | ||
| } | ||
|
|
||
| export const Triangle = ({ | ||
| triangleRotation, | ||
| animationProgress, | ||
| index, | ||
| delta, | ||
| numOfTriangles, | ||
| }: ITriangle) => { | ||
| const animatedStyle = useAnimatedStyle(() => { | ||
| return { | ||
| opacity: interpolate( | ||
| animationProgress.value, | ||
| triangleRotation === 'left' | ||
| ? [delta * index, delta * (index + 1)] | ||
| : [ | ||
| delta * Math.abs(numOfTriangles - 1 - index), | ||
| delta * Math.abs(numOfTriangles - index), | ||
| ], | ||
| [0, 0.7], | ||
| Extrapolate.CLAMP | ||
| ), | ||
| } | ||
| }) | ||
|
|
||
| return ( | ||
| <> | ||
| <Animated.View | ||
| style={[ | ||
| animatedStyle, | ||
| styles.triangle, | ||
| triangleRotation === 'right' | ||
| ? styles.triangleRight | ||
| : styles.triangleLeft, | ||
| ]} | ||
| /> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| triangle: { | ||
| width: 0, | ||
| height: 0, | ||
| backgroundColor: 'transparent', | ||
| borderStyle: 'solid', | ||
| borderLeftWidth: 10, | ||
| borderRightWidth: 10, | ||
| borderBottomWidth: 20, | ||
| borderLeftColor: 'transparent', | ||
| borderRightColor: 'transparent', | ||
| borderBottomColor: 'white', | ||
| }, | ||
| triangleLeft: { | ||
| transform: [{ rotate: '-90deg' }], | ||
| }, | ||
| triangleRight: { transform: [{ rotate: '90deg' }] }, | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { Triangle } from './Triangle' | ||
|
|
||
| export default Triangle |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,160 @@ | ||
| import React, { useRef } from 'react' | ||
| import { Dimensions, StyleSheet, View } from 'react-native' | ||
| import React, { useRef, useState } from 'react' | ||
| import { | ||
| Dimensions, | ||
| Platform, | ||
| SafeAreaView, | ||
| StyleSheet, | ||
| View, | ||
| } from 'react-native' | ||
| import { | ||
| GestureHandlerRootView, | ||
| State, | ||
| TapGestureHandler, | ||
| TapGestureHandlerStateChangeEvent, | ||
| } from 'react-native-gesture-handler' | ||
| import { useSharedValue, withSpring } from 'react-native-reanimated' | ||
| import Video from 'react-native-video' | ||
| import Triangle from '../Trinagle' | ||
|
|
||
| const { width: screenWidth } = Dimensions.get('screen') | ||
|
|
||
| const videoWidth = screenWidth * 0.9 | ||
| const videoHeight = 200 | ||
| const rewindingViewWidth = videoWidth / 4.5 | ||
| const rewindingViewHeight = videoHeight / 2 | ||
| const rewindingViewHeightMargin = rewindingViewHeight / 2 | ||
| const numOfSeconds = 15 | ||
|
|
||
| export const VideoPlayer = ({ uri }: { uri: string }) => { | ||
| const leftTriangles = [0, 1, 2] | ||
| const rightTriangles = [0, 1, 2] | ||
| const animationProgressLeft = useSharedValue(0) | ||
| const animationProgressRight = useSharedValue(0) | ||
| const numOfTriangles = 3 | ||
| const delta = 1 / numOfTriangles | ||
|
|
||
| const playerRef = useRef<Video | null>(null) | ||
| const doubleTapRef = useRef<TapGestureHandler>(null) | ||
| const [progress, setProgress] = useState(0) | ||
|
|
||
| const rewindLeft = () => { | ||
| playerRef?.current?.seek(progress - numOfSeconds) | ||
| animationProgressLeft.value = withSpring(1, undefined, (isFinished) => { | ||
| if (isFinished) { | ||
| animationProgressLeft.value = withSpring(0) | ||
| } | ||
| }) | ||
| } | ||
| const rewindRight = () => { | ||
| playerRef?.current?.seek(progress + numOfSeconds) | ||
| animationProgressRight.value = withSpring(1, undefined, (isFinished) => { | ||
| if (isFinished) { | ||
| animationProgressRight.value = withSpring(0) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const rewindOnDoubleTap = (e: TapGestureHandlerStateChangeEvent) => { | ||
| if ( | ||
| e.nativeEvent.state === State.ACTIVE && | ||
| e.nativeEvent.y < rewindingViewHeight + rewindingViewHeightMargin && | ||
| e.nativeEvent.y > rewindingViewHeightMargin | ||
| ) { | ||
| if (e.nativeEvent.x < rewindingViewWidth) rewindLeft() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please, use next styling
|
||
| if (e.nativeEvent.x > videoWidth - rewindingViewWidth) rewindRight() | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Video | ||
| ref={playerRef} | ||
| source={{ uri: uri }} | ||
| onBuffer={() => console.log(1)} | ||
| onError={() => console.log(1)} | ||
| style={styles.player} | ||
| repeat | ||
| controls | ||
| rate={1} | ||
| /> | ||
| </View> | ||
| <SafeAreaView style={styles.container}> | ||
| <GestureHandlerRootView style={{ height: '100%', width: '100%' }}> | ||
| <TapGestureHandler | ||
| ref={doubleTapRef} | ||
| onHandlerStateChange={rewindOnDoubleTap} | ||
| numberOfTaps={2} | ||
| > | ||
| <View> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. View - is necessary? |
||
| <Video | ||
| muted | ||
| ref={playerRef} | ||
| source={{ uri: uri }} | ||
| onBuffer={() => console.log(1)} | ||
| onError={() => console.log(1)} | ||
| style={styles.fullFilling} | ||
| repeat | ||
| controls | ||
| onProgress={(value) => setProgress(value.currentTime)} | ||
| rate={1} | ||
| /> | ||
| {Platform.OS === 'ios' && ( | ||
| <> | ||
| <View style={styles.leftRewindingView}> | ||
| {leftTriangles.map((item, index) => { | ||
| return ( | ||
| <Triangle | ||
| key={item} | ||
| triangleRotation="left" | ||
| animationProgress={animationProgressLeft} | ||
| index={index} | ||
| delta={delta} | ||
| numOfTriangles={numOfTriangles} | ||
| /> | ||
| ) | ||
| })} | ||
| </View> | ||
| <View style={styles.rightRewindingView}> | ||
| {rightTriangles.map((item, index) => { | ||
| return ( | ||
| <Triangle | ||
| key={item} | ||
| triangleRotation="right" | ||
| animationProgress={animationProgressRight} | ||
| index={index} | ||
| delta={delta} | ||
| numOfTriangles={numOfTriangles} | ||
| /> | ||
| ) | ||
| })} | ||
| </View> | ||
| </> | ||
| )} | ||
| </View> | ||
| </TapGestureHandler> | ||
| </GestureHandlerRootView> | ||
| </SafeAreaView> | ||
| ) | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| width: screenWidth * 0.9, | ||
| height: 200, | ||
| width: videoWidth, | ||
| height: videoHeight, | ||
| justifyContent: 'center', | ||
| alignItems: 'center', | ||
| alignSelf: 'center', | ||
| }, | ||
| player: { | ||
| fullFilling: { | ||
| width: '100%', | ||
| height: '100%', | ||
| }, | ||
| leftRewindingView: { | ||
| width: rewindingViewWidth, | ||
| height: rewindingViewHeight, | ||
| position: 'absolute', | ||
| left: 0, | ||
| top: rewindingViewHeightMargin, | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| rightRewindingView: { | ||
| width: rewindingViewWidth, | ||
| height: rewindingViewHeight, | ||
| position: 'absolute', | ||
| right: 0, | ||
| top: rewindingViewHeightMargin, | ||
| flexDirection: 'row', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,12 @@ | ||
| import { NativeStackScreenProps } from '@react-navigation/native-stack' | ||
| import React from 'react' | ||
| import { | ||
| Dimensions, | ||
| FlatList, | ||
| Image, | ||
| SafeAreaView, | ||
| StyleSheet, | ||
| Text, | ||
| TouchableOpacity, | ||
| View, | ||
| } from 'react-native' | ||
| import { RootStackParamList, SCREEN_NAMES } from '../../types/screens' | ||
|
|
||
|
|
@@ -16,54 +16,68 @@ type HomeScreenProps = NativeStackScreenProps< | |
| > | ||
|
|
||
| interface IVideos { | ||
| uri: string | ||
| videoUri: string | ||
| imageUrl: string | ||
| id: string | ||
| title: string | ||
| } | ||
|
|
||
| const { width: screenWidth } = Dimensions.get('screen') | ||
| const imgWidth = screenWidth / 2.3 | ||
|
|
||
| const VIDEOS: IVideos[] = [ | ||
| { | ||
| uri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '1', | ||
| title: 'First video', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| { | ||
| uri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '2', | ||
| title: 'Second video', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| { | ||
| uri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '3', | ||
| title: 'Third video', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| { | ||
| uri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '4', | ||
| title: 'Fourth video', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| { | ||
| uri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '5', | ||
| title: 'Fifth video', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| { | ||
| videoUri: 'https://2ch.hk/media/src/215666/16414040960200.mp4', | ||
| id: '6', | ||
| imageUrl: 'https://2ch.hk/media/thumb/215666/16414040960200s.jpg', | ||
| }, | ||
| ] | ||
|
|
||
| export const HomeScreen = ({ navigation }: HomeScreenProps) => { | ||
| return ( | ||
| <SafeAreaView style={styles.container}> | ||
| <SafeAreaView style={{ flex: 1 }}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please, not use inline styles |
||
| <FlatList | ||
| contentContainerStyle={styles.container} | ||
| data={VIDEOS} | ||
| keyExtractor={(video) => video.id} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useCallback |
||
| renderItem={({ item }) => { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please, use useCallback |
||
| return ( | ||
| <TouchableOpacity | ||
| style={styles.listItem} | ||
| onPress={() => { | ||
| navigation.navigate(SCREEN_NAMES.Video, { uri: item.uri }) | ||
| navigation.navigate(SCREEN_NAMES.Video, { uri: item.videoUri }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useCallback |
||
| }} | ||
| > | ||
| <Text style={{ fontSize: 30 }}>{item.title}</Text> | ||
| <Image | ||
| source={{ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useMemo |
||
| uri: item.imageUrl, | ||
| }} | ||
| style={{ width: '100%', height: '100%' }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please, don't use inline styles |
||
| /> | ||
| </TouchableOpacity> | ||
| ) | ||
| }} | ||
|
|
@@ -73,8 +87,15 @@ export const HomeScreen = ({ navigation }: HomeScreenProps) => { | |
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, | ||
| container: { | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-around', | ||
| flexWrap: 'wrap', | ||
| }, | ||
| listItem: { | ||
| flex: 1, | ||
| marginVertical: 20, | ||
| width: imgWidth, | ||
| height: imgWidth, | ||
| }, | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, move this logic to variable