Skip to content

Commit

Permalink
Merge pull request #359 from backbonelabs/v2-posture-monitor
Browse files Browse the repository at this point in the history
V2 posture monitor
  • Loading branch information
kevhuang committed Aug 26, 2017
2 parents 91a21d4 + 4a4a8c5 commit 80fd4b1
Show file tree
Hide file tree
Showing 18 changed files with 737 additions and 332 deletions.
38 changes: 27 additions & 11 deletions app/components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Button extends Component {
text: PropTypes.string.isRequired,
textStyle: PropTypes.object,
primary: PropTypes.bool,
secondary: PropTypes.bool,
fbBtn: PropTypes.bool,
pressStatus: PropTypes.bool,
onHideUnderlay: PropTypes.func,
Expand Down Expand Up @@ -46,17 +47,17 @@ class Button extends Component {
let buttonType;
const textStyles = [styles._text];
const buttonStyles = [styles.button];
const primaryStyles = [buttonStyles, styles.primaryBtn];
const fbBtnStyles = [buttonStyles, styles.facebookBtn];
const secondaryStyles = [buttonStyles, styles.secondaryBtn];
const secondaryActive = [buttonStyles, styles.secondaryActive];
const secondaryTextStyles = [styles._text, styles._secondaryTextStyles];
const secondaryTextActive = [styles._text, styles._secondaryTextActive];
const fbBtnStyles = [buttonStyles, styles.facebookBtn];
const defaultStyles = [buttonStyles, styles.defaultBtn];
const defaultActive = [buttonStyles, styles.defaultActive];
const defaultTextActive = [styles._text, styles._defaultTextActive];
const defaultTextStyles = [styles._text, styles._defaultTextStyles];

if (this.props.primary) {
buttonType = (
<TouchableHighlight
style={primaryStyles}
style={buttonStyles}
underlayColor={'#FB8C00'}
onHideUnderlay={this._onHideUnderlay}
onShowUnderlay={this._onShowUnderlay}
Expand All @@ -82,18 +83,32 @@ class Button extends Component {
</View>
</TouchableHighlight>
);
} else if (this.props.secondary) {
buttonType = (
<TouchableHighlight
style={secondaryStyles}
underlayColor={'#0091EA'}
onHideUnderlay={this._onHideUnderlay}
onShowUnderlay={this._onShowUnderlay}
onPress={this.props.disabled ? undefined : this.props.onPress}
>
<View>
<BodyText style={textStyles}>{this.props.text}</BodyText>
</View>
</TouchableHighlight>
);
} else {
buttonType = (
<TouchableHighlight
activeOpacity={0.4}
style={this.state.pressStatus ? secondaryActive : secondaryStyles}
underlayColor="transparent"
style={this.state.pressStatus ? defaultActive : defaultStyles}
underlayColor={'transparent'}
onHideUnderlay={this._onHideUnderlay}
onShowUnderlay={this._onShowUnderlay}
onPress={this.props.disabled ? undefined : this.props.onPress}
>
<View>
<BodyText style={this.state.pressStatus ? secondaryTextActive : secondaryTextStyles}>
<BodyText style={this.state.pressStatus ? defaultTextActive : defaultTextStyles}>
{this.props.text}
</BodyText>
</View>
Expand All @@ -104,12 +119,13 @@ class Button extends Component {
if (this.props.disabled) {
buttonStyles.push(styles.disabledButton);
textStyles.push(styles._disabledText);
secondaryStyles.push(styles.disabledSecondaryBorder);
secondaryTextStyles.push(styles._disabledSecondaryText);
defaultStyles.push(styles.disabledSecondaryBorder);
defaultTextStyles.push(styles._disabledSecondaryText);
}
buttonStyles.push(this.props.style);
textStyles.push(this.props.textStyle);
fbBtnStyles.push(this.props.style);
secondaryStyles.push(this.props.style);

return buttonType;
}
Expand Down
152 changes: 113 additions & 39 deletions app/components/posture/PostureMonitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,43 @@ const SessionControlServiceEvents = new NativeEventEmitter(SessionControlService

const MIN_POSTURE_THRESHOLD = 0.03;
const MAX_POSTURE_THRESHOLD = 0.3;
const currentRange = MAX_POSTURE_THRESHOLD - MIN_POSTURE_THRESHOLD;

const isiOS = Platform.OS === 'ios';

/**
* Maps distance values to slouch degrees for determining how much to rotate
* the monitor pointer. The degree output would range from -180 to 0, where -180
* would have the pointer pointing horizontally left and 0 would have the pointer
* pointing horizontally right. The max distance range is MAX_POSTURE_THRESHOLD.
* Scales a value to the given range.
* @param {Number} value value to normalize
* @param {Number} min min of range
* @param {Number} max max of range
* @return {Number} Value scaled in new range
*/
const normalizeValue = (value, min, max) => (((value - MIN_POSTURE_THRESHOLD) / currentRange)
* (max - min)) + min;

/**
* Monitor pointerPosition prop requires a number between -30-210(inclusive) for the pointer,
* @param {Number} distance Deviation from the control point
* @return {Number} Degree equivalent of the distance value
* @return {Number} Distance value scaled in a range of -30-210
*/
const distanceToDegrees = distance => {
const maxMappedDegree = -180;
return Math.max(-180, (distance / MAX_POSTURE_THRESHOLD) * maxMappedDegree);
const getPointerPosition = distance => {
const normalizedDistance = Math.floor(normalizeValue(distance, 210, -30));
if (normalizedDistance > 210) {
return 210;
} else if (normalizedDistance < -30) {
return -30;
}
return normalizedDistance;
};

/**
* Monitor slouchPosition prop requires a number between 0-100(inclusive) for the arc,
* so we scale the distance to a range of 0-100
* @param {Number} distance Deviation from the control point
* @return {Number} Distance value scaled in a range of 0-100
*/
const getSlouchPosition = distance => Math.floor(normalizeValue(distance, 100, 0));

/**
* Returns a number at a given magnitude
* @param {Number} number The original number
Expand Down Expand Up @@ -152,7 +173,8 @@ class PostureMonitor extends Component {
forceStoppedSession: false,
postureThreshold: this.props.user.settings.postureThreshold,
shouldNotifySlouch: true,
pointerPosition: 0,
pointerPosition: 210,
currentDistance: 0,
totalDuration: 0, // in seconds
slouchTime: 0, // in seconds
timeElapsed: 0, // in seconds
Expand Down Expand Up @@ -545,8 +567,9 @@ class PostureMonitor extends Component {
sessionDataHandler(event) {
const { currentDistance, timeElapsed } = event;
this.setState({
pointerPosition: distanceToDegrees(currentDistance),
pointerPosition: getPointerPosition(currentDistance),
timeElapsed,
currentDistance,
});

// Mark the shouldNotifySlouch to true when it's on a good posture state
Expand Down Expand Up @@ -1011,21 +1034,61 @@ class PostureMonitor extends Component {
this.props.dispatch(userActions.updateUserSettings(updatedUserSettings));
}

navigateToAlert() {
return this.props.navigator.push(routes.alerts);
}

navigateToRecalibrate() {
this.props.dispatch(appActions.showPartialModal({
topView: (
<Image source={deviceWarningIcon} />
),
title: {
caption: 'Stop Session?',
},
detail: {
caption: 'Are you sure you want to stop your current session?',
},
buttons: [
{
caption: 'STOP',
onPress: () => {
this.props.dispatch(appActions.hidePartialModal());
this.stopSession();
this.props.navigator.resetTo(routes.postureCalibrate);
},
},
{
caption: 'RESUME',
onPress: () => {
this.props.dispatch(appActions.hidePartialModal());
},
},
],
backButtonHandler: () => {
this.props.dispatch(appActions.hidePartialModal());
},
}));
}

render() {
const {
postureThreshold,
pointerPosition,
sessionState,
hasPendingSessionOperation,
currentDistance,
} = this.state;

const isDisabled = sessionState === sessionStates.RUNNING;

const getPlayPauseButton = () => {
if (sessionState === sessionStates.STOPPED) {
return <MonitorButton play onPress={this.startSession} />;
} else if (sessionState === sessionStates.RUNNING) {
return <MonitorButton pause onPress={this.pauseSession} />;
return <MonitorButton text="PLAY" icon="play-arrow" onPress={this.startSession} />;
} else if (isDisabled) {
return <MonitorButton text="PAUSE" icon="pause" onPress={this.pauseSession} />;
}
return <MonitorButton play onPress={this.resumeSession} />;
return <MonitorButton text="PLAY" icon="play-arrow" onPress={this.resumeSession} />;
};

return this.props.device.isConnecting ? (
Expand All @@ -1035,22 +1098,18 @@ class PostureMonitor extends Component {
</View>
) : (
<View style={styles.container}>
<HeadingText size={1} style={styles._timer}>
<BodyText style={styles._timer}>
{this.getFormattedTime()}
</HeadingText>
<HeadingText size={3} style={styles._heading}>SESSION TIME</HeadingText>
</BodyText>
<BodyText style={styles._heading}>Time Remaining</BodyText>
<Monitor
disable={isDisabled}
pointerPosition={pointerPosition}
slouchPosition={distanceToDegrees(postureThreshold)}
slouchPosition={getSlouchPosition(postureThreshold)}
onPress={this.navigateToRecalibrate}
rating={(currentDistance < postureThreshold)}
/>
<View style={styles.monitorRatingContainer}>
<BodyText style={styles._monitorPoor}>Poor</BodyText>
<BodyText style={styles._monitorGood}>Good</BodyText>
</View>
<BodyText style={styles._monitorTitle}>POSTURE MONITOR</BodyText>
<SecondaryText style={styles._sliderTitle}>
Tune up or down the Backbone's slouch detection
</SecondaryText>

{/*
The allowed range for the posture distance threshold is MIN_POSTURE_THRESHOLD
to MAX_POSTURE_THRESHOLD. Ideally, the minimumValue and maximumValue for the
Expand All @@ -1060,24 +1119,39 @@ class PostureMonitor extends Component {
MIN_POSTURE_THRESHOLD to make maximumValue equal to 0 in order for the slider to
work on both Android and iOS.
*/}
<MonitorSlider
value={-postureThreshold + MIN_POSTURE_THRESHOLD}
onValueChange={value => {
const correctedValue = value - MIN_POSTURE_THRESHOLD;
<View>
<MonitorSlider
value={-postureThreshold + MIN_POSTURE_THRESHOLD}
onValueChange={value => {
const correctedValue = value - MIN_POSTURE_THRESHOLD;
// The value is rounded to 3 decimals places to prevent unexpected rounding issues
this.updatePostureThreshold(-correctedValue.toFixed(3));
}}
minimumValue={MIN_POSTURE_THRESHOLD - MAX_POSTURE_THRESHOLD}
maximumValue={0}
disabled={sessionState === sessionStates.RUNNING}
/>
this.updatePostureThreshold(-correctedValue.toFixed(3));
}}
minimumValue={MIN_POSTURE_THRESHOLD - MAX_POSTURE_THRESHOLD}
maximumValue={0}
disabled={isDisabled}
/>
<SecondaryText style={styles._sliderTitle}>SLOUCH DETECTION</SecondaryText>
</View>
<View style={styles.btnContainer}>
{getPlayPauseButton()}
{sessionState !== sessionStates.PAUSED || hasPendingSessionOperation ?
<MonitorButton alertsDisabled disabled /> :
<MonitorButton alerts onPress={() => this.props.navigator.push(routes.alerts)} />
<MonitorButton
disabled
icon="notifications"
text="ALERTS"
/> :
<MonitorButton
icon="notifications"
text="ALERTS"
onPress={this.navigateToAlert}
/>
}
<MonitorButton stop onPress={this.confirmStopSession} />
<MonitorButton
text="STOP"
icon="stop"
onPress={this.confirmStopSession}
/>
</View>
</View>
);
Expand Down
73 changes: 73 additions & 0 deletions app/components/posture/postureMonitor/AnimatedCircularProgress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { Component, PropTypes } from 'react';
import { View, Animated } from 'react-native';
import CircularProgress from './CircularProgress';

const AnimatedProgress = Animated.createAnimatedComponent(CircularProgress);

export default class AnimatedCircularProgress extends Component {

constructor(props) {
super(props);
this.state = {
chartFillAnimation: new Animated.Value(props.prefill || 0),
};
}

componentDidMount() {
this.animateFill();
}

componentDidUpdate(prevProps) {
if (prevProps.fill !== this.props.fill) {
this.animateFill();
}
}

animateFill() {
const { tension, friction } = this.props;

Animated.spring(
this.state.chartFillAnimation,
{
toValue: this.props.fill,
tension,
friction,
}
).start();
}

performLinearAnimation(toValue, duration) {
Animated.timing(this.state.chartFillAnimation, {
toValue,
duration,
}).start();
}

render() {
const { fill, prefill, ...other } = this.props; // eslint-disable-line

return (
<AnimatedProgress
{...other}
fill={this.state.chartFillAnimation}
/>
);
}
}

AnimatedCircularProgress.propTypes = {
style: View.propTypes.style,
size: PropTypes.number.isRequired,
fill: PropTypes.number,
prefill: PropTypes.number,
width: PropTypes.number.isRequired,
tintColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
backgroundColor: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
tension: PropTypes.number,
friction: PropTypes.number,
};

AnimatedCircularProgress.defaultProps = {
tension: 7,
friction: 10,
};
Loading

0 comments on commit 80fd4b1

Please sign in to comment.