//
// MDL style switch component.
//
//
//
// - @see [MDL Switch](http://bit.ly/1IcHMPo)
// - [Props](#props)
// - [Defaults](#defaults)
//
// Created by ywu on 15/7/28.
//
import React, {Component, createRef} from 'react';
import {
Animated,
GestureResponderEvent,
TouchableWithoutFeedback,
TouchableWithoutFeedbackProps,
View,
} from 'react-native';
import {getTheme, Theme} from '../theme';
import {CheckedListener} from "../types";
import * as utils from '../utils';
import AnimatedThumb, {Thumb} from './SwitchThumb';
const defaultThumbRadius = 14;
const defaultTrackLength = 48;
const defaultTrackSize = 20;
// ##
export type SwitchProps = {
// Toggle status of the `Switch`
checked?: boolean,
// Callback when the toggle status is changed.
onCheckedChange?: CheckedListener,
// Color of the track, when switch is checked
onColor?: string,
// Color of the track, when switch is off
offColor?: string,
// The thickness of the Switch track
trackSize?: number,
// The length of the Switch track
trackLength?: number,
// Radius of the thumb button
thumbRadius?: number,
// Color of the thumb, when switch is checked
thumbOnColor?: string,
// Color of the thumb, when switch is off
thumbOffColor?: string,
// Duration of the thumb sliding animation, in milliseconds
thumbAniDuration?: number,
// Color of the ripple layer
rippleColor?: string,
// Duration of the ripple effect, in milliseconds
rippleAniDuration?: number,
} & TouchableWithoutFeedbackProps;
interface SwitchState {
checked: boolean,
thumbFrame: {
padding: number,
r: number,
rippleRadii: number,
x: number,
},
trackLength: number,
trackMargin: number,
trackRadii: number,
trackSize: number,
}
// ##
// The `Switch` component. Which is made up of a `Track` and a [`Thumb`](#thumb).
export default class Switch extends Component {
// ##
static defaultProps: SwitchProps = {
checked: false,
thumbRadius: defaultThumbRadius,
trackLength: defaultTrackLength,
trackSize: defaultTrackSize,
};
private theme = getTheme();
private thumbRef = createRef();
private animatedThumbLeft = new Animated.Value(0);
constructor(props: SwitchProps) {
super(props);
this.state = {
checked: false,
thumbFrame: {
padding: 0, r: 0, rippleRadii: 0, x: 0,
},
trackLength: 0,
trackMargin: 0,
trackRadii: 0,
trackSize: 0,
};
}
componentWillMount() {
this.initLayout(this.props);
}
componentWillReceiveProps(nextProps: SwitchProps) {
this.initLayout(nextProps);
}
// Rendering the `Switch`
render() {
const touchProps = {
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
onLongPress: this.props.onLongPress,
};
const mergedStyle = Object.assign({}, this.theme.switchStyle, utils.compact( {
offColor: this.props.offColor,
onColor: this.props.onColor,
rippleColor: this.props.rippleColor,
thumbOffColor: this.props.thumbOffColor,
thumbOnColor: this.props.thumbOnColor,
}));
const thumbFrame = this.state.thumbFrame;
const thumbProps = {
checked: this.state.checked,
offColor: mergedStyle.thumbOffColor,
onColor: mergedStyle.thumbOnColor,
radius: this.props.thumbRadius,
rippleAniDuration: this.props.rippleAniDuration,
rippleColor: mergedStyle.rippleColor,
rippleRadius: thumbFrame.rippleRadii,
style: {
left: this.animatedThumbLeft,
top: 0,
},
thumbStyle: {
borderRadius: this.props.thumbRadius,
left: thumbFrame.padding,
top: thumbFrame.padding,
height: (this.props.thumbRadius || defaultThumbRadius) * 2,
width: (this.props.thumbRadius || defaultThumbRadius) * 2,
},
};
return (
);
}
// Un-boxing the `Thumb` node from `AnimatedComponent`,
// in order to access the component functions defined in `Thumb`
private get thumb(): Thumb | null {
const animatedThumb = this.thumbRef.current as any;
return animatedThumb && animatedThumb.getNode() as Thumb;
}
private getBgColor(theme: Theme): string {
const onColor = this.props.onColor || theme.onColor as string;
const offColor = this.props.offColor || theme.offColor as string;
return this.state.checked ? onColor : offColor;
}
private onPress = (event: GestureResponderEvent) => {
this.confirmToggle();
this.props.onPress && this.props.onPress(event);
};
private onPressIn = (event: GestureResponderEvent) => {
this.startToggle();
this.props.onPressIn && this.props.onPressIn(event);
};
private onPressOut = (event: GestureResponderEvent) => {
this.endToggle();
this.props.onPressOut && this.props.onPressOut(event);
};
// Layout the thumb according to the size of the track
private layoutThumb(checked: boolean | undefined,
thumbRadius: number | undefined,
trackLength: number | undefined,
trackSize: number | undefined
): any {
trackSize = trackSize || defaultTrackSize;
trackLength = trackLength || defaultTrackLength;
const thumbRadii = thumbRadius || defaultThumbRadius;
const rippleRadii = trackLength - trackSize;
const trackRadii = trackSize / 2;
const trackMargin = rippleRadii - trackRadii; // make room for ripple
const thumbLeft = checked ? trackMargin + trackRadii : 0;
this.animatedThumbLeft.setValue(thumbLeft);
return {
thumbFrame: {
padding: rippleRadii - thumbRadii,
r: thumbRadii,
rippleRadii,
x: thumbLeft,
},
trackLength,
trackMargin,
trackRadii,
trackSize,
};
}
// init layout according to the props
private initLayout(props: SwitchProps) {
const nextState = this.layoutThumb(props.checked,
props.thumbRadius,
props.trackLength,
props.trackSize);
this.setState({
...nextState,
checked: props.checked || false,
});
}
// Move the thumb left or right according to the current state
private translateThumb() {
this.animatedThumbLeft.setValue(this.state.thumbFrame.x);
const newX = this.computeThumbX(this.state.checked);
Animated.timing(this.animatedThumbLeft, {
duration: this.props.thumbAniDuration || 300,
toValue: newX,
}).start(() => {
this.state.thumbFrame.x = newX;
});
}
// Calc the next position (x-axis) of the thumb
private computeThumbX(toChecked: boolean) {
if (!this.state.thumbFrame.r) {
return 0;
}
const { trackLength, trackSize } = this.state;
const dx = (toChecked ? 1 : -1) * (trackLength - trackSize);
return this.state.thumbFrame.x + dx;
}
// When a toggle action started.
private startToggle() {
this.thumb && this.thumb.startToggle();
}
// When a toggle action is confirmed.
private confirmToggle() {
const prevState = this.state.checked;
this.setState({checked: !prevState}, () => {
this.thumb && this.thumb.confirmToggle(prevState);
this.translateThumb();
if (this.props.onCheckedChange) {
this.props.onCheckedChange({checked: this.state.checked});
}
});
}
// When a toggle action is finished (confirmed or canceled).
private endToggle() {
this.thumb && this.thumb.endToggle();
}
}