This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
387 lines
12 KiB
387 lines
12 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
* |
|
* @providesModule SwipeableRow |
|
* @flow |
|
*/ |
|
'use strict'; |
|
|
|
const Animated = require('Animated'); |
|
const I18nManager = require('I18nManager'); |
|
const PanResponder = require('PanResponder'); |
|
const React = require('React'); |
|
const PropTypes = require('prop-types'); |
|
const StyleSheet = require('StyleSheet'); |
|
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error |
|
* found when Flow v0.54 was deployed. To see the error delete this comment and |
|
* run Flow. */ |
|
const TimerMixin = require('react-timer-mixin'); |
|
const View = require('View'); |
|
|
|
const createReactClass = require('create-react-class'); |
|
const emptyFunction = require('fbjs/lib/emptyFunction'); |
|
|
|
const IS_RTL = I18nManager.isRTL; |
|
|
|
// NOTE: Eventually convert these consts to an input object of configurations |
|
|
|
// Position of the left of the swipable item when closed |
|
const CLOSED_LEFT_POSITION = 0; |
|
// Minimum swipe distance before we recognize it as such |
|
const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 10; |
|
// Minimum swipe speed before we fully animate the user's action (open/close) |
|
const HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD = 0.3; |
|
// Factor to divide by to get slow speed; i.e. 4 means 1/4 of full speed |
|
const SLOW_SPEED_SWIPE_FACTOR = 4; |
|
// Time, in milliseconds, of how long the animated swipe should be |
|
const SWIPE_DURATION = 300; |
|
|
|
/** |
|
* On SwipeableListView mount, the 1st item will bounce to show users it's |
|
* possible to swipe |
|
*/ |
|
const ON_MOUNT_BOUNCE_DELAY = 700; |
|
const ON_MOUNT_BOUNCE_DURATION = 400; |
|
|
|
// Distance left of closed position to bounce back when right-swiping from closed |
|
const RIGHT_SWIPE_BOUNCE_BACK_DISTANCE = 30; |
|
const RIGHT_SWIPE_BOUNCE_BACK_DURATION = 300; |
|
/** |
|
* Max distance of right swipe to allow (right swipes do functionally nothing). |
|
* Must be multiplied by SLOW_SPEED_SWIPE_FACTOR because gestureState.dx tracks |
|
* how far the finger swipes, and not the actual animation distance. |
|
*/ |
|
const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR; |
|
|
|
/** |
|
* Creates a swipable row that allows taps on the main item and a custom View |
|
* on the item hidden behind the row. Typically this should be used in |
|
* conjunction with SwipeableListView for additional functionality, but can be |
|
* used in a normal ListView. See the renderRow for SwipeableListView to see how |
|
* to use this component separately. |
|
*/ |
|
const SwipeableRow = createReactClass({ |
|
displayName: 'SwipeableRow', |
|
_panResponder: {}, |
|
_previousLeft: CLOSED_LEFT_POSITION, |
|
|
|
mixins: [TimerMixin], |
|
|
|
propTypes: { |
|
children: PropTypes.any, |
|
isOpen: PropTypes.bool, |
|
preventSwipeRight: PropTypes.bool, |
|
maxSwipeDistance: PropTypes.number.isRequired, |
|
onOpen: PropTypes.func.isRequired, |
|
onClose: PropTypes.func.isRequired, |
|
onSwipeEnd: PropTypes.func.isRequired, |
|
onSwipeStart: PropTypes.func.isRequired, |
|
// Should bounce the row on mount |
|
shouldBounceOnMount: PropTypes.bool, |
|
/** |
|
* A ReactElement that is unveiled when the user swipes |
|
*/ |
|
slideoutView: PropTypes.node.isRequired, |
|
/** |
|
* The minimum swipe distance required before fully animating the swipe. If |
|
* the user swipes less than this distance, the item will return to its |
|
* previous (open/close) position. |
|
*/ |
|
swipeThreshold: PropTypes.number.isRequired, |
|
}, |
|
|
|
getInitialState(): Object { |
|
return { |
|
currentLeft: new Animated.Value(this._previousLeft), |
|
/** |
|
* In order to render component A beneath component B, A must be rendered |
|
* before B. However, this will cause "flickering", aka we see A briefly |
|
* then B. To counter this, _isSwipeableViewRendered flag is used to set |
|
* component A to be transparent until component B is loaded. |
|
*/ |
|
isSwipeableViewRendered: false, |
|
rowHeight: (null: ?number), |
|
}; |
|
}, |
|
|
|
getDefaultProps(): Object { |
|
return { |
|
isOpen: false, |
|
preventSwipeRight: false, |
|
maxSwipeDistance: 0, |
|
onOpen: emptyFunction, |
|
onClose: emptyFunction, |
|
onSwipeEnd: emptyFunction, |
|
onSwipeStart: emptyFunction, |
|
swipeThreshold: 30, |
|
}; |
|
}, |
|
|
|
UNSAFE_componentWillMount(): void { |
|
this._panResponder = PanResponder.create({ |
|
onMoveShouldSetPanResponderCapture: this._handleMoveShouldSetPanResponderCapture, |
|
onPanResponderGrant: this._handlePanResponderGrant, |
|
onPanResponderMove: this._handlePanResponderMove, |
|
onPanResponderRelease: this._handlePanResponderEnd, |
|
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest, |
|
onPanResponderTerminate: this._handlePanResponderEnd, |
|
onShouldBlockNativeResponder: (event, gestureState) => false, |
|
}); |
|
}, |
|
|
|
componentDidMount(): void { |
|
if (this.props.shouldBounceOnMount) { |
|
/** |
|
* Do the on mount bounce after a delay because if we animate when other |
|
* components are loading, the animation will be laggy |
|
*/ |
|
this.setTimeout(() => { |
|
this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION); |
|
}, ON_MOUNT_BOUNCE_DELAY); |
|
} |
|
}, |
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: Object): void { |
|
/** |
|
* We do not need an "animateOpen(noCallback)" because this animation is |
|
* handled internally by this component. |
|
*/ |
|
if (this.props.isOpen && !nextProps.isOpen) { |
|
this._animateToClosedPosition(); |
|
} |
|
}, |
|
|
|
render(): React.Element<any> { |
|
// The view hidden behind the main view |
|
let slideOutView; |
|
if (this.state.isSwipeableViewRendered && this.state.rowHeight) { |
|
slideOutView = ( |
|
<View style={[ |
|
styles.slideOutContainer, |
|
{height: this.state.rowHeight}, |
|
]}> |
|
{this.props.slideoutView} |
|
</View> |
|
); |
|
} |
|
|
|
// The swipeable item |
|
const swipeableView = ( |
|
<Animated.View |
|
onLayout={this._onSwipeableViewLayout} |
|
style={{transform: [{translateX: this.state.currentLeft}]}}> |
|
{this.props.children} |
|
</Animated.View> |
|
); |
|
|
|
return ( |
|
<View |
|
{...this._panResponder.panHandlers}> |
|
{slideOutView} |
|
{swipeableView} |
|
</View> |
|
); |
|
}, |
|
|
|
close(): void { |
|
this.props.onClose(); |
|
this._animateToClosedPosition(); |
|
}, |
|
|
|
_onSwipeableViewLayout(event: Object): void { |
|
this.setState({ |
|
isSwipeableViewRendered: true, |
|
rowHeight: event.nativeEvent.layout.height, |
|
}); |
|
}, |
|
|
|
_handleMoveShouldSetPanResponderCapture( |
|
event: Object, |
|
gestureState: Object, |
|
): boolean { |
|
// Decides whether a swipe is responded to by this component or its child |
|
return gestureState.dy < 10 && this._isValidSwipe(gestureState); |
|
}, |
|
|
|
_handlePanResponderGrant(event: Object, gestureState: Object): void { |
|
|
|
}, |
|
|
|
_handlePanResponderMove(event: Object, gestureState: Object): void { |
|
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) { |
|
return; |
|
} |
|
|
|
this.props.onSwipeStart(); |
|
|
|
if (this._isSwipingRightFromClosed(gestureState)) { |
|
this._swipeSlowSpeed(gestureState); |
|
} else { |
|
this._swipeFullSpeed(gestureState); |
|
} |
|
}, |
|
|
|
_isSwipingRightFromClosed(gestureState: Object): boolean { |
|
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx; |
|
return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0; |
|
}, |
|
|
|
_swipeFullSpeed(gestureState: Object): void { |
|
this.state.currentLeft.setValue(this._previousLeft + gestureState.dx); |
|
}, |
|
|
|
_swipeSlowSpeed(gestureState: Object): void { |
|
this.state.currentLeft.setValue( |
|
this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR, |
|
); |
|
}, |
|
|
|
_isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean { |
|
/** |
|
* We want to allow a BIT of right swipe, to allow users to know that |
|
* swiping is available, but swiping right does not do anything |
|
* functionally. |
|
*/ |
|
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx; |
|
return ( |
|
this._isSwipingRightFromClosed(gestureState) && |
|
gestureStateDx > RIGHT_SWIPE_THRESHOLD |
|
); |
|
}, |
|
|
|
_onPanResponderTerminationRequest( |
|
event: Object, |
|
gestureState: Object, |
|
): boolean { |
|
return false; |
|
}, |
|
|
|
_animateTo( |
|
toValue: number, |
|
duration: number = SWIPE_DURATION, |
|
callback: Function = emptyFunction, |
|
): void { |
|
Animated.timing( |
|
this.state.currentLeft, |
|
{ |
|
duration, |
|
toValue, |
|
useNativeDriver: true, |
|
}, |
|
).start(() => { |
|
this._previousLeft = toValue; |
|
callback(); |
|
}); |
|
}, |
|
|
|
_animateToOpenPosition(): void { |
|
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance; |
|
this._animateTo(-maxSwipeDistance); |
|
}, |
|
|
|
_animateToOpenPositionWith( |
|
speed: number, |
|
distMoved: number, |
|
): void { |
|
/** |
|
* Ensure the speed is at least the set speed threshold to prevent a slow |
|
* swiping animation |
|
*/ |
|
speed = ( |
|
speed > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD ? |
|
speed : |
|
HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD |
|
); |
|
/** |
|
* Calculate the duration the row should take to swipe the remaining distance |
|
* at the same speed the user swiped (or the speed threshold) |
|
*/ |
|
const duration = Math.abs((this.props.maxSwipeDistance - Math.abs(distMoved)) / speed); |
|
const maxSwipeDistance = IS_RTL ? -this.props.maxSwipeDistance : this.props.maxSwipeDistance; |
|
this._animateTo(-maxSwipeDistance, duration); |
|
}, |
|
|
|
_animateToClosedPosition(duration: number = SWIPE_DURATION): void { |
|
this._animateTo(CLOSED_LEFT_POSITION, duration); |
|
}, |
|
|
|
_animateToClosedPositionDuringBounce(): void { |
|
this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION); |
|
}, |
|
|
|
_animateBounceBack(duration: number): void { |
|
/** |
|
* When swiping right, we want to bounce back past closed position on release |
|
* so users know they should swipe right to get content. |
|
*/ |
|
const swipeBounceBackDistance = IS_RTL ? |
|
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE : |
|
RIGHT_SWIPE_BOUNCE_BACK_DISTANCE; |
|
this._animateTo( |
|
-swipeBounceBackDistance, |
|
duration, |
|
this._animateToClosedPositionDuringBounce, |
|
); |
|
}, |
|
|
|
// Ignore swipes due to user's finger moving slightly when tapping |
|
_isValidSwipe(gestureState: Object): boolean { |
|
if (this.props.preventSwipeRight && this._previousLeft === CLOSED_LEFT_POSITION && gestureState.dx > 0) { |
|
return false; |
|
} |
|
|
|
return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD; |
|
}, |
|
|
|
_shouldAnimateRemainder(gestureState: Object): boolean { |
|
/** |
|
* If user has swiped past a certain distance, animate the rest of the way |
|
* if they let go |
|
*/ |
|
return ( |
|
Math.abs(gestureState.dx) > this.props.swipeThreshold || |
|
gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD |
|
); |
|
}, |
|
|
|
_handlePanResponderEnd(event: Object, gestureState: Object): void { |
|
const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx; |
|
if (this._isSwipingRightFromClosed(gestureState)) { |
|
this.props.onOpen(); |
|
this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION); |
|
} else if (this._shouldAnimateRemainder(gestureState)) { |
|
if (horizontalDistance < 0) { |
|
// Swiped left |
|
this.props.onOpen(); |
|
this._animateToOpenPositionWith(gestureState.vx, horizontalDistance); |
|
} else { |
|
// Swiped right |
|
this.props.onClose(); |
|
this._animateToClosedPosition(); |
|
} |
|
} else { |
|
if (this._previousLeft === CLOSED_LEFT_POSITION) { |
|
this._animateToClosedPosition(); |
|
} else { |
|
this._animateToOpenPosition(); |
|
} |
|
} |
|
|
|
this.props.onSwipeEnd(); |
|
}, |
|
}); |
|
|
|
const styles = StyleSheet.create({ |
|
slideOutContainer: { |
|
bottom: 0, |
|
left: 0, |
|
position: 'absolute', |
|
right: 0, |
|
top: 0, |
|
}, |
|
}); |
|
|
|
module.exports = SwipeableRow;
|
|
|