Skip to content

Instantly share code, notes, and snippets.

@lachenmayer
Created May 18, 2017 15:49
Show Gist options
  • Save lachenmayer/941f6057185d785b2f9612d0d4629f63 to your computer and use it in GitHub Desktop.
Save lachenmayer/941f6057185d785b2f9612d0d4629f63 to your computer and use it in GitHub Desktop.
// @flow
import React, {Component} from 'react'
import {
View,
Dimensions,
Animated,
StatusBar,
Platform,
PanResponder,
Text,
} from 'react-native'
import {
tabBarHeight,
type RecordingCollectionType,
} from 'BoilerRoom/src/common/constants'
import NSNotificationObserver from '../iOS/NSNotificationObserver'
import PlayerInfo from './PlayerInfo'
import styles, {playerHeight} from './styles'
const {height: windowHeight, width: windowWidth} = Dimensions.get('window')
const minSize = 110
const padding = 15
const statusBarOffset = (Platform.OS === 'android') ? StatusBar.currentHeight : 0
const openHeight = windowHeight - statusBarOffset
const scaleRatio = minSize / playerHeight
const minimizedWidth = windowWidth * scaleRatio
const minimizedHeight = openHeight -
playerHeight
- padding
- tabBarHeight
+ ((playerHeight - (scaleRatio * playerHeight)) / 2)
const xOffset = (
(windowWidth - minimizedWidth) / 2
- padding
) * (1 / scaleRatio)
type PlayerDrawerStateType = {|
pan: Animated.Value;
offset: Animated.Value;
dismiss: Animated.Value;
finishedOpening: boolean;
isMinimizing: boolean;
isDismissing: boolean;
isUnminimizing: boolean;
|}
type PlayerDrawerPropsType = {|
recordingId: number;
open: boolean;
minimized: boolean;
fullscreen: boolean;
playerType: string;
setFullscreen: Function;
setMinimized: Function;
setOpen: Function;
viewGenre: (genre: RecordingCollectionType) => void;
shared: Function,
newDownload: Function;
togglePlayerType: () => void;
|}
type GestureStateType = {
dy: number;
dx: number;
}
const shouldOpen = (props, nextProps) => {
return nextProps.open && !props.open ||
props.recordingId !== nextProps.recordingId
}
const shouldUnMinimize = (props, nextProps, state) => {
return (
props.minimized === true &&
nextProps.minimized === false &&
!state.isUnminimizing &&
props.open
)
}
const shouldMinimize = (props, nextProps, state) => {
return (
props.minimized === false &&
nextProps.minimized === true &&
!state.isUnminimizing
)
}
const SWIPE_THRESHOLD = 0.1
class PlayerDrawer extends Component {
props: PlayerDrawerPropsType;
state: PlayerDrawerStateType;
_panResponder: PanResponder;
constructor (props: PlayerDrawerPropsType) {
super(props)
this.state = {
offset: new Animated.Value(windowHeight),
pan: new Animated.Value(0),
dismiss: new Animated.Value(windowWidth),
finishedOpening: false,
isMinimizing: false,
isDismissing: false,
isUnminimizing: false,
}
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => false,
onStartShouldSetPanResponderCapture: () => false,
onMoveShouldSetPanResponderCapture: () => false,
onMoveShouldSetPanResponder: (evt, gestureState) => {
if (this.props.fullscreen) return
return !this.props.fullscreen &&
(
gestureState.dy !== 0 ||
gestureState.dx !== 0
) &&
(
Math.abs(gestureState.vy) > SWIPE_THRESHOLD ||
Math.abs(gestureState.vx) > SWIPE_THRESHOLD
)
},
onPanResponderMove: this.onMove,
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: this.finishGesture,
onPanResponderTerminate: this.finishGesture,
})
}
componentWillReceiveProps (nextProps: PlayerDrawerPropsType) {
if (shouldOpen(this.props, nextProps)) this.open()
if (shouldUnMinimize(this.props, nextProps, this.state)) this.animateFull()
if (shouldMinimize(this.props, nextProps, this.state)) this.animateSmall()
if (nextProps.fullscreen && Platform.OS === 'ios') { // went fullscreen
NSNotificationObserver.observeExitFullscreen(() => {
this.props.setFullscreen(false)
})
}
}
onMove = (evt: any, gestureState: GestureStateType) => {
if (this.state.isMinimizing) {
this.applyMinimize(gestureState)
} else if (this.state.isDismissing) {
this.applyDismiss(gestureState)
} else {
if (Math.abs(gestureState.dy) > Math.abs(gestureState.dx)) {
this.setState({isMinimizing: true, isDismissing: false})
this.applyMinimize(gestureState)
} else if (this.props.minimized) {
this.setState({isDismissing: true, isMinimizing: false})
this.applyDismiss(gestureState)
}
}
}
applyMinimize = (gestureState: GestureStateType) => {
let dy = (this.props.minimized ? minimizedHeight : 0) + gestureState.dy
this.state.pan.setValue(dy)
}
applyDismiss = (gestureState: GestureStateType) => {
this.state.dismiss.setValue(gestureState.dx)
}
animateFull = () => {
this.state.offset.setValue(statusBarOffset)
this.setState({isUnminimizing: true, isMinimizing: true})
this.props.setMinimized(false)
Animated.spring(
this.state.pan,
{toValue: 0}
).start(() => {
this.setState({isUnminimizing: false, isMinimizing: false})
})
}
animateSmall = (callback: ?Function) => {
this.setState({isUnminimizing: false, isMinimizing: true})
this.props.setMinimized(true)
Animated.spring(
this.state.pan,
{toValue: minimizedHeight}
).start(() => {
this.setState({isMinimizing: false})
if (callback && typeof (callback) === 'function') callback()
})
}
animateDismiss = () => {
Animated.spring(
this.state.dismiss,
{toValue: -windowWidth}
).start(() => {
this.setState({
isDismissing: false,
finishedOpening: false,
})
this.props.setOpen(false)
this.props.setMinimized(false)
this.state.offset.setValue(windowHeight)
})
}
animateDismissBack = () => {
Animated.spring(
this.state.dismiss,
{toValue: 0}
).start(() => {
this.setState({isDismissing: false})
})
}
finishGesture = (evt: any, gestureState: GestureStateType) => {
if (this.state.isMinimizing) {
const dy = (this.props.minimized ? minimizedHeight : 0) + gestureState.dy
const threshold = this.props.minimized ? windowHeight * 0.6 : windowHeight * 0.35
if (dy < threshold) {
this.animateFull()
} else {
this.animateSmall()
}
} else if (this.state.isDismissing) {
if (gestureState.dx < -0.3 * windowWidth) {
this.animateDismiss()
} else {
this.animateDismissBack()
}
}
}
open = () => {
if (!this.props.minimized) {
this.state.pan.setValue(0)
this.state.dismiss.setValue(windowWidth)
// this timeout is necessary because the player info wasn't appearing
// probably because the opacity wasn't updated so it just didn't paint
setTimeout(() => {
Animated.timing(this.state.offset,
{
toValue: 0,
duration: 300,
}
).start(() => {
this.setState({finishedOpening: true})
})
}, 1)
} else {
this.animateFull()
}
}
onPress = () => {
this.animateFull()
}
viewGenre = (genre: RecordingCollectionType) => {
this.animateSmall(() => {
this.props.viewGenre(genre)
})
}
render () {
const {
recordingId,
open,
minimized,
shared,
} = this.props
const fullscreenOffset = Platform.OS === 'android' ?
((windowHeight - statusBarOffset) - windowWidth) / 2 : 0
return (
<View
style={styles.overlay}
pointerEvents={'box-none'}
>
{!Platform.isTVOS &&
<StatusBar
hidden={Platform.OS === 'ios' && open && !minimized}
barStyle='light-content'
animated={true}
/>
}
<Animated.View
pointerEvents={open && !minimized ? 'box-none' : 'none'}
style={[styles.wrapper,
{
transform: [
{
translateY: this.state.offset,
},
],
},
{
opacity: this.state.pan.interpolate({
inputRange: [0, minimizedHeight],
outputRange: [1, 0],
extrapolate: 'clamp',
}),
},
]}
scrollEnabled={open}
>
<PlayerInfo
key={recordingId}
recordingId={recordingId}
windowWidth={windowWidth}
playerHeight={playerHeight}
viewGenre={this.viewGenre}
minimized={this.props.minimized}
newDownload={this.props.newDownload}
togglePlayerType={this.props.togglePlayerType}
playerType={this.props.playerType}
shared={shared}
/>
</Animated.View>
{
this.props.open &&
<Animated.View
style={[styles.videoPlayer,
{
transform: [
{
translateY: this.state.pan.interpolate({
inputRange: [0, minimizedHeight],
outputRange: [0, minimizedHeight],
extrapolate: 'clamp',
}),
},
{
scale: this.state.pan.interpolate({
inputRange: [0, minimizedHeight],
outputRange: [1, scaleRatio],
extrapolate: 'clamp',
}),
},
{
translateX: this.state.pan.interpolate({
inputRange: [0, minimizedHeight],
outputRange: [0, xOffset],
extrapolate: 'clamp',
}),
},
{
translateX: this.state.dismiss.interpolate({
inputRange: [-windowWidth, 0],
outputRange: [-windowWidth / scaleRatio, 0],
extrapolate: 'clamp',
}),
},
],
},
this.props.fullscreen && Platform.OS === 'android' ? {
height: windowWidth,
width: (windowHeight - statusBarOffset),
transform: [
{rotate: '90deg'},
{translateY: fullscreenOffset},
{translateX: fullscreenOffset},
],
} : {},
{
opacity: this.state.dismiss.interpolate({
inputRange: [-windowWidth, 0],
outputRange: [0, 1.0],
extrapolate: 'clamp',
}),
},
]}
key={recordingId}
{...this._panResponder.panHandlers}
>
<MediaPlayer recordingId={recordingId} />
</Animated.View>
}
</View>
)
}
}
export default PlayerDrawer
const MediaPlayer = ({recordingId}) =>
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: windowWidth,
height: windowWidth * (9 / 16),
backgroundColor: 'red',
}}
>
<Text>{recordingId}</Text>
</View>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment